using EngEconomics

costA = 15000
omA = 55
benefitA = 450
costB = 10000
omB = 35
benefitB = 200
n = 30
i = 0.05

# Consider do nothing option and pick best from b/c analysis
# 1.) Rank options by smallest initial cost to biggest initial cost
#     (1) B
#     (2) A
# 2.) Compare 1 and 2
ΔEUAB_21 = (benefitB - benefitA) - (omB - omA)
ΔEUAC_21 = (costB - costA) * capitalRecoveryFactor(i, n)

ΔBCRatio_21 = ΔEUAB_21 / ΔEUAC_21

if ΔBCRatio_21 >= 1
	println("Pick the one with the bigger initial cost, i.e. B")
else
	println("Pick the one with the smaller initial cost, i.e. Do Nothing")
end

disbenefitA = 400
disbenefitB = 500

ΔEUAB_21 = (benefitB - benefitA) - (omB - omA) - (disbenefitB - disbenefitA)
ΔEUAC_21 = (costB - costA) * capitalRecoveryFactor(i, n)

ΔBCRatio_21 = ΔEUAB_21 / ΔEUAC_21

if ΔBCRatio_21 >= 1
	println("Pick the one with the bigger initial cost, i.e. B")
else
	println("Pick the one with the smaller initial cost, i.e. Do Nothing")
end


include("../../number_theo/src/number_module.jl")

module quasirandom
using LinearAlgebra
using ..number

function halton(i; base = 2)
    # von der Corput sequence
    result = 0.0
    f = 1.0
    while i > 0
        f = f / base
        result += f * mod(i, base)
        i = floor(Int, i / base)
    end
    return result
end

function halton_filling_seq(n_points; base = 2)
    return [halton(i, base = base) for i = 1:n_points]
end

function halton_filling_seq(n_points, n_dim)
    upto = max(n_dim * (log(n_dim) + log(log(n_dim))), 6) |> ceil |> Int
    bases = number.sieve_of_eratosthenes(upto)
    mat = zeros(n_points, n_dim)
    for dd = 1:n_dim
        mat[:,dd] = halton_filling_seq(n_points; base = bases[dd])
    end
    return mat
end

function pairwise_distances(X)
    # columns are the different dimensions
    # rows are the different points

    (N, n_dim)  = size(X)
    Nn          = div(N * (N-1), 2)
    n           = zeros(Nn)
    kk = 0
    @inbounds for ii = 1:(N-1)
        @inbounds for jj = (ii+1):N
            kk += 1
            v1 = view(X, ii, 1:n_dim)
            v2 = view(X, jj, 1:n_dim)
            n[kk] = norm(v1 - v2)
        end
    end
    return n
end

function morris_mitchell_criterion(X, q = 1.0)
    dists = pairwise_distances(X)
    s = sum( dists.^(-q) )^(1/q)
    return s
end

end


module YorkFit

export fit

"""Calculate the weight of the error `σX`."""
ω(σX::Real)::Real = σX^-2
α{T<:Real}(ωX::T, ωY::T)::T = sqrt(ωX * ωY)
W{T<:Real}(ωX::T, ωY::T, r::T, b::T, α::T)::T =
    (ωX * ωY) / (ωX + b^2 * ωY - 2 * b * r * α)

"""Compute Σ(Ws * Xs) / Σ(Xs)"""
barvar{T<:Real}(Ws::Vector{T}, Xs::Vector{T})::T = sum(Ws .* Xs) / sum(Ws)

function β{T<:Real}(W::T, U::T, V::T, ωX::T, ωY::T, b::T, r::T, α)::T
    W * ((U / ωY) + (b * V / ωX) - (r / α) * (b * U + V))
end

"""Calculate `a` and `b` using the method of least squares. Returns the tuple
`(a, b)` where `a` is the intercept and `b` is the gradient."""
function lsq{T<:Real}(Xs::Vector{T}, Ys::Vector{T})::Tuple{T,T}
    A = hcat(ones(length(Xs)), Xs)
    m = (A' * A) \ A' * Ys

    (m[1], m[2])
end

"""
    fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σX::T, σYs::Vector{T}, rs::Vector{T}; kwargs...)
    fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σX::T, σYs::Vector{T}, r::T=T(0); kwargs...)
    fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σXs::Vector{T}, σY::T, rs::Vector{T}; kwargs...)
    fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σXs::Vector{T}, σY::T, r::T=T(0); kwargs...)
    fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σX::T, σY::T, rs::Vector{T}; kwargs...)
    fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σX::T, σY::T, r::T=T(0); kwargs...)
    fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σXs::Vector{T}, σYs::Vector{T}, r::T=T(0); kwargs...)

    fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σXs::Vector{T}, σYs::Vector{T}, rs::Vector{T}; kwargs...)::Tuple{T,T,T,T}

Fits a slope and intercept to a set of observations using the method of York et
al. (2004).

Arguments
=========

- `Xs` -- A vector of observations.
- `Ys` -- A vector of observations related to `Xs` by `Ys = a + b * Xs`.
- `σXs` -- Errors in the observations in `Xs`. A vector of equal length to `Xs` or
  a scalar value if all the errors are equal.
- `σYs` -- Errors in the observations in `Ys`. A vector of equal length to `Ys` or
  a scalar value if all the errors are equal.
- `rs` -- Correlation coefficient between errors. A vector of equal length to
  `Xs` and `Ys` or a scalar value if the coefficient is constant. Defaults to 0.

Keyword Arguments
==================

- `niter=400` -- Number of iterations to calculate the slope `b` with.
- `tol=1E-15` -- Stopping criteria. When the difference between two successive
  iterations of `b` falls below this, the iterations stop.

Returns
=======

A 4-tuple containing `(a, b, σa, σb)` where `a` is the intercept, `b` is the
slope and `σa` and `σb` are the errors in the intercept and slope.

References
==========

York, D., Evensen, N.M., Martınez, M.L. and Delgado, J.D.B., 2004. Unified
equations for the slope, intercept, and standard errors of the best straight
line. American Journal of Physics, 72(3), pp.367-375.
"""
fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σX::T, σYs::Vector{T}, rs::Vector{T}; kwargs...) =
    fit(Xs, Ys, fill(σX, length(Xs)), σYs, rs; kwargs...)
fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σX::T, σYs::Vector{T}, r::T=T(0); kwargs...) =
    fit(Xs, Ys, fill(σX, length(Xs)), σYs, fill(r, length(Xs)); kwargs...)
fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σXs::Vector{T}, σY::T, rs::Vector{T}; kwargs...) =
    fit(Xs, Ys, σXs, fill(σY, length(Ys)), rs; kwargs...)
fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σXs::Vector{T}, σY::T, r::T=T(0); kwargs...) =
    fit(Xs, Ys, σXs, fill(σY, length(Ys)), fill(r, length(Xs)); kwargs...)
fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σX::T, σY::T, rs::Vector{T}; kwargs...) =
    fit(Xs, Ys, fill(σX, length(Xs)), fill(σY, length(Ys)), rs; kwargs...)
fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σX::T, σY::T, r::T=T(0); kwargs...) =
    fit(Xs, Ys, fill(σX, length(Xs)), fill(σY, length(Ys)), fill(r, length(Xs)); kwargs...)
fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σXs::Vector{T}, σYs::Vector{T}, r::T=T(0); kwargs...) =
    fit(Xs, Ys, σXs, σYs, fill(r, length(Xs)); kwargs...)
function fit{T<:Real}(Xs::Vector{T}, Ys::Vector{T}, σXs::Vector{T}, σYs::Vector{T}, rs::Vector{T}; niter=400, tol=1E-15)::Tuple{T,T,T,T}
    # TODO: Check for dimension mismatch of vectors
    (a, b) = lsq(Xs, Ys)

    ωXs = ω.(σXs)
    ωYs = ω.(σYs)
    αs = α.(ωXs, ωYs)

    Ws = zeros(length(Xs))
    βs = zeros(length(Xs))
    barX = 0
    barY = 0

    for i in 1:niter
        Ws = W.(ωXs, ωYs, rs, b, αs)
        barX = barvar(Ws, Xs)
        barY = barvar(Ws, Ys)
        Us = Xs - barX
        Vs = Ys - barY
        βs = β.(Ws, Us, Vs, ωXs, ωYs, b, rs, αs)

        b_old = b
        b = sum(Ws .* βs .* Vs) / sum(Ws .* βs .* Us)

        if abs((b - b_old) / b) < tol
            break
        end
    end

    a = barY - b * barX
    # Adjusted x values
    xs = barX + βs
    barx = sum(Ws .* xs) / sum(Ws)
    us = xs - barx

    σb = sqrt(sum(Ws .* us.^2)^-1)
    σa = sqrt(sum(Ws)^-1 + barx^2 * σb^2)

    return (a, b, σa, σb)
end

end # module


#--------------------------------------------------------------------
# Spacetime Discretization methods in Julia
# Soham 01-2018
#--------------------------------------------------------------------

function cheb(m::Int, x::Float64)::Float64
    return cos(m*acos(x))
end

function chebx{T<:Int}(i::T, N::T)::Float64
    return cospi((i-1)/N)
end

function chebd{T<:Int}(i::T, j::T, N::T)::Float64
	if i==j==1
		return (2N^2 + 1)/6
	elseif i==j==N+1
		return -(2N^2 + 1)/6
	elseif i==j
		return -chebx(j, N)/(2(1-chebx(j, N)^2))
	else
		ci = (i == 1 || i == N+1) ? 2 : 1
		cj = (j == 1 || j == N+1) ? 2 : 1
		s  = (i + j) % 2 != 0 ? -1 : 1
		return (ci/cj)*(s/(chebx(i,N)-chebx(j,N)))
	end
end

function chebw{T<:Int}(i::T, N::T)::Float64
	W = 0.0
	for j in 1:N+1
		w = (j == 1 ? 1 : (j-1)%2 == 0 ? 2/(1-(j-1)^2): 0)
		l = (i == 1 || i == N+1 ? (1/N)*cospi((i-1)*(j-1)/N) : (2/N)*cospi((i-1)*(j-1)/N))
		W +=  w*l
	end
	return W
end

function chebgrid(N::Int)::Array{Float64,1}
    return Float64[chebx(i,N) for i in 1:N+1]
end

function chebgrid(N::Int, M::Int, loc::Int)::Array{Float64,1}
    return Float64[coordtransL2G(M, loc, chebx(i,N)) for i in 1:N+1]
end

function chebweights(N::Int)::Array{Float64,1}
    return Float64[chebw(i,N) for i in 1:N+1]
end

function vandermonde(N::Int)::Array{Float64,2}
    return Float64[cheb(m,x) for x in chebgrid(N), m in 0:N]
end

function pseudovandermonde(N::Int, collocationpts::Array{Float64, 1})::Array{Float64,2}
    return Float64[cheb(m,x) for x in collocationpts, m in 0:N]
end

function modal2nodal(fmodal::Array{Float64,2})::Array{Float64,2}
    (Mx, My) = size(fmodal) .- 1
    fnodal   = Float64[sum(cheb(m-1,x)*cheb(n-1,y)*fmodal[m,n] for m in 1:Mx+1, n in 1:My+1) for x in chebgrid(Mx), y in chebgrid(My)]
end

function nodal2modal(fnodal::Array{Float64,2})::Array{Float64,2}
    (Nx, Ny) = size(fnodal) .- 1
    invndmx  = inv(vandermonde(Nx))
    invndmy  = inv(vandermonde(Ny))
    fmodal   = Float64[sum(invndmy[n,y]*invndmx[m,x]*fnodal[m,n] for x in 1:Nx+1, y in 1:Ny+1) for m in 1:Nx+1, n in 1:Ny+1]
end


"""
Higher-order clustering coefficient, which generalizes the traditional
clustering coefficients to closure of cliques larger than triangles.

Based on "Higher-order clustering in networks" by Hao Yin, Austin R. Benson, and
Jure Leskovec. The original paper may be found at https://arxiv.org/pdf/1704.03913.pdf.
Code from https://github.com/arbenson/HigherOrderClustering.jl.
"""

using LinearAlgebra
using SparseArrays
using StatsBase
using MatrixNetworks
include("cliques.jl")

"""
hoccf_data
----------

This is the data type returned by the function that computes higher-order clustering coefficients.

The field values are

order::Int64
    The order of the clustering coefficient (order = 2 is the classical
    clustering coefficient definition).

global_hoccf::Float64
    The global higher-order clustering coefficient.

avg_hoccf::Float64
    The average higher-order clustering coefficient (the mean is taken over
    nodes at the center of at least one wedge).

avg_hoccf2::Float64
     The average higher-order clustering coefficient, where the local clustering
    of a node not in any wedge is considered to be 0.

local_hoccfs::Vector{Float64}
    The vector of local higher-order clustering coefficients. If a node is not
    the center of at least one wedge, then the value is 0.

ho_wedge_counts::Vector{Int64}
    Higher-order wedge counts of nodes: ho_wedge_counts[v] is the number of higher-order
    wedges with node v at its center.

clique_counts::Vector{Int64}
    Clique counts of nodes: clique_counts[v] is the number of k-cliques containing
    node v where k = order + 1.
"""
struct hoccf_data
    order::Int64
    global_hoccf::Float64
    avg_hoccf::Float64
    avg_hoccf2::Float64
    local_hoccfs::Vector{Float64}
    ho_wedge_counts::Vector{Int64}
    clique_counts::Vector{Int64}
end

"""
`higher_order_ccfs`
======

Computes the global and average local l-th order clustering coefficients of a
network.

Arguments
---------
    - `A::MatrixNetwork`: The network to be analyzed
    - `l::Int64`: The order of the clustering coefficients

The authors define an l-wedge to be an l-clique with an extra node of degree 1 attached.
For example, this is a 3-wedge: https://i.imgur.com/Wc1pRzl.png
The clique node that is adjacent to the extra node is the "center" of the wedge.
The global l-th order clustering coefficient measures the ratio of (l+1)-cliques
to l-wedges: the probability that a randomly chosen l-wedge is "closed" into an
(l+1)-clique. The local l-th order clustering coefficient is defined similarly
as the ratio of the number of (l+1)-cliques a node is part of to the number of
l-wedges it is the center of.

The traditional clustering coefficients are a special case `hocc(A,2)` of the
above concept.
"""
function higher_order_ccfs(A::SparseMatrixCSC{T,Int64}, l::Int64) where T
    A = min.(A, 1)
    A -= Diagonal(A)
    n = size(A, 1)
    # Get clique counts
    clique_counts1 = kcliques(A, l)[1]
    clique_counts2 = kcliques(A, l + 1)[1]
    degs = vec(sum(A, dims=2))
    # normalize
    wedge_counts = (degs .- l .+ 1) .* clique_counts1
    nz_inds = findall(wedge_counts .> 0)
    local_hoccfs = zeros(Float64, n)
    local_hoccfs[nz_inds] = l .* clique_counts2[nz_inds] ./ wedge_counts[nz_inds]
    return hoccf_data(l, l .* sum(clique_counts2) / sum(wedge_counts),
                      mean(local_hoccfs[nz_inds]), mean(local_hoccfs),
                      local_hoccfs, wedge_counts, clique_counts2)
end


#### Kahan matrix

export Kahan
"""
[`Kahan` matrix](http://math.nist.gov/MatrixMarket/data/MMDELI/kahan/kahan.html)

```julia
julia> A=Kahan(5,5,1,35)
5x5 Kahan{Int64,Int64}:
 1.0  -0.540302  -0.540302  -0.540302  -0.540302
 0.0   0.841471  -0.454649  -0.454649  -0.454649
 0.0   0.0        0.708073  -0.382574  -0.382574
 0.0   0.0        0.0        0.595823  -0.321925
 0.0   0.0        0.0        0.0        0.501368

julia> A=Kahan(5,3,0.5,0)
5x3 Kahan{Float64,Int64}:
 1.0  -0.877583  -0.877583
 0.0   0.479426  -0.420735
 0.0   0.0        0.229849
 0.0   0.0        0.0
 0.0   0.0        0.0

julia> A=Kahan(3,5,0.5,1e-3)
3x5 Kahan{Float64,Float64}:
 1.0  -0.877583  -0.877583  -0.877583  -0.877583
 0.0   0.479426  -0.420735  -0.420735  -0.420735
 0.0   0.0        0.229849  -0.201711  -0.201711
```

For more details see: N. Higham, A Survey of Condition Number Estimation
for Triangular Matrices, SIMAX, Vol. 29, No. 4, pp. 588, 1987,
http://eprints.ma.man.ac.uk/695/01/covered/MIMS_ep2007_10.pdf
"""
struct Kahan{T<:Number,T1<:Number} <: AbstractMatrix{T}
    m::Int # dimension
    n::Int # dimension
    theta::T # angle
    pert::T1 # perturbation is pert*eps()
end # immutable

# Define its size
size(A::Kahan, r::Int) = r==1 ? A.m : A.n
size(A::Kahan) = A.m, A.n

# Index into a Kahan
function getindex(A::Kahan,i::Integer,j::Integer)
    m=minimum(size(A))
    t=tan(A.theta)
    c=1.0/sqrt(1.0+t^2)
    s=t*c
    if i>m; return 0.0
    elseif i>j; return 0.0
    elseif i==j; return s^(i-1)+A.pert*eps()*(m-i+1)
    else return -c*s^(i-1)
    end
end # getindex

# Dense version of Kahan
Matrix(A::Kahan) =[A[i,j] for i=1:size(A,1), j=1:size(A,2)]


"""
    backsub(A,B)

adalah fungsi untuk menyelesaikan SPL `Ax=B` dengan `A` adalah matriks segitiga
atas dan `B` adalah vektor ruas kanan.

# Examples
```jldoctest
julia> A = [ 4 -1 2 3
             0 -2 7 -4
             0 0 6 5
             0 0 0 3];

julia> B = [20;-7; 4; 6];

julia> X = backsub(A,B)
4×1 Array{Float64,2}:
  3.0
 -4.0
 -1.0
  2.0
```
return solusi SPL `X`
"""
function backsub(A,B)
    # Hitung ukuran matriks dan inisiasi solusi X
    n = length(B);
    X = zeros(n,1);
    # Hitung nilai solusi X ke-n
    X[n] = B[n]/A[n,n];
    # Hitung nilai solusi X ke- n-1 sampai X ke-1
    for i = n-1:-1:1
        X[i] = (B[i] - A[i,i+1:n]'X[i+1:n])/A[i,i];
    end
    return X
end


function chebyshev_quadrature(node_type::Function,n::S,domain = [1.0,-1.0]) where {S <: Integer}

  if n <= 0

    error("The number of nodes must be positive.")

  end

  nodes   = zeros(n)
  weights = zeros(n)

  if node_type == chebyshev_nodes # weighting function is (1.0 .- nodes.^2).^(-1/2) -- (first kind)

    for i = 1:n
      nodes[i] = (domain[1]+domain[2])/2.0 - cos((i-0.5)*pi/n)*(domain[1]-domain[2])/2.0
      weights[i] = pi/n
    end

    return nodes, weights

  elseif node_type == chebyshev_extrema # weighting function is (1.0 .- nodes.^2).^(1/2) -- (second kind)

    for i = 1:n
      nodes[i] = (domain[1]+domain[2])/2.0 - cos((i-1)*pi/(n-1))*(domain[1]-domain[2])/2.0
      weights[i] = (pi/(n-1))*sin(((i-1)/(n-1))*pi)^2
    end

    return nodes, weights

  else

    error("This node type is not supported")

  end

end


using Distributions
using LinearAlgebra
using Plots

# Set Parameters
M = 9  # total number of training data
N = 9  # degree of a polynomial
lambda_ = 1.75e-6  # regularization parameter

# Set Function to Generate Data
dist = Normal(0.0, 0.1)
base(x) = sin.(2pi*x)
gen(x) = base(x) + rand(dist, length(x))

# Set Function to Generate Polynomial Matrix
function polynomial_matrix(x, N)
    m = length(x)
    X = fill(1.0, m)
    for j in 1:N
        x_vec = x .^ j
        X = hcat(X, x_vec)
    end
    return X
end

# Set Function to Calculate Coefficient
function weight(x, y, N, lambda_)
    X = polynomial_matrix(x, N)
    w = (X' * X + lambda_ * Matrix{Float64}(I, N+1, N+1)) \ X' * y
    return w
end

# Set Function to Output Value
function f(x, w)
    X = polynomial_matrix(x, length(w)-1)
    y = X * w
    return y
end

# Generate Data
step = 1.0 / (M - 1)
x = collect(0.0:step:1.0)
y = gen(x)
println("x = ", x)
println("y = ", y)

# Calculate Coefficient of Polynomial
w = weight(x, y, N, lambda_)
println("w = ", w)

# Output Graph
gr()
scatter(x, y, xlabel="x", ylabel="y", label="data")
graph_x = collect(0.0:0.01:1.0)
graph_y = f(graph_x, w)
plot!(graph_x, base(graph_x), label="base")
plot!(graph_x, graph_y, label=string("f(x, w)"))

# Save Image
savefig("graph")


module NumericalIntegration

using LinearAlgebra
using Logging
using Base.Iterators
using Interpolations

export integrate, cumul_integrate
export Trapezoidal, TrapezoidalEven, TrapezoidalFast, TrapezoidalEvenFast
export SimpsonEven, SimpsonEvenFast
export RombergEven
export IntegrationMethod

abstract type IntegrationMethod end

struct Trapezoidal         <: IntegrationMethod end
struct TrapezoidalEven     <: IntegrationMethod end
struct TrapezoidalFast     <: IntegrationMethod end
struct TrapezoidalEvenFast <: IntegrationMethod end
struct SimpsonEven         <: IntegrationMethod end # https://en.wikipedia.org/wiki/Simpson%27s_rule#Alternative_extended_Simpson.27s_rule
struct SimpsonEvenFast     <: IntegrationMethod end
struct RombergEven{T<:AbstractFloat} <: IntegrationMethod
    acc::T
end # https://en.wikipedia.org/wiki/Romberg%27s_method
RombergEven() = RombergEven(1e-12)

const HALF = 1//2

#documentation

"""
    integrate(x,y...)

Compute numerical integral of y(x) from x=x[1] to x=x[end]. Return a scalar of the same type as the input. If no method is supplied, use Trapezdoial.
"""
function integrate(x,y...) end

"""
    cumul_integrate(x,y...)

Compute cumulative numerical integral of y(x) from x=x[1] to x=x[end]. Return a vector with elements of the same type as the input. If no method is supplied, use Trapezdoial.
"""
function cumul_integrate(x,y...) end

#implementation

function _zero(x,y)
    ret = zero(eltype(x)) + zero(eltype(y))
end

function _zeros(x::AbstractVector,y::AbstractVector)
    ret = zeros(eltype(x),size(x)) + zeros(eltype(y),size(y))
end

"""
    integrate(x::AbstractVector, y::AbstractVector, ::Trapezoidal)

Use Trapezoidal rule. This is the default when no method is supplied.
"""
function integrate(x::AbstractVector, y::AbstractVector, ::Trapezoidal)
    @assert length(x) == length(y) "x and y vectors must be of the same length!"
    retval = _zero(x,y)
    for i in 1 : length(y)-1
        retval += (x[i+1] - x[i]) * (y[i] + y[i+1])
    end
    return HALF * retval
end

"""
    integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalEven)

Use Trapezoidal rule, assuming evenly spaced vector x.
"""
function integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalEven)
    @assert length(x) == length(y) "x and y vectors must be of the same length!"
    return (x[2] - x[1]) * (HALF * (y[1] + y[end]) + sum(y[2:end-1]))
end

"""
    integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalFast)

Use Trapezoidal rule. Unsafe method: no bound checking.
"""
function integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalFast)
    retval = _zero(x,y)
    @fastmath @simd for i in 1 : length(y)-1
        @inbounds retval += (x[i+1] - x[i]) * (y[i] + y[i+1])
    end
    return HALF * retval
end

"""
    integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalEvenFast)

Use Trapezoidal rule, assuming evenly spaced vector x. Unsafe method: no bound checking.
"""
function integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalEvenFast)
    retval = _zero(x,y)
    N = length(y) - 1
    @fastmath @simd for i in 2 : N
        @inbounds retval += y[i]
    end
    @inbounds return (x[2] - x[1]) * (retval + HALF*y[1] + HALF*y[end])
end

"""
    integrate(x::AbstractVector, y::AbstractVector, ::SimpsonEven)

Use Simpson's rule, assuming evenly spaced vector x.
"""
function integrate(x::AbstractVector, y::AbstractVector, ::SimpsonEven)
    @assert length(x) == length(y) "x and y vectors must be of the same length!"
    retval = (17*y[1] + 59*y[2] + 43*y[3] + 49*y[4] + 49*y[end-3] + 43*y[end-2] + 59*y[end-1] + 17*y[end]) / 48
    for i in 5 : length(y) - 1
        retval += y[i]
    end
    return (x[2] - x[1]) * retval
end

"""
    integrate(x::AbstractVector, y::AbstractVector, ::SimpsonEven)

Use Simpson's rule, assuming evenly spaced vector x.  Unsafe method: no bound checking.
"""
function integrate(x::AbstractVector, y::AbstractVector, ::SimpsonEvenFast)
    @inbounds retval = 17*y[1] + 59*y[2] + 43*y[3] + 49*y[4]
    @inbounds retval += 49*y[end-3] + 43*y[end-2] + 59*y[end-1] + 17*y[end]
    retval /= 48
    @fastmath @inbounds for i in 5 : length(y)-1
        retval += y[i]
    end
    @inbounds return (x[2] - x[1]) * retval
end

"""
    integrate(x::AbstractVector, y::AbstractMatrix, method; dims=2)

When y is an array, compute integral along dimension specified by dims (default 2: columns).
"""
function integrate(x::AbstractVector, y::AbstractMatrix, M::IntegrationMethod; dims=2)
    out = [integrate(x,selectdim(y,dims,j),M) for j=1:size(y,dims)]
    return out
end

function integrate(x::AbstractVector, y::AbstractVector, m::RombergEven)
    @assert length(x) == length(y) "x and y vectors must be of the same length!"
    @assert ((length(x) - 1) & (length(x) - 2)) == 0 "Need length of vector to be 2^n + 1"
    maxsteps::Integer = Int(log2(length(x)-1))
    rombaux = zeros(eltype(y), maxsteps, 2)
    prevrow = 1
    currrow = 2
    @inbounds h = x[end] - x[1]
    @inbounds rombaux[prevrow, 1] = (y[1] + y[end])*h*HALF
    @inbounds for i in 1 : (maxsteps-1)
        h *= HALF
        npoints = 1 << (i-1)
        jumpsize = div(length(x)-1, 2*npoints)
        c = 0.0
        for j in 1 : npoints
            c += y[1 + (2*j-1)*jumpsize]
        end
        rombaux[1, currrow] = h*c + HALF*rombaux[1, prevrow]
        for j in 2 : (i+1)
            n_k = 4^(j-1)
            rombaux[j, currrow] = (n_k*rombaux[j-1, currrow] - rombaux[j-1, prevrow])/(n_k - 1)
        end

        if i > maxsteps//3 && norm(rombaux[i, prevrow] - rombaux[i+1, currrow], Inf) < m.acc
            return rombaux[i+1, currrow]
        end

        prevrow, currrow = currrow, prevrow
    end
    finalerr = norm(rombaux[maxsteps-1, prevrow] - rombaux[maxsteps, currrow], Inf)
    @warn "RombergEven :: final step reached, but accuracy not: $finalerr > $(m.acc)"
    @inbounds return rombaux[maxsteps, prevrow]
end

@inline function _midpoints(x::AbstractVector{T}) where T
    length(x) == 1 && return x
    retval = Vector{T}(undef, length(x)-1)
    @simd for i in eachindex(retval)
        retval[i] = HALF * (x[i] + x[i+1])
    end
    return retval
end
@inline function _midpoints(x::AbstractRange)
    length(x) == 1 && return x
    Δx = HALF*step(x)
    return range(first(x)+Δx, stop=last(x)-Δx, length=length(x)-1)
end

function integrate(X::NTuple{N,AbstractVector}, Y::AbstractArray{T,N}, ::Trapezoidal) where {T,N}
    @assert length.(X) == size(Y)
    return integrate(X, Y, TrapezoidalFast())
end

function integrate(X::NTuple{N,AbstractVector}, Y::AbstractArray{T,N}, ::TrapezoidalFast) where {T,N}
    midpnts = map(_midpoints, X)
    Δ(x::AbstractVector) = length(x) > 1 ? diff(x) : 1
    Δxs = map(Δ, X)
    interp = LinearInterpolation(X,Y)
    f((Δx,x)) = prod(Δx)*interp(x...)
    return sum(f, zip(product(Δxs...), product(midpnts...)))
end

function integrate(X::NTuple{N,AbstractVector}, Y::AbstractArray{T,N}, ::TrapezoidalEvenFast) where {T,N}
    midpnts = map(_midpoints, X)
    Δ(x::AbstractVector) = length(x) > 1 ? x[2] - x[1] : 1
    Δx = prod(Δ, X)
    interp = LinearInterpolation(X,Y)
    f(x) = interp(x...)
    return Δx*sum(f, product(midpnts...))
end

function integrate(X::NTuple{N,AbstractRange}, Y::AbstractArray{T,N}, ::TrapezoidalEvenFast) where {T,N}
    midpnts = map(_midpoints, X)
    Δ(x::AbstractVector) = length(x) > 1 ? step(x) : 1
    Δx = prod(Δ, X)
    interp = LinearInterpolation(X,Y)
    f(x) = interp(x...)
    return Δx*sum(f, product(midpnts...))
end

#function integrate(X::Tuple{AbstractVector}, Y::AbstractVector{T}, M::IntegrationMethod) :: T where {T}
#    return integrate(X[1], Y, M)
#end
#"""
#    integrate(X::NTuple{N,AbstractVector}, Y::AbstractArray{T,N}, method, cache=nothing)
#
#Given an n-dimensional grid of values, compute the total integral along each dim
#"""
#function integrate(X::NTuple{N,AbstractVector}, Y::AbstractArray{T,N}, M::IntegrationMethod) :: T where {T,N}
#    n = last(size(Y))
#    cache = Vector{T}(undef, n)
#    x = X[1:N-1]
#    @inbounds for i in 1:n
#        cache[i] = integrate(x, selectdim(Y,N,i), M)
#    end
#    return integrate(X[end], cache, M)
#end

# cumulative integrals

"""
    cumul_integrate(x::AbstractVector, y::AbstractVector, ::Trapezoidal)

Use Trapezoidal rule. This is the default when no method is supplied.
"""
function cumul_integrate(x::AbstractVector, y::AbstractVector, ::Trapezoidal)
    @assert length(x) == length(y) "x and y vectors must be of the same length!"
    retarr = _zeros(x,y)
    for i in 2 : length(y)
        retarr[i] = retarr[i-1] + (x[i] - x[i-1]) * (y[i] + y[i-1])
    end
    return HALF * retarr
end

"""
    cumul_integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalEven)

Use Trapezoidal rule, assuming evenly spaced vector x.
"""
function cumul_integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalEven)
    @assert length(x) == length(y) "x and y vectors must be of the same length!"
    retarr = _zeros(x,y)
    for i in 2 : length(y)
        retarr[i] = retarr[i-1] + (y[i-1] + y[i])
    end
    return (x[2] - x[1]) * HALF * retarr
end

"""
    cumul_integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalFast)

Use Trapezoidal rule. Unsafe method: no bound checking.
"""
function cumul_integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalFast)
    retarr = _zeros(x,y)
    @fastmath for i in 2 : length(y) #not sure if @simd can do anything here
        @inbounds retarr[i] = retarr[i-1] + (x[i] - x[i-1]) * (y[i] + y[i-1])
    end
    return HALF * retarr
end

"""
    cumul_integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalEvenFast)

Use Trapezoidal rule, assuming evenly spaced vector x. Unsafe method: no bound checking.
"""
function cumul_integrate(x::AbstractVector, y::AbstractVector, ::TrapezoidalEvenFast)
    retarr = _zeros(x,y)
    @fastmath for i in 2 : length(y)
        @inbounds retarr[i] = retarr[i-1] + (y[i] + y[i-1])
    end
    @inbounds return (x[2] - x[1]) * HALF * retarr
end

"""
    cumul_integrate(x::AbstractVector, y::AbstractMatrix, method; dims=2)

When y is an array, compute integral along each dimension specified by dims (default 2: columns)
"""
function cumul_integrate(x::AbstractVector, y::AbstractMatrix, M::IntegrationMethod; dims=2)
    return hcat([cumul_integrate(x,selectdim(y,dims,j),M) for j=1:size(y,dims)]...)
end

#default behaviour
integrate(x::AbstractVector, y::AbstractVector) = integrate(x, y, Trapezoidal())

integrate(x::AbstractVector, y::AbstractMatrix; dims=2) = integrate(x, y, Trapezoidal(); dims=dims)

integrate(X::NTuple, Y::AbstractArray) = integrate(X, Y, Trapezoidal())

cumul_integrate(x::AbstractVector, y::AbstractVector) = cumul_integrate(x, y, Trapezoidal())

cumul_integrate(x::AbstractVector, y::AbstractMatrix; dims=2) = cumul_integrate(x, y, Trapezoidal(); dims=dims)

end # module


@doc raw"""
    NodaHilbert{T<:AbstractFloat}

Noda-Hilbert transform.

The Noda-Hilbert transformation is a square, skew symmetric,
[Toeplitz matrix](https://en.wikipedia.org/wiki/Toeplitz_matrix)
with zero diagonal defined as follows:

```math
N_{ij}
    = \begin{cases}
        0                     & i \eq j
        \frac{1}{\pi (j - i)} & i \neq i \\
      \end{cases}
```

# Examples
```jldoctest
julia> N === NodaHilbert{Float64}()
true

julia> NodaHilbert{Float32}()
NodaHilbert{Float32}()

julia> N(3)
3×3 ToeplitzMatrices.Toeplitz{Float64,Complex{Float64}}:
 -0.0        0.31831   0.159155
 -0.31831   -0.0       0.31831
 -0.159155  -0.31831  -0.0
```
"""
# TODO: should we subtype AbstractMatrix?
struct NodaHilbert{T<:AbstractFloat} end
NodaHilbert() = NodaHilbert{Float64}()
const N = NodaHilbert()

# Elements progressively decrease in magnitude.
@inline function calcfirstcolumn(T, n, nmax=21_361_417)
    c = zeros(T, n)
    @inbounds for i in 1:min(n-1, nmax)
        c[i+1] = 1 / (π * i)
    end
    return c
end

"""
    (N::NodaHilbert)(n::Int)

Realize the Noda-Hilbert transform matrix of size `n`.

`n` is the number of rows (and columns) in resulting matrix.
"""
function (N::NodaHilbert{T})(n::Int) where {T}
    c = calcfirstcolumn(T, n)
    return Toeplitz(-c, c)
end

# TODO: check eltype(A) when realizing the transform?
# TODO: probably improve the fact that it realizes an object every time?
Base.:*(N::NodaHilbert, A::AbstractArray) = N(size(A, 1)) * A


# Auxiliary functions
function gcd(a::Integer, b::Integer)
    if a > b
        return gcd(b, a)
    else
        c = b % a
        if c == 0
            return a
            else return gcd(a, c)
        end
    end
end

function gcd(a::Vector{Integer})
    m = a[1]
    for i in 1:length(a)
        m = gcd(m, a[i])
    end
    m
end

lcm(a::Integer, b::Integer) = round(Int, a * b / gcd(a, b))

function lcm(a::AbstractVector{<:Integer})
    m = a[1]
    for i in 1:length(a)
        m = lcm(m, a[i])
    end
m
end

"""
    sieve(n)
Sieve of Eratosthenes, finding all prime numbers up to `n`, see https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes
"""
function sieve(n)
    is_prime=trues(n)
        for i in 2:ceil(Int,√n)
            if is_prime[i]
                for j in i^2:i:n
                    if j %i==0
                        is_prime[j]=false
                    end
                end
            end
        end
    return (x for x in 2:n if is_prime[x])
end

function num_of_factors(n)
    n> 1 || return 1
    key=0
    val=0
    s=1
    k=2
    while n!=k
        if n %k==0
        key=k
        val+=1
            n=round(Int,n/k)
        else
        s*=val+1
        val=0
            k+= k==2 ? 1 : 2
        end
    end
    n==key ? val+=1 : val=1
    s*=val+1
end

function is_abundant(n)
    n>=12 || return false
    s=1
    i=2
    while i*i <=n
        a= n%i
        b=div(n,i)
        if a==0
            if i != b
                s+=i+b
            else
                s+=i
            end
        end
        i+=1
        if s > n
            return true
        end
    end
    return false
end

"""
    cyclelen(x::Integer)
length of Reciprocal cycles
"""
function cyclelen(x::Integer)
    rems=zeros(x)
    loc=1
    @assert x>=1
    r=1
    d,r=divrem(r,x)
    while r!=0
        rems[loc]=r
        r*=10
        d,r=divrem(r,x)
        t=findfirst(x->x==r,rems[1:loc]) #第一个相同位置
        loc+=1 # 当前位置
        if !isnothing(t)
            return loc-t
        end
    end
    return 0
end

"""
    primecounter(f::Function)
return the amount of  prime numbers  generated by `f`.
"""
function primecounter(f::Function)
    x=0
    out=f(x)
    cnt=0
    while isprime(out)
        cnt+=1
        x+=1
        out=f(x)
    end
    return cnt
end

function isprime(n::Integer)
    n<= 1 && return false
    if iseven(n) && n!=2
        return false
    end
    if n==3
        return true
    end
    i=3
    while i*i <=n
        if n%i ==0
            return false
        end
        i+=2
    end
    return true
end

"""
    cycles(n)
return circular numbers generated by `n`, eg. 123 gives [123,231,312].
"""
function cycles(n)
    @assert n>=0
    if n <10
        return [n]
    end
    digs=digits(n)
    d=length(digs)
    cyc=zeros(Int,d)
    x=[10^i for i in 0:d-1]
    for i in 1:d
        cyc[i]=sum(x .* digs[vcat(i:d,1:i-1)])
    end
    return cyc
end

function istruncatableprime(n)
    @assert n >10
    if !isprime(n)
        return false
    else
        str=string(n)
        flag=true
        for i in 1:length(str)-1
            a=parse(Int,str[1:i])
            b=parse(Int,str[i+1:end])
            if !isprime(a) || !isprime(b)
                flag=false
                break
            end
        end
        return flag
    end
end


"""
    regulaFalsi(f,a,b)

adalah fungsi yang mencari nilai akar persamaan dari fungsi `f(x)` pada interval
di antara `a` dan `b`.

# Examples
```jldoctest
julia> f(x) = x*sin(x)-1;

julia> c, flag, M = regulaFalsi(f,0,2);

julia> c
1.1141571430336825

julia> flag
0

julia> M
4×5 Array{Float64,2}:
 0.0  0.0      1.09975  2.0      -0.0200192
 1.0  1.09975  1.12124  2.0       0.00983461
 2.0  1.09975  1.11416  1.12124   5.63036e-6
 3.0  1.09975  1.11416  1.11416   3.00226e-9
```
return solusi `c` dengan `flag` bernilai 0 jika metode regula falsi berhasil menemukan
solusi dan gagal jika tidak 0. Serta, matriks `M` yang berisi catatan proses tiap
iterasi `[k, a, c, b, f(c)]`

"""
function regulaFalsi(f,a,b)
    delta = 10^-7; maxi = 100; flag = 1;
    M = Array{Float64}(undef, 0, 5);
    fa = f(a); fb = f(b);
    if fa*fb > 0
        c = "error : fa fb harus beda tanda";
        flag = 2;
        return;
    end
    for k = 1:maxi
        c = b-fb*(b-a)/(fb-fa);
        fc = f(c);
        dx = min(c-a,b-c);
        M = [M ; [k-1 a c b fc] ];
        if fc == 0
            a = c;
            b = c;
        elseif fa*fc>0
            a = c;
            fa= fc;
        else
            b = c;
            fb= fc;
        end
        if abs(fc) < delta || abs(dx)< delta
            flag = 0; break;
        end
    end
    return c,flag,M
end


# Whitening

## Solve whitening based on covariance
#
# finds W, such that W'CW = I
#
function cov_whitening(C::Cholesky{T}) where {T<:Real}
    cf = C.UL
    Matrix{T}(inv(istriu(cf) ? cf : cf'))
end

cov_whitening!(C::DenseMatrix{<:Real}) = cov_whitening(cholesky!(Hermitian(C, :U)))
cov_whitening(C::DenseMatrix{<:Real}) = cov_whitening!(copy(C))

cov_whitening!(C::DenseMatrix{<:Real}, regcoef::Real) = cov_whitening!(regularize_symmat!(C, regcoef))
cov_whitening(C::DenseMatrix{<:Real}, regcoef::Real) = cov_whitening!(copy(C), regcoef)

## Whitening type

struct Whitening{T<:Real}
    mean::Vector{T}
    W::Matrix{T}

    function Whitening{T}(mean::Vector{T}, W::Matrix{T}) where {T<:Real}
        d, d2 = size(W)
        d == d2 || error("W must be a square matrix.")
        isempty(mean) || length(mean) == d ||
        throw(DimensionMismatch("Sizes of mean and W are inconsistent."))
        return new(mean, W)
    end
end
Whitening(mean::Vector{T}, W::Matrix{T}) where {T<:Real} = Whitening{T}(mean, W)

indim(f::Whitening) = size(f.W, 1)
outdim(f::Whitening) = size(f.W, 2)
mean(f::Whitening) = fullmean(indim(f), f.mean)

transform(f::Whitening, x::AbstractVecOrMat) = transpose(f.W) * centralize(x, f.mean)

## Fit whitening to data

function fit(::Type{Whitening}, X::DenseMatrix{T};
             mean=nothing, regcoef::Real=zero(T)) where {T<:Real}
    n = size(X, 2)
    n > 1 || error("X must contain more than one sample.")
    mv = preprocess_mean(X, mean)
    Z = centralize(X, mv)
    C = rmul!(Z * transpose(Z), one(T) / (n - 1))
    return Whitening(mv, cov_whitening!(C, regcoef))
end

# invsqrtm

function _invsqrtm!(C::Matrix{<:Real})
    n = size(C, 1)
    size(C, 2) == n || error("C must be a square matrix.")
    E = eigen!(Symmetric(C))
    U = E.vectors
    evs = E.values
    for i = 1:n
        @inbounds evs[i] = 1.0 / sqrt(sqrt(evs[i]))
    end
    rmul!(U, Diagonal(evs))
    return U * transpose(U)
end

invsqrtm(C::DenseMatrix{<:Real}) = _invsqrtm!(copy(C))


# This file illustrates the simulation of MFSK signalling. The simulation is performed using
# waveform AWGN channel.

using Plots
using FFTW
using LinearAlgebra
using Statistics
using DigitalCommunications

# Settings
k = 2                                       # Bits per symbol
M = 2^3                                     # Constellation size
T = 1.                                      # Symbol duration
f = 1 / T                                   # Symbols per second
fs = 10                                     # Sampling frequency
ts = 1 / fs                                 # Sampling period
t = 0 : ts : T - ts                         # Time vector for symbol duration
l = length(t)                               # Number of samples per symbol
g(t) = 0 ≤ t ≤ T ? sqrt(2 * Eg / T) : 0     # Modulation pulse: rectangular pulse
Eg = 1                                      # Modulation pulse energy: rectangular pulse
nsymbols = Int(1e6)                         # Number of symbols
Δf = 1 / T                                  # 2 times the minimum frequency seperation for orthogonality

# Define basis
basis = map(m -> (t -> g(t) * exp(1im * 2π * m * Δf * t)), 1 : M)
basisvals = map(base -> base.(t), basis)
alphabet = map(m -> setindex!(zeros(M), 1, m), 1 : M)
references = map(1 : M) do mi
    Ai = alphabet[mi] * sqrt(2 * Eg)
    sum(Ai .* basisvals)
end

# Simulate the system
ebno = collect(0 : 1 : 10)
esno = ebno .+ 10 * log10(k)
m = rand(1 : M, nsymbols)
tx = map(m) do mi
    Ai = alphabet[mi] * sqrt(Eg)
    sum(Ai .* basisvals)
end
Es = mean(txi -> sum(abs.(txi).^2) * ts, tx)
symerr = zeros(length(esno))
for i = 1 : length(symerr)
    # Generate noise
    σ = sqrt(Es * fs / 2 / (10^(esno[i] / 10)))
    n = [σ * (randn(l) + 1im * randn(l)) for i = 1 : length(tx)]
    # Corrupt transmitted signal
    rx = tx + n
    # Detect symbols
    mr = map(rx) do rxi
        argmax(map(reference -> real(rxi ⋅ reference), references))
    end
    # Calculate symbol
    ber = sum(m .!= mr) / length(m)
    symerr[i] = ber
end

# Plots
plt = plot(title="$M-FSK", xlabel="esno [dB]", ylabel="Pe")
plot!(esno, berfsk.(esno, M), marker=:circle, yscale=:log10, label="theoretical")
plot!(esno, symerr, marker=:circle, yscale=:log10, label="montecarlo")


# # Basics
#
# ## Packages
#
# We load the following packages into our environment:

using OptimalTransport
using Distances
using Plots
using PythonOT: PythonOT
using Tulip

using LinearAlgebra
using Random

Random.seed!(1234)

const POT = PythonOT

# ## Problem setup
#
# First, let us initialise two random probability measures $\mu$ (source measure)
# and $\nu$ (target measure) in 1D:

M = 200
μ = fill(1 / M, M)
μsupport = rand(M)

N = 250
ν = fill(1 / N, N)
νsupport = rand(N);

# Now we compute the quadratic cost matrix $C_{ij} = \| x_i - x_j \|_2^2$:

C = pairwise(SqEuclidean(), μsupport', νsupport'; dims=2);

# ## Exact optimal transport
#
# The **earth mover's distance** is defined as the optimal value of the
# Monge-Kantorovich problem
# ```math
# \inf_{\gamma \in \Pi(\mu, \nu)} \langle \gamma, C \rangle =
# \inf_{\gamma \in \Pi(\mu, \nu)} \sum_{i, j} \gamma_{ij} C_{ij},
# ```
# where $\Pi(\mu, \nu)$ denotes the set of couplings of $\mu$ and $\nu$, i.e., the set of joint
# distributions whose marginals are $\mu$ and $\nu$.
# If $C$ is the quadratic cost matrix, the earth mover's distance is known as the square of
# the **2-Wasserstein distance**.
#
# The function `emd` returns the optimal transport *plan* $\gamma$

γ = emd(μ, ν, C, Tulip.Optimizer());

# whilst using `emd2` returns the optimal transport *cost*:

emd2(μ, ν, C, Tulip.Optimizer())

# ## Entropically regularised optimal transport
#
# We may add an entropy term to the Monge-Kantorovich problem to obtain the
# **entropically regularised** optimal transport problem
# ```math
# \inf_{\gamma \in \Pi(\mu, \nu)} \langle \gamma, C \rangle + \epsilon \Omega(\gamma),
# ```
# where $\Omega(\gamma) = \sum_{i, j} \gamma_{ij} \log(\gamma_{ij})$ is the negative
# entropy of the coupling $\gamma$ and $\epsilon$ controls the strength of the regularisation.
#
# This problem is *strictly convex* and admits a very efficient iterative scaling scheme for
# its solution known as the [Sinkhorn algorithm](https://doi.org/10.1214/aoms/1177703591).
#
# We compute the optimal entropically regularised transport plan:

ε = 0.01
γ = sinkhorn(μ, ν, C, ε);

# We can check that one obtains the same result with the Python Optimal Transport (POT) package:

γpot = POT.sinkhorn(μ, ν, C, ε)
norm(γ - γpot, Inf)

# We can compute the optimal cost (a scalar) of the entropically regularized optimal
# transport problem with `sinkhorn2`:

sinkhorn2(μ, ν, C, ε)

# ## Quadratically regularised optimal transport
#
# Instead of the common entropically regularised optimal transport problem, we can solve the
# [quadratically regularised optimal transport problem](https://doi.org/10.1007/s00245-019-09614-w)
# ```math
# \inf_{\gamma \in \Pi(\mu, \nu)} \langle \gamma, C \rangle + \epsilon \frac{\| \gamma \|_F^2}{2}.
# ```
# One property of the quadratically regularised optimal transport problem is that the
# resulting transport plan $\gamma$ is *sparse*. We take advantage of this and represent it as
# a sparse matrix.

quadreg(μ, ν, C, ε; maxiter=100);

# ## Stabilized Sinkhorn algorithm
#
# When $\epsilon$ is very small, we can use a
# [log-stabilised version of the Sinkhorn algorithm](https://doi.org/10.1137/16M1106018).

ε = 0.005
γ = sinkhorn_stabilized(μ, ν, C, ε; maxiter=5_000);

# Again we can check that the same result is obtained with the POT package:

γ_pot = POT.sinkhorn(μ, ν, C, ε; method="sinkhorn_stabilized", numItermax=5_000)
norm(γ - γ_pot, Inf)

# ## Stabilized Sinkhorn algorithm with $\epsilon$-scaling
#
# In addition to log-stabilisation, we can use [$\epsilon$-scaling](https://doi.org/10.1137/16M1106018):

γ = sinkhorn_stabilized_epsscaling(μ, ν, C, ε; maxiter=5_000);

# The POT package yields the same result:

γpot = POT.sinkhorn(μ, ν, C, ε; method="sinkhorn_epsilon_scaling", numItermax=5000)
norm(γ - γpot, Inf)

# ## Unbalanced optimal transport
#
# [Unbalanced optimal transport](https://doi.org/10.1090/mcom/3303) deals with general
# positive measures which do not necessarily have the same total mass. For unbalanced
# source and target marginals $\mu$ and $\nu$ and a cost matrix $C$, entropically
# regularised unbalanced optimal transport solves
# ```math
# \inf_{\gamma \geq 0} \langle \gamma, C \rangle + \epsilon \Omega(\gamma) + \lambda_1 \mathrm{KL}(\gamma 1 | \mu) + \lambda_2 \mathrm{KL}(\gamma^{\mathsf{T}} 1 | \nu),
# ```
# where $\epsilon$ controls the strength of the entropic regularisation, and $\lambda_1$ and
# $\lambda_2$ control how strongly we enforce the marginal constraints.
#
# We construct two random measures, now with different total masses:

M = 100
μ = fill(1 / M, M)
μsupport = rand(M)

N = 200
ν = fill(1 / M, N)
νsupport = rand(N);

# We compute the quadratic cost matrix:

C = pairwise(SqEuclidean(), μsupport', νsupport'; dims=2);

# Now we solve the corresponding unbalanced, entropy-regularised transport problem with
# $\epsilon = 0.01$ and $\lambda_1 = \lambda_2 = 1$:

ε = 0.01
λ = 1
γ = sinkhorn_unbalanced(μ, ν, C, λ, λ, ε);

# We check that the result agrees with POT:

γpot = POT.sinkhorn_unbalanced(μ, ν, C, ε, λ)
norm(γ - γpot, Inf)

# ## Plots
#
# ### Entropically regularised transport
#
# Let us construct source and target measures again:

μsupport = νsupport = range(-2, 2; length=100)
C = pairwise(SqEuclidean(), μsupport', νsupport'; dims=2)
μ = normalize!(exp.(-μsupport .^ 2 ./ 0.5^2), 1)
ν = normalize!(νsupport .^ 2 .* exp.(-νsupport .^ 2 ./ 0.5^2), 1)

plot(μsupport, μ; label=raw"$\mu$", size=(600, 400))
plot!(νsupport, ν; label=raw"$\nu$")

# Now we compute the entropically regularised transport plan:

γ = sinkhorn(μ, ν, C, 0.01)
heatmap(
    μsupport,
    νsupport,
    γ;
    title="Entropically regularised optimal transport",
    size=(600, 600),
)

# ### Quadratically regularised transport
#
# Notice how the "edges" of the transport plan are sharper if we use quadratic regularisation
# instead of entropic regularisation:

γquad = quadreg(μ, ν, C, 5; maxiter=100);
heatmap(
    μsupport,
    νsupport,
    γquad;
    title="Quadratically regularised optimal transport",
    size=(600, 600),
)

# ### Sinkhorn barycenters
#
# For a collection of discrete probability measures $\{\mu_i\}_{i=1}^N \subset \mathcal{P}$,
# cost matrices $\{C_i\}_{i=1}^N$, and positive weights $\{\lambda_i\}_{i=1}^N$ summing to $1$,
# the entropically regularised **barycenter** in $\mathcal{P}$ is the discrete probability
# measure $\mu$ that solves
# ```math
# \inf_{\mu \in \mathcal{P}} \sum_{i = 1}^N \lambda_i \operatorname{OT}_{\epsilon}(\mu, \mu_i)
# ```
# where $\operatorname{OT}_\epsilon(\mu, \mu_i)$ denotes the entropically regularised
# optimal transport cost with marginals $\mu$ and $\mu_i$, cost matrix $C$, and entropic
# regularisation parameter $\epsilon$.
#
# We set up two measures and compute the weighted barycenters. We choose weights
# $\lambda_1 \in \{0.25, 0.5, 0.75\}$.

support = range(-1, 1; length=250)
mu1 = normalize!(exp.(-(support .+ 0.5) .^ 2 ./ 0.1^2), 1)
mu2 = normalize!(exp.(-(support .- 0.5) .^ 2 ./ 0.1^2), 1)

plt = plot(; size=(800, 400), legend=:outertopright)
plot!(plt, support, mu1; label=raw"$\mu_1$")
plot!(plt, support, mu2; label=raw"$\mu_2$")

mu = hcat(mu1, mu2)
C = pairwise(SqEuclidean(), support'; dims=2)
for λ1 in (0.25, 0.5, 0.75)
    λ2 = 1 - λ1
    a = sinkhorn_barycenter(mu, C, 0.01, [λ1, λ2], SinkhornGibbs())
    plot!(plt, support, a; label="\$\\mu \\quad (\\lambda_1 = $λ1)\$")
end
plt


using SeisPlot, SeisAcoustic, FFTW, LinearAlgebra

# velocity and density model
nz = 101; nx = 301
vel = 3000 * ones(nz, nx);  # m/s
vel[51:end,:] .= 3500;  # m/s
rho = 2000 * ones(nz, nx);  # kg/m^3

# top boundary condition
free_surface = false    #(pml or free_surface)

# grid size
h = 10

# organize these parameters into a structure
fparams = FdParams(rho, vel, free_surface, h; data_format=Float64);

# generate observations of one frequency slice by a point source
isz = 5; isx = 151; dt = 0.001;
src = Source(isz, isx, dt, fparams; amp=1/rho[isz,isx]/vel[isz,isx]^2*10^5);

# total modelling length
tmax = 2.0;
nt   = floor(Int64, tmax/dt) + 1

# working frequency
df = 1.0 / (dt * nt);
f_work = 20.0;
iw = floor(Int64, f_work/df) + 1;
f_work = (iw-1) * df;
omega  = 2.0 * pi * f_work;
wlet   = zeros(nt)
copyto!(wlet, src.it_min, src.p)
fwave  = fft(wlet)

# LU decomposition of Helmhotz operator
H = get_helmholtz_LU(fparams, omega);

# source vector
s = zeros(Complex{Float64}, fparams.Nz*fparams.Nx)
s[src.src2spt] = fwave[iw]

# produce modelling true wave field
u = zeros(Complex{Float64}, fparams.Nz * fparams.Nx);
ldiv!(u, H, s);
SeisPlotTX(real(reshape(u, fparams.Nz, fparams.Nx)), wbox=9, hbox=3)

# specify the monochromatic recordings
irx = collect(1:2:nx);
irz = 2 * ones(Int64, length(irx));
dobs= MonochromaticRecordings(irz, irx, omega, fparams);
sample_spt2rec!(dobs, u);

# initial homogenous velocity model
vel = 3000 * ones(nz, nx)
fparams = FdParams(rho, vel, free_surface, h; data_format=Float64);

# helmholtz operator of homogeneous model
H = get_helmholtz_LU(fparams, omega);

# source side wavefield
u = zeros(Complex{Float64}, fparams.Nz * fparams.Nx);
ldiv!(u, H, s);
SeisPlotTX(real(reshape(u, fparams.Nz, fparams.Nx)), wbox=9, hbox=3)

dsyn = MonochromaticRecordings(irz, irx, omega, fparams);
sample_spt2rec!(dsyn, u);

# compute the residue
dres = get_residue(dsyn, dobs);
g = velocity_gradient(dres, u, H, fparams);
g = reshape(g, fparams.nz, fparams.nx)
SeisPlotTX(real(g), wbox=9, hbox=3)

# compute the gradient numerically
vel0    = 3000 * ones(nz, nx);
delta_m = 1e-7;

# one model parameter's gradient
iz = 37; ix = 137;

# positive velocity model partubation
vel0[iz,ix] = vel0[iz,ix] + delta_m

# model parameter
fparams = FdParams(rho, vel0, free_surface, h; data_format=Float64);
H = get_helmholtz_LU(fparams, omega);

# source side wavefield
u = zeros(Complex{Float64}, fparams.Nz * fparams.Nx);
ldiv!(u, H, s);

d1 = MonochromaticRecordings(irz, irx, omega, fparams);
sample_spt2rec!(d1, u);
SeisPlotTX(real(reshape(u, fparams.Nz, fparams.Nx)), wbox=9, hbox=3)

# compute the residue
dres = get_residue(d1, dobs);
J_plus = 1/2.0 * (norm(dres.p))^2

# negative velocity model partubation
vel0[iz,ix] = vel0[iz,ix] - 2*delta_m

fparams = FdParams(rho, vel0, free_surface, h; data_format=Float64);
H = get_helmholtz_LU(fparams, omega);

# source side wavefield
u = zeros(Complex{Float64}, fparams.Nz * fparams.Nx);
ldiv!(u, H, s);

d2 = MonochromaticRecordings(irz, irx, omega, fparams);
sample_spt2rec!(d2, u);

# compute the residue
dres = get_residue(d2, dobs);
J_minus = 1/2.0 * (norm(dres.p))^2;

# compute gradient numerically
g_num = (J_plus - J_minus) / (2 * delta_m)
g[iz,ix]
(g[iz,ix] - g_num) / g_num


#=
A number chain is created by continuously adding the square of the digits in a number to form a new number until it has been seen before.

For example,

44 → 32 → 13 → 10 → 1 → 1
85 → 89 → 145 → 42 → 20 → 4 → 16 → 37 → 58 → 89

Therefore any chain that arrives at 1 or 89 will become stuck in an endless loop. What is most amazing is that EVERY starting number will eventually arrive at 1 or 89.

How many starting numbers below ten million will arrive at 89?
=#
cache = Int[]
function digitchain(n)
  while true
    if n < length(cache)
      return cache[n]
    end
    n = map((x)->x^2, digits(n)) |> sum
    if n == 1
      return 1
    elseif n == 89
      return 89
    end
  end
end

function calc()
  count = 0
  for i in 1:10_000_000
    d = digitchain(i)
    push!(cache, d)
    if d == 89
      count = count + 1
    end
  end
  count
end
@time println(calc())


# Lotka-Volterra, np = 1
id = :Sahlodin2011b

pL = [2.95]
pU = [3.05]
Sahlodin2011b_x0(p::Vector{T}) where T = T[1.2*one(T); 1.1*one(T)]
function Sahlodin2011b_f!(du::Vector{T}, u::Vector{T}, p::Vector{T}, t) where T
    du[1] = p[1]*u[1]*(1.0 - u[2])
    du[2] = p[1]*u[2]*(u[1] - 1.0)
    return
end
tspan = (0.0, 2.0)

prob = DBB.ODERelaxProb(Sahlodin2011b_f!, tspan, Sahlodin2011b_x0, pL, pU)
url = "https://www.sciencedirect.com/science/article/abs/pii/S0168927411000316"
source = "Sahlodin2011"
desc = "Lotka-Volterra, np = 1"


"""
    autocovariance(v::Vector,h::Int; corrected::Bool=true)
``\\hat{\\gamma}(h) =\\frac{1}{n}\\sum_{t=1}^{n-h}(v_{t+h}-\\bar{v})(v_t-\\bar{v})^*``
Calculate the autocovariance of dataset `v` with a delay `h`. If `corrected` is `true`
(the default) then the sum is scaled with `n-h`, whereas the sum
is scaled with `n` if corrected is `false` where `n = length(v)`.
"""
function autocovariance(v::AbstractVector,h::Int; corrected::Bool=true, mean = mean(v))
    n = length(v)
    covsum = zero(mean)
    for i in 1:n-h
        @inbounds covsum += (v[i]-mean)*conj(v[i+h]-mean)
    end
    gamma = covsum/(n - Int(corrected))
    return gamma
end

@doc raw"""
    pseudo_cov(x, y; xmean = mean(x), ymean = mean(y), corrected = true)
Compute the pseudo covariance between collections `x` and `y` returning a scalar:
```math
\frac{1}{n}\sum_{i=1}^{n} (x_i - \bar{x})(y_{i} - \bar{y})
```
Optionally,
precomputed means can be passed as keyword arguments. `pseudo_cov(x,y)` is functionally
equivalent to `Statistics.cov(x, conj(y); corrected = false)` but it is found to be
significantly faster and avoids allocations.
"""
@inline function pseudo_cov(x, y; xmean = mean(x), ymean = mean(y), corrected = true)
    n = length(x)
    @assert length(y) == n
    res = zero(promote_type(eltype(x), eltype(y))) / 1
    for i = 1:n
        @inbounds res += (x[i] - xmean) * (y[i] - ymean)
    end
    return res / (n - Int(corrected))
end


using ModelingToolkit

### Define a differential equation system

@parameters t σ ρ β
@variables x(t) y(t) z(t)
@derivatives D'~t

eqs = [D(x) ~ σ*(y-x),
       D(y) ~ x*(ρ-z)-y,
       D(z) ~ x*y - β*z]
de = ODESystem(eqs, t, [x,y,z], [σ,ρ,β])
ode_f = ODEFunction(de)

### Use in DifferentialEquations.jl

using OrdinaryDiffEq
u₀ = ones(3)
tspan = (0.0,100.0)
p = [10.0,28.0,10/3]
prob = ODEProblem(ode_f,u₀,tspan,p)
sol = solve(prob,Tsit5())

using Plots
plot(sol,vars=(1,2,3))

generate_function(de)[1]

generate_function(de)[2]

jac = calculate_jacobian(de)

jac_expr = generate_jacobian(de)

ModelingToolkit.generate_factorized_W(de)[1]

@variables x y z
@parameters σ ρ β

# Define a nonlinear system
eqs = [0 ~ σ*(y-x),
       0 ~ x*(ρ-z)-y,
       0 ~ x*y - β*z]
ns = NonlinearSystem(eqs, [x,y,z], [σ,ρ,β])
nlsys_func = generate_function(ns)

nl_f = @eval eval(nlsys_func[2])
# Make a closure over the parameters for for NLsolve.jl
f2 = (du,u) -> nl_f(du,u,(10.0,26.0,2.33))

using NLsolve
nlsolve(f2,ones(3))

@derivatives D3'''~t
@derivatives D2''~t
@variables u(t), x(t)
eqs = [D3(u) ~ 2(D2(u)) + D(u) + D(x) + 1
       D2(x) ~ D(x) + 2]
de = ODESystem(eqs, t, [u,x], [])
de1 = ode_order_lowering(de)

de1.eqs

@parameters t σ ρ β
@variables x(t) y(t) z(t)
@derivatives D'~t Dx'~x Dy'~y Dz'~z
eqs = [D(x) ~ σ*(y-x),
       D(y) ~ x*(ρ-z)-y,
       D(z) ~ x*y - β*z]
J = [Dx(eqs[1].rhs) Dy(eqs[1].rhs) Dz(eqs[1].rhs)
 Dx(eqs[2].rhs) Dy(eqs[2].rhs) Dz(eqs[2].rhs)
 Dx(eqs[3].rhs) Dy(eqs[3].rhs) Dz(eqs[3].rhs)]

J = expand_derivatives.(J)

using LinearAlgebra
luJ = lu(J,Val(false))

luJ.L

invJ = inv(luJ)

function lorenz(du,u,p,t)
 du[1] = p[1]*(u[2]-u[1])
 du[2] = u[1]*(p[2]-u[3]) - u[2]
 du[3] = u[1]*u[2] - p[3]*u[3]
end

u = [x,y,z]
du = similar(u)
p = [σ,ρ,β]
lorenz(du,u,p,t)
du

J = [Dx(du[1]) Dy(du[1]) Dz(du[1])
     Dx(du[2]) Dy(du[2]) Dz(du[2])
     Dx(du[3]) Dy(du[3]) Dz(du[3])]
J = expand_derivatives.(J)

using SparseArrays
function SparseArrays.SparseMatrixCSC(M::Matrix{T}) where {T<:ModelingToolkit.Expression}
    idxs = findall(!iszero, M)
    I = [i[1] for i in idxs]
    J = [i[2] for i in idxs]
    V = [M[i] for i in idxs]
    return SparseArrays.sparse(I, J, V, size(M)...)
end
sJ = SparseMatrixCSC(J)

@parameters σ(..)
eqs = [D(x) ~ σ(t-1)*(y-x),
       D(y) ~ x*(σ(t^2)-z)-y,
       D(z) ~ x*y - β*z]

@derivatives Dₜ'~t
Dₜ(x*(σ(t^2)-z)-y)
expand_derivatives(Dₜ(x*(σ(t^2)-z)-y))

_f(x) = 2x + x^2
_f(x)

f(x) = 2x + x^2
@register f(x)

f(x)

function ModelingToolkit.derivative(::typeof(f), args::NTuple{1,Any}, ::Val{1})
    2 + 2args[1]
end
expand_derivatives(Dx(f(x)))

using SciMLTutorials
SciMLTutorials.tutorial_footer(WEAVE_ARGS[:folder],WEAVE_ARGS[:file])


"""
    lagrange(x, xd, yd)
adalah fungsi yang digunakan untuk mencari nilai interpolasi pada titik/vektor `x`, jika
diketahui suatu himpunan pasangan terurut `(xd,yd)`.

# Example
```jl
julia> xd = [1,2,3,5];

julia> yd = [1.06 1.12 1.34 1.78];

julia> lagrange(4,xd,yd)
1.6000000000000003

julia> lagrange([2.5,4,5.5],xd,yd)
3-element Array{Float64,1}:
 1.2175
 1.6000000000000003
 1.8025000000000002
```
return solusi hampiran interpolasi `y`.
"""
function lagrange(x, xd, yd)
  #% sedikit trik agar bisa menghitung nilai interpolasi pada banyak
  #% titik sekaligus. Intinya, tetap diproses satu titik demi titik.
  m=length(x);
  y=zeros(m)
  if m > 1
    for i=1:m
      # proses satu titik demi satu titik
      y[i]=lagrange(x[i], xd, yd);
    end
    return y
  end
  #% periksa jumlah titik dan tentukan derajat polinom
  ntitik = length(xd);
  n = ntitik-1;  #% derajat maksimum, sesuai jumlah titik yang ada
  #% hitung L_{n,k}(x) untuk k=1:(n+1)
  Ln=ones(1,ntitik);
  for i=1:ntitik
    for j=1:ntitik
      if i!=j
        Ln[i] = Ln[i] * (x-xd[j])/(xd[i]-xd[j]);;
      end
    end
  end
  #% hitung P_{N}(x)
  y = 0;
  for i=1:ntitik
    y = y + yd[i]*Ln[i];
  end
  return y
end


mutable struct Lattice
    n::Int64 # number of rows in basis matrix
    m::Int64 # number of columns in basis matrix
    basis::Array{Float64} # matrix whose columns form basis of lattice
    μ::Array{Float64} # lower triangular matrix of gram-schmidt coefficients
    r::Array{Float64} # squared norm of gram-schmidt basis vectors
    gs::Array{Float64} # columns are gram-schmidt basis vectors

    """
        Lattice(basis)

    Construct a Lattice generated by the input basis matrix.
    """
    Lattice(basis) = gram_schmidt(basis);
    Lattice(n, m, basis, μ, r, gs) = new(n, m, basis, μ, r, gs);
end

"""
    gram_schmidt(M)

Compute Gram-Schmidt orthogonalization on columns of input matrix.
"""
function gram_schmidt(M::Array{<:Real})
    n = size(M, 1); # number of rows of matrix
    m = size(M, 2); # number of columns of matrix
    μ = zeros(Float64,m,m); # lower triangular matrix of gram-schmidt coefficients
    r = zeros(Float64,m); # squared norm of gram-schmidt basis vectors
    gs = zeros(Float64,n,m); # columns are gram-schmidt basis vectors
    for i = 1:m
        b = M[:,i];
        for j = 1:i-1 # compute gram-schmidt coefficients for j < i
            μ[i,j] = (transpose(b)*gs[:,j])/r[j];
        end
        for j = 1:i-1 # compute i-th gram-schmidt basis vector
            b -= μ[i,j]*gs[:,j];
        end
        gs[:,i] = b;
        r[i] = norm(b)^2; # squared norm of gram-schmidt vector
        μ[i,i] = (transpose(b)*M[:,i])/r[i];
    end
    return Lattice(n, m, M, μ, r, gs);
end

"""
    update_gram_schmidt!(L)

Update Gram-Schmidt coefficients and vectors.
"""
function update_gram_schmidt!(L::Lattice)
    for i = 1:L.m
        b = L.basis[:,i];
        for j = 1:i-1 # compute gram-schmidt coefficients for j < i
            L.μ[i,j] = (transpose(b)*L.gs[:,j])/L.r[j];
        end
        for j = 1:i-1 # compute i-th gram-schmidt basis vector
            b -= L.μ[i,j]*L.gs[:,j];
        end
        L.gs[:,i] = b;
        L.r[i] = norm(b)^2; # squared norm of gram-schmidt vector
        L.μ[i,i] = (transpose(b)*L.basis[:,i])/L.r[i];
    end
    return L;
end

"""
    size_reduce(L)

Compute size reduction of basis vectors of lattice, such that all Gram-Schmidt coefficients have absolute value at most 1/2. Modify the Gram-Schmidt coefficients of input lattice accordingly.
"""
function size_reduce!(L::Lattice)
    for i =2:L.m
        for j = i-1:-1:1
            L.basis[:,i] -= round(L.μ[i,j])*L.basis[:,j];
            for k = 1:j
                L.μ[i,k] -= round(L.μ[i,j])*L.μ[j,k];
            end
        end
    end
    return L
end

"""
    check_size_reduce(L, tol=1e-5)

Check whether basis vectors of input lattice are size-reduced.
"""
function check_size_reduce(L::Lattice, tol::Real=1e-5)
    for i = 1:L.m, j = 1:i-1
        if abs(L.μ[i,j]) > 0.5 + tol
            return false
        end
    end
    return true
end

"""
    lll!(L, δ=0.75)

Compute LLL reduction of input lattice, with parameter δ such that ``0.25 < δ < 1``.
"""
function lll!(L::Lattice, δ::Real=0.75)
    k = 2;
    while k <= L.m
        size_reduce!(L);
        if norm(L.gs[:,k])^2 >= (δ - L.μ[k,k-1]^2)*norm(L.gs[:,k-1])^2
            k += 1;
        else
            b = copy(L.basis[:,k]);
            L.basis[:,k] = copy(L.basis[:,k-1]);
            L.basis[:,k-1] = b;
            update_gram_schmidt!(L);
            k = max(2, k-1);
        end
    end
    return L;
end

"""
    lll(M, δ=0.75)

Compute LLL reduction of input matrix, with parameter δ such that ``0.25 < δ < 1``.
"""
function lll(M::Array{<:Real}, δ::Real=0.75)
    return lll(Lattice(M)).basis;
end

"""
    check_lll(L, δ=0.75)

Check whether input lattice has an LLL-reduced basis, with parameter δ such that ``0.25 < δ < 1``.
"""
function check_lll(L::Lattice, δ::Real=0.75)
    if !check_size_reduce(L)
        return false;
    end
    for i=1:L.m-1
        if L.r[i+1] < (δ-L.μ[i+1,i]^2)*L.r[i]
            return false;
        end
    end
    return true;
end

"""
    svp(M)

Compute shortest vector in lattice generated by M, using Schnorr-Euchner enumeration.
"""
function svp(M::Array{<:Real})
    L = Lattice(M);
    x = zeros(Int64, L.m);
    x[L.m] = 1;
    A = norm(M[:,L.m])^2
    sol, length =  enum_svp(L, x, L.m, A, 0.0);
    vect = sum([sol[j]*L.basis[:,j] for j=1:L.m]);
    return [vect, length, sol];
end

"""
    enum_svp(L, x, i, radius, l)

Enumerate all lattice points in subtree at height i, where parent nodes are defined by x. Helper method for [`svp`](@ref).
"""
function enum_svp(L::Lattice, x::Array{<:Integer}, i::Integer, radius::Real, l::Real)
    sol = copy(x);
    c = -sum(Float64[x[j]*L.μ[j,i] for j=i+1:L.m]);
    interval = sqrt((radius-l)/L.r[i]); # interval of possibilities for x[i]
    k = floor(Int64, c); # stores value of x[i]
    Δk = 0; # used to do search in zig-zag path
    Δ²k = -1; # used to do search in zig-zag path
    lbound = floor(Int64, c - interval);
    ubound = ceil(Int64, c + interval);
    while k>= lbound & k <= ubound
        l_new = l + (k-c)^2*L.r[i];
        if l_new <= radius # possible new solution found
            if i == 1 & ~(l_new == 0) # leaf of enumeration tree
                # l_new is approximation of length of new solution. Do exact computation of length to avoid errors
                old_sol = sol[i];
                sol[i] = k;
                new_length = norm(sum([sol[j]*L.basis[:,j] for j=1:L.m]))^2;
                if radius > new_length # prune enumeration tree
                    radius = new_length;
                else
                    sol[i] = old_sol;
                end
            elseif i > 1 # solve subtree enumeration recursively
                old_sol = sol[i];
                sol[i] = k;
                subtree_sol, subtree_length = enum_svp(L, sol, i-1, radius, l_new);
                if subtree_length < radius
                    sol[1:i-1] = subtree_sol[1:i-1];
                    radius = subtree_length;
                else
                    sol[i] = old_sol;
                end
            end
        end
        Δ²k = - Δ²k;
        Δk = Δ²k - Δk;
        k += Δk;
    end
    return [sol, radius];
end


using Printf

function sternbrocot(f::Function=(x) -> length(x) ≥ 20)::Vector{Int}
    rst = Int[1, 1]
    i = 3
    while !f(rst)
        append!(rst, Int[rst[i-1] + rst[i-2], rst[i-2]])
        i += 1
    end
    return rst
end

println("First 15 elements of Stern-Brocot series:\n", sternbrocot(x -> length(x) ≥ 15)[1:15], "\n")

for i in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100)
    occurr = findfirst(x -> x == i, sternbrocot(x -> i ∈ x))
    @printf("Index of first occurrence of %3i in the series: %4i\n", i, occurr)
end

print("\nAssertion: the greatest common divisor of all the two\nconsecutive members of the series up to the 1000th member, is always one: ")
sb = sternbrocot(x -> length(x) > 1000)
if all(gcd(prev, this) == 1 for (prev, this) in zip(sb[1:1000], sb[2:1000]))
    println("Confirmed.")
else
    println("Rejected.")
end


function hammingsequence(N)
    if N < 1
        throw("Hamming sequence exponent must be a positive integer")
    end
    ham = N > 4000 ? Vector{BigInt}([1]) : Vector{Int}([1])
    base2, base3, base5 = (1, 1, 1)
    for i in 1:N-1
        x = min(2ham[base2], 3ham[base3], 5ham[base5])
        push!(ham, x)
        if 2ham[base2] <= x
            base2 += 1
        end
        if 3ham[base3] <= x
            base3 += 1
        end
        if 5ham[base5] <= x
            base5 += 1
        end
    end
    ham
end

println(hammingsequence(20))
println(hammingsequence(1691)[end])
println(hammingsequence(1000000)[end])


#=
   Meeting Under the clock (Julian Simon 1994)

   """
   Meeting Under the Clock (This problem is posed by Julian Simon(1994))

   Two persons agree to arrive at the two clock sometime between 1 pm and 2 pm
   and to stay for 20 minutes. What is the probability that they will be there
   at the same time?
   """

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

          c1   30.1994   17.2942     0.1729    0.1723    9331.2798    0.9999     7267.3518
          c2   30.1059   17.3087     0.1731    0.1659   10124.1004    1.0000     7884.8134
        prob    0.5734    0.4946     0.0049    0.0054    9476.5095    1.0000     7380.4591

    According to the WebPPL model meeting_under_the_clock.wppl
    the exact probability is 0.5742602700373461.

    Cf ~/webppl/meeting_under_the_clock.wppl

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

@model function meeting_under_the_clock(w=20)
    c1 ~ DiscreteUniform(1,60)
    c2 ~ DiscreteUniform(1,60)
    # meeting within waiting time
    prob ~ Dirac(abs(c1-c2) <= w)
end

w = 20
model = meeting_under_the_clock(w)

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

display(chns)
# display(plot(chns))


"""
    `RBF_gauss(R;A=1.0,sigma=0.2,deriv=0)`

Kernel function for Gaussian RBF Interpolation. Returns the magnitude of evaluated
function.

The optional argument `deriv` is for the order of the derivative desired, meaning
0 for base, 1 for first derivative (gradient) and 2 for second derivative (Laplacian).

# ARGUMENTS
* `R::Float64` : Magnitude of radius vector -> R = |X_j - X_i|
* `A=1.0`      : Normalization value
* `sigma=0.2`  : Gaussian spreading
* `deriv=0`    : Order of desired derivative

# OUTPUTS
* `res`        : Calculated magnitude
"""
function RBF_gauss(R::Float64;
                   A=1.0,sigma=0.2,deriv=0)

    if deriv == 0
        # const1 = 1/(2*pi)^(3/2)
        # res = const1*exp(-R^2/2)

        res = SVPM.zeta_gauserf(R)
        #res = A*exp(-(R^2)/(2*sigma^2))
    elseif deriv == 1
        res = -((A*R)/(sigma^2))*exp(-(R^2)/(2*sigma^2))
    elseif deriv == 2
        res = ((A/sigma)^2)*((R/sigma)^2 - 3)*exp(-(R^2)/(2*sigma^2))
    end

    return res
end


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

The radical of n, rad(n), is the product of the distinct prime factors of n. For
example, 504 = 2^3 × 3^2 × 7, so rad(504) = 2 × 3 × 7 = 42.

If we calculate rad(n) for 1 ≤ n ≤ 10, then sort them on rad(n), and sorting on
n if the radical values are equal, we get:

 Unsorted |   Sorted  |
 n rad(n) |  n rad(n) |  k
 ---------+-----------+---
 1   1    |  1   1    |  1
 2   2    |  2   2    |  2
 3   3    |  4   2    |  3
 4   2    |  8   2    |  4
 5   5    |  3   3    |  5
 6   6    |  9   3    |  6
 7   7    |  5   5    |  7
 8   2    |  6   6    |  8
 9   3    |  7   7    |  9
10  10    | 10  10    | 10

Let E(k) be the kth element in the sorted n column; for example, E(4) = 8 and
E(6) = 9.

If rad(n) is sorted for 1 ≤ n ≤ 100000, find E(10000).

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

using Primes
using Base.Test

function compute_table(limit)
    res = Tuple{Int,Int}[]
    for n in 1:limit
        push!(res, (prod(keys(factor(n))), n))
    end

    sort!(res)
    map(pair -> pair[2], res)
end

test_table = compute_table(10)

@test test_table[4] == 8
@test test_table[6] == 9

actual_table = compute_table(100_000)

println("Euler 124: $(actual_table[10_000])")


using ModelingToolkit, NonlinearSolve

@parameters t
@variables u1(t) u2(t) u3(t) u4(t) u5(t)

eqs = [
        0 ~ u1 - sin(u5),
        0 ~ u2 - cos(u1),
        0 ~ u3 - hypot(u1, u2),
        0 ~ u4 - hypot(u2, u3),
        0 ~ u5 - hypot(u4, u1)
]

sys = NonlinearSystem(eqs, [u1, u2, u3, u4, u5], [])

simple_sys = structural_simplify(sys)

equations(simple_sys)

prob = NonlinearProblem(simple_sys, [u5=>0.0])
sol = solve(prob, NewtonRaphson())

sol[u5]
sol[u1]

# stochastic differential equations
using ModelingToolkit, DifferentialEquations

@parameters t σ ρ β
@variables x(t) y(t) z(t)
D = Differential(t)

eqs = [
        D(x) ~ σ*(y-x),
        D(y) ~ x*(ρ-z)-y,
        D(z) ~ x*y - β*z
       ]

noise_eqs = [0.1*x, 0.1*y, 0.1*z]

de = SDESystem(eqs, noise_eqs, t, [x, y, z], [σ, ρ, β])

u0map = [x=> 1.0, y=> 0.0, z=>0.0]
parammap = [σ => 10.0, ρ => 28.0, β => 8/3]

prob = SDEProblem(de, u0map, (0.0, 100.0), parammap)

sol = solve(prob)
plot(sol, vars=(x,y,z))

# ------------ optimization problem ----------------------
using ModelingToolkit, GalacticOptim, Optim

@variables x y
@parameters a b
loss = (a-x)^2 + b * (y -x^2)^2

sys = OptimizationSystem(loss, [x,y], [a,b])

# initial guess
u0 = [x=>1.0, y=>2.0]
p = [a => 6.0, b=> 7.0]

prob = OptimizationProblem(sys, u0, grad=true, hess=true)
sol = solve(prob) # doesn't work yet

# ------------ dsl demo via Catalyst ---------------------
using DifferentialEquations, Catalyst, Latexify, ModelingToolkit, Plots

repressilator = @reaction_network begin
    hillr(P₃,α,K,n), ∅ --> m₁
    hillr(P₁,α,K,n), ∅ --> m₂
    hillr(P₂,α,K,n), ∅ --> m₃
    (δ,γ), m₁ ↔ ∅
    (δ,γ), m₂ ↔ ∅
    (δ,γ), m₃ ↔ ∅
    β, m₁ --> m₁ + P₁
    β, m₂ --> m₂ + P₂
    β, m₃ --> m₃ + P₃
    μ, P₁ --> ∅
    μ, P₂ --> ∅
    μ, P₃ --> ∅
end α K n δ γ β μ;


using Plots
gr()

################################
### Algoritmo Newton-Raphson ###
################################

function NewtonRaphson(f, fd, x; tol = 1e-8, max_iter=1000, max_tempo=10.0)
    fx = f(x)               # primeira iteracao
    i = 0                   # contador de iteracoes
    t0 = time()             # tempo de execucao
    t1 = time() - t0
    estado = "Desconhecido" # estado do algoritmo

    # define erro absoluto e erro relativo e calcula o erro
    atol = rtol = tol
    erro = atol + rtol*abs(fx)

    # regras de parada
    rp1 = (abs(fx) ≤ erro)
    rp2 = (t1 ≥ max_tempo || i ≥ max_iter)

    while !(rp1 || rp2)
        fdx = fd(x)

        x = x - fx/fdx
        if abs(fdx) ≤ erro
            estado = "Falha na convergência. Derivada nula encontrada."
            break
        end
        fx = f(x)

        i += 1
        t1 = time() - t0

        rp1 = (abs(fx) ≤ erro)
        rp2 = (t1 ≥ max_tempo || i ≥ max_iter)
    end

    if rp1
        estado = "Convergência Obtida."
    elseif rp2
        i ≥ max_iter ? estado = "Falha na convergência. O Algoritmo atingiu iteração limite" : estado = "Falha na convergência. O algoritmo atingiu tempo limite"
    end
    println("\n",estado,"\n")
    println("Raiz em $x com $i iterações.\n")
    return x, fx, estado
end

Plots.plot(x -> x^2 - 2, -5, 5, leg=false)
f(x) = x^2 - 2
fd(x) = 2x
NewtonRaphson(f, fd, 500.0, tol = 1e-20)
NewtonRaphson(f, fd, -100.0)

f(x) = x * exp(x) - 1
fd(x) = exp(x) + x * exp(x)
Plots.plot(f, -2, 5, leg=false)
NewtonRaphson(f, fd, 0.0)

f(x) = x^2 + 3
fd(x) = 2x
Plots.plot(f, -2, 2, leg=false)
NewtonRaphson(f, fd, 10)

##################################################################
### Newton-Raphson aproximando derivada com diferenças finitas ###
##################################################################

function newton_fin_dif(f, x; h = 0.1, tol = 1e-10, N = 1000)
    c = 0
    for i = 1:N
        c += 1
        xnew = x - (2*h*f(x)) / (f(x+h) - f(x-h))
        if abs(xnew - x) <= tol
            print("root at $x with $(c) iterations.\nError: $(round(xnew-x, sigdigits=5))\n")
            return xnew, c
        end
        x = xnew
    end
    print("WARNING: Function did NOT converge.\nRoot at $x with $(c) iterations.\nError: $(round(xnew-x, sigdigits=5))\n")
    return xnew, c
end

f(x) = 2x^2 - 5x
plot(f, -2, 5, leg=false)
newton_fin_dif(f, 20)

newton_fin_dif(x -> 2x^2 - 5x, 100);
newton_fin_dif(x -> 2x^2 - 5x, -0.5)
newton_fin_dif(x -> 2x^2 - 5x, -500);

plot(x -> x^3 - 2x - 5, -2, 5, leg=false)
newton_fin_dif(x -> x^3 - 2x - 5, 5; h = 0.01, tol=0.0001);

##############################################
### Newton-Raphson Com multiplas dimensões ###
##############################################

function newton_mult_dim(f, G, x; alpha = 0.1, tol = 1e-10, N = 1000)
    c = 0
    for i = 1:N
        c += 1
        xnew = x - alpha*(f(x)./G(x))
        if abs(f(xnew)) <= tol
            print("roots at $xnew with $(c) iterations.\nError: $(round.(xnew-x, sigdigits=5))\n")
            return xnew, c
        end
        x = xnew
    end
    print("WARNING: Function did NOT converge.\nRoots at $x with $(c) iterations.\nError: $(round.(xnew-x, sigdigits=5))\n")
    return xnew, c
end

a1 = range(-20,20,step=0.1)
a2 = range(-20,20,step=0.1)
zplot(x,y) = x^2 + y^2 - 5^2
Plots.surface(a1, a2, zplot)

z(x) = x[1]^2 + x[2]^2 - 5^2
G(x) = [2x[1], 2x[2]]
x0 = [3.0; 5.0]
newton_mult_dim(z, G, x0)

#############################################
### Newton-Raphson usando pacote Roots.jl ###
#############################################

using Roots
f(x) = x^3 - 2x - 5
fp(x) = 3x^2 - 2
newton_fin_dif(f, 2);
NewtonRaphson(f, fp, 10)
x = Roots.newton(f, fp, 2, verbose=true)
x = find_zero((f, fp), 2, Roots.Newton(), verbose=true)
x, f(x)


function chebyshev_polynomial_derivative(order::S,x::T) where {T<:AbstractFloat,S<:Integer}

  polynomial = ones(T,1,order+1)
  poly_deriv = zeros(T,1,order+1)

  for i = 2:order+1
    if i == 2
      polynomial[i] = x
      poly_deriv[i] = 1.0
    else
      polynomial[i] = 2*x*polynomial[i-1]-polynomial[i-2]
      poly_deriv[i] = ((i-1)*polynomial[i-1]-(i-1)*x*polynomial[i])/(1-x^2)
    end
  end

  return poly_deriv

end

function chebyshev_polynomial_derivative(order::S,x::Array{T,1}) where {T<:AbstractFloat,S<:Integer}

  polynomial = ones(T,length(x),order+1)
  poly_deriv = zeros(T,length(x),order+1)

  for i = 1:length(x)
    poly_deriv[i,:] = chebyshev_polynomial_derivative(order,x[i])
  end

#=

  for i = 2:order+1
	for j = 1:length(x)
      if i == 2
        polynomial[j,i] = x[j]
        poly_deriv[j,i] = 1.0
      else
        polynomial[j,i] = 2*x[j]*polynomial[j,i-1]-polynomial[j,i-2]
        poly_deriv[j,i] = ((i-1)*polynomial[j,i-1]-(i-1)*x[j]*polynomial[j,i])/(1-x[j]^2)
      end
    end
  end

=#

  return poly_deriv

end


module CalcDist

import StatsFuns: normpdf, normcdf, norminvcdf,
                  tdistpdf, tdistcdf, tdistinvcdf,
                  chisqpdf, chisqcdf, chisqinvcdf,
                  fdistpdf, fdistcdf, fdistinvcdf

export normalpdf, normalcdf, invNorm,
       tpdf, tcdf, invT,
       X2pdf, X2cdf, invX2,
       Fpdf, Fcdf, invF,
       binompdf, binomcdf,
       poissonpdf, poissoncdf,
       geometpdf, geometcdf,
       nCr, nPr

##
# Continuous
##

"""
   normalpdf(x, μ = 0, σ = 1) \n
      x: value
      μ: mean
      σ: standard deviation
   probability density function (Normal)
"""
function normalpdf(x, μ = 0, σ = 1)
   return normpdf(μ, σ, x)
end

"""
   normalcdf(lower, upper, μ = 0, σ = 1) \n
      lower: lower boundary
      upper: upper boundary
          μ: mean
          σ: standard deviation
   cumulative distribution function (Normal)
"""
function normalcdf(lower, upper, μ = 0, σ = 1)
   return normcdf(μ, σ, upper) - normcdf(μ, σ, lower)
end

"""
   invNorm(area, μ = 0, σ = 1) \n
      area: probability area
         μ: mean
         σ: standard deviation
   inverse cumulative distribution function (Normal)
"""
function invNorm(area, μ = 0, σ = 1)
   return norminvcdf(μ, σ, area)
end

"""
   tpdf(x, ν) \n
      x: value
      ν: degrees of freedom
   probability density function (Student's t)
"""
function tpdf(x, ν)
   return tdistpdf(ν, x)
end

"""
   tcdf(lower, upper, ν) \n
      lower: lower boundary
      upper: upper boundary
          ν: degrees of freedom
   cumulative distribution function (Student's t)
"""
function tcdf(lower, upper, ν)
   return tdistcdf(ν, upper) - tdistcdf(ν, lower)
end

"""
   invT(area, ν) \n
      area: probability area
         ν: degrees of freedom
   inverse cumulative distribution function (Student's t)
"""
function invT(area, ν)
   return tdistinvcdf(ν, area)
end

"""
   X2pdf(x, ν) \n
      x: value
      ν: degrees of freedom
   probability density function (Chi-squared)
"""
function X2pdf(x, ν)
   return chisqpdf(ν, x)
end

"""
   X2cdf(lower, upper, ν) \n
      lower: lower boundary
      upper: upper boundary
          ν: degrees of freedom
   cumulative distribution function (Chi-squared)
"""
function X2cdf(lower, upper, ν)
   return chisqcdf(ν, upper) - chisqcdf(ν, lower)
end

"""
   invX2(area, ν) \n
      area: probability area
         ν: degrees of freedom
   inverse cumulative distribution function (Chi-squared)
"""
function invX2(area, ν)
   return chisqinvcdf(ν, area)
end

"""
   Fpdf(x, nν, dν) \n
       x: value
      nν: numerator degrees of freedom
      dν: denominator degrees of freedom
   probability density function (F)
"""
function Fpdf(x, nν, dν)
   return fdistpdf(nν, dν, x)
end

"""
   Fcdf(lower, upper, nν, dν) \n
      lower: lower boundary
      upper: upper boundary
         nν: numerator degrees of freedom
         dν: denominator degrees of freedom
   cumulative distribution function (F)
"""
function Fcdf(lower, upper, nν, dν)
   return fdistcdf(nν, dν, upper) - fdistcdf(nν, dν, lower)
end

"""
   invF(area, nν, dν) \n
      area: probability area
        nν: numerator degrees of freedom
        dν: denominator degrees of freedom
   inverse cumulative distribution function (F)
"""
function invF(area, nν, dν)
   return fdistinvcdf(nν, dν, area)
end

##
# Discrete
##

"""
   binompdf(n, p, x = nothing) \n
      n: trials
      p: probability of success
      x: value
   probability mass function (Binomial)
"""
function binompdf(n, p, x = nothing)
   if n ≤ 0
      throw(DomainError(n, "\t[n > 0] must be true."))
   end
   if p < 0 || p > 1
      throw(DomainError(p, "\t[0 ≤ p ≤ 1] must be true."))
   end

   if isnothing(x)
      x = range(0, stop=n)
   end

   probs = Vector{Real}()

   for i in x
      push!(probs, binomial(n, i) * p^i * (1-p)^(n-i))
   end

   return probs
end

"""
   binomcdf(n, p, x = nothing) \n
      n: trials
      p: probability of success
      x: value
   cumulative distribution function (Binomial)
"""
function binomcdf(n, p, x = nothing)
   if n ≤ 0
      throw(DomainError(n, "\t[n > 0] must be true."))
   end
   if p < 0 || p > 1
      throw(DomainError(p, "\t[0 ≤ p ≤ 1] must be true."))
   end

   if isnothing(x)
      x = range(0, stop=n)
   end

   probs = Vector{Real}()

   for i in x
      push!(probs, sum([ binomial(n, i) * p^i * (1-p)^(n-i) for i in 0:i ]))
   end

   return probs
end

"""
   poissonpdf(λ, x) \n
      λ: mean, variance
      x: value
   probability mass function (Poisson)
"""
function poissonpdf(λ, x)
   if λ ≤ 0
      throw(DomainError(λ, "\t[λ > 0] must be true."))
   end

   probs = Vector{Real}()

   for i in x
      push!(probs, ℯ^(-λ) * (λ^i / factorial(i)))
   end

   return probs
end

"""
   poissoncdf(λ, x) \n
      λ: mean, variance
      x: value
   cumulative distribution function (Poisson)
"""
function poissoncdf(λ, x)
   if λ ≤ 0
      throw(DomainError(λ, "\t[λ > 0] must be true."))
   end

   probs = Vector{Real}()

   for i in x
      push!(probs, ℯ^(-λ) * sum([ λ^i / factorial(i) for i in 0:i ]))
   end

   return probs
end

"""
   geometpdf(p, x) \n
      p: probability of success
      x: trial on which 1st success occurs
   probability density function (Geometric)
"""
function geometpdf(p, x)
   if p < 0 || p > 1
      throw(DomainError(p, "\t[0 ≤ p ≤ 1] must be true."))
   end

   probs = Vector{Real}()

   for i in x
      push!(probs, p * (1-p)^(i-1))
   end

   return probs
end

"""
   geometcdf(p, x) \n
      p: probability of success
      x: trial on which 1st success occurs
   cumulative distribution function (Geometric)
"""
function geometcdf(p, x)
   if p < 0 || p > 1
      throw(DomainError(p, "\t[0 ≤ p ≤ 1] must be true."))
   end

   probs = Vector{Real}()

   for i in x
      push!(probs, 1 - (1-p)^i)
   end

   return probs
end

##
# Counting
##

"""
   nCr(n, r) \n
      return number of combinations of n items taken r at a time. n and r must be
      non-negative. both n and r can be vectors.
"""
function nCr(n, r)
   if n < 0
      throw(DomainError(n, "\t[n ≥ 0] must be true."))
   end

   combs = Vector{Real}()

   for i in r
      if i < 0
         throw(DomainError(i, "\t[r ≥ 0] must be true."))
      end
      push!(combs, factorial(n) / ( factorial(i) * factorial(n-i) ))
   end

   return combs
end

function nCr(n::Vector{Int}, r)
   if r < 0
      throw(DomainError(r, "\t[r ≥ 0] must be true."))
   end

   combs = Vector{Real}()

   for i in n
      if i < 0
         throw(DomainError(i, "\t[n ≥ 0] must be true."))
      end
      push!(combs, factorial(i) / ( factorial(r) * factorial(i-r) ))
   end

   return combs
end

"""
   nPr(n, r)
      return number of permutations of n items taken r at a time. n and r must be
      non-negative. both n and r can be vectors.
"""
function nPr(n, r)
   if n < 0
      throw(DomainError(n, "\t[n ≥ 0] must be true."))
   end

   perms = Vector{Real}()

   for i in r
      if i < 0
         throw(DomainError(i, "\t[r ≥ 0] must be true."))
      end
      push!(perms, factorial(n) / factorial(n-i))
   end

   return perms
end

function nPr(n::Vector{Int}, r)
   if r < 0
      throw(DomainError(r, "\t[r ≥ 0] must be true."))
   end

   perms = Vector{Real}()

   for i in n
      if i < 0
         throw(DomainError(i, "\t[n ≥ 0] must be true."))
      end
      push!(perms, factorial(i) / factorial(i-r))
   end

   return perms
end

end # module


f = x -> begin
	N = lastindex(x)
    return 100sum((x[i+1] - x[i]^2)^2 for i = 1:N-1) + sum((x[i] - 1.0)^2 for i = 1:N-1)
end

g! = (g, x) -> begin
	N = lastindex(x)
	g[1] = -2*(1 - x[1]) - 400*x[1]*(-x[1]^2 + x[2])
	for i in 2:N-1
		g[i] = -2*(1 - x[i]) + 200*(-x[i-1]^2 + x[i]) - 400*x[i]*(-x[i]^2 + x[1 + i])
	end
	g[N] = 200 * (x[N] - x[N-1]^2)
	return g
end

fg! = (g, x) -> begin
	@warn "rosen fg! not implmented"
end

init = (n::Int=500) -> begin
	x0 = [j/(n+1) for j in 1:n]
	return n, x0
end

TestSet["rosen"] = UncProgram("rosen",  f, g!, fg!, init)


"""
`SymmetricDoubleWell` - The classic symmetric doublewell potential.

### Fields
* `x` - Position x
"""
function SymmetricDoubleWell(x)
    return (x^2 -1)^2;
end

"""
`AsymmetricDoubleWell` - An asymmetric double well potential requires |δ|< 1 for
multiple minima.

### Fields
* `x` - Position x
### Optional Fields
* `δ = 0.5` - Asymmetry parameter
"""
function AsymmetricDoubleWell(x; δ = 0.5)
    return x^4 - 1.5 * x^2 - δ * x;
end

"""
`FatSkinnyDoubleWell10` - The fat-skinny double well of degree 10. This introduces
entropic effects in dimension 1.

### Fields
* `x` - Position x
"""
function FatSkinnyDoubleWell10(x)
    return ((8-5*x)^8 * (2+5*x)^2)/(2^26);
end


"""
    fft_freqs(ts)

    Returns an array of DFT frequency points associated with the set of
    time-domain points.

    Assumes unit of argument `x` is sec. Output unit is in Hz (not rad/sec).
"""
function fft_freqs(ts)
    fs = (ts[2]-ts[1])^(-1) # sampling frequency
    N = Int(length(ts))
    deltaf = fs/N
    if N%2 ==0
        freqs = deltaf*(-N/2:N/2-1)
    else
        freqs = deltaf*(-(N-1)/2:(N-1)/2)
    end
    if length(ts) !== length(freqs)
        @printf("length(ts): %d, length(freqs): %d, delta_f: %f.\n",
                length(ts), length(freqs), deltaf)
        error("Number of time points should match the number of frequency
              points.")
    end
    return freqs
end


using LinearAlgebra,Statistics
include("plotshelper.jl")
x_vals = [0 0 0 ; 2 -3 -4]
y_vals = [0 0 0; 4 3 -3.50]
plt(x_vals,y_vals)
vals = [ 0 0 0; 2* [-2 1 2]]
plt(vals,vals)

f(x,y) = 0.2x + 0.1y

x_vec = [0 0; 3 3]
y_vec = [0 0; 4 -4]
z_vec = [0 0; f(x_vec[2,1],y_vec[2,1]) f(x_vec[2,2],y_vec[2,2])]

n=20
grid = range(-5,5,length=n)
z2 = [ f(grid[row],grid[col]) for row in 1:n, col in 1:n]
wireframe(grid,grid,z2,fill=:blue,gridalpha=1)
plot!(x_vec,y_vec,z_vec,color=[:blue,:green],linewidth=1,labels="", colorbar=false)

# linear equation

f(x) = 0.6cos(4x)+ 1.3
grid = range(-2,2,length=100)
y_min,y_max = extrema(f(x) for x in grid)
plt1 = plot(f,xlim=(-2,2),lable="f")
hline!(plt1,[f(0.5)],linestyle=:dot,linewidth=2, label="h line")
vline!(plt1,[-1.07, -0.5, 0.5, 1.07],linestyle=:dot,linewidth=2, label="v line")
plot!(plt1,fill(0,2),[y_min y_min;y_max y_max],lw=6,color=:blue,label="tange of f")

#eigen values and eigen vectors

A = [1 2;2 1]
evals, evacs = eigen(A)
a1, a2 = evals
eig_1 = [0 0;evacs[:,1]']


using PyPlot
A,B,C=1,2,3 # give variables a number
Nlevels=10

function demoSkill()
    A,B,C=1,2,3 # give variables a number
    Nlevels=10

    sigmoid(x)=1./(1+exp(-x))

    pLeftPlayerWins=zeros(Nlevels,Nlevels)
    for levelleft=1:10
        for levelright=1:10
            pLeftPlayerWins[levelleft,levelright]=sigmoid(levelleft-levelright)
        end
    end

    #prior_pot=exp.(-0.05*(Nlevels/2-linspace(0,Nlevels,Nlevels)).^2) # peaked around skill 5
    prior_pot=ones(Nlevels,1) # uniform prior on skill
    prior_pot=prior_pot./sum(prior_pot)
    priorA=PotArray(A,prior_pot)
    priorB=PotArray(B,prior_pot)
    priorC=PotArray(C,prior_pot)

    fAbeatsB=PotArray([A B],pLeftPlayerWins)
    fBbeatsC=PotArray([B C],pLeftPlayerWins)
    fAbeatsC=PotArray([A C],pLeftPlayerWins)

    Data = "A>B, B>C"
    pABC=fAbeatsB*fBbeatsC*priorA*priorB*priorC # p(A,B,C,Data)
    plotstuff(Data,pABC,fAbeatsC)

    Data = "A>B, A>B"
    pABC=fAbeatsB*fAbeatsB*priorA*priorB*priorC # p(A,B,C,Data)
    plotstuff(Data,pABC,fAbeatsC)

    Data = "(A>B)^6, (B>C)^6"
    pABC=fAbeatsB^6*fBbeatsC^6*priorA*priorB*priorC # p(A,B,C,Data)
    plotstuff(Data,pABC,fAbeatsC)

    Data = "A>B, C>B"
    pABC=fAbeatsB*(1-fBbeatsC)*priorA*priorB*priorC # p(A,B,C,Data)
    plotstuff(Data,pABC,fAbeatsC)

end

function plotstuff(Data,pABC,fAbeatsC)
    pABC=pABC./sum(pABC) # normalise to get p(A,B,C|Data)
    pAC=sum(pABC,B)
    pAbeatsC=sum(fAbeatsC*pAC)
    # What is the probability that A will beat C?
    println(Data*" : probability that A beats C = $(pAbeatsC.content)")
    pA=sum(pABC,[B C]); pB=sum(pABC,[A C]); pC=sum(pABC,[B A])
    figure(); title(Data)
    plot(pA.content,label="A skill")
    plot(pB.content,label="B skill")
    plot(pC.content,label="C skill")
    legend()
    # Can calculate mean skill levels using:
    println("mean skill for A=",sum(pA.content.*(1:Nlevels))) # mean skill level for A
    println("mean skill for B=",sum(pB.content.*(1:Nlevels))) # mean skill level for B
    println("mean skill for C=",sum(pC.content.*(1:Nlevels))) # mean skill level for C
end