"""
    kahanSumOneValue(a, b, c)

`a`+`b`=`sum` is done, where the lost information in that `sum` is saved in `c` (according to Kahan Summation Algorithm). Each input only represents a number.

# Args

* `a`: it is a number, where `a`>`b` should be true for the appropiate work of the algorithm.
* `b`: the value that will be added to `a`.
* `c`: the first time this function is called in an iteration, the value of `c` should be 0. After that, this value should contain the lost information of the last sum, for its proper function.

# Returns

* `sum`: `a` + `b`.
* `c`: information lost in the sum `a`+`b`.
"""
function kahanSumOneValue(a, b, c)
    sum = a
    y = b - c          # So far, so good: c is zero.
    t = sum + y        # Alas, sum is big, y small, so low-order digits of y are lost.
    c = (t - sum) - y  # (t - sum) cancels the high-order part of y; subtracting y recovers negative (low part of y)
    sum = t            # Algebraically, c should always be zero. Beware overly-aggressive optimizing compilers!
    return sum, c
end

"""
    kahanSumOneValue(a, b, c)

`a`+`b`=`sum` is done, where the lost information in that `sum` is saved in `c` (according to Kahan Summation Algorithm). Each input can represent an array or a matrix.

# Args

* `a`: it is a number, where `a`>`b` should be true for the appropiate work of the algorithm. `sum` values are overwritten here.
* `b`: the value that will be added to `a`.
* `c`: the first time this function is called in an iteration, the value of `c` should be 0. After that, this value should contain the lost information of the last sum, for its proper function. `c` values are overwritten here.
"""
function kahanSum!(a, b, c)

    if length(size(a)) == 1 # Vector
        for i in 1:length(a)
            y = b[i] - c[i]        # So far, so good: c is zero.
            t = a[i] + y           # Alas, a is big, y small, so low-order digits of y are lost.
            c[i] = (t - a[i]) - y  # (t - a) cancels the high-order part of y; subtracting y recovers negative (low part of y)
            a[i] = t               # Algebraically, c should always be zero. Beware overly-aggressive optimizing compilers!
        end
    elseif length(size(a)) == 2 # Matrix
        for i in 1:size(a)[1]
            for j in 1:size(a)[2]
                y = b[i,j] - c[i,j]        # So far, so good: c is zero.
                t = a[i,j] + y           # Alas, a is big, y small, so low-order digits of y are lost.
                c[i,j] = (t - a[i,j]) - y  # (t - a) cancels the high-order part of y; subtracting y recovers negative (low part of y)
                a[i,j] = t               # Algebraically, c should always be zero. Beware overly-aggressive optimizing compilers!
            end
        end
    else
        printf("Warning: kahanSum! works only for arrays and matrices.")
    end

end


# This is Julia code for discretizing AR(1) process by using Rouwenhorst method (1995)
# written on 2017/10/17
#
# Model:
# z' = rho*z+e  e~N(0,(sig_e)^2)
#
# INPUTS
# N:     Number of grids
# rho:   Coeffecient of AR(1) process z'=mew+rho*z+e, e~N(0,sigmasq)
# sig_e: Std of innovation, e, in AR(1) process
#
# OUTPUT
# vZ:    grids
# mPI:   transition prob. matrix

#########################################################################
## 1. Parameters
#########################################################################

# Ex. Standard quarterly calibration for productivity process
# of US economy

const N = 5;            # Number of grids, N
const rho = 0.95;       # AR(1) coefficient
const sig_e = 0.007;    # std of innovation

#########################################################################
## 2. Generating grids && Parameterization
#########################################################################

sig_z = sqrt(sig_e^2/(1-rho^2));    # std of AR(1) r.v.
z_max = sig_z*sqrt(N-1);            # max grid
z_min = -z_max;                     # min grid
p = (1+rho)/2;                      # initial prob
q = p;

vZ = linspace(z_min, z_max, N)      # Grids

#########################################################################
## 3. Transition matrix
#########################################################################

mPI_old = [p 1-p; 1-q q]; # Initial PI
mPI_new = zeros(3,3);

for i in 3:N
    mPI_new  = zeros(i,i);
    mPI_new1 = zeros(i,i);  mPI_new1[1:i-1,1:i-1]    = mPI_old;
    mPI_new2 = zeros(i,i);  mPI_new2[1:i-1,2:i]      = mPI_old;
    mPI_new3 = zeros(i,i);  mPI_new3[2:i,1:i-1]      = mPI_old;
    mPI_new4 = zeros(i,i);  mPI_new4[2:i,2:i]        = mPI_old;

    mPI_new = p*mPI_new1 + (1-p)*mPI_new2 + (1-q)*mPI_new3 + q*mPI_new4;
    mPI_new[2:i-1,:] = 0.5*mPI_new[2:i-1,:]; # devide by 2

    mPI_old = mPI_new;
end

###########################################################
## 4. Display result with 4 decimals
###########################################################

print("Grid points are \n")
for i in 1:N
    @printf "%6.4f" vZ[i]
    print(" ")
end
print("\n")
print("Transition prob. matrix is\n")
for i in 1:N
    for j in 1:N
        @printf "%6.4f" mPI_new[i,j]
        print(" ")
    end
    print("\n")
end


using Plots

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

function quadratic_fit_search(f, a, b, c, n)
    ya, yb, yc = f(a), f(b), f(c)
    for i in 1:n-3
        x = 0.5*(ya*(b^2-c^2)+yb*(c^2-a^2)+yc*(a^2-b^2)) /
            (ya*(b-c) +yb*(c-a) +yc*(a-b))
        yx = f(x)
        if x > b
            if yx > yb
                c, yc = x, yx
            else
                a, ya, b, yb = b, yb, x, yx
            end
        elseif x < b
            if yx > yb
                a, ya = x, yx
            else
                c, yc, b, yb = b, yb, x, yx
            end
        end
    end
    return (a, b, c)
end

#definicao da funcao
f = x-> -(0.4/((x^2+1))^(1/2))+((x^2+1)^(1/2))*(1-0.4/(x^2+1))-x    #multiplicada por -1 para encontrar o minimo
g = x-> -(-(0.4/((x^2+1))^(1/2))+((x^2+1)^(1/2))*(1-0.4/(x^2+1))-x) #funcao original

#definiciao do ponto inicial
x0 = 0
#busca do intervalo
a,b,c = bracket_minimum(f,x0)

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

am,bm,cm = quadratic_fit_search(f,a,b,c,n)
#(am,f(am))
#am,bm,cm

println("x* = ", cm)

plot(g,a-1.,c+1.,lw=2,draw_arrow="true",label="Function")
scatter!([a,b,c],[g(a),g(b),g(c)],label="interval")
scatter!([cm],[g(cm)],label="x*")


"""Julia program to Find Noble Integer in an array.
Given an array arr, an integer ele is said to be noble integer if
the number of integers greater than ele is equal to ele.
If such an element is not found Return -1.
"""

function search_noble_integer(arr, n)
    # Traverse the entire array
    for i in 1:(n-1)
        if (arr[i] == arr[i+1])
            continue
        end
        #  If arr[i] equals the number of elements after arr[i], it is a noble Integer
        if(arr[i] == (n - i))
            return arr[i]
        end
    end
    if(arr[n] == 0)
        return arr[n]
    end
    return -1
end

print("How many numbers are present in the array? ")
n = readline()
n = parse(Int, n)
if (n <= 0)
    println("Array is Empty!!!")
    exit()
end
arr = Int[]
print("Enter the numbers: ")
arr = [parse(Int, num) for num in split(readline())]
# Sort the array in ascending order
arr = sort(arr)
res = search_noble_integer(arr, n)
if (res == -1)
    print("A Noble Integer is not present in the array.")
else
    print("A Noble Integer $res is  present in the array.")
end

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

SAMPLE INPUT AND OUTPUT

SAMPLE I

How many numbers are present in the array? 5
Enter the numbers: 12 34 3 1 99
A Noble Integer 3 is  present in the array.

SAMPLE II

How many numbers are present in the array? 5
Enter the numbers: 5 4 3 2 1
A Noble Integer is not present in the array.

"""


module OptimNewton

export get_root;
export optimize;

"""
	Try to find a root of (fun). You need to provide the first derivative of fun (fder) and an initial x0 value.	Additional parameters are used as stop signals.	xtol, ytol: are the tolerances for x and f(x) respectively, if further iteration do not	improve the result in more that this tolerance values, the method returns. itn: is the max number of iterations that the method will do. If verbose is true, the method will print a status message in every iteration.
"""
function get_root(x0, fun, fder; xtol = 10e-10, ytol = 10e-10,
 itn = 10, verbose = false)

	r = x0;
	lastr = r;
	for i in 1:itn
		verbose && print("iteration $i: initial root $r,");
		r = r - fun(r)/fder(r);
		verbose && println(" oprox root $r");
		abs(lastr - r) <= xtol && break;
		lastr = r;
	end

	return r;

end # get_root

"""
	Try to find a optimus value of (fun). You need to provide the first derivative of fun (fder), de secund derivative of fun (sder) and an initial x0 value. Additional parameters are used as stop signals. xtol, ytol: are the tolerances for x and f(x) respectively, if further iteration do not	improve the result in more that this tolerance values, the method return. itn: is the max number of iterations that the method will do. If verbose is true, the method will print a status message in every iteration.
"""
function optimize(x0, fun, fder, sder; xtol = 10e-10, ytol = 10e-10, itn = 10, verbose = false)

	r = x0;
	lastr = r;
	for i in 1:itn
		verbose && print("iteration $i: before value $r,");
		r = r - fder(r)/sder(r);
		verbose && println(" after value $r");
		abs(lastr - r) <= xtol && break;
		abs(fun(r) - fun(lastr)) <= ytol && break;
		lastr = r;
	end

	return r;

end # optimize

end # module


export wignersurmise,unfoldedeigvals ,unfoldedSpacings

"""
```julia
wignersurmise(s ; beta)
```
- `beta` : default `1`,

    `1` for the spacing density corresponds to GOE(2)

    ``p_1(s)=\\frac{\\pi s}{2} e^{-\\pi s^{2} / 4}``

    `2` for the spacing density corresponds to GUE(2)

    ``p_2(s)=\\frac{32 s^{2}}{\\pi^{2}} e^{-4 s^{2} / \\pi}``

-  For more info see [Wigner surmise on Wikipedia](https://en.wikipedia.org/wiki/Wigner_surmise)

# Examples
Evaluate Wigner surmise for GOE at `2`
```julia
wignersurmise(2)

0.1357605281502967
```

Evaluate Wigner surmise for GUE at `2`
```julia
wignersurmise(2,beta=2)

0.07962814352725955
```
"""
function wignersurmise(s::Real; beta = 1 :: Int)
    if beta == 1
        return (pi/2) * s * exp(-pi * s^2 / 4)
    elseif beta == 2
        return 32*(s/pi)^2 * exp(- 4 * s^2 / pi)
    end
end

"""
```julia
unfoldedeigvals(E; deg, goe)
```
- `E` : The array of eigenvalues
- `deg` : default `10`
- `goe` : default `false`
"""
function unfoldedeigvals(E::Array; deg=10::Int, goe = false::Bool)
    if goe
        return length(E)*cdf(Semicircle(2),E)
    end

    return polyfit(E,(x->sum(E.<x)).(E),deg).(E)

end
function unfoldedSpacings(E::Array;deg=10::Int, goe = false::Bool)
    return diff(unfoldedeigvals(E,deg=deg,goe=goe))
end


#!/usr/bin/env julia

"""
# Problem 5: Smallest multiple

2520 is the smallest number that can be divided by each of the numbers from 1
to 10 without any remainder.

What is the smallest positive number that is evenly divisible by all of the
numbers from 1 to `max_num`?
"""
module Problem005

"""
    solve(; max_num::Integer = 20)

Returns the solution for [`Problem005`](@ref) with the given parameters.

## Preconditions
- `max_num > 0`
"""
function solve(; max_num::Integer = 20)
    return lcm((oneunit(max_num):max_num)...)
end

end


using Plots
using FFTW

tan_taylor(x) = x+( x^3/3 )+( x^5*2/15.0 )+( x^7*17/315.0 )+( x^9*62/2835.0 )+( x^11*1382/155925.0 )+( x^13*21844/6081075.0 )+( x^15*929569/638512875.0 );

x = LinRange(-π/2+0.1, π/2-0.1, 1001);

tanx_taylor = tan_taylor.(x);
tanx_native = tan.(x);

delta = tanx_native-tanx_taylor

# Time domain comparison.
plot( 	x, tanx_native,
	 	label = "Julia native tan",
        size = (1024, 720),)
plot!( 	x, tanx_taylor,
	 	label = "8-term Taylor series",)

savefig("figures/tan/time_domain")


@doc """`care(A, B, Q, R)`

Compute 'X', the solution to the continuous-time algebraic Riccati equation,
defined as A'X + XA - (XB)R^-1(B'X) + Q = 0, where R is non-singular.

Algorithm taken from:
Laub, "A Schur Method for Solving Algebraic Riccati Equations."
http://dspace.mit.edu/bitstream/handle/1721.1/1301/R-0859-05666488.pdf
""" ->
function care(A, B, Q, R)
    G = try
        B*inv(R)*B'
    catch
        error("R must be non-singular.")
    end

    Z = [A  -G;
        -Q  -A']

    S = schurfact(Z)
    S = ordschur(S, real(S.values).<0)
    U = S.Z

    (m, n) = size(U)
    U11 = U[1:div(m, 2), 1:div(n,2)]
    U21 = U[div(m,2)+1:m, 1:div(n,2)]
    return U21/U11
end

@doc """`dare(A, B, Q, R)`

Compute `X`, the solution to the discrete-time algebraic Riccati equation,
defined as A'XA - X - (A'XB)(B'XB + R)^-1(B'XA) + Q = 0, where A and R
are non-singular.

Algorithm taken from:
Laub, "A Schur Method for Solving Algebraic Riccati Equations."
http://dspace.mit.edu/bitstream/handle/1721.1/1301/R-0859-05666488.pdf
""" ->
function dare(A, B, Q, R)
    G = try
        B*inv(R)*B'
    catch
        error("R must be non-singular.")
    end

    Ait = try
        inv(A)'
    catch
        error("A must be non-singular.")
    end

    Z = [A + G*Ait*Q   -G*Ait;
         -Ait*Q        Ait]

    S = schurfact(Z)
    S = ordschur(S, abs(S.values).<=1)
    U = S.Z

    (m, n) = size(U)
    U11 = U[1:div(m, 2), 1:div(n,2)]
    U21 = U[div(m,2)+1:m, 1:div(n,2)]
    return U21/U11
end

@doc """`dlyap(A, Q)`

Compute the solution "X" to the discrete Lyapunov equation
"AXA' - X + Q = 0".
""" ->
function dlyap(A, Q)
    lhs = kron(A, conj(A))
    lhs = eye(size(lhs, 1)) - lhs
    x = lhs\reshape(Q, prod(size(Q)), 1)
    return reshape(x, size(Q))
end

@doc """`gram(sys, opt)`

Compute the grammian of system `sys`. If `opt` is `:c`, computes the
controllability grammian. If `opt` is `:o`, computes the observability
grammian.""" ->
function gram(sys::StateSpace, opt::Symbol)
    if !isstable(sys)
        error("gram only valid for stable A")
    end
    func = iscontinuous(sys) ? lyap : dlyap
    if opt == :c
        return func(sys.A, sys.B*sys.B')
    elseif opt == :o
        return func(sys.A', sys.C'*sys.C)
    else
        error("opt must be either :c for controllability grammian, or :o for
                observability grammian")
    end
end

@doc """`obsv(A, C)` or `obsv(sys)`

Compute the observability matrix for the system described by `(A, C)` or `sys`.

Note that checking for observability by computing the rank from `obsv` is
not the most numerically accurate way, a better method is checking if
`gram(sys, :o)` is positive definite.""" ->
function obsv(A, C)
    n = size(A, 1)
    ny = size(C, 1)
    if n != size(C, 2)
        error("C must have the same number of columns as A")
    end
    res = zeros(n*ny, n)
    res[1:ny, :] = C
    for i=1:n-1
        res[(1 + i*ny):(1 + i)*ny, :] = res[((i - 1)*ny + 1):i*ny, :] * A
    end
    return res
end
obsv(sys::StateSpace) = obsv(sys.A, sys.C)

@doc """`ctrb(A, B)` or `ctrb(sys)`

Compute the controllability matrix for the system described by `(A, B)` or
`sys`.

Note that checking for controllability by computing the rank from
`obsv` is not the most numerically accurate way, a better method is
checking if `gram(sys, :c)` is positive definite.""" ->
function ctrb(A, B)
    n = size(A, 1)
    nu = size(B, 2)
    if n != size(B, 1)
        error("B must have the same number of rows as A")
    end
    res = zeros(n, n*nu)
    res[:, 1:nu] = B
    for i=1:n-1
        res[:, (1 + i*nu):(1 + i)*nu] = A * res[:, ((i - 1)*nu + 1):i*nu]
    end
    return res
end
ctrb(sys::StateSpace) = ctrb(sys.A, sys.B)

@doc """`P = covar(sys, W)`

Calculate the stationary covariance `P = E[y(t)y(t)']` of an lti-model `sys`, driven by gaussian
white noise 'w' of covariance `E[w(t)w(τ)]=W*δ(t-τ)` where δ is the dirac delta.

The ouput is if Inf if the system is unstable. Passing white noise directly to
the output will result in infinite covariance in the corresponding outputs
(D*W*D.' .!= 0) for contunuous systems.""" ->
function covar(sys::StateSpace, W::StridedMatrix)
    (A, B, C, D) = (sys.A, sys.B, sys.C, sys.D)
    if size(B,2) != size(W, 1) || size(W, 1) != size(W, 2)
        error("W must be a square matrix the same size as `sys.B` columns")
    end
    if !isstable(sys)
        return fill(Inf,(size(C,1),size(C,1)))
    end
    func = iscontinuous(sys) ? lyap : dlyap
    Q = try
        func(A, B*W*B')
    catch
        error("No solution to the Lyapunov equation was found in covar")
    end
    P = C*Q*C'
    if iscontinuous(sys)
        #Variance and covariance infinite for direct terms
        directNoise = D*W*D'
        for i in 1:size(C,1)
            if directNoise[i,i] != 0
                P[i,:] = Inf
                P[:,i] = Inf
            end
        end
    else
        P += D*W*D'
    end
    return P
end

covar(sys::TransferFunction, W::StridedMatrix) = covar(ss(sys), W)

# Note: the following function returns a Float64 when p=2, and a Tuple{Float64,Float64} when p=Inf.
# This varargout behavior might not be a good idea.
# The H∞ norm computation is probably not as accurate as with SLICOT, but this seems to be still
# reasonably ok as a first step
@doc """
`..  norm(sys[, p; tol=...])`

Compute the H`p`-norm of the LTI system `sys`. `p` can be either `2` or `Inf`
(default is 2). For the H∞ norm, the function returns the pair `(val,fpeak)`,
where `fpeak` (in rad/TimeUnit) is where the gain achieves its peak value `val`.

sys is first converted to a state space model if needed.

`tol` is an optional keyword argument, used currently only for the computation of H∞ norms.
It represents the desired relative accuracy for the computed H∞ norm
(default 1e-12 - not an absolute certificate however).

The H∞ norm computation implements the 'two-step algorithm' in:
N.A. Bruinsma and M. Steinbuch, 'A fast algorithm to compute the H∞-norm
of a transfer function matrix', Systems and Control Letters 14 (1990), pp. 287-293.
""" ->
function Base.norm(sys::StateSpace, p::Real=2; tol=1e-12)
    if p == 2
        return sqrt(trace(covar(sys, eye(size(sys.B, 2)))))
    elseif p == Inf
        if sys.Ts == 0
            return normHinf_twoSteps_ct(sys,tol)
        else
            error("H∞ norm computation not yet implemented for discrete-time systems!")
        end
    else
        error("`p` must be either `2` or `Inf`")
    end
end

function Base.norm(sys::TransferFunction, p::Real=2; tol=1e-12)
    return Base.norm(ss(sys),p)
end

function normHinf_twoSteps_ct(sys::StateSpace,tol=1e-12,maxIters=1000,approximag=1e-10)
    # `maxIters`: the maximum  number of iterations allowed in the algorithm (default 1000)
    # approximag is a tuning parameter: what does it mean for a number to be on the imaginary axis
    # Because of this tuning for example, the relative precision that we provide on the norm computation
    # is not a true guarantee, more an order of magnitude
    # outputs: pair of Float64, namely H∞ norm approximation and frequency fpeak at which it is achieved
    p = pole(sys)
    if any(real(p).>=0)
        # N.B.: more efficient than calling isstable, which would recompute the poles
        return Inf
    else
        # Initialization: computation of a lower bound from 3 terms
        lb = maximum(svdvals(sys.D)); fpeak = Inf
        (lb, idx) = findmax([lb, maximum(svdvals(evalfr(sys,0)))])
        if idx == 2
            fpeak = 0
        end
        p = pole(sys)
        if isreal(p)  # only real poles
            omegap = minimum(abs(p))
        else  # at least one pair of complex poles
            tmp = maximum(abs(imag(p)./(real(p).*abs(p))))
            omegap = abs(p[indmax(tmp)])
        end
        (lb, idx) = findmax([lb, maximum(svdvals(evalfr(sys,omegap*1im)))])
        if idx == 2
            fpeak = omegap
        end

        # Iterations
        iter = 1;
        while iter <= maxIters
            res = (1+2tol)*lb
            R = sys.D'*sys.D - res^2*eye(sys.nu)
            S = sys.D*sys.D' - res^2*eye(sys.ny)
            M = sys.A-sys.B*(R\sys.D')*sys.C
            H = [         M              -res*sys.B*(R\sys.B') ;
                   res*sys.C'*(S\sys.C)            -M'            ]
            omegas = eigvals(H)
            omegaps = imag(omegas[ (abs(real(omegas)).<=approximag) & (imag(omegas).>=0) ])
            if isempty(omegaps)
                return (1+tol)*lb, fpeak
            else  # if not empty, omegaps contains at least two values
                ms = [(x+y)/2 for x=omegaps[1:end-1], y=omegaps[2:end]]
                for mval in ms
                    (lb, idx) = findmax([lb, maximum(svdvals(evalfr(sys,mval*1im)))])
                            if idx == 2
                                fpeak = mval
                            end
                end
            end
            iter += 1
        end
        println("The computation of the H-infinity norm did not converge in $maxIters iterations")
    end
end

@doc """`T, B = balance(A[, perm=true])`

Compute a similarity transform `T` resulting in `B = T\\A*T` such that the row
and column norms of `B` are approximately equivalent. If `perm=false`, the
transformation will only scale, and not permute `A`.""" ->
function balance(A, perm::Bool=true)
    n = Base.LinAlg.checksquare(A)
    B = copy(A)
    job = perm ? 'B' : 'S'
    ilo, ihi, scaling = LAPACK.gebal!(job, B)

    S = diagm(scaling)
    for j = 1:(ilo-1)   S[j,j] = 1 end
    for j = (ihi+1):n   S[j,j] = 1 end

    P = eye(Int, n)
    if perm
        if ilo > 1
            for j = (ilo-1):-1:1 cswap!(j, round(Int, scaling[j]), P) end
        end
        if ihi < n
            for j = (ihi+1):n    cswap!(j, round(Int, scaling[j]), P) end
        end
    end
    return S, P, B
end

function cswap!{T<:Number}(i::Integer, j::Integer, X::StridedMatrix{T})
    for k = 1:size(X,1)
        X[i, k], X[j, k] = X[j, k], X[i, k]
    end
end

"""
`sysr, G = balreal(sys::StateSpace)`

Calculates a balance realization of the system sys, such that the observability and reachability gramians of the balanced system are equal and diagonal `G`

See also `gram`

Glad, Ljung, Reglerteori: Flervariabla och Olinjära metoder
"""
function balreal(sys::StateSpace)
P = gram(sys, :c)
Q = gram(sys, :o)
Q1 = chol(Q)
U,Σ,V = svd(Q1*P*Q1')
Σ = sqrt(Σ)
Σ1 = diagm((sqrt(Σ)))
T = Σ1\(U'Q1)

Pz = T*P*T'
Qz = inv(T')*Q*inv(T)
if vecnorm(Pz-Qz) > sqrt(eps())
    warn("balreal: Result may be inaccurate")
    println("Controllability gramian before transform")
    display(P)
    println("Controllability gramian after transform")
    display(Pz)
    println("Observability gramian before transform")
    display(Q)
    println("Observability gramian after transform")
    display(Qz)
    println("Singular values of PQ")
    display(Σ)
end

sysr = ss(T*sys.A/T, T*sys.B, sys.C/T, sys.D), diagm(Σ)
end


function ABCDQR_kfilter_s(y,u,A,B,C,D,Q,R,x10)
	#
	# STATIONARY Kalman filter for model
	#
	# x_{t+1} = A*x_{t} + B*u_{t} + w_{t}
	# y_{t}   = C*x_{t} + D*u_{t} + v_{t}
	#
	# cov(w_{t},v_{t}) = [Q 0;0 R]
	#
	# javier.cara@upm.es, 2015-10
	#

	(ny,nt) = size(y)
	nx = size(A,1)

	# allocation
	xtt = zeros(nx,nt)
	xtt1 = zeros(nx,nt+1)
	et = zeros(ny,nt)
	loglik = 0.0

	# Discrete-time Algebraic Riccati Equation (DARE) solution
	S = zeros(nx,ny)
	Ptt1 = dare(A',C',Q,R,S)

	# variance of innovations
	St = C*Ptt1*C' + R
	Stinv = inv(St)

	# Kalman gain matrix
	Kt = Ptt1*C'*Stinv

	# Var[x_t|y_t]
	Ptt = (Matrix(1.0I,nx,nx)-Kt*C)*Ptt1

	# Filter
	xtt1[:,1] = x10
	for t in 1:nt
		#  innovations
		et[:,t] = y[:,t] - C*xtt1[:,t] - D*u[:,t]

		# filtered values
		xtt[:,t] = xtt1[:,t] + Kt*et[:,t]

		# one-step ahead prediction
		xtt1[:,t+1] = A*xtt[:,t] + B*u[:,t]

		# likelihood
		l0 = et[:,t]'*Stinv*et[:,t] # typeof(l0) = Array{Float64,1}
		loglik = loglik + l0[1]
	end

	loglik = - ny*nt/2*log(2*pi) - nt/2*log(det(St)) - 1/2*loglik

	return xtt,Ptt,xtt1,Ptt1,et,St,Kt,loglik

end


export
    sqrtmod,
    multiplicativeorder,
    linearmod,
    primitiveroot

Nemo.sqrtmod(x::Integer, m::T) where T<:Integer = convert(T, sqrtmod(fmpz(x), fmpz(m)))

#=
http://rosettacode.org/wiki/Multiplicative_order

One possible algorithm that is efficient also for large numbers is the
following: By the Chinese Remainder Theorem, it's enough to calculate the
multiplicative order for each prime exponent p^k of m, and combine the
results with the least common multiple operation. Now the order of a wrt.
to p^k must divide Φ(p^k). Call this number t, and determine it's factors
q^e. Since each multiple of the order will also yield 1 when used as
exponent for a, it's enough to find the least d such that (q^d)*(t/(q^e))
yields 1 when used as exponent.
=#
multiplicativeorder(a::Integer, m::Integer) = multiplicativeorder(promote(a, m)...)
multiplicativeorder(a::T, m::T) where {T<:Integer} = begin
    iscoprime(a, m) || error("$a and $m are not coprime")

    res = one(m)
    for (p, k) in factor(m)
        m = p^k
        t = (m ÷ p) * (p-1)
        for f in factors(t)
            if powermod(a, f, m) == 1
                res = lcm(res, f)
                break
            end
        end
    end
    res
end

##  Solve linear modulo equation a * x = b mod m
# https://github.com/hwborchers/Numbers.jl/blob/master/src/primes.jl
linearmod(a::Integer, b::Integer, m::Integer) = linearmod(promote(a, b, m)...)
linearmod(a::T, b::T, m::T) where {T<:Integer} = begin
    m > 1 || error("Argument 'm' must be an integer greater 1")

    g, u, v = gcdx(a, m)
    mod(b, g) == 0 || return T[]

    x0 = mod(u * (b ÷ g), m)
    [mod(x0 + i * (m ÷ g), m) for i in 0:(g-1)]
end

##  Find a primitive root modulo m
# https://github.com/hwborchers/Numbers.jl/blob/master/src/primes.jl
primitiveroot(m::T) where {T<:Integer} = begin
    isprime(m) || error("Argument 'm' must be a prime number")
    m == 2 && return one(T)

    P = primefactors(m-1)
    for r in 2:(m-1)
        not_found = true
        for p in P
            if powermod(r, (m-1) ÷ p, m) == 1
                not_found = false
                break
            end
        end
        not_found && return r
    end
    zero(T)
end


"""
Assumes x,y points go around the polygon in one direction.
Credits: https://rosettacode.org/wiki/Shoelace_formula_for_polygonal_area#Julia
"""
shoelacearea(x, y) =
    abs(sum(i * j for (i, j) in zip(x, append!(y[2:end], y[1]))) -
        sum(i * j for (i, j) in zip(append!(x[2:end], x[1]), y))) / 2


"""

  Construct the sylvester matrix of degree k from polynomials f and g = f'.
  The matrix has 2k+1 columns. Each column contains a shifted version of f or g.
  Order of columns: k+1 * g, k * f
"""
function sylves1(f::AbstractVector{T}, g::AbstractVector{T}, k::Int) where {T<:Number}
#
#  The k-th sylvester matrix of f(x), f'(x) = g(x)
#
    l = length(f)
    m = l + k - 1
    n = 2 * k + 1
    A = zeros(T, m, n)
	for j = 1:k+1
        A[j:j+l-2,j] = g
    end
    for j = 1:k
        A[j:j+l-1,j+k+1] = f
    end
    A
end

"""
Extract result polynomials u and v if x is a solution of sylves1(f,g) * x == 0.
  u * g + v * f == 0, degree of v is k.
"""
function extract_sylves1(x::AbstractVector{T}) where {T<:Number}
  k = length(x)
  k > 2 && k % 2 == 1 || error("vector too short or not odd length")
  k = (k + 1) ÷ 2
  x[1:k] / x[1], x[k+1:end] / x[k+1]  # u, v
end

"""

  Construct the sylvester matrix of degree k from polynomials f and g = f'.
  The matrix has 2k+1 columns. Each column contains a shifted version of f or g.
  Order of columns: g, k * [f g]
"""
function sylves2(f::AbstractVector{T}, g::AbstractVector{T}, k::Int) where {T<:Number}
#
#  The k-th sylvester matrix of f(x), f'(x) = g(x)
#
    l = length(f)
    m = l + k - 1
    n = 2 * k + 1
    A = zeros(T, m, n)
	for j = 1:k+1
        A[j:j+l-2,j*2-1] = g
    end
    for j = 1:k
        A[j:j+l-1,j*2] = f
    end
    A
end

"""
Extract result polynomials u and v if x is a solution of sylves2(f,g) * x == 0.
  u * g + v * f == 0, degree of v is k.
"""
function extract_sylves2(x::AbstractVector{T}) where {T<:Number}
  k = length(x)
  k > 2 && k % 2 == 1 || error("vector too short or not odd length")
  x[1:2:k] / x[1], x[2:2:k] / x[2]  # u, v
end


function scalematrix(s::Vec{3, T}) where T
    T0, T1 = zero(T), one(T)
    Mat{4}(
        s[1],T0,  T0,  T0,
        T0,  s[2],T0,  T0,
        T0,  T0,  s[3],T0,
        T0,  T0,  T0,  T1,
    )
end

translationmatrix_x(x::T) where {T} = translationmatrix(Vec{3, T}(x, 0, 0))
translationmatrix_y(y::T) where {T} = translationmatrix(Vec{3, T}(0, y, 0))
translationmatrix_z(z::T) where {T} = translationmatrix(Vec{3, T}(0, 0, z))

function translationmatrix(t::Vec{3, T}) where T
    T0, T1 = zero(T), one(T)
    Mat{4}(
        T1,  T0,  T0,  T0,
        T0,  T1,  T0,  T0,
        T0,  T0,  T1,  T0,
        t[1],t[2],t[3],T1,
    )
end

rotate(angle, axis::Vec{3}) = rotationmatrix4(qrotation(convert(Array, axis), angle))
rotate(::Type{T}, angle::Number, axis::Vec{3}) where {T} = rotate(T(angle), convert(Vec{3, T}, axis))

function rotationmatrix_x(angle::Number)
    T0, T1 = (0, 1)
    Mat{4}(
        T1, T0, T0, T0,
        T0, cos(angle), sin(angle), T0,
        T0, -sin(angle), cos(angle),  T0,
        T0, T0, T0, T1
    )
end
function rotationmatrix_y(angle::Number)
    T0, T1 = (0, 1)
    Mat{4}(
        cos(angle), T0, -sin(angle),  T0,
        T0, T1, T0, T0,
        sin(angle), T0, cos(angle), T0,
        T0, T0, T0, T1
    )
end
function rotationmatrix_z(angle::Number)
    T0, T1 = (0, 1)
    Mat{4}(
        cos(angle), sin(angle), T0, T0,
        -sin(angle), cos(angle),  T0, T0,
        T0, T0, T1, T0,
        T0, T0, T0, T1
    )
end

"""
    Create view frustum

    Parameters
    ----------
        left : float
         Left coordinate of the field of view.
        right : float
         Left coordinate of the field of view.
        bottom : float
         Bottom coordinate of the field of view.
        top : float
         Top coordinate of the field of view.
        znear : float
         Near coordinate of the field of view.
        zfar : float
         Far coordinate of the field of view.

    Returns
    -------
        M : array
         View frustum matrix (4x4).
"""
function frustum(left::T, right::T, bottom::T, top::T, znear::T, zfar::T) where T
    (right == left || bottom == top || znear == zfar) && return Mat{4,4,T}(I)
    T0, T1, T2 = zero(T), one(T), T(2)
    return Mat{4}(
        T2 * znear / (right - left), T0, T0, T0,
        T0, T2 * znear / (top - bottom), T0, T0,
        (right + left) / (right - left), (top + bottom) / (top - bottom), -(zfar + znear) / (zfar - znear), -T1,
        T0, T0, (-T2 * znear * zfar) / (zfar - znear), T0
    )
end

"""
`proj = perspectiveprojection([T], fovy, aspect, znear, zfar)` defines
a projection matrix with a given angular field-of-view `fovy` along
the y-axis (measured in degrees), the specified `aspect` ratio, and
near and far clipping planes `znear`, `zfar`. Optionally specify the
element type `T` of the matrix.
"""
function perspectiveprojection(fovy::T, aspect::T, znear::T, zfar::T) where T
    (znear == zfar) && error("znear ($znear) must be different from tfar ($zfar)")
    h = T(tan(fovy / 360.0 * pi) * znear)
    w = T(h * aspect)
    return frustum(-w, w, -h, h, znear, zfar)
end

function perspectiveprojection(
        ::Type{T}, fovy::Number, aspect::Number, znear::Number, zfar::Number
    ) where T
    perspectiveprojection(T(fovy), T(aspect), T(znear), T(zfar))
end
"""
`proj = perspectiveprojection([T], rect, fov, near, far)` defines the
projection ratio in terms of the rectangular view size `rect` rather
than the aspect ratio.
"""
function perspectiveprojection(wh::Rect2D, fov::T, near::T, far::T) where T
    perspectiveprojection(fov, T(wh.w/wh.h), near, far)
end

function perspectiveprojection(
        ::Type{T}, wh::Rect2D, fov::Number, near::Number, far::Number
    ) where T
    perspectiveprojection(T(fov), T(wh.w/wh.h), T(near), T(far))
end

"""
`view = lookat(eyeposition, lookat, up)` creates a view matrix with
the eye located at `eyeposition` and looking at position `lookat`,
with the top of the window corresponding to the direction `up`. Only
the component of `up` that is perpendicular to the vector pointing
from `eyeposition` to `lookat` will be used.  All inputs must be
supplied as 3-vectors.
"""
function lookat(eyePos::Vec{3, T}, lookAt::Vec{3, T}, up::Vec{3, T}) where T
    zaxis  = normalize(eyePos-lookAt)
    xaxis  = normalize(cross(up,    zaxis))
    yaxis  = normalize(cross(zaxis, xaxis))
    T0, T1 = zero(T), one(T)
    return Mat{4}(
        xaxis[1], yaxis[1], zaxis[1], T0,
        xaxis[2], yaxis[2], zaxis[2], T0,
        xaxis[3], yaxis[3], zaxis[3], T0,
        T0,       T0,       T0,       T1
    ) * translationmatrix(-eyePos)
end

function lookat(::Type{T}, eyePos::Vec{3}, lookAt::Vec{3}, up::Vec{3}) where T
    lookat(Vec{3,T}(eyePos), Vec{3,T}(lookAt), Vec{3,T}(up))
end

function orthographicprojection(wh::Rect2D, near::T, far::T) where T
    orthographicprojection(zero(T), T(wh.w), zero(T), T(wh.h), near, far)
end

function orthographicprojection(
        ::Type{T}, wh::Rect2D, near::Number, far::Number
    ) where T
    orthographicprojection(wh, T(near), T(far))
end

function orthographicprojection(
        left  ::T, right::T,
        bottom::T, top  ::T,
        znear ::T, zfar ::T
    ) where T
    (right==left || bottom==top || znear==zfar) && return Mat{4,4,T}(I)
    T0, T1, T2 = zero(T), one(T), T(2)
    Mat{4}(
        T2/(right-left), T0, T0,  T0,
        T0, T2/(top-bottom), T0,  T0,
        T0, T0, -T2/(zfar-znear), T0,
        -(right+left)/(right-left), -(top+bottom)/(top-bottom), -(zfar+znear)/(zfar-znear), T1
    )
end

function orthographicprojection(::Type{T},
        left  ::Number, right::Number,
        bottom::Number, top  ::Number,
        znear ::Number, zfar ::Number
    ) where T
    orthographicprojection(
        T(left),   T(right),
        T(bottom), T(top),
        T(znear),  T(zfar)
    )
end

mutable struct Pivot{T}
    origin      ::Vec{3, T}
    xaxis       ::Vec{3, T}
    yaxis       ::Vec{3, T}
    zaxis       ::Vec{3, T}
    rotation    ::Quaternion
    translation ::Vec{3, T}
    scale       ::Vec{3, T}
end

GeometryBasics.origin(p::Pivot) = p.origin

rotationmatrix4(q::Quaternion{T}) where {T} = Mat4{T}(q)

function transformationmatrix(p::Pivot)
    translationmatrix(p.origin) * #go to origin
    rotationmatrix4(p.rotation) * #apply rotation
    translationmatrix(-p.origin)* # go back to origin
    translationmatrix(p.translation) #apply translation
end

function transformationmatrix(translation, scale)
    T = eltype(translation)
    T0, T1 = zero(T), one(T)
    Mat{4}(
        scale[1],T0,  T0,  T0,
        T0,  scale[2],T0,  T0,
        T0,  T0,  scale[3],T0,
        translation[1],translation[2],translation[3], T1
    )
end

function transformationmatrix(translation, scale, rotation::Quaternion)
    trans_scale = transformationmatrix(translation, scale)
    rotation = Mat4f0(rotation)
    trans_scale*rotation
end

function transformationmatrix(
        translation::Vec{3}, scale::Vec{3}, rotation::Quaternion,
        align, flip::NTuple{3, Bool}, boundingbox::Nothing
    )
    return transformationmatrix(translation, scale, rotation)
end

function transformationmatrix(
        translation::Vec{3}, scale::Vec{3}, rotation::Quaternion,
        align, flip::NTuple{3, Bool}, boundingbox::Rect3D
    )
    full_width = widths(boundingbox)
    mini = minimum(boundingbox)
    half_width = full_width ./ 2
    to_origin = (half_width + mini)
    if isnan(to_origin)
        to_origin = Vec3f0(0)
    end
    align_middle = translationmatrix(-to_origin)
    align_back = translationmatrix(to_origin)
    flipsign = map(x-> ifelse(x, -1f0, 1f0), Vec{3}(flip))
    flipped = align_back * scalematrix(flipsign) * align_middle
    aligned = flipped #translationmatrix(align .* full_width) * flipped
    trans_scale = transformationmatrix(translation, scale)
    rotation = Mat4f0(rotation)
    aligned * trans_scale * rotation
end

function transformationmatrix(
        translation, scale, rotation::Vec{3,T}, up = Vec{3,T}(0,0,1)
    ) where T
    q = rotation(rotation, up)
    transformationmatrix(translation, scale, q)
end

#Calculate rotation between two vectors
function rotation(u::Vec{3, T}, v::Vec{3, T}) where T
    # It is important that the inputs are of equal length when
    # calculating the half-way vector.
    u, v = normalize(u), normalize(v)
    # Unfortunately, we have to check for when u == -v, as u + v
    # in this case will be (0, 0, 0), which cannot be normalized.
    if (u == -v)
        # 180 degree rotation around any orthogonal vector
        other = (abs(dot(u, Vec{3, T}(1,0,0))) < 1.0) ? Vec{3, T}(1,0,0) : Vec{3, T}(0,1,0)
        return qrotation(normalize(cross(u, other)), T(180))
    end
    half = normalize(u+v)
    return Quaternion(cross(u, half)..., dot(u, half))
end

function to_world(scene::Scene, point::T) where T <: StaticVector
    cam = scene.camera
    x = to_world(
        point,
        inv(transformationmatrix(scene)[]) *
        inv(cam.view[]) *
        inv(cam.projection[]),
        T(widths(pixelarea(scene)[]))
    )
    Point2f0(x[1], x[2])
end

w_component(x::Point) = 1.0
w_component(x::Vec) = 0.0
function to_world(
        p::StaticVector{N, T},
        prj_view_inv::Mat4,
        cam_res::StaticVector
    ) where {N, T}
    VT = typeof(p)
    clip_space = ((VT(p) ./ VT(cam_res)) .* T(2)) .- T(1)
    pix_space = Vec{4, T}(
        clip_space[1],
        clip_space[2],
        T(0), w_component(p)
    )
    ws = prj_view_inv * pix_space
    ws ./ ws[4]
end

function to_world(
        p::Vec{N, T},
        prj_view_inv::Mat4,
        cam_res::StaticVector
    ) where {N, T}
    to_world(Point(p), prj_view_inv, cam_res) .-
        to_world(zeros(Point{N, T}), prj_view_inv, cam_res)
end

function project(scene::Scene, point::T) where T<:StaticVector
    cam = scene.camera
    project(
        cam.projection[] *
        cam.view[] *
        transformationmatrix(scene)[],
        Vec2f0(widths(pixelarea(scene)[])), point
    )
end

function project(matrix::Mat4f0, p::T, dim4 = 1.0) where T <: VecTypes
    p = to_ndim(Vec4f0, to_ndim(Vec3f0, p, 0.0), dim4)
    p = matrix * p
    to_ndim(T, p, 0.0)
end

function project(proj_view::Mat4f0, resolution::Vec2, point::Point)
    p4d = to_ndim(Vec4f0, to_ndim(Vec3f0, point, 0f0), 1f0)
    clip = proj_view * p4d
    p = (clip / clip[4])[Vec(1, 2)]
    p = Vec2f0(p[1], p[2])
    return (((p .+ 1f0) / 2f0) .* (resolution .- 1f0)) .+ 1f0
end

function project_point2(mat4::Mat4, point2::Point2)
    Point2f0(mat4 * to_ndim(Point4f0, to_ndim(Point3f0, point2, 0), 1))
end

function transform(model::Mat4, x::T) where T
    x4d = to_ndim(Vec4f0, x, 0.0)
    to_ndim(T, model * x4d, 0.0)
end


# SIR_v2.jl
# ===============================
using DrWatson
@quickactivate "NonlinearDynamicsTextbook"
include(srcdir("style.jl"))
#using DynamicalSystems
using PyPlot, OrdinaryDiffEq
using StaticArrays

function SIR_ODE(u,p,t)
    beta, gamma, N = p
    S, I, R = u
    dS =  - beta * I * S / N
    dI =  beta * I * S / N  - gamma * I
    dR = gamma * I
    return SVector(dS, dI, dR)
end

N = 10000
beta = 0.3
gamma = 0.1
p = beta, gamma, N

u0 = SVector(N-1., 1., 0.)

tspan = (0., 120)

prob = ODEProblem(SIR_ODE, u0, tspan, p)
sol = solve(prob, Tsit5(), reltol=1e-8, abstol=1e-8,tsamp = 0.1)

tvec = sol.t
Svec = [u[1] for u in sol.u]
Ivec = [u[2] for u in sol.u]
Rvec = [u[3] for u in sol.u]

# plot
# ----
fig = figure("SIR",figsize=(0.5*figx,figy))

plot(tvec,Svec/N, c = COLORS[1] )
plot(tvec,Rvec/N, c = COLORS[3] )
plot(tvec,Ivec/N, c = COLORS[2] )

xlabel(L"$t$")
ax = gca()
ax.set_xlim([0.,120.])
# ax.set_ylim([-2.1,2.5])
ax.legend([L"S/N"; L"I/N"; L"R/N"], fontsize = 25)
fig.tight_layout(pad=0.3)

# save
# ----
# dname = "/Users/parlitz/ownCloud/NonlinearDynamicsTextbook/figure_generation/10/SIR/"
# fname = "SIR_model"
# ftype = ".png"

wsave(plotsdir("10", "SIR_model"), fig)


#--------------------------------------------------------------------
# Spacetime Discretization methods in Julia
# Soham 01-2018
# Test summation-by-parts property for our derivative operator
# See Summation by Parts, Projections, and Stability. I Olsson 1995
#--------------------------------------------------------------------

D  = [-1.0  1.0  0.0  0.0;
      -0.5  0.0  0.5  0.0;
       0.0 -0.5  0.0  0.5;
       0.0  0.0 -1.0  1.0]

W  = [ 0.5  0.0  0.0  0.0;
       0.0  1.0  0.0  0.0;
       0.0  0.0  1.0  0.0;
       0.0  0.0  0.0  0.5]

B  = [-1.0  0.0  0.0  0.0;
       0.0  0.0  0.0  0.0;
       0.0  0.0  0.0  0.0;
       0.0  0.0  0.0  1.0]

u = [x^2 for x in linspace(-1, 1, 4)]
v = [x^3 for x in linspace(-1, 1, 4)]
@test W*D + (W*D)' == B
@test u'*W*D*v + u'*(W*D)'*v == u'*B*v

# Now test the spectral discretization operators
@testset "Summation-by-Parts" begin
    @testset "N = $N" for N in 1:6
        D = Float64[chebd(i,j, N) for i in 1:N+1, j in 1:N+1]
        W = diagm(Float64[chebw(i, N) for i in 1:N+1])
        B = zeros(N+1, N+1)
        B[1,1] = 1
        B[end, end] = -1
        u = [x^2 for x in chebgrid(N)]
        v = [x^3 for x in chebgrid(N)]
        @test u'*W*D*v + u'*(W*D)'*v ≈ u'*B*v atol=1e-15
        @test_broken (W*D) + (W*D)' ≈ B atol=1e-15
    end
end;


# This file includes the theoretical functions of the ber performance cureves.

export berask, berpam, berpsk, berfsk, berqam, plotber

"""
    $SIGNATURES

Returns the probability of symbol error for the snr per bit `γb` and constallation size `M` for ASK and PAM signalling
"""
berask(γb, M) = 2 * (1 - 1 / M) * Q(sqrt(6log2(M) / (M^2 - 1) * dbtoval(γb)))
berpam = berask

"""
    $SIGNATURES

Returns the probability of symbol error for the snr per bit `γb` and constallation size `M` for PSK signalling
"""
berpsk(γs, M) = 1 - quadgk(θ -> pθ(θ, dbtoval(γs)), -π/M, π/M)[1]
pθ(θ, γs) = 1 / (2π) * exp(-γs * sin(θ) * sin(θ)) * quadgk(υ -> integrand(υ, γs, θ), 0, Inf)[1]
integrand(υ, γs, θ) = υ * exp(-1 / 2 * (υ - sqrt(2 * γs) * cos(θ))^2)

"""
    $SIGNATURES

Returns the probability of symbol error for the snr per bit `γb` and constallation size `M` for FSK signalling
"""
function berfsk(γs, M)
    k = log2(M)
    pe = 1 / sqrt(2π) * quadgk(x -> (1 - (1  - Q(x))^(M - 1)) * exp(-1/2 * (x - sqrt(2 * dbtoval(γs)))^2), -Inf, Inf)[1]
    # # Return probability of symbol error, not probability of bits error
    # pb = 2^(k -1) / (2^k - 1) * pe
end

"""
    $SIGNATURES

Returns the probability of symbol error for the snr per bit `γb` and constallation size `M` for QAM signalling
"""
function berqam(γb, M)
    qval = Q(√(3log2(M) / (M - 1) * dbtoval(γb)))
    4 * (1 - 1 / √(M)) * qval * (1 - (1 - 1 / √(M)) * qval)
end

#------------------------ Plots ---------------------------------------
"""
    $SIGNATURES

Plots the probability of symbol error versus snr per bit for the signaling `scheme`. `snr_per_bit_range` is the snr per bit range and `krange` is the symbol size of the M-ary signalling where ``M=2^k``.
"""
function plotber(;scheme="ASK", snr_per_bit_range=collect(-4 : 1 : 20), krange = 1 : 4,
    pltkwargs...)
    # Extract the ber function
    schemes = ["ASK", "PSK", "QAM", "FSK"]
    scheme in schemes || throw(ArgumentError("Expected `$schemes`, got `$scheme` instead"))
    @eval pe_vs_snr = $(Symbol("ber" * lowercase(scheme)))

    # Plot
    plt = plot(xlabel="γb", ylabel="Pe", title="M-$(scheme)", pltkwargs...)
    for k in krange
        M = 2^k
        γs = k * dbtoval.(snr_per_bit_range)
        plot!(snr_per_bit_range,  pe_vs_snr.(γs, M), yscale=:log10, label="M=$M")
    end
    plt
end


# The sum of the squares of the first ten natural numbers is,
#
# 1^2 + 2^2 + ... + 10^2 = 385
#
# The square of the sum of the first ten natural numbers is,
#
# (1 + 2 + ... + 10)^2 = 552 = 3025
#
# Hence the difference between the sum of the squares of the first ten natural
# numbers and the square of the sum is 3025 − 385 = 2640.
#
# Find the difference between the sum of the squares of the first one hundred
# natural numbers and the square of the sum.

using ProjectEulerSolutions

# Brute force solution, using Julia built-ins.
function p006solution_sequence(n::Integer=1)::Integer
    seq = 1:n
    return sum(seq)^2 - sum(seq.^2)
end

# Closed form solution for the sum of a sequence and the sum of a sequence of
# squares.
function p006solution_closedform(n::Integer=1)::Integer
    return (n*(n+1)/2)^2 - (n*(n+1)*(2n+1)/6)
end

p006 = Problems.Problem(Dict("Closed form" => p006solution_closedform,
                             "Sequence" => p006solution_sequence))

Problems.benchmark(p006, 100)


#=
Problem 3
The prime factors of 13195 are 5, 7, 13 and 29.
What is the largest prime factor of the number 600851475143 ?
=#

import Primes.factor

target = 600851475143
factors = collect(keys(factor(target)))

maxFact = maximum(factors)

println("The greatest prime factor is: $(maxFact)")


#numerical 1d integration

#integrate func below
f(x) =sin(x)
#over
xmin = -π
xmax = π
#with number of divisions (linear)
n=1000

#Trapeziod rule
Δ = (xmax - xmin)/n
xseq = range(xmin,xmax,length = n+1)
s_trapeziod = Δ/2 *(sum(f.(xseq))+sum(f.(xseq[2:end-1])))

#error of trapeziod estimation for constinuous second differentiable f
#e_trapeziod = (xmax - xmin)^3/12*n^2 * max(abs(f))
# see this slide for more info
# https://slideplayer.com/slide/6419579/

#Mid-point rule
s_Midp = sum(f.(xseq[2:end] - xseq[1:end-1]))*Δ

# n has to be even, t.s., n+1 is odd, there are n+1 terms to play with
iseven(n)

s_Simpson = Δ/3 * sum([[1]; repeat([4; 2; 4; 1], Int(n/4))] .* f.(xseq))


#===============================================================================

The palindromic number 595 is interesting because it can be written as the sum
of consecutive squares: 6^2 + 7^2 + 8^2 + 9^2 + 10^2 + 11^2 + 12^2.

There are exactly eleven palindromes below one-thousand that can be written as
consecutive square sums, and the sum of these palindromes is 4164. Note that 1 =
0^2 + 1^2 has not been included as this problem is concerned with the squares of
positive integers.

Find the sum of all the numbers less than 108 that are both palindromic and can
be written as the sum of consecutive squares.

--------------------------------------------------------------------------------

The solution is easy: we generate all i^2 + ... + j^2 below the limit and we
check that the number obtained is smaller than the limit.

Trap: a same number can be the sum of different sums of squares...

===============================================================================#

include("Euler.jl")

function euler(limit)
    i = 1
    ns = Set{Int}()

    while true
        n = i^2
        if limit <= n
            break
        end

        j = i+1
        n += j^2

        while n < limit
            if is_palindromic(n)
                push!(ns, n)
            end
            j += 1
            n += j^2
        end

        i += 1
    end

    sum(ns)
end

println("10^3: $(euler(10^3))")
println("10^8: $(euler(10^8))")


### A Pluto.jl notebook ###
# v0.15.1

using Markdown
using InteractiveUtils

# ╔═╡ 7f621598-8ce7-11eb-1d78-89ab395c5aac
using MultivariateStats

# ╔═╡ 1e960125-0783-4932-9b98-7b09c72bc951
md"[Reference Chapter](https://www.deeplearningbook.org/contents/linear_algebra.html)"

# ╔═╡ 811b4842-775b-11eb-3690-830857abe1b5
md"### Eigenvectors"

# ╔═╡ ec3044cc-7768-11eb-17ff-c19d9ac19ae4
md"
Cosider a matrix of order ``n × n``

$A = \begin{bmatrix}a_{11} & a_{12} & a_{13} & a_{14} & ... & a_{1n}\\a_{21} & a_{22} & a_{23} & a_{24} & ... & a_{2n}\\a_{31} & a_{32} & a_{33} & a_{34} & ... & a_{3n}\\a_{41} & a_{42} & a_{43} & a_{44} & ... & a_{4n}\\\vdots & \vdots & \vdots & \vdots & \ddots & \vdots\\a_{n1} & a_{n2} & a_{n3} & a_{n4} & ... & a_{nn}\end{bmatrix}$

When we multiply it by a vector of length `` n ``...

$B = \begin{bmatrix} b_1 \\ b_2 \\ b_3 \\ b_4 \\\vdots \\ b_n \end{bmatrix}$

`` C = AB ``

Therefore, ``C = \begin{bmatrix}a_{11} & a_{12} & a_{13} & a_{14} & ... & a_{1n}\\a_{21} & a_{22} & a_{23} & a_{24} & ... & a_{2n}\\a_{31} & a_{32} & a_{33} & a_{34} & ... & a_{3n}\\a_{41} & a_{42} & a_{43} & a_{44} & ... & a_{4n}\\\vdots & \vdots & \vdots & \vdots & \ddots & \vdots\\a_{n1} & a_{n2} & a_{n3} & a_{n4} & ... & a_{nn}\end{bmatrix}$ $\begin{bmatrix} b_1 \\ b_2 \\ b_3 \\ b_4 \\\vdots \\ b_n \end{bmatrix}``

which can be rewritten as,

$C = \begin{bmatrix} a_{11} \times b_1 + a_{12} \times b_2 + a_{13} \times b_3 + a_{14} \times b_4 ... a_{1n} \times b_n \\ a_{21} \times b_1 + a_{22} \times b_2 + a_{23} \times b_3 + a_{24} \times b_4 ... a_{2n} \times b_n \\ a_{31} \times b_1 + a_{32} \times b_2 + a_{33} \times b_3 + a_{34} \times b_4 ... a_{3n} \times b_n \\ a_{41} \times b_1 + a_{42} \times b_2 + a_{43} \times b_3 + a_{44} \times b_4 ... a_{4n} \times b_n \\\vdots \\ a_{n1} \times b_1 + a_{n2} \times b_2 + a_{n3} \times b_3 + a_{n4} \times b_4 ... a_{nn} \times b_n \end{bmatrix}$

If there exists a scalar `` \lambda ``, such that `` B \times \lambda = C ``, then `` B `` is the eigenvector of `` A ``, and `` \lambda `` is its eigenvalue.
"

# ╔═╡ dd42dfa8-7aa9-11eb-3312-fbed21eb12d5
md"## Eigendecomposition"

# ╔═╡ ec9fae4a-7aa9-11eb-10a9-bfed672dfaca
md"If a matrix 𝐴 has 𝑛 linearly independent eigenvectors and 𝑛 corresponding eigenvalues, then if we make a matrix out of the eigenvectors, and we call it 𝑉, where every column in 𝑉 is an eigenvector of 𝐴, and make a (column)vector 𝜆 which has eigenvalues, corresponding to eigenvectors of 𝐴, then we can say that :

Eigendecomposition of 𝐴 = 𝑉_diag_(𝜆)𝑉$^{-1}$
"

# ╔═╡ 6dca1270-7aab-11eb-23a6-c5db84f32eb5
let
	import LinearAlgebra
	A = rand(1:10,3,3)
	F = LinearAlgebra.eigen(A)
	A, F
end

# ╔═╡ 5b37c0c6-7ab5-11eb-2fe2-4fefc5a9e193
md"""
In the above code, we find the eigendecomposition of 𝐴, and store it in 𝐹, where `F.vectors` give us all the eigenvalues(or the matrix 𝑉 in the above context), and `F.values` stores the corresponding eigenvalues(the vector 𝜆 in the above context).
"""

# ╔═╡ 8f59c323-4326-4c5a-b013-6e0cfa442e02
md"""
!!! note "Some terms"

	If a matrix's eigenvalues are all positive, its called _**positive definite**_.

	If a matrix's eigenvalues are all positive or zero, its called _**positive semidefinite**_.

	If a matrix's eigenvalues are all negative, its called _**negative definite**_.

	If a matrix's eigenvalues are all negative or zero, its called _**negative semidefinite**_.

	In a **_positive semidefinite_** matrix, _for all x, **xᵀ𝐴x ≥ 𝟢**_.

	In a **_positive semidefinite_** matrix, _**xᵀ𝐴x = 𝟢**, implies that,_ **_x = 𝟢_**.
"""

# ╔═╡ 11a7a0f0-7ad8-11eb-12c7-77f971d32a1a
md"## Singular Value Decomposition"

# ╔═╡ 2c663d90-7ad8-11eb-0e6c-039ecc187c0d
md"""
There's another method of decomposing (or factorising) a matrix into sigular vectors and singular values called Singular Value Decomposition.
Every real matrix has a singular value decomposition, but not every matrix has can be decomposed into eigenvalues. For example, matrices which are not square.

#### What is it?
We can write a matrix 𝐴 as a product of 𝟥 matrices --

`` 𝐴 = 𝑈𝐷𝑉^T ``

1. 𝐴 and 𝐷, both have dimensions `` m × n ``, 𝑈 has dimensions `` m × m ``, and 𝑉 has dimensions `` n × n ``.
2. 𝑈 and 𝑉 are both orthogonal matrices.
3. 𝐷 is a diagonal matrix and is not necessarily square.
4. The diagonal elements of 𝐷, also known as singular values of matrix 𝐴, are the are the square roots of eigenvalues of 𝐴ᵀ𝐴 or 𝐴𝐴ᵀ(eigenvalues of both are equal).
5. The columns of 𝑈, also known as left-singular vectors of 𝐴, are the eigenvectors of 𝐴𝐴ᵀ.
6. The columns of 𝑉, also known as right-singular vectors of 𝐴, are the eigenvectors of 𝐴ᵀ𝐴.
"""

# ╔═╡ bdef0196-7b91-11eb-2d26-7b02fa67233c
let
	A = convert(Matrix,rand(0:9, 3,3))
	LinearAlgebra.svd(A)
end

# ╔═╡ d0fd5526-7d00-11eb-113b-47a224413bac
md"## Trace operator
A simple demonstration would be easier to understand."

# ╔═╡ 22247216-7ddf-11eb-24da-65455796685f
let
	A = convert(Matrix,rand(0:9, 3,3))
	A, LinearAlgebra.tr(A)
end

# ╔═╡ 052770a6-787d-11eb-1816-11fc224a68bb
md"It gives the sum of diagonal elements."

# ╔═╡ 0085a0f2-7ded-11eb-18c3-8b40f3a71ab9
md"## Moore Penrose Pseudo-Inverse
Assume there exists a matrix 𝐴, and its left-inverse 𝐵. Such that they satisfy,

`` 𝐴𝑥 = 𝑦 ``

`` 𝑥 = 𝐵𝑦  ``

The pseudo-inverse of 𝐴 is $A^+$, where $A^+$ given by the matrix,

`` 𝐴^+ = VD^+U^T ``

𝑈, 𝑉 and 𝐷 are from the Singular Value Decomposition of 𝐴, as defined above

`` D^+ `` is given by calculating the reciprocal of every non-zero element in 𝐷, and then the resulting matrix's transpose.

Lets just say that `` A^+ `` is pretty close to 𝐵.
"

# ╔═╡ 98be78fe-7dfd-11eb-0812-59df1018f0af
let
	A = convert(Matrix,rand(0:9, 3,3))
	A, LinearAlgebra.pinv(A), inv(transpose(A) * A) * transpose(A)#original matrix, pseudo-inverse and left-inverse
end

# ╔═╡ 0def7a46-787d-11eb-189c-8151f676c8f1
md"## Try It Yourself"

# ╔═╡ dce0bdfb-23c3-415a-9b5c-833782e223a8
md"
**Try understanding the _Principal Component Analysis_ from the book and implementing it in Julia.**
"

# ╔═╡ 22557a0e-7de0-11eb-2fa0-07c807d98310
md"""
Lets understand some terms!

##### [Features](https://www.datarobot.com/wiki/feature/)
Assume you've to predict the price of houses. The data you have is, size of houses, size of lawn, number of rooms, and of course, price.
Here, size of house, size of lawn, number of rooms, and price act as features. Also note that here, price is the _dependent variable_, while rest are independent variables.

##### Feature Elimination
It can be hard to work with multiple features. From the above features, you can try eliminating one or more features, which will better help in prediction. The accuracy of prediction will fall though. For an example, I can assume that the size of lawn variable doesn't exist here. Working with the two remaining independent variables to predict the dependent variable (_price_ in the above case), would be easier, but accuracy of prediction will fall.

##### Feature Extraction
Dropping a whole feature can't be very reliable, you say? Yeah it isn't (in many cases)! Hence, introducing _feature extraction_! We can try making new independent features from the ones we already have, but has overall lesser dimensions in a way that we can make the data in the new features, as accurate as possible compared to the old ones. Since every new feature is made from the combining old ones, all new features have much of the information of old ones!

So, is there some specified way to make those new features I talk about, in feature extraction, you ask? Yes! There are many, and one such way is called **_Principle Component Analysis_**.

Principle Component Analyis is a method of doing feature extraction.

To start, the reference book and [this article](https://towardsdatascience.com/a-one-stop-shop-for-principal-component-analysis-5582fb7e0a9c) are good to read.
"""

# ╔═╡ 73ef6fad-08fc-4294-8306-68ed5722a8bb
md"
_Hint_: [MultivariateStats.jl](https://multivariatestatsjl.readthedocs.io/en/latest/pca.html)
"

# ╔═╡ 00000000-0000-0000-0000-000000000001
PLUTO_PROJECT_TOML_CONTENTS = """
[deps]
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MultivariateStats = "6f286f6a-111f-5878-ab1e-185364afe411"

[compat]
MultivariateStats = "~0.8.0"
"""

# ╔═╡ 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.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.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 = "727e463cfebd0c7b999bbf3e9e7e16f254b94193"
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
version = "3.34.0"

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

[[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.Distributed]]
deps = ["Random", "Serialization", "Sockets"]
uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"

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

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

[[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.LinearAlgebra]]
deps = ["Libdl", "libblastrampoline_jll"]
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

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

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

[[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.MultivariateStats]]
deps = ["Arpack", "LinearAlgebra", "SparseArrays", "Statistics", "StatsBase"]
git-tree-sha1 = "8d958ff1854b166003238fe191ec34b9d592860a"
uuid = "6f286f6a-111f-5878-ab1e-185364afe411"
version = "0.8.0"

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

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

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

[[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.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.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.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.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.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.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:
# ╟─1e960125-0783-4932-9b98-7b09c72bc951
# ╟─811b4842-775b-11eb-3690-830857abe1b5
# ╟─ec3044cc-7768-11eb-17ff-c19d9ac19ae4
# ╟─dd42dfa8-7aa9-11eb-3312-fbed21eb12d5
# ╟─ec9fae4a-7aa9-11eb-10a9-bfed672dfaca
# ╠═6dca1270-7aab-11eb-23a6-c5db84f32eb5
# ╟─5b37c0c6-7ab5-11eb-2fe2-4fefc5a9e193
# ╟─8f59c323-4326-4c5a-b013-6e0cfa442e02
# ╟─11a7a0f0-7ad8-11eb-12c7-77f971d32a1a
# ╟─2c663d90-7ad8-11eb-0e6c-039ecc187c0d
# ╠═bdef0196-7b91-11eb-2d26-7b02fa67233c
# ╟─d0fd5526-7d00-11eb-113b-47a224413bac
# ╠═22247216-7ddf-11eb-24da-65455796685f
# ╟─052770a6-787d-11eb-1816-11fc224a68bb
# ╟─0085a0f2-7ded-11eb-18c3-8b40f3a71ab9
# ╠═98be78fe-7dfd-11eb-0812-59df1018f0af
# ╟─0def7a46-787d-11eb-189c-8151f676c8f1
# ╟─dce0bdfb-23c3-415a-9b5c-833782e223a8
# ╟─22557a0e-7de0-11eb-2fa0-07c807d98310
# ╟─73ef6fad-08fc-4294-8306-68ed5722a8bb
# ╠═7f621598-8ce7-11eb-1d78-89ab395c5aac
# ╟─00000000-0000-0000-0000-000000000001
# ╟─00000000-0000-0000-0000-000000000002


"""
    chinese_remainder_theorem(array::Array{Int64}, remainder::Array{Int64}))

The Chinese remainder theorem is based on simultaneous congruence. In more detail, if `x`
knows the `remainder` of the Euclidean division of an `array[i]` by several integers, then
`x` can determine the remainder of the division of `array[i]` by the product of these
integers uniquely, following the condition that the divisors are pairwise coprime. For more
information see: [https://en.wikipedia.org/wiki/Chinese_remainder_theorem](https://en.wikipedia.org/wiki/Chinese_remainder_theorem)

# Arguments
- `array::Array{Int64}`: Array with elements.
- `remainder::Array{Int64}`: Number of elements

# Examples
```julia-repl
julia> import ClassicAlgorithmsCollections
julia> num = [3, 4, 5]
julia> rem = [2, 3, 1]
julia> ClassicAlgorithmsCollections.chinese_remainder_theorem(num, rem)
11
```
"""
function chinese_remainder_theorem(array::Array{Int64}, remainder::Array{Int64})
    x = 1
    n = length(array)
    while true
        i = 1
        while i < n
            if x % array[i] != remainder[i]
                break
            end
            i += 1
        end
        if i == n
            return x
        end
        x += 1
    end
end


using LinearAlgebra
using Printf

function _polynomial(a, x)
    accumulator = a[end] * one(x)
    for i in length(a)-1:-1:1
        accumulator = accumulator * x + a[i]
    end
    accumulator
end

# definition of polynom
struct Polynom{C}
    coefficients::C
    Polynom(coefficients::CC) where CC = coefficients[end] == 0 ? throw(ArgumentError("Coefficient of the highest exponent cannot be zero.")) : new{CC}(coefficients)
end

# based on https://github.com/JuliaMath/Polynomials.jl
function from_roots(roots::AbstractVector{T}; aₙ = one(T)) where {T}
    n = length(roots)
    c = zeros(T, n+1)
    c[1] = one(T)
    for j = 1:n
        for i = j:-1:1
            c[i+1] = c[i+1]-roots[j]*c[i]
        end
    end
    return Polynom(aₙ.*reverse(c))
end

(p::Polynom)(x) = _polynomial(p.coefficients, x)
degree(p::Polynom) = length(p.coefficients) - 1

function _derivativeof(p::Polynom)
    n = degree(p)
    n > 1 ? Polynom([(i - 1)*p.coefficients[i] for i in 2:n+1]) : error("Low degree of a polynomial.")
end
LinearAlgebra.adjoint(p::Polynom) = _derivativeof(p)

function Base.show(io::IO, p::Polynom)
    n = degree(p)
    a = reverse(p.coefficients)
    for (i, c) in enumerate(a[1:end-1])
        if (c != 0)
            c < 0 && print(io, " - ")
            c > 0 && i > 1 && print(io, " + ")
            print(io, "$(abs(c))x^$(n - i + 1)")
        end
    end
    c = a[end]
    c > 0 && print(io, " + $(c)")
    c < 0 && print(io, " - $(abs(c))")
end

# default optimization parameters
atol = 1e-12
maxiter = 100
stepsize = 0.95

# definition of optimization methods
abstract type RootFindingMethod end
struct Newton <: RootFindingMethod end
struct Secant <: RootFindingMethod end
struct Bisection <: RootFindingMethod end

init!(::Bisection, p, a, b) = sign(p(a)) != sign(p(b)) ? (a, b) : throw(ArgumentError("Signs at both ends are the same."))
init!(::RootFindingMethod, p, a, b) = (a, b)

function step!(::Newton, poly::Polynom, xᵢ, step_size)
    _, x̃ = xᵢ
    dp = p'
    x̃, x̃ - step_size*p(x̃)/dp(x̃)
end

function step!(::Secant, poly::Polynom, xᵢ, step_size)
    x, x̃ = xᵢ
    dpx = (p(x) - p(x̃))/(x - x̃)
    x̃, x̃ - stepsize*p(x̃)/dpx
end

function step!(::Bisection, poly::Polynom, xᵢ, step_size)
    x, x̃ = xᵢ
    midpoint = (x + x̃)/2
    if sign(p(midpoint)) == sign(p(x̃))
        x̃ = midpoint
    else
        x = midpoint
    end
    x, x̃
end

function find_root(p::Polynom, rfm=Newton, a=-5.0, b=5.0, max_iter=100, step_size=0.95, tol=1e-12)
    x, x̃ = init!(rfm, p, a, b)
    for i in 1:maxiter
        x, x̃ = step!(rfm, p, (x, x̃), step_size)
        val = p(x̃)
        @printf "x = %.5f | x̃ = %.5f | p(x̃) = %g\n" x x̃ val
        abs(val) < atol && return x̃
    end
    println("Method did not converge in $(max_iter) iterations to a root within $(tol) tolerance.")
    return x̃
end

# test code
poly = Polynom(rand(4))
p = from_roots([-3, -2, -1, 0, 1, 2, 3])
dp = p'
p(3.0), dp(3.0)

x₀ = find_root(p, Bisection(), -5.0, 5.0)
x₀ = find_root(p, Newton(), -5.0, 5.0)
x₀ = find_root(p, Secant(), -5.0, 5.0)


using SpecialFunctions

"""
Base type of FractionalDiffEq algorithms
"""
abstract type FractionalDiffEqAlgorithm end

"""
@article{
title={A predictor-corrector approach for the numerical solution of fractional differential equations},
author={Diethelm, Kai and Ford, Neville J. and Freed, Alan D.}
doi={https://doi.org/10.1023/A:1016592219341}
}
"""

"""
Predict-Evaluate-Correct-Evaluate algorithm.

For more details, please refer to [Predictor-Corrector algorithms](https://en.wikipedia.org/wiki/Predictor%E2%80%93corrector_method)

This PECE algorithm is taken from Diethelm's paper.

```tex
@article{
title={A predictor-corrector approach for the numerical solution of fractional differential equations},
author={Diethelm, Kai and Ford, Neville J. and Freed, Alan D.}
doi={https://doi.org/10.1023/A:1016592219341}
}
```
"""
struct PECE <: FractionalDiffEqAlgorithm end
#TODO: Use Richardson extrapolation to refine the PECE algorithms

"""
    FDEProblem(f, α, u0, T, h)

Define a Fractional Differential in time interval [0, T] with initial value y(0)=u₀, α-order derivative and step size h.
"""
struct FDEProblem{T<:Float64}
    f
    α::T
    u0
    T
    h::T
end

"""
    solve(FDEProblem, PECE())

After define the FDEProblem, use **PECE(Predict-Evaluate-Correct-Evaluate) algorithm** to computing the Fractional Differential Equation
"""
function solve(FDE::FDEProblem, ::PECE)
    f, α, u0, T, h = FDE.f, FDE.α, FDE.u0, FDE.T, FDE.h
    N=Int64(T/h)
    y=zeros(N+1)
    leftsum = 0

    if floor(α)==0
        leftsum=u0
    elseif floor(α)==1
        leftsum=u0+T*u0
    end

    @fastmath @inbounds @simd for n in range(0, N, step=1)
        y[Int64(n+1)]=leftsum + h^α/gamma(α+2)*f((n+1)*h, predictor(f, y, α, n, h, u0, T))+h^α/gamma(α+2)*right(f, y, α, n, h)
    end

    return y
end

function right(f, y, α, n, h)
    temp = 0

    for j in range(0, n, step=1)
        temp+=A(j, n, α)*f(j*h, y[Int64(j+1)])
    end

    return temp
end

function predictor(f, y, α, n, h, u0, T)
     predict = 0
     leftsum = 0

     if floor(α)==0
        leftsum=u0
     elseif floor(α)==1
        leftsum=u0+T*u0
     end

     @fastmath @inbounds @simd for j in range(0, n, step=1)
        predict+=B(j, n, α, h)*f(j*h, y[Int64(j+1)])
     end

     #return u0+predict
     return leftsum+predict
end

function A(j, n, α)
    if j==0
        return n^(α+1)-(n-α)*(n+1)^α
    elseif 1 ≤ j ≤ n
        return (n-j+2)^(α+1)+(n-j)^(α+1)-2*(n-j+1)^(α+1)
    elseif j==n+1
        return 1
    end
end

function B(j, n, α, h)
    return h^α/α*((n+1-j)^α-(n-j)^α)
end


export log_pdf, gradlog_pdf, hesslog_pdf

"""
    log_pdf(x)

Compute the log of the density of the univariate standard Gaussian distribution (zero mean and unitary standard deviation).
"""
log_pdf(x) = -0.5*(log(2*π) + x^2)

"""
    gradlog_pdf(x)

Compute the gradient of the log of the density of the univariate standard Gaussian distribution (zero mean and unitary standard deviation).
"""
gradlog_pdf(x) = -x

"""
    hesslog_pdf(x)

Compute the hessian of the log of the density of the univariate standard Gaussian distribution (zero mean and unitary standard deviation).
"""
hesslog_pdf(x) = -1.0


module MonteCarloFun

"
arxiv doc :     1008.4686
                1205.4446

plan:
        1) Fitting a model to data
                Fitting a straight line
                magic of Gaussian

"

#2018_10_05
#fitting a straight line

using Plots
using LinearAlgebra

x = [1., 2., 3., 4.]
y = [14, 26, 37, 43]
sigma = [1, 1, 2, 2]

plot(x, y, seriestype=:scatter, yerr=sigma, legend=false)

Y=y
N = length(Y)
A = zeros(N,2)

A[:,1] .= 1.;
A[:,2] .= x;

W = Diagonal(1. ./ sigma.^2)

MB1 = A \ Y
MB2 = (sqrt(W)*A) \ (sqrt(W)*Y)

plot(x, y, seriestype=:scatter, yerr=sigma, legend=:topleft)
plot!(x, A * MB1, label="Unweighted")
plot!(x, A * MB2, label="Weighted")

C = inv(A' * W * A)

dMB = randn((100,2)) * sqrt(C)

Angle = collect(range(0,stop=2π,length=100))
xa = [sin.(Angle) cos.(Angle)]
cMB = xa * sqrt(C)

plot([MB1[1]], [MB1[2]], seriestype=:scatter, label="Unweighted",xlabel="b", ylabel="m")
plot!([MB2[1]], [MB2[2]], seriestype=:scatter, label="Weighted",xlabel="b", ylabel="m")
plot!(MB2[1] .+ dMB[:,1], MB2[2] .+ dMB[:,2], seriestype=:scatter, label="Weighted")
plot!(MB2[1] .+ cMB[:,1], MB2[2] .+ cMB[:,2], seriestype=:scatter, label="Weighted")

plot(x, y, seriestype=:scatter, yerr=sigma, legend=:topleft)
plot!(x, A * MB2, line=(:blue, 1, 3), label="Weighted")
for i in 1:50
    dmb = MB2 + dMB[i,:]
    plot!(x, A * dmb, label= "" , line=(:black, 0.2, 2))
end
plot!()

function line_log_likehood(x,y,sigma,m,b)
    return -0.5 * sum(@. (y -(m*x+b))^2 / sigma^2)
end

bbest,mbest = MB2
line_log_likehood(x,y,sigma,mbest,bbest)

function line_lnl(b,m)
    return line_log_likehood(x, y, sigma, m, b)
end

function make_line_lnl_function(x,y,sigma)
    function f(m,b)
        return line_log_likehood(x,y,sigma,m,b)
    end
    return f
end

LineLnl = make_line_lnl_function(x,y,sigma)
LineLnl(mbest,bbest)

end # module


struct Basis
    Ψ::Matrix
    r::Int
end

function VandermondeBasis(x,r::Int;verbose::Bool=false)
    """Makes a vandermonde basis given a range and a number of basis modes"""
    Ψ = zeros((length(x),r))
    for (j,col) in enumerate(eachcol(Ψ))
        ψ = make_vander_column(j,x)
        if verbose
            println("ψ","_"*string(j)," size: ",size(ψ))
        end
        Ψ[:,j] = make_vander_column(j,x) #Each col is x^(k-1)
    end
    if verbose
        print("Final Ψ size: ",size(Ψ))
    end
    return Basis(Ψ,r) #Truncated basis matrix
end

function make_vander_column(k,col_k)
    ψ = [x^(k-1) for x in col_k]
    return ψ #Column of basis matrix
end


"""
    count_pythagorean_elements(array::Array{Int64,1})

Counting the number of `Pythagorean`-elements in the array, which sum is equal to
A^2 + B^2 = C^2.

# Arguments
- `array::Array{Int64,1}`: Unsorted array

# Examples
```julia-repl
julia> import ClassicAlgorithmsCollections
julia> arr = [5, 1, 3, 4, 17, 8, 15, 2, 2, 13 ,12]
julia> ClassicAlgorithmsCollections.count_pythagorean_elements(arr, sum)
3
```
"""
function count_pythagorean_elements(array::Array{Int64,1})

    # Initialize result
    ans = 0
    n = length(array)

    for i in 1:n-2
        for j in i+1:n-1
            for k in j+1:n
                AA = array[i]^2
                BB = array[j]^2
                CC = array[k]^2
                if (AA + BB == CC)
                    ans += 1
                elseif (AA + CC == BB)
                    ans += 1
                elseif (BB + CC == AA)
                    ans += 1
                end
            end
        end
    end
    return ans
end


"""
    rekursif(f,a,b,n)

berisi program untuk mencari integral numerik dari fungsi
`f(x)` pada interval `[a,b]` menggunakan Aturan Rekursif Trapesium.
Program ini secara default berisi 4 masukan, yaitu fungsi `f(x)`,
titik ujung interval `[a,b]` dan banyaknya iterasi `n`

# Example
```jl
julia> f(x) = 1/x;

julia> T = rekursif(f,1,5,5)
6×1 Array{Float64,2}:
 2.4
 1.8666666666666667
 1.6833333333333333
 1.628968253968254
 1.6144063238103485
 1.6106858960792332
```
return solusi integral numerik pada setiap iterasi `T`
"""
function rekursif(f,a,b,n)
  M = 1;
  h = b-a;
  T = Array{Float64}(undef,n+1,1)
  T[1] = h/2*(f(a)+f(b))
  for j = 1:n
    h = h/2;
    s = 0;
    for k = 1:M
      x = a+h*(2*k-1);
      s = s+f(x);
    end
    M = 2*M;
    T[j+1] = T[j]/2+h*s;
  end
  return T
end


module ZernikePolynomials

export mn2OSA, mn2Noll, OSA2mn, Noll2mn, Noll2OSA, OSA2Noll, Zernike, Zernikecoefficients, evaluateZernike, normalization

# Zernike polynomial-based functions
# Some formulas from Wikipedia, some from: "Standards for Reporting the Optical Aberrations of Eyes", Journal of Refractive Surgery Volume 18 September/October 2002
# Reinier Doelman, 23-12-2018

"""
    mn2OSA(m::Int,n::Int)

Convert the integer pair (n,m) that defines the Zernike polynomial Z_n^m(ρ,θ) to the sequential OSA/ANSI stardard index number.

Returns NaN for invalid integer pairs.

See also: [`mn2Noll`], [`OSA2mn`]

Source: "Standards for Reporting the Optical Aberrations of Eyes", Journal of Refractive Surgery Volume 18 September/October 2002

# Example:
```julia-repl
julia> [mn2OSA(n,m) for n in 0:4, m in -5:5]
```
"""
function mn2OSA(m::Int,n::Int)
  if n < abs(m) || isodd(n-m)
    return NaN
  else
    return Int((1//2)*(n*(n+2)+m))
  end
end

"""
    mn2Noll(m::Int,n::Int)

Convert the integer pair (n,m) that defines the Zernike polynomial Z_n^m(ρ,θ) to the sequential Noll index number.

Returns NaN for invalid integer pairs.

See also: [`mn2OSA`], [`Noll2mn`]

Source: (https://en.wikipedia.org/wiki/Zernike_polynomials)

# Example:
```julia-repl
julia> [mn2Noll(n,m) for n in 0:4, m in -5:5]
```
"""
function mn2Noll(m::Int,n::Int)
  if n < abs(m) || isodd(n-m)
    return NaN
  else
    if m > 0 && (mod(n,4) ∈ (0,1))
      p = 0
    elseif m < 0 && (mod(n,4) ∈ (2,3))
      p = 0
    elseif m ≥ 0 && (mod(n,4) ∈ (2,3))
      p = 1
    elseif m ≤ 0 && (mod(n,4) ∈ (0,1))
      p = 1
    else
      error("Invalid combination of (m,n) in Noll indexing")
    end
    return Int((1//2)*n*(n+1) + abs(m) + p)
  end
end

"""
    OSA2mn(j::Int)

Convert the sequential OSA/ANSI stardard index number j to the integer pair (n,m) that defines the Zernike polynomial Z_n^m(ρ,θ).

See also: [`Noll2mn`], [`mn2OSA`], [`OSA2Noll`]

Source: "Standards for Reporting the Optical Aberrations of Eyes", Journal of Refractive Surgery Volume 18 September/October 2002

# Example:
```julia-repl
julia> [OSA2mn(j) for j in 0:10]
```
"""
function OSA2mn(j::Int)
  n = Int(ceil((-3 + sqrt(9+8j))/2))
  m = 2j-n*(n+2)
  return (Int(m),Int(n))
end

"""
    Noll2mn(j::Int)

Convert the Noll index number j to the integer pair (n,m) that defines the Zernike polynomial Z_n^m(ρ,θ).

See also: [`OSA2mn`], [`mn2Noll`], [`Noll2OSA`]

Source: (https://en.wikipedia.org/wiki/Zernike_polynomials)

# Example:
```julia-repl
julia> [OSA2mn(j) for j in 0:10]
```
"""
function Noll2mn(j::Int)
  n = Int(ceil((-3 + sqrt(1+8j))/2))
  jr = j - Int(n*(n+1)/2)
  if mod(n,4) ∈ (0,1)
    m1 = jr
    m2 = -(jr-1)
    if iseven(n-m1)
      m = m1
    else
      m = m2
    end
  else # mod(n,4) ∈ (2,3)
    m1 = jr-1
    m2 = -(jr)
    if iseven(n-m1)
      m = m1
    else
      m = m2
    end
  end
  return (m,n)
end

"""
    Noll2OSA(j::Int)

Convert the Noll index number j to the OSA/ANSI stardard index number for Zernike polynomials.

See also: [`OSA2Noll`], [`Noll2mn`]

# Example:
```julia-repl
julia> [Noll2OSA(OSA2Noll(j)) for j = 0:10]
```
"""
function Noll2OSA(j::Int)
  return mn2OSA(Noll2mn(j)...)
end

"""
    OSA2Noll(j::Int)

Convert OSA/ANSI stardard index number to the Noll index number for Zernike polynomials.

See also: [`OSA2Noll`], [`Noll2mn`]

# Example:
```julia-repl
julia> [Noll2OSA(OSA2Noll(j)...) for j = 0:10]
```
"""
function OSA2Noll(j::Int)
  return mn2Noll(OSA2mn(j)...)
end

"""
    R(m::Int,n::Int)

Obtain the function ρ -> R_n^|m|(ρ), where R is the radial polynomial in Zernike polynomials

# Example:
```julia-repl
julia> R(1,1)
```
"""
function R(m::Int,n::Int) # radial polynomial
  p = s -> ((-1)^s * factorial(n-s))/ (factorial(s)*factorial(Int(0.5*(n+abs(m))-s))*factorial(Int(0.5*(n-abs(m))-s)))
  f = x -> sum([p(s)*x.^(n-2s) for s in 0:Int((n-abs(m))/2)])
  return f
end

function normalization(m::Int,n::Int) # normalization constant of the zernike polynomial
  δ = (x,y) -> Int(x == y)
  c = sqrt(2(n+1)/(1+δ(m,0)))
  return c
end

"""
    Zernike(m::Int,n::Int;coord=:polar)

Obtain the function (ρ,θ) -> Z_n^m(ρ,θ), where Z is the Zernike polynomial with coefficients n and m. ρ is the radius and θ the angle.

If coord=:cartesian the function is (x,y) -> Z_n^m(x,y) in Cartesian coordinates.

# Example:
```julia-repl
julia> Zernike(1,1)
julia> Zernike(1,1,coord=:polar)
julia> Z = Zernike(1,1,:cartesian)
julia> Z(0.5,0.2)
```
"""
function Zernike(m::Int,n::Int;coord=:polar)
  δ = ρ -> (abs(ρ) <= 1.)
  if m ≥ 0
    Z = (ρ,θ) -> normalization(m,n)*R(m,n)(ρ) * cos(m*θ) * δ(ρ)
  else
    Z = (ρ,θ) -> -normalization(m,n)*R(m,n)(ρ) * sin(m*θ) * δ(ρ)
  end

  if coord == :cartesian
    g = (x,y) -> (sqrt(x.^2 + y.^2), atan(y,x))
    return (x,y) -> Z(g(x,y)...)
  elseif coord == :polar
    return Z
  else
    error("Unrecognized coordinate system")
  end
end

"""
    Zernike(j::Int;index=:OSA,coord=:polar)

Obtain the function (ρ,θ) -> Z_j(ρ,θ), where Z is the Zernike polynomial with sequential index j, according to the indexing by :OSA or :Noll.

If coord=:cartesian the function is (x,y) -> Z_j(x,y) in Cartesian coordinates.

# Example:
```julia-repl
julia> Zernike(1)
julia> Zernike(1,coord=:polar)
julia> Z = Zernike(5,index=:Noll,coord=:cartesian)
julia> Z(0.5,0.2)
```
"""
function Zernike(j::Int;index=:OSA,coord=:polar)
  if index == :OSA
    return Zernike(OSA2mn(j)...,coord=coord)
  elseif index == :Noll
    return Zernike(Noll2mn(j)...,coord=coord)
  else
    error("Unknown Zernike sequential index")
  end
end

"""
    Zernikecoefficients(phase::AbstractArray{Float64,2}, J::Vector{Int}; index=:OSA)

Compute the Zernike coefficients (OSA normalization) that in a least-squares sense optimally describe the phase.
The Zernike polynomials used are specified with an index vector J, according to OSA indexing.

"""
function Zernikecoefficients(phase::AbstractArray{Float64,2}, J::Vector{Int}; index=:OSA)
  s = size(phase)
  X = range(-1.,stop=1,length=s[1])
  Y = range(-1.,stop=1,length=s[2])

  D = [[Zernike(j,coord=:cartesian,index=index)(x,y) for x in X, y in Y] for j in J ]
  G = cat([D[i][:] for i in 1:length(J)]...; dims=2)
  return G\phase[:]
end

"""
    Zernikecoefficients(x::AbstractArray{<: AbstractFloat,1}, phase::AbstractArray{Float64,2}, J::Vector{Int}; index=:OSA)

Compute the Zernike coefficients (OSA normalization) that in a least-squares sense optimally describe the phase.
The Zernike polynomials used are specified with an index vector J, according to OSA indexing.

"""
function Zernikecoefficients(X::AbstractArray{<: AbstractFloat,1}, phase::AbstractArray{Float64,2}, J::Vector{Int}; index=:OSA)
  s = size(phase)
  if !(s[1] == s[2] && s[1] == length(X))
    error("Non-matching size")
  end

  D = [[Zernike(j,coord=:cartesian,index=index)(x,y) for x in X, y in X] for j in J ]
  G = cat([D[i][:] for i in 1:length(J)]...; dims=2)
  return G\phase[:]
end

"""
    evaluateZernike(N::Int, J::Vector{Int}, coefficients::Vector{Float64}; index=:OSA)

Evaluate the Zernike polynomials on an N-by-N grid as specified by the Zernike coefficients of the polynomials J

# Example:
```julia-repl
julia> W = evaluateZernike(64,[5, 6],[0.3, 4.1])
```
"""
function evaluateZernike(N::Int, J::Vector{Int}, coefficients::AbstractArray{Float64,1}; index=:OSA)
  X = range(-1.,stop=1,length=N)
  Y = range(-1.,stop=1,length=N)

  D = [[Zernike(j,coord=:cartesian,index=index)(x,y) for x in X, y in Y] for j in J ]
  return reduce(+,map(*,D,coefficients))
end

"""
    evaluateZernike(x::AbstractArray{<: AbstractFloat,1}, J::Vector{Int}, coefficients::Vector{Float64}; index=:OSA)

Evaluate the Zernike polynomials on a grid as specified by the Zernike coefficients of the polynomials J, on a grid with points in x

# Example:
```julia-repl
julia> W = evaluateZernike(64,[5, 6],[0.3, 4.1])
```
"""
function evaluateZernike(X::AbstractArray{<: AbstractFloat,1}, J::Vector{Int}, coefficients::Vector{Float64}; index=:OSA)
  D = [[Zernike(j,coord=:cartesian,index=index)(x,y) for x in X, y in X] for j in J ]
  return reduce(+,map(*,D,coefficients))
end

function evaluateZernike(n::Int, J::Int, coefficients::Float64; index=:OSA)
  return evaluateZernike(n, [J], [coefficients]; index=index)
end

function evaluateZernike(X::AbstractArray{<: AbstractFloat,1}, J::Int, coefficients::Float64; index=:OSA)
  return evaluateZernike(X, [J], [coefficients]; index=index)
end

end # module


# Model Lion & Zebra (predetor vs. prey) over the course of 100 years.

# Using Lotka-Volterra equation, we have:
# dx/dt=x(α−βy)  dy/dt=−y(γ−δx)

start_time = 0;
end_time = 100;

# How much time passes between each successive calculation
time_step = 1/4 # In years
end_step = int(((end_time - start_time)/ time_step));

# The number of zebras when the simulation is started
initial_zebras = 30000;

# The number of lions when the simulation is started
initial_lions = 15;

# The number of zebras that must be killed for a lion to be born. (Zebras / Lion)

zebras_per_lion = 1000;

# The chance a zebra will die when a zebra lion cross paths
fighting_chance = 0.50;

meeting_chance = 0.02;

zebra_growth_rate = 0.20;

lion_death_rate = 0.10;

# Initialization

zebras_over_time = fill(0.0, end_step+1);
lions_over_time = fill(0.0, end_step+1);
model_time = fill(0.0, end_step+1);

zebras = initial_zebras;
lions = initial_lions;

zebras_over_time[1]	= zebras;
lions_over_time[1] = lions;

# Run the model

for sim_step = 1:end_step

	sim_time = start_time + sim_step * time_step;
	model_time[sim_step] = sim_time;

	# First we must calculate our flows (our rates)
	zebra_births = zebras * zebra_growth_rate;
	zebra_deaths  = min(zebras, meeting_chance*fighting_chance*zebras*lions);

	lion_births = 1/zebras_per_lion * zebra_deaths;
	lion_deaths = lions * lion_death_rate;

	# Update the stock levels
	zebras = zebras + zebra_births - zebra_deaths;
	lions = lions + lion_births - lion_deaths

	# Stock values always update in the next time step
	zebras_over_time[sim_step+1] = zebras;
	lions_over_time[sim_step+1] = lions;
end

println("Time, Zebras (Thousands), Lions");

for i = 1:end_step
	print(model_time[i]);
	print(",");
	print(zebras_over_time[i]/1000);
	print(",");
	println(lions_over_time[i]);
end


using NumericalMethodsforEngineers, DataFrames, Plots

@sym begin
  LagrangePolynomial(xi_, yi_) := Module([N, sum, num, den],
    begin
      N = Length(xi)
      sum = 0
      For( i=1, i <= N, Increment(i),
        begin
          num = 1
          den = 1
          For( j=1, j <= N, Increment(j),
            begin
              If(j != i, num *= (x-xi[j]))
              If(j != i, den *= (xi[i]-xi[j]))
            end
          )
          sum += yi[i] * num/den
        end
      ),
      Return(Simplify(sum))
    end
  )
end

println()
@sym begin
  xi = [1, 3, 6]
  yi = [1, 5, 10]
  # LagrangePolynomial defined in ../../src/ch07/sym_fcns.jl
  Q(x_) = LagrangePolynomial(xi, yi)
  SetJ(qs, ToString(Q(x)))
  Print("Q(x) = $(Q(x))\n")
end

@sym Print("Q(4.5) = $(N(Q(4.5)))\n");
println("\n")

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

  x = [1.0, 3.0, 6.0]
  y = [1.0, 5.0, 10.0]
  xi = [2.0, 4.5]
  (dfin, dfxi) = lagrangianpolynomial(length(x), x, y, xi)

  dfin |> display
  println()

  dfxi |> display
  println()

  @eval f(x) = $(Meta.parse(qs))

  xint = 1:1:10
  p = plot(xint, f.(xint), line=(:path, 1), label="interpolated curve")
  scatter!(p, dfin[:x], dfin[:y], marker=(:circle, 4), label="input points", color=:blue)
  scatter!(p, dfxi[:xi], dfxi[:yi], marker=(:star, 8), color=:red, label="interpolated points")

  plot(p)
  savefig("ex.5.1_symata.png")
  #gui()

  #end


using NumericalMethodsforEngineers, Test

a = [1. 2. -2.; 2. 5. -4.; -2. -4. 5.]

println("\n|a[1, 1]| = $(det(a[1, 1]))\n")
println("|a[1:2, 1:2]| = $(det(a[1:2, 1:2])))\n")
println("|a| = $(det(a))")
println("\ncholesky(a):")
cholesky(a) |> display

a = [16. 4. 8.; 4. 5. -4.; 8. -4. 22.]

println("\n|a[1, 1]| = $(det(a[1, 1]))\n")
println("|a[1:2, 1:2]| = $(det(a[1:2, 1:2])))\n")
println("|a| = $(det(a))")

println("\ncholesky(a) - upper:")
upper = cholesky(a).U
upper |> display

println("\ncholesky(a) - lower:")
lower = cholesky(a).L
lower |> display

println("\nlower * upper:")
lower * upper |> display

@test a == lower * upper


module Factorions
using Memoize, ProgressMeter, Distributed

export is_factorion, fact_upper_bound, find_factorions, string_base
export find_in_range, find_in_parallel

@memoize function fast_fact(n::Int)
    return factorial(n)
end

"""
`sum_fact_digs(n)` returns the sum of the factorials of the
base-10 digits of `n`.
"""
function sum_fact_digs(n::Int, b::Int = 10)
    digs = digits(n, base = b)
    sum(map(fast_fact, digs))
end

"""
`is_factorion(n,b=10)` determines if `n` is a base-`b` *factorion*;
that is, if `n` is equal to the sum of the factorials of its base-`b` digits.
"""
function is_factorion(n::Int, b::Int = 10)::Bool
    if n <= 0
        return false
    end
    return n == sum_fact_digs(n, b)
end

"""
`fact_upper_bound(b=10)` returns an upper bound on the number of digits in
a base-`b` factorion.
"""
function fact_upper_bound(b::Int = 10)
    n::Int = 1
    fmax = fast_fact(b - 1)
    while b^(n - 1) < fmax * n
        n += 1
    end
    return n
end

function find_factorions(b::Int = 10)
    stop = b^fact_upper_bound(b)
    println("Trying $stop possibilities (up to $(fact_upper_bound(b)) digits)")
    list = Int[]
    PM = Progress(stop)
    for n = 1:stop
        if is_factorion(n, b)
            push!(list, n)
        end
        next!(PM)
    end
    for x in list
        println("$(string_base(x,b)) = $x")
    end

    return Set(list)

end

function find_in_range(rng, b::Int = 10)
    list = Int[]
    for x in rng
        if is_factorion(x, b)
            push!(list, x)
        end
    end
    return Set(list)
end

function find_in_parallel(b::Int = 10)
    stop = b^fact_upper_bound(b)
    println("Trying $stop possibilities (up to $(fact_upper_bound(b)) digits)")
    np = Threads.nthreads()
    #println(addprocs(np))
    results = Vector{Any}(undef, np)

    for j = 1:np
        rng = j:np:stop
        @info "Spawning process #$j"
        results[j] = @spawnat j + 1 find_in_range(rng, b)
    end
    sets = fetch.(results)
    X = union(sets...)
    for n in sort(collect(X))
        println("$n \t $(string_base(n,b))")
    end
    return union(sets...)
end

"""
`string_base(n,b)` renders the nonnegative integer `n` in
base `b` (we require `1 < b < 37`).
"""
function string_base(n::Int, b::Int = 10)
    symbs = "0123456789abcdefghijklmnopqrstuvwxyz"
    digs = digits(n, base = b)
    chars = [symbs[k+1] for k in digs]
    return join(reverse(chars)) * "_$b"
end

end # module


n = 2
m = 1

A = [1.0 1.0; 0.0 1.0]
B = [0.0; 1.0]

Q = [1.0 0.0; 0.0 1.0]
R = [1.0]

# x_init = [1.0; 0.0]

nz = n + m + n

function unpack(z)
	x2 = z[1:n]
	u1 = z[n .+ (1:m)]
	y2 = z[n + m .+ (1:n)]

	return x2, u1, y2
end

z0 = rand(nz)

function lagrangian(z, x1)
	x2, u1, y2 = unpack(z)

	L = 0.0

	# objective
	L += transpose(x2) * Q * x2
	L += (transpose(u1) * R * u1)[1]

	# constraints
	L += transpose(y2) * (A * x1 + B * u1[1] - x2)

	return L
end

@variables z_sym[1:nz]
@variables x1_sym[1:n]

L = lagrangian(z_sym, x1_sym)
L = simplify.(L)

dL = Symbolics.gradient(L, z_sym)
ddL = Symbolics.hessian(L, z_sym)
ddLx1 = Symbolics.jacobian(dL, x1_sym)

L_grad = eval(Symbolics.build_function(dL, z_sym, x1_sym)[1])
L_hess = eval(Symbolics.build_function(ddL, z_sym, x1_sym)[1])
L_hess_x1 = eval(Symbolics.build_function(ddLx1, z_sym, x1_sym)[1])

function solve(z0, x1)
	z = copy(z0)

	∇L = L_grad(z, x1)

	println()
	for i = 1:10
		r_norm = norm(∇L)
		println("iter ($i)")
		println("	grad norm: $(r_norm)")
		r_norm < 1.0e-8 && break

		∇²L = L_hess(z, x1)

		Δ = ∇²L \ ∇L

		α = 1.0
		ẑ = z - α * Δ

		∇L̂ = L_grad(ẑ, x1)

		iter = 1
		while norm(∇L̂) > norm(∇L)
			α *= 0.5
			ẑ = z - α * Δ
			∇L̂ = L_grad(ẑ, x1)
			iter += 1

			if iter > 100
				break
			end
		end

		z = ẑ
		∇L = ∇L̂
	end

	return z, ∇L, L_hess(z, x1)
end

z_sol, ∇L_sol, ∇²L_sol = solve(z0, x1)
K_direct = (∇²L_sol \ L_hess_x1(z_sol, x1))[n .+ (1:m), :]

# LQR solution
K, P = tvlqr([A], [B], [Q, Q], [R[1,1]])

norm(K_direct - K[1])


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

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

yellowbanana = load("images/104_100.jpg")

size(yellowbanana)

yellowbanana[60,50] # Each element in the array is a color

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

banana = Gray.(yellowbanana)

# ------------------------------------------------------------------------------------------
# Note that `banana` is a (100 x 100) matrix that stores a pixel -- information about the
# color -- at each entry.
# ------------------------------------------------------------------------------------------

size(banana)

# ------------------------------------------------------------------------------------------
# If we take the `channelview` of `banana`, we'll see the numbers that describe the color at
# each point in the array:
# ------------------------------------------------------------------------------------------

channelview(banana)

# ------------------------------------------------------------------------------------------
# Note that you can call `Gray` on the `channelview` of an image to display the image!
# ------------------------------------------------------------------------------------------

Gray.(channelview(banana))

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

function compress_image(img, factor)
    img = channelview(Gray.(img))
    img_svd = svd(img)
    U, S, V = img_svd.U, img_svd.S, img_svd.V
    num_sv = size(S)[1]
    use_sv = fld(num_sv, factor) # `fld` returns an integer
    # Reconstruct image via matrix multiplies
    compressed_image = U[:, 1:use_sv] * Diagonal(S[1:use_sv]) * V[:, 1:use_sv]'
    Gray.(compressed_image)
end

compress_image(banana, 10)

# ------------------------------------------------------------------------------------------
# ### Solution 2
# ------------------------------------------------------------------------------------------

function compress_image(img, factor)
    img = channelview(Gray.(img))
    img_svd = svd(img)
    U, S, V = img_svd.U, img_svd.S, img_svd.V
    num_sv = size(S)[1]
    use_sv = fld(num_sv, factor) # `fld` returns an integer
    # Reconstruct image with a `for` loop that sequentially performs outer products
    compressed_image = fill(zero(N0f8), (num_sv, num_sv))
    for i in 1:use_sv
       compressed_image += S[i] * U[:, i] * V[:, i]'
    end
    Gray.(compressed_image)
end

compress_image(banana, 20)


using LinearAlgebra

# k_local(Elastic Modulus,thickness,height,length,number of nodes,row/column one, row/column two)

#creates local stiffness matrix and maps to global
function k_local(k,l,n,rc_1,rc_2)
    k_global = zeros(n,n)
    k_global[rc_1,rc_1] = k
    k_global[rc_1,rc_2] = -k
    k_global[rc_2,rc_1] = -k
    k_global[rc_2,rc_2] = k
    return k_global
end

# constants from problem statement
k1 = 40
k2 = 30
k3 = 60
k4 = 50
# combining all globally mapped matrices
kG = k_local(k1,l,5,1,2)+
     k_local(k2,l,5,2,4)+
     k_local(k3,l,5,3,4)+
     k_local(k4,l,5,4,5)

# Adjusts Global Matrix for fixed-end reactions
function adj_fix(kG,x,n)
    default = zeros(Float64,1,n)
    default[x] = 1
    kG[x,:] = default
    kG[:,x] = transpose(default)
end
adj_fix(kG,1,5)
adj_fix(kG,3,5)

# Force Vector
f = Float64[0 0 0 100 60]
F = transpose(f)

# Displacement vector
u = kG\F
println("Nodal displacement vector (mm):")
display(u)