#Pkg.add("Plots")
using Plots

x = -3:0.1:3
f(x) = x^2

y = f.(x)

gr()

plot(x, y, label="linea")
scatter!(x, y, label="puntos")

plotlyjs()

plot(x, y, label="line")
scatter!(x, y, label="points")

globaltemperatures = [14.4, 14.5, 14.8, 15.2, 15.5, 15.8]
numpirates = [45000, 20000, 15000, 5000, 400, 17]

# Primero plotteamos los datos
plot(numpirates, globaltemperatures, legend=false)
scatter!(numpirates, globaltemperatures, legend=false)

# Agregamos Títulos y etiquetas/labels.
xlabel!("Número de piratas [Apróx.]")
ylabel!("Temperatura Global (C)")
title!("Influencia de población de piratas en calentamiento global")

# Primero graficamos datos
plot(numpirates, globaltemperatures, legend=false)
scatter!(numpirates, globaltemperatures, legend=false)

# Este comando invierte el eje x para que podamos ver los cambios hacia adelante en el tiempo, de 1860 a 2000
xflip!()

# Add titles and labels
xlabel!("Number of Pirates [Approximate]")
ylabel!("Global Temperature (C)")
title!("Influence of pirate population on global warming")

p1 = plot(x, x)
p2 = plot(x, x.^2)
p3 = plot(x, x.^3)
p4 = plot(x, x.^4)
plot(p1,p2,p3,p4,layout=(2,2),legend=false)


module CubicHermiteSpline

using ArgCheck

export CubicHermiteSplineInterpolation
export interp, grad

struct CubicHermiteSplineInterpolation
    x
    y
    gradient
end

"""
    basis(t)

Compute the basis functions of cubic Hermite spline interpolation. They are:
    H_{00} = 2t^3 - 3t^2 + 1 = (1+2t)(t-1)^2
    H_{10} = t^3 - 2t^2 + t = t(t-1)^2
    H_{01} = -2t^3 + 3t^2 = t^2(3-2t)
    H_{11} = t^3 - t^2 = t^2(t - 1)
"""
function basis(t)
    t2 = t * t
    it = t - 1
    it2 = it * it
    tt = 2 * t
    h00 = (1 + tt) * it2
    h10 = t * it2
    h01 = t2 * (3 - tt)
    h11 = t2 * it
    return h00, h10, h01, h11
end

"""
    basis_derivative(t)

Compute the basis functions of cubic Hermite spline interpolation. They are:
    H_{00} = 6t^2 - 6t = 6t(t-1)
    H_{10} = 3t^2 - 4t + 1 = (3t-1)(t-1)
    H_{01} = -6t^2 + 6t = -6t(t-1)
    H_{11} = 3t^2 - 2t = t(3t - 2)
"""
function basis_derivative(t)
    t2 = t * t
    h00 = 6*t2 - 6*t
    h10 = 3*t2 - 4*t + 1
    h01 = -6*t2 + 6*t
    h11 = 3*t2 - 2*t
    return h00, h10, h01, h11
end

function findinterval(v, x)
    for i in eachindex(x)
        if x[i] == v
            return i, x[i], nothing
        elseif x[i] > v
            return i-1, x[i-1], x[i]
        end
    end
end

function _interp(spl::CubicHermiteSplineInterpolation, v; grad=false)
    x = spl.x
    y = spl.y
    gradient = spl.gradient
    @argcheck v >= x[1]
    @argcheck v <= x[end]

    idx, x1, x2 = findinterval(v, x)
    if x2 === nothing
       return grad ? gradient[idx] : y[idx]
    end

    # mapping (x1, x2) to (0, 1), h is the scaling constant
    t = (v - x1) / (x2 - x1)
    h = x2 - x1
    y1, y2 = y[idx], y[idx+1]
    k1, k2 = gradient[idx], gradient[idx+1]

    h00, h10, h01, h11 = grad ? basis_derivative(t) : basis(t)
    r = y1*h00 + h*k1*h10 + y2*h01 + h*k2*h11

    # Need to rescale the interpolated gradient before return
    return grad ? (r/h) : r
end

(spl::CubicHermiteSplineInterpolation)(v::Real; grad=false) = _interp(spl, v; grad=grad)
(spl::CubicHermiteSplineInterpolation)(x::AbstractVector; grad=false) = spl.(x; grad=grad)

# handy methods
interp(spl::CubicHermiteSplineInterpolation, p) = spl(p)
grad(spl::CubicHermiteSplineInterpolation, p) = spl(p; grad=true)

end # module


# Locally Linear Embedding (LLE)
# ------------------------
# Nonlinear dimensionality reduction by locally linear embedding,
# Roweis, S. & Saul, L., Science 290:2323 (2000)

using Logging

#### LLE type
struct LLE{T <: Real} <: AbstractDimensionalityReduction
    k::Int
    λ::AbstractVector{T}
    proj::Projection{T}

    LLE{T}(k::Int, λ::AbstractVector{T}, proj::Projection{T})  where T = new(k, λ, proj)
end

## properties
outdim(R::LLE) = size(R.proj, 1)
eigvals(R::LLE) = R.λ
neighbors(R::LLE) = R.k

## show
summary(io::IO, R::LLE) = print(io, "LLE(outdim = $(outdim(R)), neighbors = $(neighbors(R)))")

## interface functions
function fit(::Type{LLE}, X::AbstractMatrix{T};
             maxoutdim::Int=2, k::Int=12, tol::Real=1e-5,
             use_naive=false, use_eigen=false) where {T<:Real}
    # Construct NN graph
    D, E = find_nn(X, k)
    _, C = largest_component(SimpleWeightedGraph(adjmat(D,E)))
    X = X[:, C]
    n = length(C)

    # Correct indexes of neighbors if more then one connected component
    Ec = E
    if size(E,2) != n
        R = Dict(zip(C, collect(1:n)))
        Ec = zeros(Int,k,n)
        for i in 1 : n
            Ec[:,i] = map(j->get(R,j,C[i]), E[:,C[i]])
        end
    end

    if k > maxoutdim
        @warn("k > maxoutdim: regularization will be used")
    end

    # Reconstruct weights and compute embedding:
    # M = (I - w)'(I - w) = I - w'I - Iw + w'w
    M = spdiagm(0 => fill(one(T), n))
    Ones = fill(one(T), k, 1)
    for i in 1 : n
        J = Ec[:,i]
        Z = view(X, :, J) .- view(X, :, i)
        G = transpose(Z)*Z
        G += I * tol # regularize
        w = vec(G \ Ones)
        w ./= sum(w)
        ww = w*transpose(w)
        for (l, j) in enumerate(J)
            M[i,j] -= w[l]
            M[j,i] -= w[l]
            for (m, jj) in enumerate(J)
                M[j,jj] = ww[l,m]
            end
        end
    end

    if use_eigen
        λ, V = decompose(M, maxoutdim)
    else
        λ, V = specDecompose(M, tol, maxoutdim, use_naive)
    end
    return LLE{T}(k, λ, rmul!(transpose(V), sqrt(n)))
end

transform(R::LLE) = R.proj


"""
 Convert cartesian coordinates to spherical coordinates (θ,ϕ,r)
 θ, ϕ are in radians
 ϕ is the latitude, i.e., the angle from the equator.
"""
function xyz2θϕr(x)
  r = sqrt(x[1]^2 + x[2]^2 + x[3]^2)
  θ = atan(x[2], x[1])
  ϕ = asin(x[3]/r)
  VectorValue(θ,ϕ,r)
end

"""
  Map point on the unit sphere on ambientspace to the parametric space of the unit sphere
  (x,y,z) ↦ (θ,ϕ)
"""
function xyz2θϕ(x)
  θ = atan(x[2], x[1])
  ϕ = asin(x[3])
  VectorValue(θ,ϕ)
end

"""
  Map point on the parametric space of the unit sphere to the ambient space
  (θ,ϕ) ↦ (x,y,z)
"""
function θϕ2xyz(θϕ)
  θ,ϕ = θϕ
  x = cos(θ)*cos(ϕ)
  y = sin(θ)*cos(ϕ)
  z = sin(ϕ)
  VectorValue(x,y,z)
end

"""
Matrix transformation from spherical vector field to Cartesian vector field.
  θ∈(0,2π)
  ϕ∈(-π/2,π/2)
  r=constant for the sphere
"""
function spherical_to_cartesian_matrix(θϕr)
  θ,ϕ,r = θϕr
  TensorValue(-sin(θ)       , cos(θ)       ,      0,
              -sin(ϕ)*cos(θ),-sin(ϕ)*sin(θ), cos(ϕ),
               cos(ϕ)*cos(θ), cos(ϕ)*sin(θ), sin(ϕ))
end

"""
Matrix transformation from cartesian vector field to spherical vector field
"""
function cartesian_to_spherical_matrix(xyz)
  x,y,z = xyz
  sr = sqrt(x^2+y^2+z^2)
  cr = sqrt(x^2+y^2)
  TensorValue(-y/cr, (x*z)/(sr*cr), x/sr,
               x/cr, (y*z)/(sr*cr), y/sr,
                  0,   -cr/(sr*cr), z/sr)
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

f = x->sin(10x)+cos(3x)
x0 = 3.4
#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

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


export ROC, auc

struct ROC{T <: Real} <: AbstractBinaryMetric
  labels::BitVector
  scores::Vector{T}
  AUC::T
  FPR::Vector{T}
  TPR::Vector{T}
  OP::Vector{T}
  function ROC(labels,scores)
    B = BinaryMetrics(labels, scores)
    OP = op(B)
    FPR = fpr(B, OP)
    TPR = tpr(B, OP)
    AUC = get_auc([0.0;FPR], [0.0;TPR])
    new{eltype(FPR)}(B.labels, B.scores, AUC, FPR, TPR, OP)
  end
end

function Base.show(io::IO, B::ROC)
  println(io,"Receiver Operating Characteristic (ROC) curve")
  AUC = round(auc(B),digits=2)
  print(io,"Samples:$(length(B)), P:$(positives(B)), N:$(negatives(B)), AUC:$(AUC)")
end

"""
  ROC(labels, scores)

Generates data for plotting a Receiver Operating Characteristic (ROC) curve.

* `labels` must be a boolean array e.g. either containing `true`,`false` or `0`,`1`.
* `scores` must be an array with the same length of `labels` containing the scores of the binary classifier.

You can generate a plot automatically using the following commands:
```julia
roc = ROC(labels, scores)

using Plots
plot(roc)
```

The Area Under Curve (AUC) FPR and TPR can be accessed by `auc(roc)`, `fpr(roc)` and `tpr(roc)` respectively.

"""
ROC(B::BinaryMetrics) = ROC(B.labels, B.scores)

auc(curve::ROC) = curve.AUC
fpr(curve::ROC) = curve.FPR
tpr(curve::ROC) = curve.TPR
op(curve::ROC) = curve.OP

function get_curve(curve::ROC)
  return [0.0;curve.FPR], [0.0;curve.TPR]
end


# This file investigates mean square error (MSE) with respect to free variables in hidden 1D interpolation

using FractalTools
using Makie

# Construct interpolation data
# f(x) = [FractalTools.sinusoid(x), FractalTools.sinusoid(x)]  # For regular data
f(x) = [FractalTools.wen(x), FractalTools.weierstrass(x)]  # For irregular data
line = [[0.], [1.]]
pts = getdata(f, line, 11)

# Construct test data
tpts = getdata(line, 101)
npts = length(tpts)

# Compute errors
fvals = getindex.(map(pnt -> f(pnt[1]), tpts), 1)
freevars = 0.001 : 0.01 : 0.5
mse = map(freevars) do freevar
    interp = interpolate(pts, HInterp1D(fill(freevar, 2, 2)))
    ivals = getindex.(map(pnt -> interp(pnt[1]), tpts), 1)
    sum((fvals - ivals).^2) / npts
end

# Plot mse
fig = Figure()
ax = fig[1, 1] = Axis(fig, xlabel="Free Variable", ylabel="MSE", title="1D Hidden Interpolation MSE")
stem!(ax, freevars, mse, color=:black)
save(joinpath(@__DIR__, "hinterp1d_mse.png"), fig)
display(fig)


export accuracy, bce, logistic, nll, zeroone

"""
    nll(scores, labels; dims=1, average=true)

Return the negative log likelihood for a single batch of data given an unnormalized `scores`
matrix and an `Integer` array of correct `labels`. The `scores` matrix should have size
`(classes,instances)` if `dims=1` or `(instances,classes)` if `dims=2`. `labels[i]` should be
in `1:classes` to indicate the correct class for instance i, or 0 to skip instance i.

The return value is `(total/count)` if `average=true` and `(total,count)` if `average=false`
where `count` is the number of instances not skipped (i.e. `label != 0`) and `total` is their
total negative log likelihood.

## Example

Let's assume that there are three classes (cat, dog, ostrich) and just 2 instances with
the unnormalized score `scores[:,1]` and `scores[:,2]` respectively. The first instance
is actually a cat and the second instance a dog:

    scores = [12.2    0.3;
               2.0   21.5;
               0.0  -21.0]
    labels = [1, 2]
    nll(scores,labels)
    # returns 2.1657e-5

The probabilites are derived from the scores and the negative log-probabilities corresponding
to the labels are averaged:

    probabilites = exp.(scores) ./ sum(exp.(scores),dims=1)
    -(log(probabilites[labels[1],1]) + log(probabilites[labels[2],2]))/2
    # returns 2.1657e-5

"""
function nll(scores,labels::AbstractArray{<:Integer}; dims=1, average=true)
    indices = findindices(scores,labels,dims=dims)
    lp = logsoftmax(scores,dims=dims)[indices]
    average ? (-sum(lp) / length(lp)) : (-sum(lp), length(lp))
end

"""
    accuracy(scores, labels; dims=1, average=true)

Given an unnormalized `scores` matrix and an `Integer` array of correct `labels`, return the
ratio of instances where the correct label has the maximum score. `dims=1` means instances are
in columns, `dims=2` means instances are in rows. Use `average=false` to return the pair
(ncorrect,count) instead of the ratio (ncorrect/count). The valid labels should be integers in
the range `1:numclasses`, if `labels[i] == 0`, instance i is skipped.

"""
function accuracy(scores,labels::AbstractArray{<:Integer}; dims=1, average=true)
    indices = findindices(scores,labels,dims=dims)
    ycpu = convert(Array,value(scores))
    (maxval,maxind) = findmax(ycpu,dims=dims)
    maxind = LinearIndices(ycpu)[maxind]
    maxind = vec(maxind)[vec(labels) .!= 0]
    correct = (maxind .== indices)
    average ? (sum(correct) / length(correct)) : (sum(correct), length(correct))
end

"""
    logistic(scores, labels; average=true)

Computes logistic loss given predicted unnormalized scores and answer labels for a binary
prediction task.

    log.(1 .+ exp.(-labels .* scores))

Label values should be {-1,1}. Scores are unrestricted.  The return value is `(total/count)`
if `average=true` and `(total,count)` if `average=false` where `count` is the number of
instances and `total` is their total loss.

See also `bce` which computes the same loss with {0,1} labels.

Reference: https://towardsdatascience.com/nothing-but-numpy-understanding-creating-binary-classification-neural-networks-with-e746423c8d5c
"""
function logistic(scores, labels::AbstractVector{<:Integer}; average=true)
    labels = ((labels .+ 1) .÷ 2) # (-1,1)->(0,1)
    bce(scores,labels; average=average)
end

"""
    bce(scores, labels; average=true)

Computes binary cross entropy loss given predicted unnormalized scores and answer labels for a
binary prediction task. Label values should be in {0,1}. Scores are unrestricted and will be
converted to probabilities using

    probs = 1 ./ (1 .+ exp.(-scores))

The loss calculated is

    -(labels .* log.(probs) .+ (1 .- labels) .* log.(1 .- probs))

The return value is `(total/count)` if `average=true` and `(total,count)` if `average=false`
where `count` is the number of instances and `total` is their total loss.

See also `logistic` which computes the same loss with {-1,1} labels.

Reference: https://towardsdatascience.com/nothing-but-numpy-understanding-creating-binary-classification-neural-networks-with-e746423c8d5c
"""
function bce(scores, labels::AbstractVector{<:Integer}; average=true)
    labels = oftype(scores,labels)
    l = max.(0, scores) .- labels .* scores .+ log.(1 .+ exp.(-abs.(scores)))
    average ? (sum(l)/length(l)) : (sum(l),length(l))
end

# Indexing help for nll and accuracy

function findindices(scores, labels::AbstractArray{<:Integer}; dims=1)
    ninstances = length(labels)
    nindices = 0
    indices = Vector{Int}(undef,ninstances)
    if dims == 1                   # instances in first dimension
        y1 = size(scores,1)
        y2 = div(length(scores),y1)
        if ninstances != y2; throw(DimensionMismatch()); end
        @inbounds for j=1:ninstances
            if labels[j] == 0; continue; end
            indices[nindices+=1] = (j-1)*y1 + labels[j]
        end
    elseif dims == 2               # instances in last dimension
        y2 = size(scores,ndims(scores))
        y1 = div(length(scores),y2)
        if ninstances != y1; throw(DimensionMismatch()); end
        @inbounds for j=1:ninstances
            if labels[j] == 0; continue; end
            indices[nindices+=1] = (labels[j]-1)*y1 + j
        end
    else
        error("findindices only supports dims = 1 or 2")
    end
    return (nindices == ninstances ? indices : view(indices,1:nindices))
end

"""
    nll(model; data, dims=1, average=true, o...)

Compute the negative log likelihood for a model over a dataset:

    nll(model(inputs; kwargs...), labels; dims) for (inputs,labels) in data

and return `(total/count)` if `average=true` or `(total,count)` if `average=false` where
`count` is the number of instances not skipped (instances with `label==0` are skipped) and
`total` is their total negative log likelihood.

The `model` should be a function returning scores given inputs, and data should be an iterable
of `(inputs,labels)` pairs. The valid labels should be integers in the range `1:numclasses`,
if `labels[i] == 0`, instance i is skipped.

"""
function nll(model; data, dims=1, average=true, o...)
    sum = cnt = 0
    for (x,y) in data
        (z,n) = nll(model(x; o...), y; dims=dims, average=false)
        sum += z; cnt += n
    end
    average ? sum / cnt : (sum, cnt)
end

"""
    accuracy(model; data, dims=1, average=true, o...)

Compute the number of correct predictions of a model over a dataset:

    accuracy(model(inputs; kwargs...), labels; dims) for (inputs,labels) in data

and return `(ncorrect/count)` if `average=true` or `(ncorrect,count)` if `average=false` where
`count` is the number instances not skipped (instances with `label==0` are skipped) and
`ncorrect` is the number of them correctly labeled by the model.

The `model` should be a function returning scores given inputs, and data should be an iterable
of `(inputs,labels)` pairs. The valid labels should be integers in the range `1:numclasses`,
if `labels[i] == 0`, instance i is skipped.

"""
function accuracy(model; data, dims=1, average=true, o...)
    sum = cnt = 0
    for (x,y) in data
        (z,n) = accuracy(model(x; o...), y; dims=dims, average=false)
        sum += z
        cnt += n
    end
    average ? sum / cnt : (sum, cnt)
end

# DEPRECATE:

# The two-arg model,data calls cause mix-ups with the scores,labels calls:
function nll(model,data; o...)
    @warn "nll(model,data; o...) is deprecated, please use nll(model; data=data, o...)" maxlog=1
    nll(model; data=data, o...)
end

function accuracy(model,data; o...)
    @warn "accuracy(model,data; o...) is deprecated, please use accuracy(model; data=data, o...)" maxlog=1
    accuracy(model; data=data, o...)
end

# We need the (model,x,y) interface to implement regularization:
function nll(f, x, y; dims=1, average=true, o...)
    @warn "nll(f,x,y; o...) is deprecated, please use nll(f(x),y; o...) instead." maxlog=1
    nll(f(x; o...), y; dims=dims, average=average)
end

function accuracy(f, x, y; dims=1, average=true, o...)
    @warn "accuracy(f,x,y; o...) is deprecated, please use accuracy(f(x),y; o...) instead." maxlog=1
    accuracy(f(x; o...), y; dims=dims, average=average)
end

# We need the (weights,data,predict) interface to support the old interface:
function nll(w, data, f::Function; dims=1, average=true, o...)
    @warn "nll(weights,data,func; o...) is deprecated, please use nll(x->func(weights,x); data=data, o...) instead." maxlog=1
    nll(x->f(w,x;o...), data; dims=dims, average=average)
end

function accuracy(w, data, f::Function; dims=1, average=true, o...)
    @warn "accuracy(weights,data,func; o...) is deprecated, please use accuracy(x->func(weights,x); data=data, o...) instead." maxlog=1
    accuracy(x->f(w,x;o...), data; dims=dims, average=average)
end

"zeroone loss is equal to 1 - accuracy"
function zeroone(x...; o...)
    @warn "zeroone() is deprecated, please use 1-accuracy()" maxlog=1
    1 - accuracy(x...; o...)
end


using NumericalMethodsforEngineers, Plots

old = pwd()
ProjDir = dirname(@__FILE__)
cd(ProjDir) do

  f(x::Float64, y::Vector{Float64}) = (x + y[1])/x
  steps = 3
  h = 0.333

  x = 2.0
  y = [2.0]

  # y(3.0) = 4.2165

  r = Array{Float64,2}[]
  push!(r, euler(f, x, y, steps, h))
  push!(r, modified_euler(f, x, y, steps, h))
  push!(r, mid_point_euler(f, x, y, steps, h))
  push!(r, runga_kutta_4(f, x, y, steps, h))
  println()
  r |> display
  println()

  xs = x + Float64[(i-1)*h for i in 1:steps+1]
  titles = ["Euler", "Modified_Euler", "Mid_Point_Euler", "Runga_Kutta_4"]

  p1 = plot(xs, r[1][:, 2])
  plot!(p1, xs, r[2][:, 2])
  plot!(p1, xs, r[3][:, 2])
  plot!(p1, xs, r[4][:, 2])
  show(p1)
  savefig("Ex7.2.png")
  gui()

end


import RowEchelon
import LinearAlgebra

#=
Homework 7
Section 3.2
Problems 9
=#

A = [1 -1 -3 0 ;
     0 1 5 4 ;
     -1 2 8 5;
     3 -1 -2 3]

detA = LinearAlgebra.det(A)

println("Original Matrix:")
println(A)
println("Determinate:")
println(detA)


# Utilities for tests.

using Distributions
using LinearAlgebra: norm
using Random
using Test: @test

# Add noise on top of one factorisation WH
function add_matrix_noise(;WH::Matrix{T},
                        dist_E::Distribution=Uniform(0,1),
                        rng::AbstractRNG=MersenneTwister(1234)) where T <: Real
    max.(WH + rand(rng, dist_E, size(WH,1), size(WH, 2)), 0)
end

# Simulate one matrix factorisation V = W*H + E
function get_one_simulated_V()
    F, K, N, rng = 10, 5, 25, MersenneTwister(1234)
    W, H = rand(rng, Uniform(0,1), F, K), rand(rng, Uniform(0,1), K, N)

    add_matrix_noise(WH=W*H, dist_E=Uniform(0,0.1), rng=rng)
end

# Simulate different noises on top of one factorisation WH
function get_many_simulated_V()
    F, K, N = 10, 5, 25
    rng = MersenneTwister(1234)
    W, H = rand(rng, Uniform(0,1), F, K), rand(rng, Uniform(0,1), K, N)

    #### simulate different factorisation
    noise_Vs = Matrix{Float64}[]
    noise_names = String[]

    push!(noise_Vs, add_matrix_noise(WH=W*H, dist_E=Uniform(0,0.01), rng=rng))
    push!(noise_names, "uniform_small")
    push!(noise_Vs, add_matrix_noise(WH=W*H, dist_E=Uniform(0,0.2), rng=rng))
    push!(noise_names, "uniform_large")
    push!(noise_Vs, add_matrix_noise(WH=W*H, dist_E=Normal(0,0.01), rng=rng))
    push!(noise_names, "normal_small")
    push!(noise_Vs, add_matrix_noise(WH=W*H, dist_E=Normal(0,0.2), rng=rng))
    push!(noise_names, "normal_large")
    push!(noise_Vs, add_matrix_noise(WH=W*H, dist_E=Gamma(0.05,1), rng=rng))
    push!(noise_names, "gamma_small")
    push!(noise_Vs, add_matrix_noise(WH=W*H, dist_E=Gamma(0.05,4), rng=rng))
    push!(noise_names, "gamma_large")

    noise_Vs, noise_names
end

# Simulate V, W and H.
function simulate_VWH(;F::Integer,
                      N::Integer,
                      K::Integer,
                      dist_W::Distribution=Uniform(0,1),
                      dist_H::Distribution=Uniform(0,1),
                      dist_E::Distribution=Uniform(0,0.1),
                      rng::AbstractRNG=MersenneTwister(1234))
    W = rand(rng, dist_W, F, K)
    H = rand(rng, dist_H, K, N)
    E = rand(rng, dist_E, F, N)

    W*H+E, W, H
end


using LinearAlgebra

function swap_rows(i::T, nlinha::T) where {T<:Integer}
    for n ∈ (i+1):nlinha        # iterate over lines above to check if could be swap
        if A[n,i] ≠ 0.0         # condition to swap row
            L = copy(A[i,:])    # copy line to swap
            A[i,:] = A[n,:]     # swap occur
            A[n,:] = L
            break
        end
    end
end

function gauss_jordan(A::Matrix{T}) where {T<:Number}

    # convert to float to avoid InexactError: Int64()
    (T <: Integer) && (A = convert.(Float64, A))

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

    for i ∈ axes(A, 1)
        if A[i,i] == 0.0                            # check if need swap rows
            swap_rows(i, m)
        end

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

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

    return A
end

## logic in the for loops
# A[1,:] = A[1,:]./ A[1,1]
# A[2,:] = A[2,:] - A[1,:]*A[2,1]
# A[3,:] = A[3,:] - A[1,:]*A[3,1]

# A[2,:] = A[2,:]./ A[2,2]
# A[1,:] = A[1,:] - A[2,:].*A[1,2]
# A[3,:] = A[3,:] - A[2,:].*A[3,2]

# A[3,:] = A[3,:]./ A[3,3]
# A[1,:] = A[1,:] - A[3,:].*A[1,3]
# A[2,:] = A[2,:] - A[3,:].*A[2,3]


module test
function sir_ode(du, u, p, t)
    #Infected per-Capita Rate
    β = p[1]
    #Recover per-capita rate
    γ = p[2]

    #Susceptible Individuals
    S = u[1]
    #Infected by Infected Individuals
    I = u[2]

    du[1] = -β * S * I
    du[2] = β * S * I - γ * I
    du[3] = γ * I
end

function sir_ode2(du,u,p,t)
    S,I,R = u
    b,g = p
    du[1] = -b*S*I
    du[2] = b*S*I-g*I
    du[3] = g*I
end

using DifferentialEquations

function main()
#Pram = (Infected Per Capita Rate, Recover Per Capita Rate)
pram = [0.1,0.05]
#Initial Prams = (Susceptible Individuals, Infected by Infected Individuals)
init = [0.99,0.01,0.0]
tspan = (0.0,200.0)

sir_prob = ODEProblem(sir_ode, init, tspan, pram)

sir_sol = solve(sir_prob, saveat = 0.1);

parms = [0.1,0.05]
init = [0.99,0.01,0.0]
tspan = (0.0,200.0)
sir_prob2 = ODEProblem(sir_ode2,init,tspan,parms)
sir_sol = solve(sir_prob2,saveat = 0.1)
end

end


isharshad(x)   = x % sum(digits(x)) == 0
nextharshad(x) = begin while !isharshad(x+1) x += 1 end; return x + 1 end

function harshads(n::Integer)
	h = Vector{typeof(n)}(n)
	h[1] = 1
	for j in 2:n
		h[j] = nextharshad(h[j-1])
	end
	return h
end

println("First 20 harshad numbers: ", join(harshads(20), ", "))
println("First harshad number after 1001: ", nextharshad(1000))


import Base.show

using Colors
using CSV
using LinearAlgebra
using Plots

"""
    getpoints(path::String, [usingnames::Bool])

Read the x and y coordinates from a list in a CSV file at `path`.

Also read the names if `usingnames`.

# Example
```julia-repl
julia> getpoints("points.txt")
```
"""
function getpoints(path::String, usingnames::Bool = false)
    xs = Int64[]
    ys = Int64[]
    names = String[]
    for row ∈ CSV.Rows(path)
        push!(xs,parse(Int64,row.x))
        push!(ys,parse(Int64,row.y))
        if usingnames
            push!(names,row.name)
        end
    end
    return usingnames ? (names, xs, ys) : (xs, ys)
end

"""
    bruteforcevoronoi(xs::Vector{Int64}, ys::Vector{Int64})

Draw a Voronoi diagram from lists of coordinates `xs` and `ys` by individually colouring every square of a 1000x1000 grid.

# Example
```julia-repl
julia> bruteforcevoronoi(rand(1:1000, 10), rand(1:1000, 10))
```
"""
function bruteforcevoronoi(xs::Vector{Int64}, ys::Vector{Int64})
    m = length(xs)
    colours = distinguishable_colors(m, lchoices = 50:5:100)

    pointcolours = fill(RGB(0,0,0), (1000,1000))
    for y ∈ 1:1000
        for x ∈ 1:1000
            pointcolours[x,y] = colours[findmin(hypot.(xs .- x, ys .- y))[2]]
        end
    end

    diagram = scatter(
        [(x,y) for x ∈ 1:1000, y ∈ 1:1000][:];
        c = pointcolours[:],
        markersize = 1,
        markerstrokewidth = 0,
        legend = false,
        showaxis = false,
        ticks = false,
        size = (1000, 1000)
    )
    return scatter!(
        diagram,
        xs,
        ys;
        c = :black,
        markersize = 2,
        markerstrokewidth = 0
    )
end

"""
    montecarlovoronoi(n::Int64, xs::Vector{Int64}, ys::Vector{Int64})

Sample `n` times from a 1000x1000 grid to approximate a Voronoi diagram from lists of coordinates `xs` and `ys`.

# Example
```julia-repl
julia> montecarlovoronoi(100000, rand(1:1000, 10), rand(1:1000, 10))
```
"""
function montecarlovoronoi(n::Int64, xs::Vector{Int64}, ys::Vector{Int64})
    m = length(xs)
    colours = distinguishable_colors(m, lchoices = 50:5:100)

    randxs = Int64[]
    randys = Int64[]
    randcolours = RGB[]

    for _ ∈ 1:n
        x, y = rand(1:1000), rand(1:1000)
        push!(randxs, x)
        push!(randys, y)
        push!(randcolours, colours[findmin(hypot.(xs .- x, ys .- y))[2]])
    end

    diagram = scatter(
        randxs,
        randys;
        c = randcolours,
        markersize = 1,
        markerstrokewidth = 0,
        legend = false,
        showaxis = false,
        ticks = false,
        size = (1000, 1000)
    )
    return scatter!(
        diagram,
        xs,
        ys;
        c = :black,
        markersize = 2,
        markerstrokewidth = 0,
    )
end

"""
    VoronoiPoint(x::Real, y::Real, neighbours::Vector{VoronoiPoint})
    VoronoiPoint(x::Real, y::Real)

Construct a `VoronoiPoint` at the coordinates `x`, `y`, representing a point in a Voronoi diagram.

If no neighbours are specified, default to an empty `Vector{VoronoiPoint}`.

# Examples
```julia-repl
julia> p = VoronoiPoint(3,4)
(3,4)

julia> q = VoronoiPoint(4,5,[p])
(4,5)

julia> p.neighbours
VoronoiPoint[]

julia> q.neighbours
1-element Vector{VoronoiPoint}:
 (3,4)
```
"""
mutable struct VoronoiPoint
    x::Real
    y::Real
    neighbours::Vector{VoronoiPoint}
end
VoronoiPoint(x::T, y::T) where T <: Real = VoronoiPoint(x, y, VoronoiPoint[])

show(io::IO, p::VoronoiPoint) = print(io, "(", p.x, ",", p.y, ")")

"""
    pos(p::VoronoiPoint)

Return the `x` and `y` coordinates of the `VoronoiPoint` `p` as a `Vector`.

# Example
```julia-repl
julia> pos(VoronoiPoint(3,4))
2-element Vector{Int64}:
 3
 4
```
"""
pos(p::VoronoiPoint) = [p.x, p.y]

"""
    perpbisector(p::VoronoiPoint, q::VoronoiPoint)

Return the midpoint of `VoronoiPoint`s `p` and `q`, and direction vector along the perpendicular bisector between of the same length as `p - q` and direction such that `p` is to the right and `q` is to the left.

# Example
```julia-repl
julia> perpbisector(VoronoiPoint(0,1), VoronoiPoint(2,1))
([1.0, 1.0], [0, -2])
```
"""
function perpbisector(p::VoronoiPoint, q::VoronoiPoint)
    return ( (pos(p) + pos(q))/2 , [[0,1] [-1,0]] * (pos(p) - pos(q)) )
end

"""
    circumcentre(p::VoronoiPoint, q::VoronoiPoint, r::VoronoiPoint)

Return the circumcentre of the three `VoronoiPoint`s `p`, `q`, and `r`.

If they are collinear, return `[Inf, Inf]`.

# Example
```julia-repl
julia> circumcentre(VoronoiPoint(0,0), VoronoiPoint(1,0), VoronoiPoint(0,1))
2-element Vector{Float64}:
 0.5
 0.5
```
"""
function circumcentre(p::VoronoiPoint, q::VoronoiPoint, r::VoronoiPoint)
    (x₁, v₁) = perpbisector(p,q)
    (x₂, v₂) = perpbisector(p,r)
    det([v₁ v₂]) == 0 && return [Inf, Inf]
    M = [v₁ v₂]^-1
    t₁ = (M * (x₂ - x₁))[1]
    return x₁ + t₁*v₁
end

"""
    findnextpoint(p::VoronoiPoint, q::VoronoiPoint, z::Vector{T}, reversedirection::Bool) where T <: Real

Find the `r` from `q.neighbours` where the circumcentre of `p`, `q`, and `r` is closest to `z` in the direction determined by `perpbisector`.

If `reversedirection == true`, find `r` based on the opposite direction.

# Examples
```julia-repl
julia> p = VoronoiPoint(0,0)
(0,0)

julia> q = VoronoiPoint(0,1,[p, VoronoiPoint(3,4), VoronoiPoint(-3,4)])
(0,1)

julia> findnextpoint(p, q, [0, 0.5], false)
(3,4)

julia> findnextpoint(p, q, [0, 0.5], true)
(-3,4)
```
"""
function findnextpoint(p::VoronoiPoint, q::VoronoiPoint, z::Vector{T}, reversedirection::Bool) where T <: Real
    directionvector = perpbisector(p,q)[2] .* (-1)^reversedirection

    mindisplacement = Inf
    nextpoint = VoronoiPoint(Inf,Inf)
    for r ∈ q.neighbours
        r ∈ [p,q] && continue
        displacement = dot(directionvector, circumcentre(p,q,r) - z)
        displacement > 0 && displacement < mindisplacement && (mindisplacement = displacement; nextpoint = r)
    end
    return nextpoint
end

"""
    addpoint(x::Real, y::Real, allpoints::Vector{VoronoiPoint})

Add a new `VoronoiPoint` at coordinates `x` and `y`, including calculating all neighbours from `allpoints`.

Also, update neighbour lists of other points in `allpoints` to agree with the new addition.

# Example
```julia-repl
julia> p = VoronoiPoint(1,1)
(1,1)

julia> q = VoronoiPoint(2,2)
(2,2)

julia> p.neighbours = [VoronoiPoint(Inf,Inf), q, VoronoiPoint(Inf,Inf)]
3-element Vector{VoronoiPoint}:
 (Inf,Inf)
 (2,2)
 (Inf,Inf)

julia> q.neighbours = [VoronoiPoint(Inf,Inf), p, VoronoiPoint(Inf,Inf)]
3-element Vector{VoronoiPoint}:
 (Inf,Inf)
 (1,1)
 (Inf,Inf)

julia> r = addpoint(1, 2, [p,q])
(1,2)

julia> p.neighbours
4-element Vector{VoronoiPoint}:
 (Inf,Inf)
 (2,2)
 (1,2)
 (Inf,Inf)

julia> q.neighbours
4-element Vector{VoronoiPoint}:
 (Inf,Inf)
 (1,2)
 (1,1)
 (Inf,Inf)

julia> r.neighbours
4-element Vector{VoronoiPoint}:
 (Inf,Inf)
 (1,1)
 (2,2)
 (Inf,Inf)
```
"""
function addpoint(x::Real, y::Real, allpoints::Vector{VoronoiPoint})
    p = VoronoiPoint(x, y)
    nearestpoint = allpoints[findmin([hypot(x-q.x, y-q.y) for q ∈ allpoints])[2]]

    z = perpbisector(p, nearestpoint)[1]
    q = nearestpoint
    reversedirection = false
    while true
        push!(p.neighbours, q)
        r = findnextpoint(p, q, z, reversedirection)

        # Edge goes to infinity
        if pos(r) == [Inf,Inf]
            push!(p.neighbours, r)
            if reversedirection
                break
            else
                reversedirection = true
                q = nearestpoint
                z = perpbisector(p, nearestpoint)[1]
                pop!(reverse!(p.neighbours))
                continue
            end

        # Back to start
        elseif pos(r) == pos(nearestpoint)
            reverse!(p.neighbours)
            break

        else
            z = circumcentre(p,q,r)
            q = r
        end
    end

    n = length(p.neighbours)
    for i ∈ 1:n
        q = p.neighbours[i]
        pos(q) == [Inf, Inf] && continue
        j = findlast(r -> pos(r) == pos(p.neighbours[mod(i-1,1:n)]), q.neighbours)
        k = findfirst(r -> pos(r) == pos(p.neighbours[mod(i+1,1:n)]), q.neighbours)
        if j < k
            q.neighbours = vcat(q.neighbours[j:k], p)
        else
            q.neighbours = vcat(q.neighbours[1:k], p, q.neighbours[j:end])
        end
    end

    return p
end

"""
    voronoipoints(xs::Vector{T}, ys::Vector{T}) where T <: Real

Calculate the lists of neighbouring regions of a Voronoi diagram with points at coordinates given by `xs` and `ys`.

Return a list of `VoronoiPoint`s containing this information, with `Rational` parameters.

# Example
```julia-repl
voronoipoints([1,2,3],[2,1,3])
julia> points = voronoipoints([1,2,3],[2,1,3])
3-element Vector{VoronoiPoint}:
 (1//1,2//1)
 (2//1,1//1)
 (3//1,3//1)

julia> points[1].neighbours
4-element Vector{VoronoiPoint}:
 (Inf,Inf)
 (2//1,1//1)
 (3//1,3//1)
 (Inf,Inf)

julia> points[2].neighbours
4-element Vector{VoronoiPoint}:
 (Inf,Inf)
 (3//1,3//1)
 (1//1,2//1)
 (Inf,Inf)

julia> points[3].neighbours
4-element Vector{VoronoiPoint}:
 (Inf,Inf)
 (1//1,2//1)
 (2//1,1//1)
 (Inf,Inf)
```
"""
function voronoipoints(xs::Vector{T}, ys::Vector{T}) where T <: Real
    # Rational numbers!
    xs, ys = Rational{BigInt}.(xs), Rational{BigInt}.(ys)
    points = [VoronoiPoint(xs[1],ys[1],[VoronoiPoint(Inf,Inf)])]
    for i ∈ 2:length(xs)
        push!(points,addpoint(xs[i],ys[i],points))
    end
    return points
end

"""
    voronoi(xs::Vector{T}, ys::Vector{T}; [names::Vector{String}]) where T <: Real

Draw a Voronoi diagram from points at coordinates `xs` and `ys` in the region (x,y) ∈ [0,1000]².

Points given `names` in a legend if specified.

# Example
```julia-repl
julia> voronoi(rand(1:1000, 10), rand(1:1000, 10))
```
"""
function voronoi(xs::Vector{T}, ys::Vector{T}; names::Vector{String} = String[]) where T <: Real
    points = voronoipoints(xs,ys)
    m = length(xs)
    colours = distinguishable_colors(m, lchoices = 50:5:100)
    usingnames = !isempty(names)

    polygons = Shape[]
    for p ∈ points
        n = length(p.neighbours)
        vertices = Tuple{Rational{Int64}, Rational{Int64}}[]

        if pos(p.neighbours[1]) == [Inf,Inf]
            startindex, endindex = 2, n-2

            x₁, v₁ = perpbisector(p.neighbours[n-1],p)
            x₁, v₁ = Rational{Int64}.(x₁), Rational{Int64}.(v₁)
            # Values of t₁ where the line x₁ + t₁v₁ intersects the borders
            t₁vals = [-x₁[2]/v₁[2], (1000-x₁[1])/v₁[1], (1000-x₁[2])/v₁[2], -x₁[1]/v₁[1]]
            t₁ = minimum(t₁vals[t₁vals .> 0])
            push!(vertices,Tuple(x₁ + t₁*v₁))

            x₂, v₂ = perpbisector(p,p.neighbours[2])
            x₂, v₂ = Rational{Int64}.(x₂), Rational{Int64}.(v₂)
            t₂vals = [-x₂[2]/v₂[2], (1000-x₂[1])/v₂[1], (1000-x₂[2])/v₂[2], -x₂[1]/v₂[1]]
            t₂ = minimum(t₂vals[t₂vals .> 0])

            t₁index = findfirst(==(t₁),t₁vals)
            t₂index = findfirst(==(t₂),t₂vals)
            indexdifference = mod(t₂index - t₁index,4)
            corners = [(0,0), (1000,0), (1000,1000), (0,1000)]
            for j ∈ 1:indexdifference
                push!(vertices,corners[mod(t₁index+j, 1:4)])
            end

            push!(vertices,Tuple(x₂ + t₂*v₂))

        else
            startindex, endindex = 1, n
        end

        for j ∈ startindex:endindex
            q,r = p.neighbours[j], p.neighbours[mod(j+1,1:n)]
            push!(vertices,Tuple(circumcentre(p,q,r)))
        end

        push!(polygons, Shape(vertices))
    end

    diagram = plot(
        polygons,
        c = colours',
        label = (usingnames ? hcat(names...) : false),
        showaxis = false,
        ticks = false,
        size = (1000, 1000),
        xlims = (0, 1000),
        ylims = (0, 1000)
    )
    return scatter!(diagram, xs, ys, c = :black, markersize = 2, label = false)
end

"""
    voronoidistribution(points::Vector{VoronoiPoint})

Count the number of edges of each of the polygons from a Voronoi diagram, as represented by `points`.

`points` should usually be the output of the function `voronoipoints`.

Display the counts in a bar chart.

# Example
```julia-repl
voronoidistribution(voronoipoints(rand(1:1000, 10), rand(1:1000, 10)))
```
"""
function voronoidistribution(points::Vector{VoronoiPoint})
    neighbourcount = broadcast(p -> pos(p.neighbours[1]) == [Inf, Inf] ? 0 : length(p.neighbours), points)

    m = maximum(neighbourcount)
    total = count(!=(0), neighbourcount)
    return bar(
        1:m,
        [count(==(i), neighbourcount)//total for i ∈ 1:m],
        legend = false,
        xticks = 1:m,
        yticks = 0:0.1:1
    )
end

"""
    randomlattice(shape::Symbol, n::Int64, σ::Rational{Int64})

Create a lattice of `n^2` points, with each perturbed randomly both vertically and horizontally by a normal distribution of standard deviation `σ`.

If `shape == :square`, the lattice is a square lattice.

If `shape == :hexagonal`, the lattice is an approximately equilateral hexagonal lattice.

Return a bar chart of the distribution of edges of the polygons in the Voronoi diagram generated by these points.

# Examples
```julia-repl
julia> randomlattice(:square, 10, 1)

julia> randomlattice(:hexagonal, 20, 1//10)
```
"""
function randomlattice(shape::Symbol, n::Int64, σ::Rational{Int64})
	if shape == :square
		coords = [(x,y) for x ∈ 1:n, y ∈ 1:n]
	elseif shape == :hexagonal
		coords = hcat([(x,(6//7)*y) for x ∈ 1:n, y ∈ 1:2:n],
			[(x+1//2,(6//7)*y) for x ∈ 1:n, y ∈ 2:2:n])
	else
		error("Invalid shape")
	end

	xs,ys = [xy[1] for xy ∈ coords[:]], [xy[2] for xy ∈ coords[:]]
	randxs = xs .+ σ * round.(Int64, n * randn(n^2))//n
	randys = ys .+ σ * round.(Int64, n * randn(n^2))//n
	return voronoidistribution(voronoipoints(randxs,randys))
end


using Test
import Geometry

@testset "volume" begin

    solids = [
        (Geometry.Cylinder(1, 2), pi),
        (Geometry.Cone(1, [2]), 1.04720),
        (Geometry.Cube(4), 64.0),
        (Geometry.RectangularPrism(3, 5, 7), 105.0)
    ]

    for i in solids
        @test round(Geometry.volume(i[1]), digits=5) === round(i[2], digits=5)
    end

    # @test_throws MethodError foo(x, y)
end

@testset "lateralarea" begin

    solids = [
        (Geometry.Cylinder(1, 2), 6.28319),
        (Geometry.Cone(1, [2]), 4.44288),
        (Geometry.Cube(4), 64.0),
        (Geometry.RectangularPrism(3, 5, 7), 72)
    ]

    for i in solids
        @test round(Geometry.lateralarea(i[1]), digits=5) === round(i[2], digits=5)
    end

    # @test_throws MethodError foo(x, y)
end

@testset "surfacearea" begin

    solids = [
        (Geometry.Cylinder(1, 2), 12.56637),
        (Geometry.Cone(1, [2]), 7.58448),
        (Geometry.Cube(4), 6*4^2),
        (Geometry.RectangularPrism(3, 5, 7), 142.0)
    ]

    for i in solids
        @test round(Geometry.surfacearea(i[1]), digits=5) === round(i[2], digits=5)
    end

    # @test_throws MethodError foo(x, y)
end

# @testset "methods" begin
#     ms = [x.sig for x in Geometry.methods(Geometry.volume)]
#     for i in ms
#         println(i.parameters[2])
#     end
# end

# @testset "bar" begin
#     z = 4.
#     @test bar(z) == 1.
# end


ENV["GKSwstype"]="100" #src

# ### Optimizing Julia code is often done at the expense of transparency

using Random, LinearAlgebra, BenchmarkTools

function test(A, B, C)
    C = C - A * B
    return C
end

A = rand(1024, 256); B = rand(256, 1024); C = rand(1024, 1024)
@btime test(A, B, C); #C, A and B are matrices.
#md nothing # hide

#md # --

function test_opt(A, B, C)
    BLAS.gemm!('N','N', -1., A, B, 1., C)
    return C
end
@btime test_opt(A, B, C) # avoids taking two unnecessary copies of the matrix C.
#md nothing # hide

#md # --

C = rand(1024, 1024)
all(test(A, B, C) .== test_opt(A, B, C))

#md # ---

# ### Derivative computation with FFT

using FFTW

xmin, xmax, nx = 0, 4π, 1024
ymin, ymax, ny = 0, 4π, 1024

x = range(xmin, stop=xmax, length=nx+1)[1:end-1]
y = range(ymin, stop=ymax, length=ny+1)[1:end-1]

ky  = 2π ./ (ymax-ymin) .* [0:ny÷2-1;ny÷2-ny:-1]
exky = exp.( 1im .* ky' .* x)

function df_dy( f, exky )
    ifft(exky .* fft(f, 2), 2)
end

f = sin.(x) .* cos.(y') # f is a 2d array created by broadcasting

@btime df_dy(f, exky)
#md nothing # hide

#md # ---

# ### Memory alignement, and inplace computation.

f  = zeros(ComplexF64, (nx,ny))
fᵗ = zeros(ComplexF64, reverse(size(f)))
f̂ᵗ = zeros(ComplexF64, reverse(size(f)))

f .= sin.(x) .* cos.(y')

fft_plan = plan_fft(fᵗ, 1, flags=FFTW.PATIENT)

function df_dy!( f, fᵗ, f̂ᵗ, exky )
    transpose!(fᵗ,f)
    mul!(f̂ᵗ,  fft_plan, fᵗ)
    f̂ᵗ .= f̂ᵗ .* exky
    ldiv!(fᵗ, fft_plan, f̂ᵗ)
    transpose!(f, fᵗ)
end

@btime df_dy!(f, fᵗ, f̂ᵗ, exky )
#md nothing # hide

#md # ---

#md # # Why use Julia language!
#md #
#md # - **You develop in the same language in which you optimize.**
#md # - Packaging system is very efficient (3173 registered packages)
#md # - PyPi (198,360 projects) R (14993 packages)
#md # - It is very easy to create a package (easier than R and Python)
#md # - It is very easy to use GPU device.
#md # - Nice interface for Linear Algebra and Differential Equations
#md # - Easy access to BLAS and LAPACK
#md # - Julia talks to all major Languages - mostly without overhead!

#md # ---

#md # # What's bad
#md #
#md # - It is still hard to build shared library or executable from Julia code.
#md # - Compilation times can be unbearable.
#md # - Plotting takes time (20 seconds for the first plot)
#md # - OpenMP is better than the Julia multithreading library but it is progressing.
#md # - There is a MPI and PETSc package but they are not very active.
#md # - For parallelization, The Julia community seems to prefer the ditributed processing approach.
#md # - Does not work well with vectorized code, you need to do a lot of inplace computation to avoid memory allocations and use explicit views to avoid copy.
#md # - Julia website proclaims that it is faster than Fortran but this is not true. But it is very close and it is progressing.
#md
#md # [What's Bad About Julia by Jeff Bezanson](https://www.youtube.com/watch?v=TPuJsgyu87U)

#md # ---

#md # ## So when should i use Julia?
#md #
#md # - Now! If you need performance and plan to write your own libraries.
#md # - In ~1-2 Years if you want a smooth deploy.
#md # - In ~3-5 Years if you want a 100% smooth experience.
#md
#md # ## Julia Munich Meetup
#md # Every two months, poll for the next meeting that
#md # will take place at Garching Campus : https://doodle.com/poll/z3ft2dytnaebyhh7.
#md #
#md # ## Python-Julia benchmarks by Thierry Dumont
#md #
#md # https://github.com/Thierry-Dumont/BenchmarksPythonJuliaAndCo/wiki
#md
#md # ---
#md
#md # # Julia is a language made for Science.
#md #
#md #  [Some State of the Art Packages](http://www.stochasticlifestyle.com/some-state-of-the-art-packages-in-julia-v1-0)
#md #
#md #  * JuliaDiffEq – Differential equation solving and analysis.
#md #  * JuliaDiff – Differentiation tools.
#md #  * JuliaGeometry – Computational Geometry.
#md #  * JuliaGraphs – Graph Theory and Implementation.
#md #  * JuliaIntervals - Rigorous numerics with interval arithmetic & applications.
#md #  * JuliaMath – Mathematics made easy in Julia.
#md #  * JuliaOpt – Optimization.
#md #  * JuliaPolyhedra – Polyhedral computation.
#md #  * JuliaSparse – Sparse matrix solvers.
#md #  * JuliaStats – Statistics and Machine Learning.
#md #  * JuliaPlots - powerful convenience for visualization.
#md #  * JuliaGPU - GPU Computing for Julia.
#md #  * FluxML - The Elegant Machine Learning Stack.
#md #

@test true #src


### A Pluto.jl notebook ###
# v0.12.16

using Markdown
using InteractiveUtils

# ╔═╡ e077ab16-22e5-11eb-15ed-5f0654bf7328
using Pkg, DrWatson

# ╔═╡ e75a9a06-22e5-11eb-2e36-8d4f62b830ed
begin
	@quickactivate "StatisticsWithJuliaPlutoNotebooks"
	using Random, Distributions, Plots, LaTeXStrings
	Random.seed!(0)
end;

# ╔═╡ ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
md"## Listing5.03"

# ╔═╡ fa68607e-22e5-11eb-0558-c9a4d9f77426
function statPair(dist,n)
    sample = rand(dist,n)
    [mean(sample),var(sample)]
end

# ╔═╡ 1111d8c4-252c-11eb-3813-81de856f7fba
begin
	stdUni = Uniform(-sqrt(3),sqrt(3))
	n, N = 3, 10^4

	dataUni     = [statPair(stdUni,n) for _ in 1:N]
	dataUniInd  = [[mean(rand(stdUni,n)),var(rand(stdUni,n))] for _ in 1:N]
	dataNorm    = [statPair(Normal(),n) for _ in 1:N]
	dataNormInd = [[mean(rand(Normal(),n)),var(rand(Normal(),n))] for _ in 1:N]
end;

# ╔═╡ 11120d28-252c-11eb-1c1e-0dba64d8ae5c
begin
	p1 = scatter(first.(dataUni), last.(dataUni),
		c=:blue, ms=1, msw=0, label="Same group")
	p1 = scatter!(first.(dataUniInd), last.(dataUniInd),
		c=:red, ms=0.8, msw=0, label="Separate group",
		xlabel=L"\overline{X}", ylabel=L"S^2")

	p2 = scatter(first.(dataNorm), last.(dataNorm),
		c=:blue, ms=1, msw=0, label="Same group")
	p2 = scatter!(first.(dataNormInd), last.(dataNormInd),
		c=:red, ms=0.8, msw=0, label="Separate group",
		xlabel=L"\overline{X}", ylabel=L"$S^2$")

	plot(p1, p2, ylims=(0,5), size=(800, 400))
end

# ╔═╡ 475ff888-22e6-11eb-2354-09f8f40a8e12
md"## End of listing5.03"

# ╔═╡ Cell order:
# ╟─ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
# ╠═e077ab16-22e5-11eb-15ed-5f0654bf7328
# ╠═e75a9a06-22e5-11eb-2e36-8d4f62b830ed
# ╠═fa68607e-22e5-11eb-0558-c9a4d9f77426
# ╠═1111d8c4-252c-11eb-3813-81de856f7fba
# ╠═11120d28-252c-11eb-1c1e-0dba64d8ae5c
# ╟─475ff888-22e6-11eb-2354-09f8f40a8e12


#=
   Lotto Problem

   From some unknown source (but I quoted it here: http://www.hakank.org/sims/simulering.html)
   """
   One day I was asked

   What is the probability that no two numbers are consecutive in
   a lottery draw?

   Information:

   National Lottery in UK is to draw 6 different numbers from 1 to 49.
   so, for example,

    (a)   1  4  7   12  19  44    - no consecutive numbers
    (b)   3  6  17  18  44  46    - 17 and 18 are consecutive
    (c)   1  2  3   17  29  49    - 1, 2 and 3 are consecutive

   We are asking the probability that class (a) occurs. Hope this is clear.

   Observation shows that it is NOT a small number (actually near half-and-half).
   ......

   A Monte Carlo simulation experiment produced 50,558 sets of six numbers
   between 1 and 49 with none consecutive out of 100,000 trials giving an
   estimated probability of .50558.
   """

   Summary Statistics
   parameters      mean       std   naive_se      mcse          ess      rhat   ess_per_sec
      Symbol   Float64   Float64    Float64   Float64      Float64   Float64       Float64

        prob    0.5054    0.5000     0.0025    0.0034   22057.4684    1.0000     1884.6094

=#
using Turing, StatsPlots, Distributions
include("jl_utils.jl")

@model function lotto_not_two_consecutive_numbers(n=49,m=6)

    lotto ~ filldist(DiscreteUniform(1,n),m)
    # Ensure unicity
    true ~ Dirac(sum([lotto[i] == lotto[j] for i in 1:m for j in i+1:m]) == 0)
    # Are there any consecutive numbers?
    prob ~ Dirac(sum([ abs(lotto[i]-lotto[j]) == 1 for i in 1:m for j in i+1:m]) == 0)
end

n = 49
m = 6
model = lotto_not_two_consecutive_numbers(n,m)

# chns = sample(model, Prior(), 10_000)
# chns = sample(model, MH(), 10_000)
chns = sample(model, PG(5), 40_000)
# chns = sample(model, SMC(), 10_000)
# chns = sample(model, IS(), 10_000)

display(chns[[:prob]])
# display(plot(chns))


"""
    householderqr(R1, R2) computes the QR decompsition of
    (m+l)-by-l block matrix [R1;R2]

```math
          l               l
    m  (  R1  )  =  m  (  Q1  ) * R
    l  (  R2  )     l  (  Q2  )
```

- Q1 is m-by-l orthogonal matrix,
- Q2 is l-by-l orthogonal matrix,
- R is l-by-l upper triangular matrix.

# arguments:
R1: m-by-l matrix,
R2: m-by-l matrix.

# returns:
Q1: m-by-l matrix,
Q2: l-by-l matrix,
R: l-by-l matrix.
"""
function householderqr(R1, R2)
    r, c = size(R1)
    F = qr!([R1;R2])
    Q = Matrix(F.Q)
    Q1 = Q[1:r,:]
    Q2 = Q[r+1:end,:]
    return Q1, Q2, F.R
end


"""
sigmoid activation function
"""
sigmoid(z::Number) = 1 / (1 + e^(-z))
sigmoid{T<:Number}(arr::Array{T}) = map(sigmoid, arr)

partial_sigmoid(z::Number) = z * (1 - z)

"""
tanh activation function
"""
tanh = Base.tanh

partial_tanh(z::Number) = 2 / (e^z + e^-z)

"""
ReLU activation function
"""
relu(x::Real) = max(0, x)
relu{T<:Real}(arr::Array{T}) = map(relu, arr)


using Distributions, Plots; pyplot()

mu, sig = 2, 3
eta = sqrt(3)*sig/pi
n, N = 15, 10^7
dNormal   = Normal(mu, sig)
dLogistic = Logistic(mu, eta)
xGrid = -8:0.1:12

sNormal   = [var(rand(dNormal,n)) for _ in 1:N]
sLogistic = [var(rand(dLogistic,n)) for _ in 1:N]

p1 = plot(xGrid, pdf.(dNormal,xGrid), c=:blue, label="Normal")
p1 = plot!(xGrid, pdf.(dLogistic,xGrid), c=:red, label="Logistic",
	xlabel="x",ylabel="Density", xlims= (-8,12), ylims=(0,0.16))

p2 = stephist(sNormal, bins=200, c=:blue, normed=true, label="Normal")
p2 = stephist!(sLogistic, bins=200, c=:red, normed=true, label="Logistic",
	xlabel="Sample Variance", ylabel="Density", xlims=(0,30), ylims=(0,0.14))

plot(p1, p2, size=(800, 400))


using Symata, NumericalMethodsforEngineers
using Test

@sym begin
  ClearAll(xi, yi, Y, Ydot, Ydotdot, ytilde1, ytilde2, R)
  xi = [0, 1//3, 2//3, 1]
  yi = [0, a, b, 1]
  Y(x_) := LagrangePolynomial(xi, yi)
  #
  # Can be re-formulated as Y(x) = F(x) + C1(a) * Ψ1(x) + C2(a) * Ψ2(x)
  #
  # F(x_) := (1/2)*(9x^3 - 9x^2 + 2x)
  # C1(a_) := (27/2)*(a-b)
  Ψ1(x_) := x^3 - x^2
  # C2(a_) := -(9/2)*(2a-b)
  Ψ2(x_) := x^2 - x
  #
  # Y(x_) := F(x) + C1(a)*Ψ1(x) + C2(a)*Ψ2(x)
  #
  R(x_) = Simplify(D(Y(x), x, 2) - 3*x - 4*Y(x))
  SetJ(r, ToString(Simplify(R(x))))
end

println("\n\nExample 7.14: y'' = 3x + 4y, y(0)=0, y(1)=1")
println("Residual for Weighted Residual Method using 2 point Lagragian Polynomial")
@sym Println("\nY(x): ", Expand(Y(x)), "\n")
println("( Example 7.14 gives: ytilde = 1/2*x^3*(27a-27b+9)-x^2*(45a-36b+9)+x*(18a-9b+2) )")
@sym Println("\nR(x) = ", R(x), "\n")

@test r == "-9 - 45a + 36b + 20x - 63b*x + 45a*x + 18*x^2 - 72b*x^2 + 90a*x^2 - 18x^3 - 54a*x^3 + 54b*x^3"


# method of moments estimators for a sample from Chi^2(theta)
# increase n to observe consistency
# note that the two estimators are different from one another
using Distributions
n = 30
theta = 3

# the distribution doesn't matter, what matters is
# that the moments are correctly specified
#y = rand(Chisq(theta), n)
y = rand(Normal(theta, sqrt(2*theta)), n)

# a MM estimator
thetahat = mean(y)
println("mm estimator based on mean: ", thetahat)

# a second MM estimator
thetahat = 0.5*var(y)
println("mm estimator based on variance: ", thetahat)


using MDP

# This example describes a Markov decision process where
#
# S = {1, 2}
# A = {1, 2}

# r = S x A matrix

r = [ 5  10
     -1  -Inf ]

# Transition matrix
# P(1) = [0.5, 0.5; 0.0, 1.0]
# P(2) = [0.0, 1.0; 0.5, 0.5]
P = Matrix[ [ 0.5  0.5
              0.0  1.0 ],
            [ 0.0  1.0
              0.5  0.5 ] ]

# Construct the MDP model
model = ProbModel(r, P; objective=:Max)

# Compute the optimal solution
(v,g) = valueIteration(model; discount=0.95, tolerance=1e-4)

println(g) # [1, 1]
println(v) # [-8.571401228528563,-19.999971289954992]

# For this model, the optimal solution is:

# g = [1, 1]
# w[1] = (5 - 5.5 β) / (1 -0.5 β)(1 - β)
# w[2] = -1/(1-β)

# which equals
#
# w = [ -8.57142857142855, -19.999999999999982]

# As expected, v is within 1e-4 of w (in sup-norm).


function main()
    A, B, C = parse.(Float64, split(readline()))
    g(t; A=A, B=B, C=C) = A * t + B * sin(C * t * π) - 100
    t̲, t̄ = 0, (B + 100) / A
    ε = 1e-6

    t = (t̄ + t̲) / 2
    while abs(g(t)) > ε
        if g(t) > 0
            t̄ = t
        else
            t̲ = t
        end
        t = (t̄ + t̲) / 2
    end

    println(t)
    println()
end

main()


using Gadfly

function lossDemo()
    qs = [.2, 1, 2]
    loss(x, q) = abs(x).^q

    x = -2:0.01:2

    function lossPlot(q)
        plot(x=x, y=loss(x, q), Geom.line, Theme(default_color=colorant"black", line_width=1px))
    end

    [lossPlot(q) for q in qs]
end

function hingeLossDemo(ε)
    hinge(x) = (abs(x)-ε).*(abs(x).>=ε)

    x = -2:0.01:2
    plot(x=x, y=hinge(x), Geom.line, Theme(default_color=colorant"black", line_width=1px))
end


# Computes the SVD of a 2-by-2 matrix M, returning U, S, and V such that A == U*S*V'.
# Reference:
# Blinn, J. (1996). Consider the lowly 2 x 2 matrix. IEEE Computer Graphics and Applications, 16(2), 82-88.
# https://scicomp.stackexchange.com/questions/8899/robust-algorithm-for-2-times-2-svd
function svd2x2(M::AbstractArray)
    E = (M[1,1] + M[2,2]) / 2
    F = (M[1,1] - M[2,2]) / 2
    G = (M[2,1] + M[1,2]) / 2
    H = (M[2,1] - M[1,2]) / 2
    Q = sqrt(E^2 +H^2)
    R = sqrt(F^2 + G^2)
    sx = Q + R
    sy = Q - R
    a₁ = atan2(G,F)
    a₂ = atan2(H,E)
    θ = (a₂ - a₁) / 2
    ϕ = (a₂ + a₁) / 2
    s = sign(sy)
    U = SMatrix{2,2,Float64}(cos(ϕ), sin(ϕ), -s*sin(ϕ), s*cos(ϕ))
    S = SMatrix{2,2,Float64}(sx, 0.0, 0.0, abs(sy))
    V = SMatrix{2,2,Float64}(cos(θ), -sin(θ), sin(θ), cos(θ))
    U,S,V
end

#  Computes the Moore-Penrose pseudoinverse for a 2-by-2 matrix M by only inverting
#  singular values above the threshold `tol = sqrt(eps(real(float(one(eltype(M))))))`
function pinv2x2(M::AbstractArray)
     tol = sqrt(eps(real(float(one(eltype(M))))))
     U,S,V = svd2x2(M)
     D = SMatrix{2,2,Float64}( S[1,1] > tol ?  1/S[1,1] : 0.0 , 0.0, 0.0, S[2,2] > tol ?  1/S[2,2] : 0.0 )
     U*D*V'
end


# functions definition

function continued_fraction(x, terms)
    val = x
    for i = 1:terms
        if i==terms
            val = 1/(1-val)
        else
            val = x/(1-val)
        end
    end
    return val
   end

function continued_exp(x, terms)
    val = 1
    for i = 1:terms
        val = exp(val*x)
    end
    return val
end

# continued fraction call section

N = 200
conv = 100
df = 0.01
s = zeros(N, N)
pt = zeros(Complex{Float64},conv)
for x = 1:N , y = 1:N
  dx = df * x - N * df/2
  dy = df * y - N * df/2
  for i = 1:conv
    pt[i] = continued_fraction(dx+dy*im,i)
  end
  s[x,y] = length(Set(pt))
end

using ImageView
using Images
ImageView.imshow(s)

# continued exponential call section

N = 2000
conv = 100
df = 0.01
s = zeros(N, N)
pt = zeros(Complex{Float64},conv)
for x = 1:N , y = 1:N
  dx = df * x - N * df/2
  dy = df * y - N * df/2
  for i = 1:conv
    pt[i] = continued_exp(dx+dy*im,i)
  end
  s[x,y] = length(Set(pt))
end

using ImageView
using Images
ImageView.imshow(s)


# define macro for statically AD-ing through activation function
#macro frule()

# Define activation functions
@inline ℒ2(x::T) where T = T(2)*cosh(x)
@inline ∂logℒ2(x) = tanh(x)
@inline logℒ2(x::T) where T<:Real =
    (abs(x)<= T(12.0) ? log(cosh(x)) : abs(x) - log(2one(x)))
@inline logℒ2(x::Complex{T}) where T<:Real =
    logℒ2(real(x)) + log(cos(imag(x)) + im * tanh(real(x)) * sin(imag(x)))

@inline ℒ(x) = one(x) + exp(x)
@inline ∂logℒ(x) = one(x)/(one(x)+exp(-x))
#const ∂logℒ = NNlib.σ
#NNlib.σ(x::Complex) = one(x)/(one(x)+exp(-x))
@inline logℒ(x) = log1p(exp(x))#log(one(x) + exp(x))
#const logℒ = NNlib.softplus
#NNlib.softplus(x::Complex) = log1p(exp(x))#log(one(x) + exp(x))

@inline fwd_der(f::typeof(logℒ)) = ∂logℒ
@inline fwd_der(f::typeof(logℒ), x) = ∂logℒ(x)

@inline fwd_der(f::typeof(logℒ2)) = ∂logℒ
@inline fwd_der(f::typeof(logℒ2), x) = ∂logℒ2(x)

@inline fwd_der(f::typeof(identity)) = identity
@inline fwd_der(f::typeof(identity), x::T) where T = one(T)

export af_softplus, af_logcosh
"""
    af_softplus(x) = log( 1 + exp(x) )

Classic softplus (https://en.wikipedia.org/wiki/Sigmoid_function) activation function.
"""
const af_softplus = logℒ

# TODO use NNlib implementation
"""
  logcosh(x)

  Return log(cosh(x)) which is computed in a (somewhat) numerically stable way.
"""
const af_logcosh = logℒ2


let
    n = mean_motion(R_EARTH + 500e3, use_degrees=false)
    @test isapprox(n, 0.0011067836148773837, atol=1e-12)

    n = mean_motion(R_EARTH + 500e3, use_degrees=true)
    @test isapprox(n, 0.0634140299667068, atol=1e-12)
end

let
    a = semimajor_axis(0.0011067836148773837, use_degrees=false)
    @test isapprox(a, R_EARTH + 500e3, atol=1e-6)

    a = semimajor_axis(0.0634140299667068, use_degrees=true)
    @test isapprox(a, R_EARTH + 500e3, atol=1e-6)
end

let
    T = orbit_period(R_EARTH + 500e3)
    @test isapprox(T, 5676.977164028288, atol=1e-9)
end

let
    iss = sun_sync_inclination(R_EARTH + 574e3, 0.0, use_degrees=false)
    @test isapprox(iss, 97.685*pi/180, atol=1.0e-3)

    iss = sun_sync_inclination(R_EARTH + 574e3, 0.0, use_degrees=true)
    @test isapprox(iss, 97.685, atol=1.0e-3)
end

let
    # 0
    M = anomaly_eccentric_to_mean(0.0, 0.0, use_degrees=false)
    @test M == 0

    M = anomaly_eccentric_to_mean(0.0, 0.0, use_degrees=true)
    @test M == 0

    # 180
    M = anomaly_eccentric_to_mean(pi/2, 0.1, use_degrees=false)
    @test isapprox(M, 1.4707, atol=1e-3)

    M = anomaly_eccentric_to_mean(90.0, 0.1, use_degrees=true)
    @test isapprox(M, 84.270, atol=1e-3)

    # 180
    M = anomaly_eccentric_to_mean(pi, 0.0, use_degrees=false)
    @test isapprox(M, pi, atol=1e-12)

    M = anomaly_eccentric_to_mean(180.0, 0.0, use_degrees=true)
    @test M == 180.0
end

let
    # 0
    E = anomaly_mean_to_eccentric(0.0, 0.0, use_degrees=false)
    @test E == 0

    E = anomaly_mean_to_eccentric(0.0, 0.0, use_degrees=true)
    @test E == 0

    # 180
    E = anomaly_mean_to_eccentric(1.4707, 0.1, use_degrees=false)
    @test isapprox(E, pi/2, atol=1e-3)

    E = anomaly_mean_to_eccentric(84.270, 0.1, use_degrees=true)
    @test isapprox(E, 90.0, atol=1e-3)

    # 180
    E = anomaly_mean_to_eccentric(pi, 0.0, use_degrees=false)
    @test isapprox(E, pi, atol=1e-12)

    E = anomaly_mean_to_eccentric(180.0, 0.0, use_degrees=true)
    @test E == 180.0

    # Large Eccentricities
    E = anomaly_mean_to_eccentric(180.0, 0.9, use_degrees=true)
    @test E == 180.0

    # Max Iteration exit on hyperbolic trajectory
    # @test_throws ErrorException anomaly_mean_to_eccentric(pi, 2.0)
end

let
    oe  = [R_EARTH + 500e3, 0, 90.0, 0, 0, 0]
    eci = sOSCtoCART(oe, use_degrees=true)

    tol = 1e-6
    @test isapprox(eci[1], R_EARTH + 500e3, atol=tol)
    @test isapprox(eci[2], 0.0, atol=tol)
    @test isapprox(eci[3], 0.0, atol=tol)
    @test isapprox(eci[4], 0.0, atol=tol)
    @test isapprox(eci[5], 0.0, atol=tol)
    @test isapprox(eci[6], sqrt(GM_EARTH/(R_EARTH + 500e3)), atol=tol)
end

let
    # Using radians
    oe   = [R_EARTH + 500e3, 0, pi/2.0, 0, 0, 0]
    eci  = sOSCtoCART(oe, use_degrees=false)
    eci2 = sOSCtoCART(oe, use_degrees=false)

    tol = 1e-6
    @test isapprox(eci[1], eci2[1], atol=tol)
    @test isapprox(eci[2], eci2[2], atol=tol)
    @test isapprox(eci[3], eci2[3], atol=tol)
    @test isapprox(eci[4], eci2[4], atol=tol)
    @test isapprox(eci[5], eci2[5], atol=tol)
    @test isapprox(eci[6], eci2[6], atol=tol)

    # Using degrees
    oe   = [R_EARTH + 500e3, 0, 90.0, 0, 0, 0]
    eci  = sOSCtoCART(oe, use_degrees=true)
    eci2 = sOSCtoCART(oe, use_degrees=true)

    tol = 1e-6
    @test isapprox(eci[1], eci2[1], atol=tol)
    @test isapprox(eci[2], eci2[2], atol=tol)
    @test isapprox(eci[3], eci2[3], atol=tol)
    @test isapprox(eci[4], eci2[4], atol=tol)
    @test isapprox(eci[5], eci2[5], atol=tol)
    @test isapprox(eci[6], eci2[6], atol=tol)
end

let
    eci   = [R_EARTH + 500e3, 100e3, 575e3, 0, 0, 7300]
    eci2  = sOSCtoCART(sCARTtoOSC(eci))

    tol = 1e-6
    @test isapprox(eci[1], eci2[1], atol=tol)
    @test isapprox(eci[2], eci2[2], atol=tol)
    @test isapprox(eci[3], eci2[3], atol=tol)
    @test isapprox(eci[4], eci2[4], atol=tol)
    @test isapprox(eci[5], eci2[5], atol=tol)
    @test isapprox(eci[6], eci2[6], atol=tol)

    # Equatorial circulator
    a   = R_EARTH + 1000e3
    e   = 0.0
    eci = [a, 0, 0, 0, 0, sqrt(GM_EARTH/a)]
    oe  = sCARTtoOSC(eci, use_degrees=true)

    tol = 1e-6
    @test isapprox(oe[1], a,     atol=tol)
    @test isapprox(oe[2], e,     atol=tol)
    @test isapprox(oe[3], 90.0,  atol=tol)
end

let
    # Test near-circular conversions
    a   = R_EARTH + 500e3
    eci = [a, 0.0, 0.0, 0.0, 0.0, sqrt(GM_EARTH/a)]
    oe  = sCARTtoOSC(eci, use_degrees=true)

    tol = 1e-6
    @test isapprox(oe[1], a, atol=tol)
    @test isapprox(oe[2], 0.0, atol=tol)
    @test isapprox(oe[3], 90.0, atol=tol)
    @test isapprox(oe[4], 0.0, atol=tol)
    @test isapprox(oe[5], 0.0, atol=tol)
    @test isapprox(oe[6], 0.0, atol=tol)
end


"""
Solve system of linear equations: L*x = b, where L is Laplacian matrix
"""
function linsolve_cg_v2!( LF::LF3dGrid, x::Array{Float64,1}, b::Array{Float64,1};
                          NiterMax=1000, verbose=false, TOL=5.e-10 )
  #
  Npoints = size(x)[1]
  #
  L_x = apply_Laplacian( LF, x )
  #
  r = b[:] - L_x[:]
  z = r[:]
  p = z[:]

  r_old = r[:]
  z_old = z[:]
  p_old = p[:]
  for iter = 1 : NiterMax
    #
    L_x = apply_Laplacian( LF, p )
    #
    α = dot( r, z ) / dot( p, L_x )
    #
    x[:] = x[:] + α * p[:]  # FIXME use x[:] to force x to be copied, not referenced
    r[:] = r[:] - α * L_x[:]
    #
    norm_res = sqrt( dot( r, r ) )
    if verbose
      @printf("%8d %20.10f\n", iter, norm_res)
    end
    #
    if norm_res < TOL
      if verbose
        @printf("#CONV in linsolve_cg_v2: iter, norm_res: %8d %10.5e\n", iter, norm_res)
      end
      break
    end
    #
    z = r[:]  #XXX call appropriate preconditioner here
    # Polak-Ribiere
    β = dot( z, r - r_old ) / dot( z_old, r_old )
    p = z[:] + β * p_old[:]
    #
    z_old = z[:]
    r_old = r[:]
    p_old = p[:]
  end
  #
  return
  #
end # of function


"""
    Ormsby(; <keyword arguments>)

Create a Ormsby wavelet sampled every dt seconds with corner frequencies
defined by the vector f = [f1, f2, f3, f4]. The final wavelet is multiplied by
a Hamming window.

# Arguments
- `dt::AbstractFloat=0.002`: sampling interval in secs.
- `f::Vector{AbstractFloat}=[2.0, 10.0, 40.0, 60.0]`: corner frequencies in Hz.
      ^
    1 |     ***************
      |    *               *
      |   *                 *
      |  *                   *
      | *                     *
      -----------------------------> f
        f1  f2           f3  f4
# Example
```julia
julia> w = Ormsby(); plot(w);
```
"""
function Ormsby(; dt::Tf=0.002, f::Vector{Tf}=[2.0, 10.0, 40.0, 60.0]) where {Tf<:AbstractFloat}

    f1,f2,f3,f4 = f

    fc = (f2+f3)/2.0
    nw = 2.2/(fc*dt)
    nc = floor(Int, nw/2)
    t = dt*collect(-nc:1:nc)
    nw = 2*nc + 1
    a4 = (pi*f4)^2/(pi*(f4-f3))
    a3 = (pi*f3)^2/(pi*(f4-f3))
    a2 = (pi*f2)^2/(pi*(f2-f1))
    a1 = (pi*f1)^2/(pi*(f2-f1))

    u = [ a4 * (sinc(f4 * t[i] ))^2 - a3 * (sinc(f3 * t[i] ))^2 for i in eachindex(t) ]
    v = [ a2 * (sinc(f2 * t[i] ))^2 - a1 * (sinc(f1 * t[i] ))^2 for i in eachindex(t) ]

    w  = u - v
    Hm = Hamming(nw)
    w  = [w[i] * Hm[i] for i in eachindex(w)]
    w  = w/maximum(w)

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, c) : (c, a)
        end
        a, ya, b, yb = b, yb, c, yc
        s *= k
    end
end

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

#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

#ponto inicial
initial_point = 0
#busca do intervalo
a,c = bracket_minimum(f,initial_point)

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

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

println("x* = ", am)

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


module DRAGO

using LinearAlgebra
using Plots
using Distributions
using BandedMatrices

include("utility.jl")

export drago, simulate_data, find_gaps, get_present

"""
    drago(y, λ1, λ2, S = I; ΔFmin = 0.01, maxiter = 10, verbose = false)

Nonlinear Smoothing of Data with Random Gaps and Outliers (DRAGO). A detailed description of the algorithm can be found in [1]

[1] Parekh, A., et al. "Nonlinear Smoothing of Core Body Temperature Data with Random Gaps and Outliers (DRAGO)." Biomedical Signal Processing. Springer, Cham, 2021. 63-84.
# Arguments

* y:    Signal with gaps and outlier
* λ1:   Influences the sparsity of the estimated outliers `x`. As a reasonable heuristic is λ1 = 3σ, where σ is the standard deviation of the noise.
* λ2:   Influences the smoothness of the estimate `f`
* S:    A matrix encoding gaps defaulting to `I` if none are present. Otherwise it can be created by starting with `I` and deleting all rows corresponding to a gap.

# Keyword Arguments

* ΔFmin:    The minimal change in the objective function F that defines convergence.
* maxiter:  Maximum number of iterations
* verbose:  Print some information

# Returns

* fRes:     The smoothed signal
* xRes:     The estimated outliers
* residual: The residual signal y - (f + x)
"""
function drago(y, λ1, λ2, S = I(length(y)); ΔFmin = 0.01, maxiter = 10, verbose = false)
    # Initialize
    N = length(y)
    xRes = S * y
    present = [1:N;][diag(S' * S) .== 1]
    Dn = Tridiagonal(ones(N-1), -2ones(N), ones(N-1))
    Dn = Dn[2:end-1, 1:end] # make it N-2 x N
    λ2DDn = λ2 .* Dn' * Dn

    # Objective function
    F(f, x) = 0.5norm2(S * y .- S * f .- x)^2 + 0.5λ1 * norm1(x) + .5λ2 *  norm2(Dn * f)^2

    # Iterate
    iter = 1
    fRes = similar(y)
    Fold = ΔF = Inf
    while ΔF > ΔFmin && iter <= maxiter
        A = Diagonal(λ1 ./ (abs.(xRes) .+ λ1))
        # B = Matrix(S' * A * S .+ λ2 .* Dn' * Dn) # faster for low N but more Memory gets allocated
        B = BandedMatrix(S' * A * S .+ λ2DDn) # less Memory but apparently slower
        fRes = inv(B) * S' * A * S * y
        xRes = abs.(xRes) .* (S * (y .- fRes)) ./ (abs.(xRes) .+ λ1)
        Fnow = F(fRes, xRes)
        verbose && @info "Iteration: $iter with F = $Fnow"
        ΔF = Fold .- Fnow
        Fold = Fnow
        iter += 1
    end
    residual = y .- (S' * xRes .+ fRes)

    return fRes, xRes, residual
end
end #module


n = rand(2: 10)

# test Hessenberg

A = matrixdepot("rohess", n)

B = tril(A) - diagm(diag(A)) - diagm(diag(A, -1), -1)

@test_approx_eq_eps B zeros(n,n) 1.0e-7

# test orithogonality

@test_approx_eq_eps A'*A eye(n) 1.0e-7

println("'rohess' passed test...")


module BigCombinatorics

export Fibonacci
export Factorial, DoubleFactorial, FallingFactorial, RisingFactorial, HyperFactorial
export Binomial, Catalan
export Derangements, MultiChoose, Multinomial
export Bell, Stirling1, Stirling2
export IntPartitions, IntPartitionsDistinct
export Euler, PowerSum, Menage

"""
This is where we cache values already computed
"""
_master_table = Dict{Function,Dict}()

"""
    _save(f::Function, x, val::BigInt)

Save the computed value of `f` with argument(s) `x` and value `val`.
"""
function _save(f::Function, x, val::BigInt)
    d = _master_table[f]
    d[x] = val
    nothing
end

"""
    _has(f::Function, x)

Check if argument `x` for function `f` is already in the `_master_table`.
"""
function _has(f::Function, x)::Bool
    d = _master_table[f]
    return haskey(d, x)
end

"""
    _max_arg(f::Function)

Find the largest argument known for `f`. Note that `f` must be a function
of just a single value.
"""
function _max_arg(f::Function)
    tab = _master_table[f]
    return maximum(keys(tab))
end

"""
    _get(f::Function, x)

Retrieve the function value `f(x)` from the `_master_table`.
"""
function _get(f::Function, x)::BigInt
    d = _master_table[f]
    return d[x]
end

"""
    _make(f::Function, T::Type)

Create an entry in the `_master_table` for the function `f`.
"""
function _make(f::Function, T::Type)
    _master_table[f] = Dict{T,BigInt}()
    nothing
end

"""
`BigCombinatorics.cache_report()` reports how many
entries are saved for each function in the `BigCombinatorics`
module.
"""
function cache_report()
    total = 0
    for func in keys(_master_table)
        parse_func = split(string(func), ".")
        func_name = last(parse_func)
        sz = length(_master_table[func])
        total += sz
        println("$sz\t$func_name")
    end
    println()
    println("$total\tTotal entries")
    nothing
end

"""
    BigCombinatorics.cache_clear()

Clears all cached values.
"""
function cache_clear()
    for f in keys(_master_table)
        f()
    end
    nothing
end

"""
`Fibonacci(n)` returns the `n`-th Fibonacci number.
We begin with `Fibonacci(0)==0` and `Fibonacci(1)==1`.
See https://oeis.org/A000045
"""
function Fibonacci(n::Integer)::BigInt
    if n < 0
        throw(DomainError(n, "argument must be nonngative"))
    end

    if _has(Fibonacci, n)
        return _get(Fibonacci, n)
    end

    start = _max_arg(Fibonacci) + 1
    for m = start:n
        val = _get(Fibonacci, m - 1) + _get(Fibonacci, m - 2)
        _save(Fibonacci, m, val)
    end

    return _get(Fibonacci, n)
end

function Fibonacci()
    _make(Fibonacci, Integer)
    _save(Fibonacci, 0, big(0))
    _save(Fibonacci, 1, big(1))
end
Fibonacci()

"""
`Factorial(n)` returns `n!` for nonnegative integers `n`.
See https://oeis.org/A000142

`Factorial(n,k)` returns `n!/k!` (to be consistent with Julia's
`factorial`.) Requires `0 <= k <= n`.

See also `FallingFactorial` and `RisingFactorial`.
"""
function Factorial(n::Integer)::BigInt
    if n < 0
        throw(DomainError(n, "argument must be nonngative"))
    end
    return factorial(big(n))
end

function Factorial(n::Integer, k::Integer)::BigInt
    if k > n
        throw(DomainError((n, k), "$k cannot exceed $n"))
    end
    return div(Factorial(n), Factorial(k))
end

"""
    FallingFactorial(n,k)

returns `n*(n-1)*(n-2)*...*(n-k+1)`
(with a total of `k` factors). Requires `n,k >= 0`.
If `k>n` then `0` is returned.
"""
function FallingFactorial(n::Integer, k::Integer)::BigInt
    if n < 0 || k < 0
        throw(DomainError((n, k), " arguments must be nonnegative"))
    end
    if k > n
        return big(0)
    end
    return Factorial(n, n - k)
end

"""
    RisingFactorial(n,k)

returns `n*(n+1)*(n+2)*...*(n+k-1)`
(with a total of `k` factors). Requires `n,k >= 0`.
"""
function RisingFactorial(n::Integer, k::Integer)::BigInt
    if n < 0 || k < 0
        throw(DomainError((n, k), "arguments must be nonnegative"))
    end
    if k == 0
        return big(1)
    end
    if n == 0
        return big(0)
    end

    return FallingFactorial(n + k - 1, k)
end

"""
    DoubleFactorial(n)

returns `n!!`, i.e., `n*(n-2)*...` with `(-1)!! == 0!! == 1!! == 1`.
See https://oeis.org/A006882
"""
function DoubleFactorial(n::Integer)::BigInt
    if n < -1
        throw(DomainError(n, "argument must be at least -1"))
    end
    if _has(DoubleFactorial, n)
        return _get(DoubleFactorial, n)
    end

    start = _max_arg(DoubleFactorial) + 1
    for m = start:n
        val = m * _get(DoubleFactorial, m - 2)
        _save(DoubleFactorial, m, val)
    end

    return _get(DoubleFactorial, n)
end

function DoubleFactorial()
    _make(DoubleFactorial, Integer)
    _save(DoubleFactorial, -1, big(1))
    _save(DoubleFactorial, 0, big(1))
    _save(DoubleFactorial, 1, big(1))
end
DoubleFactorial()

"""
    HyperFactorial(n)

returns the hyperfactorial of `n`, that is
`1^1 * 2^2 * 3^3 * ... * n^n`.
See https://oeis.org/A002109
"""
function HyperFactorial(n::Integer)::BigInt
    if n < 0
        throw(DomainError(n, "arument must be nonnegative"))
    end

    if _has(HyperFactorial, n)
        return _get(HyperFactorial, n)
    end
    start = _max_arg(HyperFactorial) + 1
    for m = start:n
        val = (big(m))^m * _get(HyperFactorial, m - 1)
        _save(HyperFactorial, m, val)
    end
    return _get(HyperFactorial, n)
end

function HyperFactorial()
    _make(HyperFactorial, Integer)
    _save(HyperFactorial, 0, big(1))
    _save(HyperFactorial, 1, big(1))
end
HyperFactorial()

"""
    Binomial(n,k)

returns the binomial coefficient `n`-choose-`k`.
This is the number of `k`-element subsets of an `n`-element set.
See https://oeis.org/A007318
"""
Binomial(n::Integer, k::Integer) = binomial(big(n), big(k))::BigInt

"""
    Multinomial(vec)

returns the multinomial coefficient whose
top index is the sum of `vec` (an array of `Int`s) and whose
bottom indices are given by `vec`.

This may also be called with a common-separated list of arguments,
that is, either of `Multinomial([1,2,3])` or `Multinomial(1,2,3)`.
The result is `60` in both cases as these equal `6!/(1! 2! 3!)`.

**Warning**: This is not the same as `MultiChoose`.
"""
function Multinomial(v...)::BigInt
    nv = length(v)
    for i = 1:nv
        typeof(v[i]) <: Integer || throw(DomainError(v, "arguments must be integers"))
        v[i] >= 0 || throw(DomainError(v, "arguments must be nonngative"))
    end
    vals = [t for t in v]
    return Multinomial(vals)
end

Multinomial() = big(1)::BigInt

function Multinomial(vals::Vector{T})::BigInt where {T<:Integer}
    if any([t < 0 for t in vals])
        throw(DomainError(vals, "arguments must be nonnegative"))
    end

    nv = length(vals)
    n = sum(vals)
    # base cases
    if nv <= 1 || n == 0
        return big(1)
    end
    # reduce
    return Binomial(n, vals[end]) * Multinomial(vals[1:nv-1])
end

"""
    MultiChoose(n,k)

returns the number of `k`-element
*multisets* that can be formed using the elements of an
`n`-element set.

**Warning**: This is not the same as `Multinomial`.
"""
function MultiChoose(n::Integer, k::Integer)::BigInt
    return Binomial(n + k - 1, k)
end

"""
    Catalan(n)

returns the `n`-th Catalan number.
See https://oeis.org/A000108
"""
function Catalan(n::Integer)::BigInt
    n >= 0 || throw(DomainError(n, "argument must be nonnegative"))
    return div(Binomial(2n, n), n + 1)
end

"""
    Derangements(n)

returns the number of permutations of
an `n`-set that have no fixed point.
See https://oeis.org/A000166
"""
function Derangements(n::Integer)::BigInt
    if n < 0
        throw(DomainError(n, "argument must be nonnegative"))
    end
    if _has(Derangements, n)
        return _get(Derangements, n)
    end

    start = _max_arg(Derangements) + 1
    for m = start:n
        s = (m % 2 == 0) ? 1 : -1
        val = m * _get(Derangements, m - 1) + s
        _save(Derangements, m, val)
    end
    return _get(Derangements, n)
end

function Derangements()
    _make(Derangements, Integer)
    _save(Derangements, 0, big(1))
    _save(Derangements, 1, big(0))
end
Derangements()

"""
    Bell(n)

gives the `n`-th Bell number, that is,
the number of partitions of an `n`-element set.
See https://oeis.org/A000110
"""
function Bell(n::Integer)::BigInt
    if n < 0
        throw(DomainError(n, "argument must be nonnegative"))
    end

    if _has(Bell, n)
        return _get(Bell, n)
    end

    start = _max_arg(Bell) + 1
    for m = start:n
        val = big(0)
        for k = 0:m-1
            val += Binomial(m - 1, k) * Bell(k)
        end
        _save(Bell, m, val)
    end

    return _get(Bell, n)
end

function Bell()
    _make(Bell, Integer)
    _save(Bell, 0, big(1))
    _save(Bell, 1, big(1))
end
Bell()

"""
    Stirling2(n,k)

gives the Stirling number of the second kind,
that is, the number of paritions of an `n`-set into `k`-parts."
See https://oeis.org/A008277
"""
function Stirling2(n::Integer, k::Integer)::BigInt
    # special cases
    if k < 0 || n < 0
        throw(DomainError((n, k), "arguments must be nonnegative"))
    end

    if k > n
        return big(0)
    end

    if n == 0  # and by logic, k==0
        return big(1)
    end

    if k == 0
        return big(0)
    end

    if n == k
        return big(1)
    end
    # END OF SPECIAL CASES, invoke recursion
    if _has(Stirling2, (n, k))
        return _get(Stirling2, (n, k))
    end

    val = Stirling2(n - 1, k - 1) + Stirling2(n - 1, k) * k
    _save(Stirling2, (n, k), val)
    return val
end
function Stirling2()
    _make(Stirling2, Tuple{Integer,Integer})
end
Stirling2()

"""
    Stirling1(n,k)

gives the (signed) Stirling number
of the first kind, that is, the coefficient of `x^k`
in the poynomial `x(x-1)(x-2)...(x-n+1)`.
See https://oeis.org/A008275
"""
function Stirling1(n::Integer, k::Integer)::BigInt
    # special cases
    if k < 0 || n < 0
        throw(DomainError((n, k), "arguments must be nonnegative"))
    end

    if k > n
        return big(0)
    end

    if n == 0  # and, by logic, k==0
        return big(1)
    end

    if k == 0  # and, by logic, n>0
        return big(0)
    end

    if _has(Stirling1, (n, k))
        return _get(Stirling1, (n, k))
    end

    # end of special cases, invoke recursion

    val = Stirling1(n - 1, k - 1) - (n - 1) * Stirling1(n - 1, k)
    _save(Stirling1, (n, k), val)
    return val
end
function Stirling1()
    _make(Stirling1, Tuple{Integer,Integer})
end
Stirling1()

"""
`IntPartitions(n)` is the number of partitions of the integer `n`.
See https://oeis.org/A000041

`IntPartitions(n,k)` is the number of partitions of the integer
`n` with exactly `k` (nonzero) parts.
"""
function IntPartitions(n::Integer, k::Integer)::BigInt
    if n < 0 || k < 0
        throw(DomainError((n, k), "arguments must be nonnegative"))
    end
    # lots of special cases
    if k > n
        return big(0)
    end
    if n == 0
        return big(1)
    end
    if k == 0
        return big(0)
    end
    if k == n || k == 1
        return big(1)
    end
    if _has(IntPartitions, (n, k))
        return _get(IntPartitions, (n, k))
    end

    val = sum([IntPartitions(n - k, i) for i = 0:k])
    _save(IntPartitions, (n, k), val)
    return val
end

function IntPartitions(n::Integer)::BigInt
    if n < 0
        throw(DomainError(n, "argument must be nonnegative"))
    end
    if _has(IntPartitions, n)
        return _get(IntPartitions, n)
    end
    val = sum([IntPartitions(n, k) for k = 0:n])
    _save(IntPartitions, n, val)
    return val
end
function IntPartitions()
    _make(IntPartitions, Union{Tuple{Integer,Integer},Integer})
end
IntPartitions()

"""
`IntPartitionsDistinct(n,k)` is the number of partitions of
the integer `n` into exactly `k` *distinct* parts.

`IntPartitionsDistinct(n)` is the number of partitions of `n`
into *distinct* parts.
"""
function IntPartitionsDistinct(n::Integer, k::Integer)::BigInt
    if n < 0 || k < 0
        throw(DomainError((n, k), "arguments must be nonnegative"))
    end
    Ck2 = div(k * (k - 1), 2)
    if n < Ck2
        return big(0)
    end
    return IntPartitions(n - Ck2, k)
end

function IntPartitionsDistinct(n::Integer)::BigInt
    if n < 0
        throw(DomainError(n, "argument must be nonngative"))
    end
    result = big(0)
    for k = 1:n
        s = IntPartitionsDistinct(n, k)
        if s == 0
            break
        end
        result += s
    end
    return result
end

"""
    Euler(n)

returns the `n`-th Euler number. Starting with `n=0`
this is the sequence
1, 0, -1, 0, 5, 0, -61, 0, 1385 and so on.
See https://oeis.org/A122045

Not to be confused with `Eulerian`.
"""
function Euler(n::Integer)::BigInt
    n >= 0 || throw(DomainError(n, "argument must be nonngative"))
    if n % 2 == 1
        return big(0)
    end

    if _has(Euler, n)
        return _get(Euler, n)
    end

    start = _max_arg(Euler) + 2

    for m = start:2:n
        last = div(m, 2) - 1
        val = -sum([Binomial(m, 2k) * Euler(2k) for k = 0:last])
        _save(Euler, m, val)
    end

    return _get(Euler, n)
end

function Euler()
    _make(Euler, Integer)
    _save(Euler, 0, big(1))
end
Euler()

"""
    PowerSum(n,k)

returns the sum of the `k`-th powers of the
integers `1` through `n`, i.e.,
`1^k + 2^k + 3^k + ... + n^k`.
"""
function PowerSum(n::Integer, k::Integer)::BigInt
    (n >= 0 && k >= 0) || throw(DomainError((n, k), "arguments must be nonngative"))
    # Base and special cases
    if n == 0
        return big(0)
    end
    if k == 0
        return big(n)
    end
    if k == 1
        return Binomial(n + 1, 2)
    end
    if _has(PowerSum, (n, k))
        return _get(PowerSum, (n, k))
    end

    val = big(n)^k + PowerSum(n - 1, k)
    _save(PowerSum, (n, k), val)
    return val
end
function PowerSum()
    _make(PowerSum, Tuple{Integer,Integer})
end
PowerSum()

export Eulerian
"""
    Eulerian(n,k)

returns the number of permutations of `{1,2,...,n}`
with `k` ascents. See https://oeis.org/A008292

Not to be confused with `Euler`.
"""
function Eulerian(n::Integer, k::Integer)::BigInt
    @assert (n >= 0 && k >= 0) "$n,$k must both be nonnegative"

    if n == 0
        return big(0)
    end

    if k > n || k == 0
        return big(0)
    end

    if n < 2
        return big(1)
    end

    if k == n      # includes the case n=k=0
        return big(1)
    end

    if k == 1
        return big(1)
    end

    if _has(Eulerian, (n, k))
        return _get(Eulerian, (n, k))
    end

    val = (n - k + 1) * Eulerian(n - 1, k - 1) + k * Eulerian(n - 1, k)
    _save(Eulerian, (n, k), val)
    return val
end
function Eulerian()
    _make(Eulerian, Tuple{Integer,Integer})
end
Eulerian()

"""
    Menage(n)

Number of solutions to the Menage problem with `n` male/female couples.
See https://oeis.org/A059375
"""
function Menage(n::Int)::BigInt
    if n < 0
        throw(DomainError(n, "Argument must be nonnegative"))
    end
    if _has(Menage, n)
        return _get(Menage, n)
    end
    start = _max_arg(Menage) + 1
    for m = start:n
        s = (m % 2 == 0) ? -1 : 1
        a1 = _get(Menage, m - 1) ÷ (2 * Factorial(m - 1))
        a2 = _get(Menage, m - 2) ÷ (2 * Factorial(m - 2))

        am = m * a1 + (m * a2 + 4s) ÷ (m - 2)
        val = 2 * Factorial(m) * am

        _save(Menage, m, val)
    end
    return _get(Menage, n)
end
function Menage()
    _make(Menage, Int)
    _save(Menage, 0, big(1))
    _save(Menage, 1, big(0))
    _save(Menage, 2, big(0))
    _save(Menage, 3, big(12))
end
Menage()

end  #end of module


using BenchmarkTools
using Test

import Random
Random.seed!(321)

# Distance function, considering PBC
function r(x,y,side)
  d = 0.
  for i in 1:2
    dx = (x[i]-y[i])%side
    if dx > side/2
      dx = dx - side
    elseif  dx < -side/2
      dx = dx + side
    end
    d = d + dx^2
  end
  return sqrt(d)
end

# Potential energy function
U(r,eps,sig) = 4*eps*((sig/r)^12 - (sig/r)^6)

# Compute total potential energy for all pairs
function UT1(x,eps,sig,side)
  ut = 0.
  for i in 1:length(x)-1, j in i+1:length(x)
    rij = r(x[i],x[j],side)
    ut += U(rij,eps,sig)
  end
  return ut
end

# Compute total potential energy, but do not compute u if rij > 2.
function UT2(x,eps,sig,side)
  ut = 0.
  for i in 1:length(x)-1, j in i+1:length(x)
    rij = r(x[i],x[j],side)
    if rij <= 2.
      ut += U(rij,eps,sig)
    end
  end
  return ut
end

# Generate 100 points
x = [ 10*rand(2) for i in 1:100 ]
# Test if UT1 provides the same result as UT2
eps = 2.0
sig = 0.5
side = 10.
@test UT1(x,eps,sig,side) ≈ UT2(x,eps,sig,side)

# Evaluate code speed
print("UT1: "); @btime UT1($x,$eps,$sig,$side)
print("UT2: "); @btime UT2($x,$eps,$sig,$side)


using Plots, Printf, LinearAlgebra

# enable plotting by default
if !@isdefined do_visu; do_visu = true end

@views function diffusion_1D(; do_visu=true)
    # Physics
    lx     = 10.0       # domain size
    D      = 1.0        # diffusion coefficient
    ttot   = 0.6        # total simulation time
    dt     = 0.1        # physical time step
    # Numerics
    nx     = 256        # numerical grid resolution
    tol    = 1e-6       # tolerance
    itMax  = 1e5        # max number of iterations
    damp   = 1-41/nx    # damping (this is a tuning parameter, dependent on e.g. grid resolution)
    # Derived numerics
    dx     = lx/nx      # grid size
    dtau   = (1.0/(dx^2/D/2.1) + 1.0/dt)^-1 # iterative "timestep"
    xc     = LinRange(dx/2, lx-dx/2, nx)
    # Array allocation
    qH     = zeros(nx-1)
    dHdtau = zeros(nx-2)
    ResH   = zeros(nx-2)
    # Initial condition
    H0     = exp.(-(xc.-lx/2).^2)
    Hold   = copy(H0)
    H      = copy(H0)
    t = 0.0; it = 0; ittot = 0
    # Physical time loop
    while t<ttot
        iter = 0; err = 2*tol
        # Pseudo-transient iteration
        while err>tol && iter<itMax
            qH         .= -D*diff(H)/dx              # flux
            ResH       .= -(H[2:end-1] - Hold[2:end-1])/dt - diff(qH)/dx # residual of the PDE
            dHdtau     .= ResH + damp*dHdtau         # damped rate of change
            H[2:end-1] .= H[2:end-1] + dtau*dHdtau   # update rule, sets the BC as H[1]=H[end]=0
            iter += 1; err = norm(ResH)/length(ResH)
        end
        ittot += iter; it += 1; t += dt
        Hold .= H
    end
    # Analytic solution
    Hana = 1/sqrt(4*(ttot+1/4)) * exp.(-(xc.-lx/2).^2 /(4*(ttot+1/4)))
    @printf("Total time = %1.2f, time steps = %d, iterations tot = %d, error vs analytic = %1.2e \n", round(ttot, sigdigits=2), it, ittot, norm(H-Hana))
    # Visualize
    if do_visu
        plot(xc, H0, linewidth=3); display(plot!(xc, H, legend=false, framestyle=:box, linewidth=3, xlabel="lx", ylabel="H", title="damped diffusion (nt=$it, iters=$ittot)"))
    end
    return xc, H0
end

diffusion_1D(; do_visu=do_visu);


using InfiniteOpt, Ipopt, Plots
using KNITRO

# Parmeters
# Time span
t0 = 0
tf = 1
# Initial values
x1 = 1

# Model
model = InfiniteModel(KNITRO.Optimizer)

# infinite_parameter
@infinite_parameter(model, t ∈ [t0, tf], num_supports = 101)

# variable
@variable(model, x ≥ 0, Infinite(t))

@variable(model, u, Infinite(t), start = 0)

# objective
@objective(model, Min, x(1)^2 + ∫(u^2, t))

# constraint
@constraint(model, x(0) == x1)
@constraint(model, x_constr, ∂(x, t) == x + u)

# Optimization

print(model)

optimize!(model)

# Visulization
ts = value(t)
u_value = value(u)
x_value = value(x)

display(plot(ts, u_value))
display(plot(ts, x_value))

using DiffEqFlux, DifferentialEquations
const solveeq = DifferentialEquations.solve

tspan = (t0, tf)
ann = FastChain(FastDense(1, 64, tanh), FastDense(64, 1))
θ = initial_params(ann)
function sir_nn(du, u, p, t)
    du[1] = u[1] + ann([t], p)[1]
end
u0 = [x1]
ts = Float32.(collect(0.0:0.01:tspan[2]))
prob = ODEProblem(sir_nn, u0, tspan, θ)
sol_init = solveeq(prob, Vern9(), abstol = 1e-10, reltol = 1e-10)
plot(sol_init)
function predict_adjoint(θ)
    Array(solveeq(prob, Vern9(), p = θ, saveat = ts, sensealg = InterpolatingAdjoint(autojacvec = ReverseDiffVJP(true))))
end
function loss_adjoint(θ)
    x = predict_adjoint(θ)
    x[1, end]^2 + 0.01 * sum(abs2, [first(ann([t], θ)) for t in ts])
end
l = loss_adjoint(θ)

const losses = []
callback(θ, l, pred) = begin
    push!(losses, l)
    if length(losses) % 50 == 0
        println(losses[end])
        p = plot(solveeq(remake(prob, p = θ), Tsit5(), saveat = ts), lw = 3)
        #plot!(p, ts, [first(ann([t], θ)) for t in ts], label = "u(t)", lw = 3)
        display(p)
    end
    false
end
# cb = function (θ, l)
#     println(l)
#     p = plot(solveeq(remake(prob, p = θ), Tsit5(), saveat = ts), lw = 3)
#     #plot!(p, ts, [first(ann([t], θ)) for t in ts], label = "u(t)", lw = 3)
#     display(p)
#     return false
# end
# Display the ODE with the current parameter values.
cb(θ, l)
loss1 = loss_adjoint(θ)
res1 = DiffEqFlux.sciml_train(loss_adjoint, θ, ADAM(0.05), cb = cb, maxiters = 100)
res2 = DiffEqFlux.sciml_train(loss_adjoint, res1.u,
    BFGS(initial_stepnorm = 0.01), cb = cb, maxiters = 100,
    allow_f_increases = false)

using DataFrames
using CSV
p_value = res2.u
results_saving_opt = DataFrame()
results_saving_opt.t = ts
results_saving_opt.I = x_value
results_saving_opt.u = u_value
results_saving_opt.I_nn = Array(solveeq(remake(prob, p = p_value), Tsit5(), saveat = ts))[1, :]
results_saving_opt.u_nn = [first(ann([t], p_value)) for t in ts]
CSV.write("results_saving_Example3_2.csv", results_saving_opt)

plot(results_saving_opt.t, results_saving_opt.I, lw = 2, label = "I(t) by direct collection")
plot!(results_saving_opt.t, results_saving_opt.I_nn, lw = 2, label = "I(t) by deep learning")
xlabel!("t(day)")
ylabel!("I(t)")
savefig("Example3_2_I.png")

plot(results_saving_opt.t[2:end-1], results_saving_opt.u[2:end-1], lw = 2, label = "u(t) by direct collection")
plot!(results_saving_opt.t[2:end-1], results_saving_opt.u_nn[2:end-1], lw = 2, label = "u(t) by deep learning")
xlabel!("t(day)")
ylabel!("u(t)")
savefig("Example3_2_control.png")

#
opt_loss = objective_value(model)
# opt_loss = 1.75
opt_loss_nn = loss_adjoint(p_value)