# Этот файл --- первый рабочий скрипт, который получил начальное приближение.
# Оставлен в оригинальном виде.
# Сейчас этот функционал предоставляется одной функцией: `initial_guess`,
# но там внутри все разбито на более мелкие функции.
# Здесь же, всё в одном месте!

using NonLinearReactionAdvectionDiffusionWithFrontData
using Dierckx
using LinearAlgebra;
using Plots; pyplot();

# -----------------------------------------------------------------------------
u_l(t) = -8;
u_r(t) =  4;
qf(x) = 4*sin(3 * π * x);       # Коэффициент линейного усиления,
ε = 0.01;                       # Малый параметр при старшей производной
a, b = 0, 1;                    # Область по X
t₀, T = 0, 0.45;                # Область по T
N, M = 1000, 800;               # Кол-во разбиений по X, T
h = (b-a)/N;                    # шаг по X
τ = (T-t₀)/M;                   # шаг по T
Xₙ = [a  + n*h for n in 0:N];   # Сетка по Х
Tₘ = [t₀ + m*τ for m in 0:M];   # Сетка по Т
qₙ =     qf.(Xₙ);               # Сеточные значения коэффициента лин. усиления
ulₘ=    u_l.(Tₘ);               # Сеточные значения левого  ГУ
urₘ=    u_r.(Tₘ);               # Сеточные значения правого ГУ
u₀ = u_init.(Xₙ, ε=ε, x_tp = 0.01);               # Начальные условия
nothing #hide
#
u, XX, TP = solve(u₀, Xₙ, N, Tₘ, M, ε, ulₘ, urₘ, qₙ);
ϕl, ϕr, ϕ, f1_data, f2_data = generate_obs_data(u, Xₙ, N, Tₘ, M, qₙ, ulₘ, urₘ);
nothing # hide
# -----------------------------------------------------------------------------

# Набросаем эскиз прямой задачи.
# В дальнейшем, нарисуем на нем скорость фронта, которой он обладал проходя
# данный узел пространственной сетки.
psol = plot(title="Эскиз решения");
plot!(Xₙ, u[:, 1], label="u(x, 0)");
plot!(Xₙ, u[:, div(end,2)], label="u(x, 0.5)");
plot!(Xₙ, u[:, end], label="u(x, 1)");
plot!(Xₙ, ϕl[:,1], label="");
plot!(Xₙ, ϕr[:,1], label="");
plot!(Xₙ, ϕ[:,1], label="")

# Вычислим ``\frac{d f_1}{dt}`` Скорость движения фронта, как функцию времени.
# -----------------------------------------------------------------------------
df1dt = zero(f1_data);
df1dt[1] = (-3 * f1_data[1] + 2 * f1_data[2] - f1_data[3]) / (2 * τ);
for m in 2:M-2
    df1dt[m] = (f1_data[m+1] - f1_data[m-1]) / (2 * τ);
end
df1dt[M+1] = (f1_data[M+1 - 2] - 2 * f1_data[M+1 - 1] + 3 * f1_data[M+1]) / (2 * τ);
pvel = plot(title="Скорость движения переходного слоя");
plot!(Tₘ, f1_data, label="f_1");
plot!(Tₘ, df1dt, label="v_m")
#
# Последние точки некрасивые, обрежем их
x = f1_data[2:end-1];
y = df1dt[2:end-1];
tm = Tₘ[2:end-1];
#
pvel = plot(title="Скорость движения переходного слоя");
plot!(tm, x, label="f_1");
plot!(tm, y, label="v_m")
# -----------------------------------------------------------------------------

# Найдем минимальный и максимальный номер узлов сетки Xₙ, которые пересекает
# переходный слой
# -----------------------------------------------------------------------------
# .     Узлы сетки
# x     f1(t) положение переходного слоя
#                     k                                   K
# .   .   .   .   .   .   .   .   .   .   .   .   .   .   .   .   .   .   .
#                   x x x x x x x x x x x x x x x x x x x x x
k = findfirst(z -> z > minimum(x), Xₙ)
K = findfirst(z -> z > maximum(x), Xₙ) - 1
@info "Выбор точек, которые прошел фронт" minimum(x) Xₙ[k] maximum(x) Xₙ[K]
# -----------------------------------------------------------------------------

# Найдем скорость, которой обладал фронт, проходя узлы сетки,
# т.е. найдем ``v_n`` скорость, как функцию пространственной координаты.
# -----------------------------------------------------------------------------
vspl = Spline1D(x, y);
Vn = zero(Xₙ);          # Скорость в узла, которые фронт не проходил --- 0
for n in k:K            # В остальных, найдем интерполяцией.
        Vn[n] = vspl(Xₙ[n])
end
scatter!(psol, Xₙ, Vn, label="v_n")
# -----------------------------------------------------------------------------

# Матрица системы
# -----------------------------------------------------------------------------
Al = LowerTriangular(h * ones(K-k+1, K-k+1));
Ar = UpperTriangular(-h * ones(K-k+1, K-k+1));
Al[:, 1]    .= (k-1)*h;
Ar[:, end]  .= (N+1-K)*h;
# Повторим чертеж для объяснения двух прошлых строк
# N = 17, тогда нумерация массива будет следующей
# k = 6, K = 16
# Интервалов лежащих слева от k -- 5, (k-1)
# Интервалов лежащих справа от K -- 2, (N+1 - K)
# 1   2   3   4   5   6   7   8   9   11  12  13  14  15  16  17  18
#                     k                                   K
# .   .   .   .   .   .   .   .   .   .   .   .   .   .   .   .   .
#                   x x x x x x x x x x x x x x x x x x x x x
A = -(Al + Ar) / 2.0;
# -----------------------------------------------------------------------------

α = 0.001;      # Эмпирический параметр регуляризации
B = Vn[k:K];    # Вектор правой части
B = B .- ( -(ulₘ[1] + ulₘ[1])  / 2.0);          # Константы от интегралов
qx = ( transpose(A) * A + α*I) \ transpose(A)*B
l = [qx[1] for i in 1:k-1];         # Дополняем слева
r = [qx[end-1] for i in K+1:N+1];   # и справа.
qx[end] = qx[end-1];                # Эти точки всегда плохо восстанавливаются,
qx[2] = qx[1];                      # обрежем их.
q_guess = [l; qx; r];
plot(Xₙ, qₙ, label="Истинное")
plot!(Xₙ, q_guess, label="Начальное приближение")

savefig("guess.png")


using LinearAlgebra, GenericLinearAlgebra, Arblib

if !isdefined(@__MODULE__, :ColumnNorm)
    # Make ColumnNorm work on old julia versions, it is introduced in Julia 1.7
    ColumnNorm() = Val(true)
end

function approximate_fekete(initial_points, basis; show_det = false, s = 3,alg=[:BigFloat, :Arb, :high_prec][2])
    # P contains the basis transformation from the monomial basis to the new basis.
    V = [pol(point...) for point in initial_points, pol in basis]
    if alg == :BigFloat
        V, P, point_indices = approximate_fekete_bigfloat(V,s=s,show_det=show_det)
    elseif alg == :Arb
        V, P, point_indices = approximate_fekete_arb(V,s=s,show_det=show_det)
    elseif alg == :high_prec
        V, P, point_indices = approximate_fekete_highprec(V,s=s,show_det=show_det)
    end
    return V, P, initial_points[point_indices]
end

function approximate_fekete_bigfloat(V; s = 3, show_det = false)
    # we do the QR factorizations in floating point precision, and the basis changes in high precision (BigFloats)
    P = Matrix{BigFloat}(I, size(V, 2), size(V, 2)) # to keep track of the basis change
    for k = 1:s
        F = qr(Float64.(V))
        U = BigFloat.(I / F.R)
        V = V * U # should be approximately equal to Q
        P = P * U #keep track of the basis change
    end
    # Find the good points:
    F2 = qr(Float64.(V'), ColumnNorm()) # this is deprecated in 1.7, but the new thing (ColumnNorm()) doesn't work in 1.6
    point_indices = F2.p[1:size(V,2)]
    # Do a last basis change to get a good basis for these points
    F = qr(Float64.(V[point_indices,:]))
    U = BigFloat.(I / F.R)
    V = V[point_indices,:] * U # the new V is square
    P = P * U
    if show_det
        println(det(V))
    end
    return V, P, point_indices
end

function approximate_fekete_arb(V::Matrix{T}; s = 3,show_det = false) where T
    # We do the QR factorizations in floating point precision, and the basis changes in high precision (Arb)
    prec = precision(eltype(V))
    V = ArbMatrix(V,prec=prec)
    P = ArbMatrix(size(V, 2), size(V, 2),prec=prec) # to keep track of the basis change
    Arblib.one!(P)
    for k = 1:s
        F = qr(Float64.(V))
        U = ArbMatrix(I / F.R,prec=prec)
        Arblib.approx_mul!(V,V,U)
        Arblib.approx_mul!(P,P,U)
    end
    # Find the good points:
    F2 = qr(Float64.(V'), ColumnNorm())
    point_indices = F2.p[1:size(V,2)]
    # Do a last basis change to get a good basis for these points
    V = V[point_indices,:]
    F = qr(Float64.(V))
    U = ArbMatrix(I / F.R,prec=prec)
    Arblib.approx_mul!(V,V,U)
    Arblib.approx_mul!(P,P,U)
    if show_det
        det_V = Arb(prec=prec)
        Arblib.det!(det_V,V)
        println(det_V)
    end
    return T.(V), T.(P), point_indices
end

function approximate_fekete_highprec(V::Matrix{T}; s = 3,show_det = false) where T
    # We do one basis change, and for that we compute the qr factorization in high precision with BigFloats
    # Then we do the basis change with Arb for speed. Note that especially the QR factorization can take long for large matrices (say 200+ rows/cols)
    # Find the basis change matrix (from the QR factorization)
    prec=precision(V[1,1])
    F = qr(BigFloat.(V))
    U = ArbMatrix(F.R,prec=prec)
    Arblib.approx_inv!(U,U)

    #Do the basis change for V
    V = ArbMatrix(V,prec=prec)
    Arblib.approx_mul!(V,V,U)

    # Do a QR factorization of V^T to get a subset of good sample points
    F2 = qr(BigFloat.(V'), ColumnNorm())
    point_indices = F2.p[1:size(V,2)]
    # Do a last basis change to get a good basis for these points
    V = V[point_indices,:]

    F = qr(BigFloat.(V))
    U2 = ArbMatrix(F.R,prec=prec)
    Arblib.approx_inv!(U2,U2)
    Arblib.approx_mul!(V,V,U2)
    Arblib.approx_mul!(U,U,U2)

    if show_det
        det_V = Arb(prec=prec)
        Arblib.det!(det_V,V)
        println(det_V)
    end
    return T.(V), T.(U), point_indices
end

# end #of module


include("splinesSerial.jl")

#controldata = [[0,1],[0,0],[1,1],[1,0]]
controldata = [[0,1],[0,0],[1,1],[1,0],[0.1,0.1],[0.2,0.2],[0.3,0.3],[0.41,0.4],[0.89,0.73],[0.5,0.5],[0.6,0.6],[0.7,0.7],[0.8,0.8],[0.9,0.9]]
point = [0.5, 0.7, 1.2]
u = S2
larBezier(u, controldata)(point)


"""
    normalize!(pcloud::PointCloud)

Normalize the PointCloud `pcloud` with mean centered at origin and unit standard deviation
and overwrite the pcloud with normalized PointCloud.

See also: [`normalize`](@ref)

### Examples:

```julia
julia> p = PointCloud(rand(1024,3))
julia> normalize!(p)
```
"""
function normalize!(pcloud::PointCloud)
    centroid = mean(pcloud.points, dims = 2)
    pcloud.points =
        (pcloud.points .- centroid) ./
        (std(pcloud.points, mean = centroid, dims = 2) .+ EPS)
    return pcloud
end

"""
    normalize(pcloud::PointCloud)

Normalize the PointCloud `pcloud` with mean centered at origin and unit standard deviation

See also: [`normalize!`](@ref)

### Examples:

```julia
julia> p = PointCloud(rand(1024,3))
julia> p = normalize(p)
```
"""
function normalize(pcloud::PointCloud)
    p = deepcopy(pcloud)
    normalize!(p)
    return p
end

"""
    scale!(pcloud::PointCloud, factor::Number)

Scale the PointCloud `pcloud` by scaling factor `factor`
and overwrite `pcloud` with scaled PointCloud.

Scaling factor `factor` should be strictly greater than `0.0`.

See also: [`scale`](@ref)

### Examples:
```julia
julia> p = PointCloud(rand(1024,3))
julia> scale!(p, 1.0)
```
"""
function scale!(pcloud::PointCloud, factor::Float32)
    (factor > 0.0) || error("factor must be greater than 0.0")
    lmul!(factor, pcloud.points)
    return pcloud
end

scale!(pcloud::PointCloud, factor::Number) = scale!(pcloud, Float32(factor))

"""
    scale(pcloud::PointCloud, factor::Number)

Scale the PointCloud `pcloud` by scaling factor `factor`.

Scaling factor `factor` should be strictly greater than `0.0`.

See also: [`scale!`](@ref)

### Examples:
```julia
julia> p = PointCloud(rand(1024,3))
julia> p = scale(1.0)
```
"""
function scale(pcloud::PointCloud, factor::Float32)
    p = deepcopy(pcloud)
    scale!(p, factor)
    return p
end

scale(pcloud::PointCloud, factor::Number) = scale(pcloud, Float32(factor))

"""
    rotate!(pcloud::PointCloud, rotmat::AbstractArray{Number,2})
    rotate!(pcloud::PointCloud, rotmat::AbstractArray{Number,3})

Rotate the PointCloud `pcloud` by rotation matrix `rotmat`
and overwrite `pcloud` with rotated PointCloud.

Rotation matrix `rotmat` should be of size `(3,3)` or `(3,3,B)`
where B is the batch size of PointCloud.

See also: [`rotate`](@ref)

### Examples:
```julia
julia> p = PointCloud(rand(1024,3))
julia> rotmat = rand(3,3)
julia> rotate!(p, rotmat)
```
"""
function rotate!(pcloud::PointCloud, rotmat::AbstractArray{Float32,2})
    size(rotmat) == (3, 3) ||
        error("rotmat must be (3, 3) array, but instead got $(size(rotmat)) array")
    size(pcloud.points, 1) == 3 || error("dimension of points in PointCloud must be 3")
    points_packed = transpose(rotmat) * reshape(pcloud.points, 3, :)
    pcloud.points = reshape(points_packed, size(pcloud.points)...)
    return pcloud
end

function rotate!(pcloud::PointCloud, rotmat::AbstractArray{Float32,3})
    _B = size(pcloud.points, 3)
    size(rotmat) == (3, 3, _B) ||
        error("rotmat must be (3, 3, $(_B)) array, but instead got $(size(rotmat)) array")
    size(pcloud.points, 1) == 3 || error("dimension of points in PointCloud must be 3")
    pcloud.points = Flux.batched_mul(Flux.batched_transpose(rotmat), pcloud.points)
    return pcloud
end

rotate!(pcloud::PointCloud, rotmat::AbstractArray{<:Number}) =
    rotate!(pcloud, Float32.(rotmat))

"""
    rotate(pcloud::PointCloud, rotmat::Array{<:Number})

Rotate the PointCloud `pcloud` by rotation matrix `rotmat`.

Rotation matrix `rotmat` should be of size `(3,3)`

See also: [`rotate!`](@ref)

### Examples:
```julia
julia> p = PointCloud(rand(1024,3))
julia> rotmat = rand(3,3)
julia> p = rotate(p, rotmat)
```
"""
function rotate(pcloud::PointCloud, rotmat::AbstractArray{<:Number})
    p = deepcopy(pcloud)
    rotate!(p, rotmat)
    return p
end

"""
    realign!(src::PointCloud, tgt::PointCloud)
    realign!(src::PointCloud, tgt_min::AbstractArray{<:Number,2}, tgt_max::AbstractArray{<:Number,2})

Re-Align the PointCloud `src` with the axis aligned bounding box of PointCloud `tgt`
and overwrite `pcloud` with re-aligned PointCloud.

PointCloud `src` and `tgt` should be of same dimension.

See also: [`realign`](@ref)

### Examples:
```julia
julia> src = PointCloud(rand(1024,3))
julia> tgt = PointCloud(rand(1024,3))
julia> realign!(src, tgt)
```
"""
function realign!(
    src::PointCloud,
    tgt_min::AbstractArray{Float32,2},
    tgt_max::AbstractArray{Float32,2},
)
    size(src.points, 1) == size(tgt_max, 1) ||
        error("source and target pointcloud dimension mismatch")
    src_min = minimum(src.points, dims = 2)
    src_max = maximum(src.points, dims = 2)
    src.points =
        ((src.points .- src_min) ./ (src_max - src_min .+ EPS)) .* (tgt_max - tgt_min) .+
        tgt_min
    return src
end

realign!(
    src::PointCloud,
    tgt_min::AbstractArray{<:Number,2},
    tgt_max::AbstractArray{<:Number,2},
) = realign!(src, Float32.(tgt_min), Float32.(tgt_max))

function realign!(src::PointCloud, tgt::PointCloud, index::Number = 1)
    points = tgt[index]
    tgt_min = minimum(points, dims = 2)
    tgt_max = maximum(points, dims = 2)
    realign!(src, tgt_min, tgt_max)
    return src
end

"""
    realign(src::PointCloud, tgt::PointCloud)
    realign(src::PointCloud, tgt_min::AbstractArray{<:Number,2}, tgt_max::AbstractArray{<:Number,2})

Re-Align the PointCloud `src` with the axis aligned bounding box of PointCloud `tgt`.

PointCloud `src` and `tgt` should be of same dimension.

See also: [`realign!`](@ref)

### Examples:
```julia
julia> src = PointCloud(rand(1024,3))
julia> tgt = PointCloud(rand(1024,3))
julia> src = realign!(src, tgt)
```
"""
function realign(
    src::PointCloud,
    tgt_min::AbstractArray{<:Number,2},
    tgt_max::AbstractArray{<:Number,2},
)
    p = deepcopy(src)
    realign!(p, tgt_min, tgt_max)
    return p
end

function realign(src::PointCloud, tgt::PointCloud, index::Number = 1)
    p = deepcopy(src)
    realign!(p, tgt, index)
    return p
end


include("utils.jl")

@testset "CenteredMatrices" begin
    M = CenteredMatrices(3, 2)
    M_complex = CenteredMatrices(3, 2, ℂ)
    A = [1 2; 4 5; -5 -7]
    B = [1 2 3; 4 5 6; -5 -7 -9]    #wrong dimensions
    C = [-3 -im; 2 im; 1 0]         #complex
    D = [1 2; 3 4; 5 6]             #not centered
    types = [Matrix{Float64}]
    @testset "Real Centered Matrices Basics" begin
        @test repr(M) == "CenteredMatrices(3, 2, ℝ)"
        @test representation_size(M) == (3, 2)
        @test base_manifold(M) === M
        @test typeof(get_embedding(M)) === Euclidean{Tuple{3,2},ℝ}
        @test check_manifold_point(M, A) === nothing
        @test_throws DomainError is_manifold_point(M, B, true)
        @test_throws DomainError is_manifold_point(M, C, true)
        @test_throws DomainError is_manifold_point(M, D, true)
        @test check_tangent_vector(M, A, A) === nothing
        @test_throws DomainError is_tangent_vector(M, A, D, true)
        @test_throws DomainError is_tangent_vector(M, D, A, true)
        @test_throws DomainError is_tangent_vector(M, A, B, true)
        @test manifold_dimension(M) == 4
        @test A == project!(M, A, A)
        @test A == project(M, A, A)
        A2 = similar(A)
        embed!(M, A2, A)
        A3 = embed(M, A)
        @test A2 == A
        @test A3 == A
        E = [0 0; 1 -1; -1 1]
        F = [0.5 1; -1 -0.7; 0.5 -0.3]
        test_manifold(
            M,
            [A, E, F],
            test_injectivity_radius = false,
            test_reverse_diff = false,
            test_project_tangent = true,
            test_musical_isomorphisms = true,
            test_default_vector_transport = true,
            is_tangent_atol_multiplier = 1,
            is_point_atol_multiplier = 1,
        )
    end
    @testset "Complex Centered Matrices Basics" begin
        @test repr(M_complex) == "CenteredMatrices(3, 2, ℂ)"
        @test manifold_dimension(M_complex) == 8
        G = [1.0 1.0im; -1.0im 0.0; -1.0+1.0im -1.0im]
        H = [1.0im 0.0; -2.0im 1.0im; 1.0im -1.0im]
        test_manifold(
            M_complex,
            [C, G, H],
            test_injectivity_radius = false,
            test_reverse_diff = false,
            test_project_tangent = true,
            test_musical_isomorphisms = true,
            test_default_vector_transport = true,
            is_tangent_atol_multiplier = 1,
            is_point_atol_multiplier = 1,
        )
    end
end


"""
    detrend(x, y, poly_order)

Subtract polynomial from `y` data.
"""
detrend(x, y, poly_order) = poly_order == 0 ? y .- mean(y) : y - fit(x, y, poly_order).(x)
detrend(c::Curve, poly_order) = Curve(c.x, detrend(c.x, c.y, poly_order))

"""
    NoiseSample <: AbstractCurve

Curve holding a noise sample. At minimum, a constant offset
is removed from the noise sample upon construction.
If provided, a polynomial of order `n` is subtracted.

# Fields
- `x::Vector{Float64}` : x-grid
- `y::Vector{Float64}` : y-value

# Constructors

    NoiseSample(x::Vector{T}, y::Vector{T})
    NoiseSample(x::Vector{T}, y::Vector{T}, n::Int)
    NoiseSample(s::Curve)
    NoiseSample(s::Curve, n::Int)

# Notes

Noise samples should not have a slow trend on top of the noise,
otherwise noise parameters cannot be reliably infered.
Construct `NoiseSample` objects with a high enough order
`n` so as to remove any slow variations.
"""
struct NoiseSample{T} <: AbstractCurve
    x::Vector{T}
    y::Vector{T}
    function NoiseSample{T}(x, y, n::Int=0) where T
        verify_same_length(x, y)
        return new{T}(x, detrend(x, y, n))
    end
end

NoiseSample(x::Vector{T}, y::Vector{T}, n::Int=0) where T = NoiseSample{T}(x, y, n)
NoiseSample(y::Vector{T}, n::Int=0) where T = NoiseSample(collect(T, 1:length(y)), y, n)
NoiseSample(s::Curve, n::Int=0) = NoiseSample(s.x, s.y, n)

"""
    std(ns::NoiseSample; kw...)

Retrieve the standard deviation of the `NoiseSample` `ns`.
"""
Statistics.std(ns::NoiseSample; kw...) = Statistics.std(ns.y; kw...)

# -------------------------------------
# Noise models
# -------------------------------------

"""
    AbstractNoiseModel

Supertype of noise models.
"""
abstract type AbstractNoiseModel end

"""
    GaussianNoiseModel{T <: Real} <: AbstractNoiseModel

Model to describe noise following a univariate Gaussian distribution
(uncorrelated noise).

# Fields

- `σ::T` : standard deviation
"""
struct GaussianNoiseModel{T <: Real} <: AbstractNoiseModel
    σ::T
end

Base.eltype(::GaussianNoiseModel{T}) where T = T

function Base.show(io::IO, nm::GaussianNoiseModel)
    println(io, "GaussianNoiseModel(σ = $(nm.σ))")
end

"""
    MvGaussianNoiseModel{T <: Real} <: AbstractNoiseModel

Model to describe noise following a multivariate Gaussian distribution
(correlated noise).

# Fields
- `δx::T`: noise grid spacing
- `α::T` : autocovariance amplitude
- `λ::T` : autocovaraiance lag
"""
struct MvGaussianNoiseModel{T <: Real} <: AbstractNoiseModel
    δx::T
    α::T
    λ::T
end

Base.eltype(::MvGaussianNoiseModel{T}) where T = T

function Base.show(io::IO, nm::MvGaussianNoiseModel)
    println(io, "MvGaussianNoiseModel(α = $(nm.α), λ = $(nm.λ))")
end

"""
    gauss_kernel(Δx, β)

Fit function for noise autocorrelation.

    β = [α, λ]
    f(δx) = α^2 * exp(-0.5 * (Δx/λ)^2)
"""
function gauss_kernel(Δx, β)
    α, λ = β
    return @. α^2 * exp(-0.5 * (Δx/λ)^2)
end

"""
    get_cov(δx::T, n::Int, α::T, λ::T) where {T <: Real}

Return covariance matrix `Σ` with size `n` × `n` for the exponentiated quadratic kernel
with amplitude `α` and lag `λ` calculated on a grid of spacing `δx`.
"""
function get_cov(δx::T, n::Int, α::T, λ::T) where {T <: Real}
    β = [α, λ]
    Σ = Array{T}(undef, n, n)
    for i = (1:n), j = (1:n)
        Δx = δx * (i - j)
        Σ[i,j] = gauss_kernel(Δx, β)
        if i == j
            Σ[i,j] *= 1.0000001 # for numerical stability (?)
        end
    end
    return Σ
end
get_cov(nm::MvGaussianNoiseModel{T}, n::Int) where {T <: Real} = get_cov(nm.δx, n, nm.α, nm.λ)

function estimate_autocov(n::NoiseSample)
    # check that spectral grid is equally spaced
    if !allapproxequal(diff(n.x))
        throw(ArgumentError("Noise analysis only works on an evenly spaced spectral grid."))
    end
    δx = abs(n.x[1]-n.x[2]) # x-values must be evenly spaced
    lag_units = begin
        N = convert(Integer, round(length(n)/2))
        collect(1:N)
    end
    lags = δx .* lag_units
    acov = autocov(n.y, lag_units)
    return lags, acov
end

function _fit_noise(lags::Vector{T}, autocov::Vector{T}; α_guess=1.0, λ_guess=1.0) where {T <: Real}
    ma = maximum(autocov) # rescale autocov to make fit more robust
    fit = curve_fit(gauss_kernel, lags, autocov./ma, [α_guess, λ_guess])
    α, λ = fit.param
    return MvGaussianNoiseModel(lags[1], α * √ma, λ)
end

"""
    fit_noise(ns::NoiseSample; α_guess=1.0, λ_guess=1.0)

Build a `MvGaussianNoiseModel` for the `NoiseSample` `ns`.

Estimates parameters `α` and `λ` by fitting the exponentiated quadratic covariance function

    k(Δx) = α² exp(-0.5 (Δx/λ)²)

to the autocovariance (k) vs. the lag (Δx) of the noise sample.

Change initial guess if fit does not converge to sensible result.
"""
function fit_noise(n::NoiseSample; α_guess=1.0, λ_guess=1.0)
    lags, acov = estimate_autocov(n)
    return _fit_noise(lags, acov; α_guess=α_guess, λ_guess=λ_guess)
end

"""
Generate correlated noise from a noise sample or noise model.
"""
function generate_noise(nm::MvGaussianNoiseModel{T}, len::Int, samples::Int) where {T <: Real}
    Σ = get_cov(nm, len)
    return Particles(samples, MvNormal(Σ))
end
function generate_noise(ns::NoiseSample, samples::Int; kw...)
    nm = fit_noise(ns; kw...)
    δx = ns.x[2] - ns.x[1]
    return generate_noise(nm, length(ns), samples)
end

"""
Generate uncorrelated noise from a GaussianNoiseModel.
"""
function generate_noise(nm::GaussianNoiseModel{T}, len::Int, samples::Int) where {T <: Real}
    return Particles(samples, MvNormal(zeros(T, len), nm.σ))
end

"""
    add_noise(c::Curve, nm::AbstractNoiseModel, N::Int=10_000)

Add noise from noise model to curve, retrieve `UncertainCurve` with `N` samples.
"""
function add_noise(c::Curve, nm::GaussianNoiseModel, N::Int=10_000)
    return UncertainCurve(c.x, Particles(N, MvNormal(c.y, nm.σ)))
end
function add_noise(c::Curve, nm::MvGaussianNoiseModel, N::Int=10_000)
    δx = c.x[2] - c.x[1]
    Σ = get_cov(nm, length(c))
    return UncertainCurve(c.x, Particles(N, MvNormal(c.y, Σ)))
end


using HPAT
using MPI
using DocOpt
ParallelAccelerator.ParallelIR.PIRHoistParfors(1)
ParallelAccelerator.CGen.setRecreateLoops(true)
ParallelAccelerator.CGen.setRecreateConds(true)
HPAT.DistributedPass.DistOptimize(true)
HPAT.DistributedPass.set_debug_level(3)

@acc hpat function logistic_regression(iterations, file_name)
    points = DataSource(Matrix{Float64},HDF5,"/points", file_name)
    responses = DataSource(Vector{Float64},HDF5,"/responses", file_name)
    D,N = size(points) # number of features,samples
    labels = reshape(responses,1,N)
    w = ones(1,D)-.5
    tic()
    for i in 1:iterations
       w -= ((1.0./(1.0.+exp(-labels.*(w*points))).-1.0).*labels)*points'
    end
    toc()
    w
end

function main()
    doc = """Logistic regression statistical method.

Usage:
  logistic_regression.jl -h | --help
  logistic_regression.jl [--iterations=<iterations>] [--file=<file>]

Options:
  -h --help                  Show this screen.
  --iterations=<iterations>  Specify number of iterations; defaults to 20.
  --file=<file>              Specify input file; defaults to HPAT's default generated data file.

"""
    arguments = docopt(doc)
    iterations = 20
    file_name = HPAT.getDefaultDataPath()*"logistic_regression.hdf5"

    if (arguments["--iterations"] != nothing)
        iterations = parse(Int, arguments["--iterations"])
    end

    if (arguments["--file"] != nothing)
        file_name = arguments["--file"]
    end

    W = logistic_regression(iterations, file_name)

    if MPI.Comm_rank(MPI.COMM_WORLD)==0 println("result = ", W) end
end

main()


"""
Translated to Python and modified based on:
https://www.mathworks.com/matlabcentral/fileexchange/34208-uniform-distribution-over-a-convex-polytope
Copyright (c) 2011-2012 Tim J. Benham, School of Mathematics and Physics, University of Queensland.

CHEBYCENTER Compute Chebyshev center of polytope Ax <= b.
The Chebyshev center of a polytope is the center of the largest
hypersphere enclosed by the polytope.
"""
function chebyshev_center(A, b, opt=Ipopt.Optimizer)
    m, p = size(A)

    an = sqrt.(sum(A.^2, dims=2))

    A_r = zeros((m, p+1))
    A_r[:, 1:p] = A
    A_r[:, p+1] = an

    f = zeros((p+1, 1))
    f[p+1] = -1

	model = JuMP.Model(opt)
	JuMP.set_silent(model)
	JuMP.@variable(model, 0 <= x[1:p+1])
	JuMP.@constraint(model, A_r * x .<= b)
	JuMP.@objective(model, Max, x[p+1])  # Was f'*x, but not supported by JuMP?
	#TASK: Postive or negative for objective?
    JuMP.optimize!(model)
	result = JuMP.value.(x)

	center = result[1:p]'
    radius = result[p+1]

    return center, radius
end

"""
# NOT WORKING
Finds the maximum volume inscribed ellipsoids in the given polytope Ax<b
"""
function maximum_volume_inscribed_ellipsoid(A, b, opt=GLPK.Optimizer)
    m, p = size(A)

	model = JuMP.Model(opt)
	JuMP.set_silent(model)
	JuMP.@variable(model, 0 <= d[1:p])
	JuMP.@variable(model, 0 <= B[1:p,1:p])
	for i in 1:m
		#JuMP.@constraint(model, norm(B * A[i,:]) + dot(A[i,:], d) .<= b[i])
		JuMP.@constraint(model, [b[i] - dot(A[i,:], d) ; B * A[i,:]] in SecondOrderCone())
	end
	JuMP.@objective(model, Max, logdet(B))
    JuMP.optimize!(model)

	center = value.(d)
    return center, radius
end

"""
Translated to Julia and modified based on:
https://www.mathworks.com/matlabcentral/fileexchange/34208-uniform-distribution-over-a-convex-polytope
Copyright (c) 2011-2012 Tim J. Benham, School of Mathematics and Physics, University of Queensland.

CPRND Draw from the uniform distribution over a convex polytope.
	X = cprnd(N,A,b) Returns an N-by-P matrix of random vectors drawn
	from the uniform distribution over the interior of the polytope
	defined by Ax <= b. A is a M-by-P matrix of constraint equation
	coefficients. b is a M-by-1 vector of constraint equation
	constants

...
- 'method::String': 'gibbs': Gibbs sampler, 'hitandrun': Hit and Run, 'achr': Adaptive Centering Hit-and-Run
- 'iso::Integer': Isotropic Transformation (0: no xfrm, 1: Xfrm during runup, 2: Xfrm throughout sampling
- 'runup::Integer': # of initial iterations of the algorithm in the untransformed space for gibbs or hitandrun
- 'discard::Integer': # of initial samples (post run_up) to discard. Randomly selects n_samples from the
		discard + n_samples samples.
...
"""
function sample_polytope_cprnd(A, b, x0, n_samples=1; seed=nothing, method="achr", runup=nothing, iso=nothing, discard=nothing)
    m, p = size(A)

    # Setting up default options
    if isnothing(iso)
        if method != "achr"
            iso = 2
        else
            iso = 0
		end
	end

    if isnothing(runup)
		if method == "achr"
			runup = 10 * (p+1)
		elseif iso != 0
			runup = 10 * p * (p + 1)
		else
			runup = 0
		end
	end

    if isnothing(discard)
        if method == "achr"
            discard = 1 * (p + 1)
        else
            discard = runup
		end
	end

    # Initializations
    rng = Random.MersenneTwister(seed)

    rnd_unit_vector(rng, p) = normalize(randn(rng, (p,1)))

    X = zeros((p, n_samples + runup + discard))

    n = 0  # Number of samples generated
    x = x0

    # Containers for mean, covariance, and isotropic transform tracking
    M_inc = zeros((p, 1))  # Incremental mean
    S_inc = zeros((p, p))  # Incremental sum of products

    S = I(p)
    T = I(p)
    W = A

    while n < (n_samples + runup + discard)
        y = x

        if iso > 0
            if n == runup | (iso == 2 & n > runup)
                T = cholesky(S).L
				W = A * T
			end
            y = T \ y
		end

        if method == "gibbs"
            for i in 1:p
                e = circshift(Matrix(I, p, 1), i - 1)
                z = W * e  # Selects row i?
                c = (b - W * y) ./ z
                t_min = maximum(c[z .< 0])
                t_max = minimum(c[z .> 0])

                y = y + (t_min + (t_max - t_min) * Random.rand(rng)) * e
			end
        elseif method == "hitandrun"
			u = rnd_unit_vector(rng, p)
            z = W * u
            c = (b - W * y) ./ z
            t_min = maximum(c[z .< 0])
            t_max = minimum(c[z .> 0])

            y = y + (t_min + (t_max - t_min) * Random.rand(rng)) * u
        else  # achr
            if n < runup
                u = rnd_unit_vector(rng, p)
            else
                v = X[:, rand(rng, 1:n)]
                u = normalize(v - M_inc)
			end

            z = A * u
            c = (b - W * y) ./ z
            t_min = maximum(c[z .< 0])
            t_max = minimum(c[z .> 0])

            y = y + (t_min + (t_max - t_min) .* Random.rand(rng)) .* u
		end

        if iso > 0
            x = T * y
        else
            x = y
		end
        X[:, n+1] = x
        n = n + 1

        delta_0 = x - M_inc  # Delta between new point & old mean
        M_inc = M_inc + delta_0 / n  # Sample mean
        delta_1 = x - M_inc  # Delta between new point & new mean

        if iso > 0
			if n > 1
				# Time Bottleneck here for iso
				S_inc = S_inc + (n - 1) / (n * n) * delta_0 * delta_0' + delta_1 * delta_1'

				S_0 = S
				S = S_inc / (n - 1)
			else
				S = Matrix(I,p,p)
			end
			S = (S + S') / 2  # Make array Hermitian Symmetric since computational error is likely
		end
	end
    X = X[:, runup + 1: n_samples + discard + runup]

	#TEST: Make discard pick random values from after runup to n_samples
	keep = sample(rng, 1:(n_samples + discard), n_samples, replace=false)
	X = X[:, keep]
    return X
end

"Used to call sample_uniform from create_samples"
function sample_uniform(A, b, x0, n_samples=1; nominal_load=nothing, dist_perc=0.2)
	isnothing(nominal_load) && error("nominal_load must not be nothing. Include nominal load in the
									  sampler_opts dictionary with the key ':nominal_load'")
	samples = sample_uniform(nominal_load, dist_perc=dist_perc)
	for _ in 1:n_samples
		s = sample_uniform(nominal_load, dist_perc=dist_perc)
		samples = vcat(samples, s)
	end
	return samples
end

"""
Samples from a uniform distribution of the given percentage
around the given base load, x_0 = (pd\\_0 + qd\\_0). Power factors
are held constant.

x_il = U[0.8 x_i0, 1.2 x_i0] for all i in load buses
"""
function sample_uniform(nominal_load; dist_perc=0.2)
	@assert length(nominal_load) % 2 == 0 "Nominal load profile must contain an even number of load values (active and reactive load pairs). Currently contains $(length(nominal_load))."
	num_loads = Int(length(nominal_load) / 2)
	sampled_dist = (1 .+ dist_perc .* (rand(num_loads) .* 2 .- 1))

	new_load = nominal_load .* [sampled_dist ; sampled_dist]
	return new_load
end

"Used to call sample_uniform_w_pf from create_samples"
function sample_uniform_w_pf(A, b, x0, n_samples=1; nominal_load=nothing, dist_perc=0.2, pf_range=(0.8,1.0))
	isnothing(nominal_load) && error("nominal_load must not be nothing. Include nominal load in the
									  sampler_opts dictionary with the key ':nominal_load'")

	samples = sample_uniform_w_pf(nominal_load, dist_perc=dist_perc, pf_range=pf_range)
	for _ in 1:n_samples
		s = sample_uniform_w_pf(nominal_load, dist_perc=dist_perc, pf_range=pf_range)
		samples = vcat(samples, s)
	end
	return samples
	return
end

"""
Samples from a uniform distribution of the given percentage
around the given base load real power, pd_0. Then sample a
power factor from the given pf_range to determine the reactive
load of the sample. By default the pf_range refers to lagging
power factors. If a value larger than 1.0 is given in the range,
it will be converted to a leading power factor of pf = (1.0 - x).

- p\\_il = U[(1-dist) * p_i0, (1+dist) * p\\_i0] for all i in load buses
- d\\_il = U[pf\\_range] for all i in load buses
- q\\_il = tan(d\\_il) * p\\_il
"""
function sample_uniform_w_pf(nominal_load; dist_perc=0.2, pf_range=(0.8,1.0))
	@assert length(nominal_load) % 2 == 0 "Nominal load profile must contain an even number of load values (active and reactive load pairs). Currently contains $(length(nominal_load))."
	num_loads = Int(length(nominal_load) / 2)
	sampled_pl = nominal_load[1:num_loads] .* (1 .+ dist_perc .* (rand(num_loads) .* 2 .- 1))
	sampled_pf = (pf_range[2] - pf_range[1]) .* rand(num_loads) .+ pf_range[1]
	if sampled_pf > 1.0
		sampled_pf = 1.0 - sampled_pf
	end
	sampled_ql = sampled_pl .* tan.(acos.(sampled_pf))

	new_load = [sampled_pl; sampled_ql]
	return new_load
end

"Return the unit vector of vector, v"
function normalize(v)
    norm_ = norm(v)
    if norm_ == 0
        norm_ = eps()
	end
    return v / norm_
end


# Optimization of bin counts for histograms, heatmaps, hexbin plots, etc.
#
# I'm using the penalized maximum-likelihood method proposed in
#   Birge, L, and Rozenholc, Y. (2006) How many bins should be put in a regular
#   histogram?
#
# There has been quite a bit written on this problem, but there are a number of
# methods that all seem to give good results with little difference. Birge's
# method is simple (it's just AIC with an extra logarithmic term), has a decent
# theoretical justification, and is general enough to apply to multidimensional
# and non-regular bin selecetion problems. Though, the penalty they use was
# optimized for regular histograms, so may need to be tweaked.
#
# The Birge penalty is
#    penalty(D) = D - 1 + log(D)^2.5
# where D is the number of bins. The 2.5 constant was arrived at emperically by
# optimizing over samples from example density functions.
#

# Penalized log-likelihood function for a histogram with d regular bins.
#
# Args:
#   d: Number of bins in the histogram.
#   n: Number of sample (which should equal sum(bincounts[1:d])).
#   bincounts: An array giving the number occurrences in each bin.
#   binwidth: Width of each bin in the histogram.
#
# Returns:
#   Log-likelihood with Birge's penalty applied.
#
function bincount_pll(d::Int, n::Int, bincounts::Vector{Int}, binwidth::Float64)
    ll = 0
    for i in 1:d
        if bincounts[i] > 0
            ll += bincounts[i] * log(bincounts[i] / (n * binwidth))
        end
    end
    ll - (d - 1 + log(d)^2.5)
end

# Optimize the number of bins for a regular one dimensional histogram.
#
# Args:
#   xs: A sample.
#
# Returns:
#   A tuple of the form (d, bincounts), where d gives the optimal number of
#   bins, and bincounts is an array giving the number of occurances in each bin.
#
function choose_bin_count_1d(xs::AbstractVector, min_bin_count=1, max_bin_count=150)
    n = length(xs)
    if n <= 1
        return 1, Int[0]
    end

    x_min, x_max = Gadfly.concrete_minimum(xs), Gadfly.concrete_maximum(xs)
    span = x_max - x_min

    d_min = min_bin_count
    d_max = max_bin_count
    bincounts = zeros(Int, d_max)

    d_best = d_min
    pll_best = -Inf

    # Brute force optimization: since the number of bins has to be reasonably
    # small to plot, this is pretty quick and very simple.
    for d in d_min:d_max
        binwidth = span / d
        bincounts[1:d] = 0

        for x in xs
            if !isconcrete(x)
                continue
            end
            bincounts[max(1, min(d, int(ceil((x - x_min) / binwidth))))] += 1
        end

        pll = bincount_pll(d, n, bincounts, binwidth)

        if pll > pll_best
            d_best = d
            pll_best = pll
        end
    end

    bincounts[1:d_best] = 0
    binwidth = span / d_best
    for x in xs
        if !isconcrete(x)
            continue
        end
        bincounts[max(1, min(d_best, int(ceil((x - x_min) / binwidth))))] += 1
    end

    (d_best, bincounts)
end

# Optimize the number of bins for regular two dimensional histograms.
#
# Args:
#   xs: Dimension one data.
#   ys: Dimension two data.
#
# Returns:
#   A tuple of the form (dx, dy, bincounts), where dx, dy gives the number of
#   bins in each respective dimension and bincounts is a dx by dy matrix giving
#   the count in each bin.
#
function choose_bin_count_2d(xs::AbstractVector, ys::AbstractVector,
                             xminbincount::Int, xmaxbincount::Int,
                             yminbincount::Int, ymaxbincount::Int)

    # For two demensions, I'm just going to optimize the marginal bin counts.
    # This might not be optimal, but its simple and fast.

    x_min, x_max = Gadfly.concrete_minimum(xs), Gadfly.concrete_maximum(xs)
    y_min, y_max = Gadfly.concrete_minimum(ys), Gadfly.concrete_maximum(ys)

    dx, _ = choose_bin_count_1d(xs, xminbincount, xmaxbincount)
    dy, _ = choose_bin_count_1d(ys, yminbincount, ymaxbincount)

    # bin widths
    wx = (x_max - x_min) / dx
    wy = (y_max - y_min) / dy

    bincounts = zeros(Int, (dy, dx))
    for (x, y) in zip(xs, ys)
        if !Gadfly.isconcrete(x) || !Gadfly.isconcrete(y)
            continue
        end

        i = max(1, min(dx, int(ceil((x - x_min) / wx))))
        j = max(1, min(dy, int(ceil((y - y_min) / wy))))
        bincounts[j, i] += 1
    end

    (dy, dx, bincounts)
end

# Optimize the number of bins for hexagonal 2d histograms.
#
# Args:
#   xs: Dimension one data.
#   ys: Dimension two data.
#
# Returns:
#   A tuple of the form (size, bincounts), where 'size' is the hexagon size and
#   bincounts is an arrays storing the count for each hexagon, using axial
#   coordinates.
#
function choose_hex_bin_count(xs::AbstractVector, ys::AbstractVector)
    # TODO: this should probably be a thing
end


"""
    lorenztriple(;uᵢ=rand(9),
                σ₁ = 10.0, σ₂ = 10.0, σ₃ = 10.0,
                ρ₁ = 28.0, ρ₂ = 28.0, ρ₃ = 28.0,
                β₁ = 8/3,  β₂ = 8/3,  β₃ = 8.3,
				c₁ = 1.0, c₂ = 1.0) -> ContinuousDynamicalSystem

Equations of motion for three coupled Lorenz attractors with a transitive
causality chain where X₁ → X₂ and X₂ → X₃. In total, the three 3D-subsystems
create a 9-dimensional dynamical system.

The strength of the forcing X₁ → X₂ is controlled by the parameter `c₁`, and
the forcing from X₂ → X₃ by `c₂`. The remaining parameters are the usual
parameters for the Lorenz system, where the subscript `i` refers to the
subsystem Xᵢ. This system was studied by Papana et al. (2013) for coupling
strengths `c = 0, 1, 3, 5`.

# References
Papana et al., Simulation Study of Direct Causality Measures in Multivariate Time Series. Entropy 2013, 15(7), 2635-2661; doi:10.3390/e15072635
"""
function eom_lorenz_triple(u, p, t)
    x₁, y₁, z₁, x₂, y₂, z₂, x₃, y₃, z₃ = (u...,)
    σ₁, σ₂, σ₃, ρ₁, ρ₂, ρ₃, β₁, β₂, β₃, c₁, c₂ = (p...,)

    # Subsystem 1
    dx₁ = σ₁*(y₁-x₁)
    dy₁ = ρ₁*x₁ - y₁ - x₁*z₁
    dz₁ = x₁*y₁ - β₁*z₁

    # Subsystem 2
    dx₂ = σ₂*(y₂-x₂) + c₁*(x₁ - x₂)
    dy₂ = ρ₂*x₂ - y₂ - x₂*z₂
    dz₂ = x₂*y₂ - β₂*z₂

    # Subsystem 3
    dx₃ = σ₃*(y₃-x₃) + c₂*(x₂ - x₃)
    dy₃ = ρ₃*x₃ - y₃ - x₃*z₃
    dz₃ = x₃*y₃ - β₃*z₃
    return SVector{9}(dx₁, dy₁, dz₁, dx₂, dy₂,dz₂, dx₃, dy₃, dz₃)
end

function lorenz_triple(uᵢ, σ₁, σ₂, σ₃, ρ₁, ρ₂, ρ₃, β₁, β₂, β₃, c₁, c₂)
    p = [σ₁, σ₂, σ₃, ρ₁, ρ₂, ρ₃, β₁, β₂, β₃, c₁, c₂]
    ContinuousDynamicalSystem(eom_lorenz_triple, uᵢ, p)
end

"""
    lorenztriple(;uᵢ=rand(9),
                σ₁ = 10.0, σ₂ = 10.0, σ₃ = 10.0,
                ρ₁ = 28.0, ρ₂ = 28.0, ρ₃ = 28.0,
                β₁ = 8/3,  β₂ = 8/3,  β₃ = 8.3,
                c₁ = 1.0, c₂ = 1.0) -> ContinuousDynamicalSystem

Initalise a dynamical system consisting of three coupled Lorenz attractors with
a transitive causality chain where X₁ → X₂ and X₂ → X₃. In total, the three
3D-subsystems create a 9-dimensional dynamical system.

The strength of the forcing X₁ → X₂ is controlled by the parameter `c₁`, and
the forcing from X₂ → X₃ by `c₂`. The remaining parameters are the usual
parameters for the Lorenz system, where the subscript `i` refers to the
subsystem Xᵢ. This system was studied by Papana et al. (2013) for coupling
strengths `c = 0, 1, 3, 5`.

# References
Papana et al., Simulation Study of Direct Causality Measures in Multivariate Time Series. Entropy 2013, 15(7), 2635-2661; doi:10.3390/e15072635
"""
lorenz_triple(;uᵢ=rand(9),
            σ₁ = 10.0, σ₂ = 10.0, σ₃ = 10.0,
            ρ₁ = 28.0, ρ₂ = 28.0, ρ₃ = 28.0,
            β₁ = 8/3,  β₂ = 8/3,  β₃ = 8.3,
            c₁ = 1.0, c₂ = 1.0) =
    lorenz_triple(uᵢ, σ₁, σ₂, σ₃, ρ₁, ρ₂, ρ₃, β₁, β₂, β₃, c₁, c₂)


using ASP

facts("as_nnls") do

    context("Wide and sparse NNLS problem.") do

        m, n = 600, 2560              # No. of rows, cols
        k = 20                        # No. of nonzeros
        p = randperm(n)[1:k]          # Position of nonzeros in x
        x = zeros(n)                  # Generate sparse nonnegative solution
        x[p] = max(randn(k),0)
        A = randn(m,n)                # Gaussian m-by-n ensemble
        b = A*x                       # Compute the RHS vector
        x, r, inform = as_nnls(A,b)   # Solve the nonnegative LS problem

        g = -(A'*r)
        @fact vecnorm( min(x,g) ) --> less_than(1e-8)

    end

    context("Overdetermined NNLS problem.") do

        m, n = 100, 10
        x = max(randn(n), 0)
        A = randn(m,n)
        b = A*x
        x, r, inform = as_nnls(A, b)

        g = -(A'*r)
        @fact vecnorm( min(x,g) ) --> less_than(1e-8)
    end

end


import StatsBase: countmap

abstract Dist

type Euclidean <: Dist
end

abstract KNN

type KnnClassifier <: KNN
    k::Integer
    dis_func::Dist
    X::Matrix
    y::Vector
end

type KnnRegression <: KNN
    k::Integer
    dis_func::Dist
    X::Matrix
    y::Vector
end

function KnnClassifier(;
                       k::Integer = 5,
                       dist_func::Dist = Euclidean(),
                       X::Matrix = zeros(2,2),
                       y::Vector = zeros(2))
    return KnnClassifier(k,dist_func,X,y)
end

function KnnRegression(;
                       k::Integer = 5,
                       dist_func::Dist = Euclidean(),
                       X::Matrix = zeros(2,2),
                       y::Vector = zeros(2))
    return KnnClassifier(k,dist_func,X,y)
end

function train!(model::KnnClassifier, X::Matrix, y::Vector)
    model.X = X
    model.y = y
end

function predict(model::KnnClassifier,
                 x::Matrix)
    n = size(x,1)
    res = zeros(n)
    for i = 1:n
        res[i] = predict(model, x[i,:])
    end
    return res
end

function predict(model::KnnClassifier,
                 x::Vector)
    n = size(model.X,1)
    res = zeros(n)
    for i = 1:n
        res[i] = dist(model.X[i,:],x,model.dis_func)
    end
    ind = sortperm(res)
    y_cos = model.y[ind[1:model.k]]
    label = 0
    label_freq = 0
    for (key,value) in countmap(y_cos)
        if value > label_freq
            label_freq = value
            label = key
        end
    end
    return label

end

function predict(model::KnnRegression,
                 x::Vector)
    n = size(model.X,1)
    res = zeros(n)
    for i = 1:n
        res[i] = dist(model.X[i,:],x,model.dis_func)
    end
    ind = sortperm(res)
    y_cos = model.y[ind[1:model.k]]
    return mean(y_cos)
end

function dist(x::Vector, y::Vector, dist_func::Euclidean)
    return norm(x-y)
end

function test_kneast_regression()

    X_train, X_test, y_train, y_test = make_reg(n_features = 1)

    model = KnnRegression()
    train!(model,X_train, y_train)
    predictions = predict(model,X_test)
    print("regression msea", mean_squared_error(y_test, predictions))
    PyPlot.scatter(X_test, y_test, color = "black")
    PyPlot.scatter(X_test, predictions, color = "green")
    legend(loc="upper right",fancybox="true")
end

function test_kneast_classification()
    X_train, X_test, y_train, y_test = make_digits()
    model = KnnClassifier()
    train!(model,X_train, y_train)
    predictions = predict(model,X_test)
    print("classification accuracy", accuracy(y_test, predictions))

    #PCA

    pca_model = PCA()
    train!(pca_model, X_test)
    plot_in_2d(pca_model, X_test, predictions, "kneast_classification")

end


include("euler/euler.jl")
using .Primes: get_primes

compute(n::Int)::Int = sum(get_primes(n))


using Test, CornerTransferMethods, TensorOperations, TensorNetworkTensors, LinearAlgebra
@testset "CornerTransferMethods" begin
    @testset "tfisingmpo" begin
        β = randn()
        σz = [1 0; 0 -1]
        σx = [0 1; 1  0]
        id = [1 0; 0  1]
        @test ozofβ(β)[:,:] == exp(β * σz)
        oxx = oxxofβ(β, ComplexF64)
        @tensor res[1,2,3,4] := oxx[1,-1,3,-2] * oxx[2,-2,4,-1]
        ref = exp(2β*kron(σx,σx))
        resmat = fuselegs(res, ((1,2),(3,4)))[1]
        @test toarray(resmat) ≈ ref

        let β = 1e-5
            refe = exp(β * (kron(σz, id) + kron(id, σz) + 2kron(σx,σx)))
            ref = exp(β/2 * (kron(σz, id) + kron(id, σz))) *
                exp(2β * kron(σx,σx)) * #factor two probably edge-case
                exp(β/2 * (kron(σz, id) + kron(id, σz)))
            op = tfisingpropagator(β)
            @tensor op2[1,2,3,4] := op[1,-1,3,-2] *
                                    op[2,-2,4,-1];
            res = toarray(fuselegs(op2,((1,2),(3,4)))[1])
            @test ref ≈ refe && ref ≈ res
        end

        let β=1e-3
            refe = exp(β * (kron(σz,id,id) + kron(id,σz,id) + kron(id,id,σz) +
                            kron(σx,σx,id) + kron(σx,id,σx) + kron(id,σx,σx)))
            ref = exp(β/2 * (kron(σz,id,id) + kron(id,σz,id) + kron(id,id,σz))) *
                  exp(β *   (kron(σx,σx,id) + kron(id,σx,σx) + kron(σx,id,σx))) *
                  exp(β/2 * (kron(σz,id,id) + kron(id,σz,id) + kron(id,id,σz)))
            op = tfisingpropagator(β)
            @tensor op2[1,2,3,4,5,6] := op[1,-2,4,-1] *
                                        op[2,-3,5,-2] *
                                        op[3,-1,6,-3];
            res = toarray(fuselegs(op2,((1,2,3),(4,5,6)))[1])
            @test ref ≈ refe && ref ≈ res
        end
        op = tfisingpropagator(1e-5)
        @tensor op2[1,2,3,4,5,6] := op[1,-2,4,-1] * op[2,-3,5,-2] * op[3,-1,6,-3];
        prop = toarray(fuselegs(op2,((1,2,3),(4,5,6)))[1])
        ham = -(kron(σz,id,id) + kron(id,σz,id) + kron(id,id,σz) +
                kron(σx,σx,id) + kron(σx,id,σx) + kron(id,σx,σx))
        λp, vecsp = eigen(prop)
        λh, vecsh = eigen(ham)
        @test vecsp[:,argmax(λp)] ≈ vecsh[:,argmin(λh)]

        @test kron(-σx,σx) + kron(id,-σz) + kron(-σz,id) ≈ toarray(fuselegs(tfisinghamiltonian(true),((1,2),(3,4)))[1])

        sth0 = tfisingctm(1e-3, 8, 0, maxit=10^4, period = 10^2, verbose=false)
        @test mag(sth0) ≈ 1
        @test energy(sth0) ≈ -1/2

        sth∞ = tfisingctm(1e-3, 8, 1e5, maxit=10^4, period = 10^2, verbose=false)
        @test isapprox(mag(sth∞), 0; atol=1e-20)
        @test energy(sth∞) ≈ -1
    end

    @testset "algorithms equality" begin
        A = CornerTransferMethods.isingtensor(CornerTransferMethods.βc)
        χ = 4
        rotsymiter = rotsymctmiterable(A, χ)
        tciter = transconjctmiterable(A, χ)
        iter = ctmiterable(A, χ)
        rotsymstate = ctm_kernel(rotsymiter, maxit=10^6, verbose=false)
        tcstate = ctm_kernel(tciter, maxit=10^6, verbose=false)
        state = ctm_kernel(iter, maxit=10^6, verbose=false)
        eoes = cornereoe.([state.Cs[1], state.Cs[2], state.Cs[3], state.Cs[4],
                           rotsymstate.C, tcstate.C])
        @test all((x ≈ y for x in eoes, y in eoes))

        cls = clength.([state.Ts[1], state.Ts[2], state.Ts[3], state.Ts[4],
                        rotsymstate.T, tcstate.Ts[1], tcstate.Ts[2]])
        @test all((x ≈ y for x in cls, y in cls))
    end
end


# Encode MNIST images as compressed vectors that can later be decoded back into
# images.
using Flux, Flux.Data.MNIST
using Flux: @epochs, onehotbatch, mse, throttle
using Base.Iterators: partition
using Parameters: @with_kw
using CUDAapi
if has_cuda()
    @info "CUDA is on"
    import CuArrays
    CuArrays.allowscalar(false)
end

@with_kw mutable struct Args
    lr::Float64 = 1e-3		# Learning rate
    epochs::Int = 10		# Number of epochs
    N::Int = 32			# Size of the encoding
    batchsize::Int = 1000	# Batch size for training
    sample_len::Int = 20 	# Number of random digits in the sample image
    throttle::Int = 5		# Throttle timeout
end

function get_processed_data(args)
    # Loading Images
    imgs = MNIST.images()
    #Converting image of type RGB to float
    imgs = channelview.(imgs)
    # Partition into batches of size 1000
    train_data = [float(hcat(vec.(imgs)...)) for imgs in partition(imgs, args.batchsize)]

    train_data = gpu.(train_data)
    return train_data
end

function train(; kws...)
    args = Args(; kws...)

    train_data = get_processed_data(args)

    @info("Constructing model......")
    # You can try to make the encoder/decoder network larger
    # Also, the output of encoder is a coding of the given input.
    # In this case, the input dimension is 28^2 and the output dimension of
    # encoder is 32. This implies that the coding is a compressed representation.
    # We can make lossy compression via this `encoder`.
    encoder = Dense(28^2, args.N, leakyrelu) |> gpu
    decoder = Dense(args.N, 28^2, leakyrelu) |> gpu

    # Defining main model as a Chain of encoder and decoder models
    m = Chain(encoder, decoder)

    @info("Training model.....")
    loss(x) = mse(m(x), x)
    ## Training
    evalcb = throttle(() -> @show(loss(train_data[1])), args.throttle)
    opt = ADAM(args.lr)

    @epochs args.epochs Flux.train!(loss, params(m), zip(train_data), opt, cb = evalcb)

    return m, args
end

using Images

img(x::Vector) = Gray.(reshape(clamp.(x, 0, 1), 28, 28))

function sample(m, args)
    imgs = MNIST.images()
    #Converting image of type RGB to float
    imgs = channelview.(imgs)
    # `args.sample_len` random digits
    before = [imgs[i] for i in rand(1:length(imgs), args.sample_len)]
    # Before and after images
    after = img.(map(x -> cpu(m)(float(vec(x))), before))
    # Stack them all together
    hcat(vcat.(before, after)...)
end

cd(@__DIR__)
m, args= train()
# Sample output
@info("Saving image sample as sample_ae.png")
save("sample_ae.png", sample(m, args))


# DTU02435 Decision Making under Uncertainty
# Assignment 3, Task 2
# Out-of-Sample Test of the Plan
# Edward J. Xu
# May 5th, 2019
push!(LOAD_PATH, "$(homedir())/Desktop/StochasticAirplaneRent")
cd("$(homedir())/Desktop/StochasticAirplaneRent")
using JuMP
# using GLPKMathProgInterface
# using Gurobi
using CPLEX
using CSV
using DataFrames

function processData()
    datf_cost = CSV.read("Data/OperationCosts.csv", header = true, delim = ',')
    datf_cap = CSV.read("Data/PlaneCapacity.csv", header = ["p", "cap"], delim = ',')
    datf_route = CSV.read("Data/DemandsRoute.csv", header = true, delim = ',')
    datf_costAdmin = CSV.read("Data/CostAdquisition.csv", header = ["p", "cost"], delim = ',')
    datf_demand = CSV.read("Data/OutofSample.csv", header = true, delim = ',')
    # Calculate the demand matrix
    mat4_demand = zeros(4, 10, 12, 500)
    for i = 1: 4
        for j = 1: 10
            for w = 1: 500
                for t = 1: 12
                    mat4_demand[i, j, t, w] = datf_demand[t, w+1] * datf_route[i, j+1]
                end
            end
        end
    end
    return (datf_cost, datf_cap.cap, datf_costAdmin.cost, mat4_demand)
end

function optim(vec_y, datf_cost, vec_cap, vec_costAdmin, mat_demand)
    model = Model(solver = CplexSolver(CPX_PARAM_SCRIND = 0))
    # model = Model(solver = GurobiSolver(Presolve=0))
    # model = Model(solver = GLPKSolverMIP())
    @variable(model, mat3_z[1:4, 1:10, 1:10] >= 0, Int)
    @variable(model, mat_m[1:4, 1:10] >= 0)
    @objective(model, Min, sum(datf_cost.cost[.&((datf_cost[:i] .== i), (datf_cost[:j] .== j),
        (datf_cost[:p] .== p))][1] * mat3_z[i, j, p] for p = 1:10, i = 1:4, j = 1:10) + sum(mat_m) * 1000)
    @constraint(model, [i = 1:4, j = 1:10], mat_demand[i, j] - sum(mat3_z[i, j, p] * vec_cap[p] for p = 1:10)
        <= mat_m[i, j])
    @constraint(model, [p = 1:10], sum(mat3_z[i, j, p] for i = 1:4, j = 1:10) <= vec_y[p])
    solve(model)
    result_obj = getobjectivevalue(model)
    return result_obj
end

function testOutOfSample(vec_y, datf_cost, vec_cap, vec_costAdmin, mat4_demand)
    mat_result_obj = zeros(500, 12)
    vec_result_obj = zeros(500)
    ## 1,  Begin Optimization
    for w = 1:500
        for t = 1:12
            mat_result_obj[w, t] = optim(vec_y, datf_cost, vec_cap, vec_costAdmin, mat4_demand[:, :, t, w])
        end
        vec_result_obj[w] = sum(mat_result_obj[w, tt] for tt = 1:12) + sum(vec_y[p] * vec_costAdmin[p] for p = 1: 10)
        println("Result: obj = $(vec_result_obj[w]), when w = $(w).")
    end
    return vec_result_obj
end

function main()
    (datf_cost, vec_cap, vec_costAdmin, mat4_demand) = processData()
    println("################################################################################\n",
            "############### 1/4, Out-of-Sample Test of Deterministic Program ###############\n",
            "################################################################################")
    vec_y_1 = [0.0, 1.0, 2.0, 2.0, 0.0, 12.0, 5.0, 4.0, 8.0, 8.0]
    timeStart = time()
    vec_result_obj_1 = testOutOfSample(vec_y_1, datf_cost, vec_cap, vec_costAdmin, mat4_demand)
    println("Elapsed time: $(time()-timeStart) seconds.")
    CSV.write("result/out-of-sample_1.csv", DataFrame(vec_result_obj_1'), writeheader = false, delim = ',')
    println("################################################################################\n",
            "################# 2/4, Out-of-Sample Test of Stochastic Program ################\n",
            "################################################################################")
    vec_y_2 = [0.0, 7.0, 2.0, 3.0, 0.0, 12.0, 4.0, 4.0, 8.0, 8.0]
    timeStart = time()
    vec_result_obj_2 = testOutOfSample(vec_y_2, datf_cost, vec_cap, vec_costAdmin, mat4_demand)
    println("Elapsed time: $(time()-timeStart) seconds.")
    CSV.write("result/out-of-sample_2.csv", DataFrame(vec_result_obj_2'), writeheader = false, delim = ',')
    println("################################################################################\n",
            "################## 3/4, Out-of-Sample Test of Robust Program ###################\n",
            "################################################################################")
    vec_y_3 = [0.0, 3.0, 1.0, 2.0, 0.0, 12.0, 2.0, 8.0, 8.0, 8.0]
    timeStart = time()
    vec_result_obj_3 = testOutOfSample(vec_y_3, datf_cost, vec_cap, vec_costAdmin, mat4_demand)
    println("Elapsed time: $(time()-timeStart) seconds.")
    CSV.write("result/out-of-sample_3.csv", DataFrame(vec_result_obj_3'), writeheader = false, delim = ',')
    println("################################################################################\n",
            "################################### 4/4, End ###################################\n",
            "################################################################################")
end

main()


module poly2D

using QHull, Statistics, LinearAlgebra, Clipper, Devices, PyPlot, LazySets

"""
"""

function  checkConvex(V)
# Given a set of points determine if they form a convex polygon

    px=V[:,1];
    py=V[:,2];

    isConvex = false;

    numPoints = length(px);
    if numPoints < 4
        isConvex = true;
        return isConvex
    end

    # can determine if the polygon is convex based on the direction the angles
    # turn.  If all angles are the same direction, it is convex.
    v1 = [px[1] - px[end], py[1] - py[end]];
    v2 = [px[2] - px[1], py[2] - py[1]];
    signPoly = sign(det([v1'; v2']));

    # check subsequent vertices
    for k = 2:numPoints-1
        v1 = v2;
        v2 = [px[k+1] - px[k], py[k+1] - py[k]];
        curr_signPoly = sign(det([v1'; v2']));
        # check that the signs match
        if curr_signPoly != signPoly
            isConvex = false;
            return isConvex
        end
    end

    # check the last vectors
    v1 = v2;
    v2 = [px[1] - px[end], py[1] - py[end]];
    curr_signPoly = sign(det([v1'; v2']));
    if curr_signPoly != signPoly
        isConvex = false;
    else
        isConvex = true;
    end

    return isConvex
end

function createLine(p1,p2)

    # first input parameter is first point, and second input is the
    # second point.
    line = [p1[1] p1[2] p2[1]-p1[1] p2[2]-p1[2]];

end

function distanceMat(points1,points2)

    numPoints1=size(points1,1)
    numPoints2=size(points2,1)

    distMat=zeros(numPoints1,numPoints2)
    for i=1:numPoints1
        point_i=points1[i,:]
        for j=1:numPoints2
            point_j=points2[j,:]
            distMat[i,j]=sqrt(sum((point_i-point_j).^2))
        end
    end

    return distMat

end

function expandPolygon(V, dist)
    numVertices = size(V,1)
    poly = Devices.Polygon([Devices.Point(V[i,1],V[i,2]) for i=1:numVertices])
    poly_ = offset(poly, dist)

    poly__ = poly_[1].p
    numVertices_ = size(poly__,1)
    V_ = zeros(numVertices_,2)
    for i=1:numVertices_
        V_[i,1] = poly__[i][1]
        V_[i,2] = poly__[i][2]
    end

    V__ = poly2D.expandPolygonSides(V, dist, collect(1:numVertices))

    V_out = zeros(size(V__,1),2)
    posVec = zeros(size(V__,1),1)
    for i = 1:size(V__,1)
        for j = 1:size(V_,1)
            if sqrt(sum((V__[i,:] - V_[j,:]).^2)) <= 1
                V_out[i,:] =  V_[j,:]
                posVec[i] = j
            end
        end
    end
    pos = setdiff(collect(1:size(V_,1)), unique(posVec))

    if length(pos) >= 0
        V_out[V_out[:,1] .== 0,: ] .= V_[pos,:]
    end

    return V_out

end

function expandPolygonSide(poly_in, dist, edge)
    poly=copy(poly_in);

    # eventually copy first point at the end to ensure closed polygon
    if sum(poly[end, :] == poly[1,:]) != 2
        poly = [poly; poly[1,:]'];
    end

    # number of vertices of the polygon
    N = size(poly, 1)-1;

    # find lines parallel to polygon edges located at distance DIST
    lines = zeros(N, 4);
    for i = 1:N
        side = createLine(poly[i,:], poly[i+1,:]);
        if i==edge
            lines[i, 1:4] = parallelLine(side, dist);
        else
            lines[i, 1:4] = side;
        end
    end

    # compute intersection points of consecutive lines
    lines = [lines;lines[1,:]'];
    poly_out = zeros(N, 2);
    for i = 1:N
        pa1=lines[i,:];
        pa2=lines[i+1,:];
        poly_aux = intersectLines(pa1, pa2);
        poly_out[i,1:2] = poly_aux;
    end

    poly_out = [poly_out[end,:]'; poly_out[1:N-1,:]];

end

function expandPolygonSides(poly, sep, edges)

    numEdgesTotales = size(poly,1)
    conjuntoEdgesTotales = 1:numEdgesTotales
    conjuntoEdgesNoSeleccionados = setdiff(conjuntoEdgesTotales,edges)
    numEdges = length(edges);

    poly_ = copy(poly);

    if numEdges >= 1
        for i in edges
            poly_aux = expandPolygonSide(poly_, sep, i);
            poly_ = copy(poly_aux);
        end
        poly_out = poly_;
    end

    return poly_out
end

function inpoly_mat(points, poly, edge)

    nvrt = size(points,1);
    nnod = size(poly,1);
    nedg = size(edge,1);

    stat = falses(nvrt,1);
    bnds = falses(nvrt,1);

#----------------------------------- loop over polygon edges
    for epos = 1 : size(edge,1)
        inod = edge[epos,1] ;
        jnod = edge[epos,2] ;

    #------------------------------- calc. edge bounding-box
        yone = poly[inod,2];
        ytwo = poly[jnod,2];
        xone = poly[inod,1];
        xtwo = poly[jnod,1];

        xmin = min(xone, xtwo);
        xmax = max(xone, xtwo);

        ymin = yone;
        ymax = ytwo;

        ydel = ytwo - yone;
        xdel = xtwo - xone;

    #------------------------------- find top VERT(:,2)<YONE
        ilow = 1;
        iupp = nvrt;

        while (ilow < iupp - 1)    # binary search
            imid = ilow + Int(floor((iupp-ilow) / 2));

            if (points[imid,2] < ymin)
                ilow = imid;
            else
                iupp = imid ;
            end
        end

        if (points[ilow,2] >= ymin)
            ilow = ilow - 1 ;
        end

    #------------------------------- calc. edge-intersection
        for jpos = ilow+1:nvrt

            if (bnds[jpos])
                 continue;
            end

            xpos = points[jpos,1];
            ypos = points[jpos,2];

            if (ypos <= ymax)
                if (xpos >= xmin)
                    if (xpos <= xmax)

                #------------------- compute crossing number
                    mul1 = ydel * (xpos - xone) ;
                    mul2 = xdel * (ypos - yone) ;

                        if (ypos == yone &&  xpos == xone )

                    #------------------- BNDS -- match about ONE
                            bnds[jpos]= true ;
                            stat[jpos]= true ;

                        elseif (ypos == ytwo &&  xpos == xtwo )

                    #------------------- BNDS -- match about TWO
                            bnds[jpos]= true ;
                            stat[jpos]= true ;

                        elseif (mul1 < mul2)

                            if (ypos >= yone && ypos <  ytwo)

                        #------------------- advance crossing number
                                stat[jpos] = ~stat[jpos] ;
                            end
                        end

                    end
                else

                    if (ypos >= yone && ypos <  ytwo)
                #------------------- advance crossing number
                        stat[jpos] = !stat[jpos] ;
                    end

                end
            else

                break ;            # done -- due to the sort

            end

        end

    end

    return stat, bnds
end

function inpoly(points, poly)

    nnod = size(poly,1);
    nvrt = size(points,1);

    edge = [(1:nnod-1) (2:nnod); [nnod 1]];
#----------------------------------- prune points using bbox
    mask =  (points[:,1] .>= minimum(poly[:,1])) .* (points[:,1] .<= maximum(poly[:,1])) .* (points[:,2] .>= minimum(poly[:,2])) .* (points[:,2] .<= maximum(poly[:,2]));

    vert = points[mask, :];

#----------------------------------- sort points via y-value
    swap = poly[edge[:,2],2] .< poly[edge[:,1],2];

    edge[swap,[1,2]] = edge[swap,[2,1]];

    ivec = sortperm(points[:,2]);
    points = points[ivec,:];

    #-- call the native m-code version

    stat, bnds = inpoly_mat(points, poly, edge);

    stat[ivec] = stat;
    bnds[ivec] = bnds;

    #STAT[mask] = stat;
    #BNDS[mask] = bnds;

    STAT = stat;
    BNDS = bnds;

    return STAT, BNDS
end

function intersectEdges(edge1, edge2)

    x1_ini  = edge1[1];
    y1_ini  = edge1[2];
    x1_fin  = edge1[3];
    y1_fin  = edge1[4];
    dx1 = x1_fin - x1_ini;
    dy1 = y1_fin - y1_ini;
    m1 = dy1 / dx1;

    x2_ini  = edge2[1];
    y2_ini  = edge2[2];
    x2_fin  = edge2[3];
    y2_fin  = edge2[4];
    dx2 = x2_fin - x2_ini;
    dy2 = y2_fin - y2_ini;
    m2 = dy2 / dx2;

    min_e1_x = min(x1_ini,x1_fin)
    min_e2_x = min(x2_ini,x2_fin)
    max_e1_x = max(x1_ini,x1_fin)
    max_e2_x = max(x2_ini,x2_fin)

    min_e1_y = min(y1_ini,y1_fin)
    min_e2_y = min(y2_ini,y2_fin)
    max_e1_y = max(y1_ini,y1_fin)
    max_e2_y = max(y2_ini,y2_fin)

# tolerance for precision
    tol = 1e-3; #1e-14;

# initialize result array
    x0  = 0;
    y0  = 0;

# indices of parallel edges
    par = abs(m1 - m2) < tol;

# Parallel edges have no intersection -> return [NaN NaN]
    if par
        x0 = NaN;
        y0 = NaN;
    end

# Process non parallel cases

    # compute intersection points of supporting lines
    delta = dx2 * dy1 - dx1 * dy2;
    x0 = ((y2_ini - y1_ini) * dx1 * dx2 + x1_ini * dy1 * dx2 - x2_ini * dy2 * dx1) / delta;
    y0 = ((x2_ini - x1_ini) * dy1 * dy2 + y1_ini * dx1 * dy2 - y2_ini * dx2 * dy1) / -delta;

    if  x0 < min_e1_x-tol || x0 < min_e2_x-tol || x0 > max_e1_x+tol || x0 > max_e2_x+tol ||
        y0 < min_e1_y-tol || y0 < min_e2_y-tol || y0 > max_e1_y+tol || y0 > max_e2_y+tol
        point = [NaN NaN];
#    elseif (x0-x1_ini)^2 + (y0-y1_ini)^2 <= 1  || (x0-x1_fin)^2 + (y0-y1_fin)^2 <= 1 ||  (x0-x2_ini)^2 + (y0-y2_ini)^2 <= 1  || (x0-x2_fin)^2 + (y0-y2_fin)^2  <= 1
#        point = [NaN NaN];
    else
        point = [x0 y0];
    end

end

function expantLine(line, f)
    #intersectLines([x_0, y_0, x_1, y_1],2)
    x_0 = line[1]
    y_0 = line[2]
    x_1 = line[3]
    y_1 = line[4]
    m = (y_1 - y_0) / (x_1 - x_0)
    d = sqrt((y_1-y_0)^2+(x_1-x_0)^2)
    df = d*f
    y=x*m + a
end

function intersectLines(line1, line2)
    # intersectLines([x1_0, y1_0, x1_1, y1_1], [x2_0, y2_0, x2_1, y2_1])

    # extract tolerance
    tol = 1e-14;

    # Check parallel and colinear lines

    # coordinate differences of origin points
    dx = line2[1] - line1[1];
    dy = line2[2] - line1[2];

    # indices of parallel lines
    denom = line1[3] .* line2[4] - line2[3] .* line1[4];
    par = abs(denom) < tol;

    # initialize result array
    x0 = 0;
    y0 = 0;

    # initialize result for parallel lines
    if par
        x0 = Inf;
        y0 = Inf;
        point = [x0 y0];
        return;
    end

    # Extract coordinates of itnersecting lines

    # extract base coordinates of first lines
    x1 =  line1[1];
    y1 =  line1[2];
    dx1 = line1[3];
    dy1 = line1[4];

    # extract base coordinates of second lines
    x2 =  line2[1];
    y2 =  line2[2];
    dx2 = line2[3];
    dy2 = line2[4];

    # re-compute coordinate differences of origin points
    dx = line2[1] - line1[1];
    dy = line2[2] - line1[2];

    # Compute intersection points

    x0 = (x2 .* dy2 .* dx1 - dy .* dx1 .* dx2 - x1 .* dy1 .* dx2) ./ denom ;
    y0 = (dx .* dy1 .* dy2 + y1 .* dx1 .* dy2 - y2 .* dx2 .* dy1) ./ denom ;

    # concatenate result
    point = [x0 y0];
end

function intersectPoly2d(V1, V2)

    V1 = [V1; V1[1,:]'];
    V2 = [V2; V2[1,:]'];
    N1 = size(V1, 1)
    N2 = size(V2, 1)

    # Loop over segments of V1
    i=1
    X=[NaN NaN NaN NaN]
    for n1 = 1:N1-1
        for n2 = 1:N2-1
            e1 = [V1[n1,:]' V1[n1+1,:]']
            e2 = [V2[n2,:]' V2[n2+1,:]']
            pInt = intersectEdges(e1, e2)
            if abs(pInt[1])>0
                if i==1
                    X[:] = [n1 n2 pInt]
                else
                    X = [X; [n1 n2 pInt]]
                end
                i=i+1
            end
        end
    end

    return X

end

function isPointInPolygon(points, V_)

    V=copy(V_);

    numPointsToCheck=size(points,1);
    A, b, c = vert2con(V);

    flagInPoly=[];
    for i=1:numPointsToCheck
        p_i=points[i,:];
        flagInPoly_=A*p_i.<b;
        flagInPoly_i=prod(flagInPoly_);
        push!(flagInPoly,flagInPoly_i);

    end

    return flagInPoly
end

function findNonConvexVert(Vin)

    V = copy(Vin);

    V_ = poly2D.expandPolygon(V, -.5)
    V_ch = poly2D.convHull(V_)

    flagNonConvex = isPointInPolygon(V, V_ch)

    idNonConv = collect(1:size(V,1))
    idNonConv = idNonConv[flagNonConvex .== true]

    return idNonConv
end

function isRectInPoly(rect,poly)

    pos_x = rect[1];
    pos_y = rect[2];
    largo_x = rect[3];
    largo_y = rect[4];
    theta = rect[5];

    Rmat = rotationMatrix(theta);
    cr = [pos_x; pos_y];
    p1 = (Rmat * ([pos_x; pos_y] - cr) + cr)';
    p2 = (Rmat * ([pos_x + largo_x; pos_y] - cr) + cr)';
    p3 = (Rmat * ([pos_x + largo_x; pos_y + largo_y] - cr) + cr)';
    p4 = (Rmat * ([pos_x; pos_y + largo_y] - cr) + cr)';

    if collect(inpoly(p1, poly)[1])[1] && collect(inpoly(p2, poly)[1])[1] && collect(inpoly(p3, poly)[1])[1] &&
        collect(inpoly(p4, poly)[1])[1] && !(intersectPoly2d([p1; p2; p3; p4], poly)[1,1]>0)
        return true
    else
        return false
    end

end

function lineAngle(line)

    # angle of one line with horizontal
    theta = mod(atan(line[4], line[3]) + 2*pi, 2*pi);

end

function parallelLine(line, dist)

    # use a distance. Compute position of point located at distance DIST on
    # the line orthogonal to the first one.
    point = pointOnLine([line[1] line[2] line[4] -line[3]], dist);

    res = [point' (line[3:4])'];
end

function pointOnLine(line, pos)

    angle = lineAngle(line);
    point = [line[1] + pos .* cos(angle), line[2] + pos .* sin(angle)];

end

function polyArea(V)

    if V != []
      X=V[:,1];
      Y=V[:,2];
      numPoints=size(X,1);

      area = 0;   # Accumulates area
      j = numPoints;

      for i = 1:numPoints
        area = area + (X[j]+X[i])*(Y[j]-Y[i]);
        j = i;  #j is previous vertex to i
      end

      return out=abs(area/2);
    else
      return out=0;
    end
end

function randomPointInPolygon(poly, nPts)

    # polygon extreme coordinates
    xmin = minimum(poly[:,1]);  xmax = maximum(poly[:,1]);
    ymin = minimum(poly[:,2]);  ymax = maximum(poly[:,2]);

    # compute size of box
    boxSizeX = xmax - xmin;
    boxSizeY = ymax - ymin;

    # allocate memory for result
    points = zeros(nPts, 2);

    # contains indices of remaining points to process
    ind = 1;

    # iterate until all points have been sampled within the polygon
    while ind<=nPts
        x = rand() .* boxSizeX .+ xmin;
        y = rand() .* boxSizeY .+ ymin;
        points[ind, 1] = x;
        points[ind, 2] = y;
        #isPointInPolygon
        if collect(inpoly(points[ind, :]', poly)[1])[1]
            ind=ind+1;
        end
    end

    return points

end

function randomPointInExtrude(poly, alt, nPts)

    # polygon extreme coordinates
    xmin = minimum(poly[:,1]);  xmax = maximum(poly[:,1]);
    ymin = minimum(poly[:,2]);  ymax = maximum(poly[:,2]);
    zmin = minimum(0);  zmax = maximum(alt);

    # compute size of box
    boxSizeX = xmax - xmin;
    boxSizeY = ymax - ymin;
    boxSizeZ = zmax - zmin;

    # allocate memory for result
    points = zeros(nPts, 3);

    # contains indices of remaining points to process
    ind = 1;

    # iterate until all points have been sampled within the polygon
    while ind<=nPts
        x = rand() .* boxSizeX .+ xmin;
        y = rand() .* boxSizeY .+ ymin;
        z = rand() .* boxSizeZ .+ zmin;
        points[ind, 1] = x;
        points[ind, 2] = y;
        points[ind, 3] = z;
        #isPointInPolygon
        if collect(inpoly(points[ind, 1:2]', poly)[1])[1]
            ind=ind+1;
        end
    end

    return points

end

function rotationMatrix(theta)
    R=[cos(theta) -sin(theta); sin(theta) cos(theta)];
end

function convHull(V)

    n = size(V,1)
    v = [[V[i,1], V[i,2]] for i=1:n]
    ch_v = LazySets.convex_hull(v)

    V_out = [0 0]; for i=1:length(ch_v); V_out=[V_out; ch_v[i]']; end; V_out = V_out[2:end,:]

    return V_out
end

function simplicesConvHull(V_)
    V = copy(V_);
    k_ = QHull.chull(V);
    k = k_.simplices;
end

function verticesConvHull(V_)
    V = copy(V_);
    k_ = QHull.chull(V);
    k = k_.vertices;
    n = length(k);
    kk = vcat(k,k);
    K = [kk[n:2*n-1] kk[1:n]];
end

function vert2con(V_)

    V = copy(V_);

    k = verticesConvHull(V);
    c = mean!([1. 1.], V[unique(k),:]);
    V = V-repeat(c,size(V,1),1);
    A = NaN*zeros(size(k,1),size(V,2));
    rc=0;
    for ix = 1:size(k,1)
        F = V[k[ix,:],:];
        #if rank(F, 1e-5) == size(F,1)
            rc=rc+1;
            A[rc,:]=F\ones(size(F,1),1);
        #end
    end
    A=A[1:rc,:];
    b=ones(size(A,1),1);
    b=b+A*c';

    return A,b,c

end

function plotScatter3d(x,y,z, fig=nothing, ax=nothing, ax_mat=nothing, color="red", marker="o", alpha=.3)

    plt = pyimport("matplotlib.pyplot")
    pyimport("mpl_toolkits.mplot3d")

    if fig === nothing
        pygui(true)

        fig = plt.figure()
        ax = fig[:add_subplot](111, projection = "3d")

        min_ax_x = minimum(x)
        min_ax_y = minimum(y)
        min_ax_z = minimum(z)
        max_ax_x = maximum(x)
        max_ax_y = maximum(y)
        max_ax_z = maximum(z)
    else
        min_ax_x = ax_mat[1,1]
        min_ax_y = ax_mat[2,1]
        min_ax_z = ax_mat[3,1]
        max_ax_x = ax_mat[1,2]
        max_ax_y = ax_mat[2,2]
        max_ax_z = ax_mat[3,2]
        min_ax_x = minimum([min_ax_x minimum(x)])
        min_ax_y = minimum([min_ax_y minimum(y)])
        min_ax_z = minimum([min_ax_z minimum(z)])
        max_ax_x = maximum([max_ax_x maximum(x)])
        max_ax_y = maximum([max_ax_y maximum(y)])
        max_ax_z = maximum([max_ax_z maximum(z)])

    end

    ax.set_xlim(min_ax_x, max_ax_x)
    ax.set_ylim(min_ax_y, max_ax_y)
    ax.set_zlim(min_ax_z, max_ax_z)

    ax[:scatter3D](x, y, z, color=color, marker=marker, alpha=alpha)

    ax.set_xlabel("X")
    ax.set_ylabel("Y")
    ax.set_zlabel("Z")

    plt.show()

    ax_mat = [min_ax_x max_ax_x; min_ax_y max_ax_y; min_ax_z max_ax_z]

    return fig, ax, ax_mat

end

function angulosLados(V)
    x = V[:,1]
    x = [x;x[1]]
    y = V[:,2]
    y = [y;y[1]]
    dx = x[2:end] .- x[1:end - 1]
    dy = y[2:end] .- y[1:end - 1]

    theta = zeros(size(V, 1), 1)
    for i = 1:size(V, 1)
        if dy[i] > 0 && dx[i] > 00
            theta[i] = atan(dy[i] / dx[i])
        elseif dy[i] > 0 && dx[i] < 0
            theta[i] = (90 + 180 / pi * atan(abs(dx[i]) / dy[i]) ) * pi / 180
        elseif dy[i] < 0 && dx[i] < 0
            theta[i] = (180 + 180 / pi * atan(abs(dy[i]) / abs(dx[i])) ) * pi / 180
        elseif dy[i] < 0 && dx[i] > 0
            theta[i] = (270 + 180 / pi * atan(dx[i] / abs(dy[i])) ) * pi / 180
        else
            theta[i] = 0
        end
    end
    return theta
end

function rectangle(p, w, h, theta)
    R = poly2D.rotationMatrix(theta);
    x0 = p[1]
    y0 = p[2]
    cr = [x0; y0]
    p1 = R*([x0; y0]-cr) + cr
    p2 = R*([x0+w; y0]-cr) + cr
    p3 = R*([x0+w; y0+h]-cr) + cr
    p4 = R*([x0; y0+h]-cr) + cr

    V = [p1';p2';p3';p4']

    return V
end

function distPointLine(q, p1, p2)
    dist = sqrt(sum( ((q-p1) - ((q-p1)'*(p2-p1)) / ((p2-p1)'*(p2-p1)) * (p2-p1) ).^2))
    return dist
end

export  checkConvex, verticesConvHull, convHull, createLine, distanceMat, expandPolygonSide, expandPolygonSides, inpoly, inpoly_mat,
        intersectEdges, intersectLines, intersectPoly2d, isPointInPolygon, isRectInPoly, lineAngle, parallelLine,
        pointOnLine, polyArea, randomPointInPolygon, rotationMatrix, vert2con, plotScatter3d, findNonConvexVert,
        angulosLados, rectangle, distPointLine
end


using Test
using Recommenders:
    Recall, Precision, DCG, NDCG, MeanRecall, MeanPrecision, MeanDCG, MeanNDCG, _dcg

@testset "Recall and precision" begin
    recall1 = Recall(1)

    recommends = [[1, 2, 3], [4, 5], [], nothing]
    ys = [[1], [2, 5], [4], [5]]

    expected_recall1 = [1.0, 0.0, 0.0, 0.0]
    expected_precision1 = [1.0, 0.0, 0.0, 0.0]
    expected_recall2 = [1.0, 0.5, 0.0, 0.0]
    expected_precision2 = [0.5, 0.5, 0.0, 0.0]

    recall1 = Recall(1)
    recall2 = Recall(2)
    precision1 = Precision(1)
    precision2 = Precision(2)

    for i in eachindex(recommends)
        @test recall1(recommends[i], ys[i]) == expected_recall1[i]
        @test recall2(recommends[i], ys[i]) == expected_recall2[i]
        @test precision1(recommends[i], ys[i]) == expected_precision1[i]
        @test precision2(recommends[i], ys[i]) == expected_precision2[i]
    end

    # mean_recall1 = MeanMetric(Recall(1))
    mean_recall1 = MeanRecall(1)
    @test mean_recall1(recommends, ys) == 1 / 4

    mean_recall12 = MeanRecall(2)
    @test mean_recall12(recommends, ys) == (1 + 0.5) / 4

    mean_prec1 = MeanPrecision(1)
    @test mean_prec1(recommends, ys) == 1 / 4

    mean_prec2 = MeanPrecision(2)
    @test mean_prec2(recommends, ys) == (0.5 + 0.5) / 4

end

@testset "DCG" begin
    @test _dcg([1]) ≈ 1.0
    relevances = [1.0, 0.0, 3.0]
    @test _dcg(relevances) ≈ 1 + 3.5

    dcg3 = DCG(3)
    @test dcg3([1], [1]) ≈ 1.0
    @test dcg3([1, 2, 3], [1, 2, 3], [1.0, 0.0, 3.0]) ≈ 4.5
    @test dcg3([1, 2, 3, 4], [1, 2, 3, 4], [1.0, 0.0, 3.0]) ≈ 4.5
    @test dcg3([1, 4, 2], [1, 2, 3], [1.0, 3.0, 5.0]) ≈ 4.5
    @test dcg3([1, 4, 2], [1, 2, 3], [1.0, 7.0, 5.0]) ≈ 1 + 127 / 2.0

    ndcg3 = NDCG(3)
    @test ndcg3([1], [1]) ≈ 1.0
    @test ndcg3([1, 2, 3], [1, 2, 3], [1, 1, 1]) ≈ 1.0
    @test ndcg3([1, 2, 3], [1, 2], [1, 1]) ≈ 1.0
    @test ndcg3([1, 2, 3], [2, 1], [1, 1]) ≈ 1.0
    @test ndcg3([1, 2, 3], [1, 2, 3, 4]) ≈ 1.0
    @test ndcg3([6, 7, 8], [1, 2, 3, 4]) ≈ 0.0

    mean_dcg3 = MeanDCG(3)
    @test mean_dcg3([[1], [1, 2, 3]], [[1], [1]]) ≈ 1.0

    mean_ndcg3 = MeanNDCG(3)
    @test mean_ndcg3([[1, 2, 3], [6, 7, 8]], [[1, 2, 3], [1]]) ≈ 0.5
end


@testset "check softmax op's gradient" begin
    clear()
    for d in [1 2 3 (1,2) (2,3) (1,3) (1,2,3)]
        @testset "check softmax op's gradient at dim = $d" begin
            DIMS = d
            TYPE = Array{Float64};

            # [1] prepare input data and its label
            inputdims = 64;
            timeSteps = 16;
            batchsize = 32;
            x = Variable(rand(inputdims, timeSteps, batchsize), type=TYPE, keepsgrad=true);
            l = Variable(rand(inputdims, timeSteps, batchsize), type=TYPE);

            # [2] forward and backward propagation
            probs = softmax(x; dims=DIMS)
            Loss1 = crossEntropyLoss(probs, l);
            Loss2 = mseLoss(probs, l);
            COST1 = cost(0.8*Loss1 + 0.2*Loss2)
            backward();

            # [3] with a samll change of a weight
            GRAD = x.delta[1];
            DELTA = 1e-5;
            x.value[1] += DELTA;

            # [4] forward and backward propagation with a samll change of a weight
            probs = softmax(x; dims=DIMS)
            Loss1 = crossEntropyLoss(probs, l);
            Loss2 = mseLoss(probs, l);
            COST2 = cost(0.8*Loss1 + 0.2*Loss2)
            backward();

            # [5] check if the auto-grad is true or not
            dLdW = (COST2 - COST1)/DELTA;   # numerical gradient
            err  = abs((dLdW-GRAD)/(GRAD+eps(Float64)))*100;  # relative error in %
            err  = err < 1e-1 ? 0.0 : err;
            @test err<1.0
        end
    end
end


### https://darrenjw.wordpress.com/2012/06/04/metropolis-hastings-mcmc-when-the-proposal-and-target-have-differing-support/

using Distributions
using Klara

plogtarget(p::Float64, v::Vector) = (v[1]-1)*log(p)-p/v[2]

p = BasicContUnvParameter(:p, logtarget=plogtarget, nkeys=3)

model = likelihood_model([Constant(:k), Constant(:θ), p], isindexed=false)

psetproposal(x::Float64) = Truncated(Normal(x), 0, Inf)

sampler = MH(psetproposal, symmetric=false)

mcrange = BasicMCRange(nsteps=100000, burnin=10000)

v0 = Dict(:k=>2., :θ=>1., :p=>2.)

job = BasicMCJob(model, sampler, mcrange, v0)

run(job)

chain = output(job)

mean(chain)

acceptance(chain, diagnostics=false)


for PT in (:Particles, :StaticParticles)
    @eval begin
        function $PT(N, m::Measurements.Measurement{T})::$PT{T,N} where T
            $PT(N, Normal(Measurements.value(m),Measurements.uncertainty(m)))
        end
    end
end

"""
Convert an uncertain number from Measurements.jl to the equivalent particle representation with the default number of particles.
"""
function Particles(m::Measurements.Measurement{T}) where T
    Particles(DEFAULT_NUM_PARTICLES, m)
end

"""
Convert an uncertain number from Measurements.jl to the equivalent particle representation with the default number of particles.
"""
function StaticParticles(m::Measurements.Measurement{T}) where T
    StaticParticles(DEFAULT_STATIC_NUM_PARTICLES, m)
end

"""
    Measurements.value(p::AbstractParticles) = mean(p)
"""
Measurements.value(p::AbstractParticles) = mean(p)

"""
    Measurements.uncertainty(p::AbstractParticles) = std(p)
"""
Measurements.uncertainty(p::AbstractParticles) = std(p)


using Surrogates

#1D
obj = x -> sin(x) + sin(x)^2 + sin(x)^3
lb = 0.0
ub = 10.0
x = sample(5,lb,ub,LowDiscrepancySample(2))
y = obj.(x)
p = 3.5
InverseDistance = InverseDistanceSurrogate(x,y,p,lb,ub)
prediction = InverseDistance(5.0)
add_point!(InverseDistance,5.0,-0.91)
add_point!(InverseDistance,[5.1,5.2],[1.0,2.0])

#ND
lb = [0.0,0.0]
ub = [10.0,10.0]
n = 100
x = sample(n,lb,ub,SobolSample())
f = x -> x[1]*x[2]^2
y = f.(x)
p = 3.0
InverseDistance = InverseDistanceSurrogate(x,y,p,lb,ub)
prediction = InverseDistance((1.0,2.0))
add_point!(InverseDistance,(5.0,3.4),-0.91)
add_point!(InverseDistance,[(5.1,5.2),(5.3,6.7)],[1.0,2.0])


using Test
using GeoParams
@testset "EnergyParameters.jl" begin

# This tests the MaterialParameters structure
CharUnits_GEO   =   GEO_units(viscosity=1e19, length=10km);

# Heat capacity ---------

# Constant heat capacity
cp1      =  ConstantHeatCapacity()
info     =  param_info(cp1)
@test isbits(cp1)
@test cp1.cp.val == 1050.0

cp1     = nondimensionalize(cp1,CharUnits_GEO)
@test cp1.cp.val ≈ 1.3368075000000002e22
@test compute_heatcapacity(cp1, 10.0, 100.0) ≈ 1.3368075000000002e22  # compute

# Temperature-dependent heat capacity
# dimensional
T        =   250.0:100:1250;
cp2      =   T_HeatCapacity_Whittacker()
Cp       =   similar(T)
@test isbits(cp2)
compute_heatcapacity!(Cp, cp2, T)
@test sum(Cp) ≈ 11667.035717418683

# nondimensional
cp2_nd   =   T_HeatCapacity_Whittacker()
cp2_nd   =   nondimensionalize(cp2_nd,CharUnits_GEO)
T_nd     =   Float64.(T*K/CharUnits_GEO.Temperature)
Cp_nd    =   similar(T)
compute_heatcapacity!(Cp_nd, cp2_nd, T_nd)
@test sum(Cp_nd) ≈ 1.4853886523631602e23

# Dimensionalize again and double-check the results
@test sum(abs.(ustrip.(Cp_nd*CharUnits_GEO.heatcapacity) - Cp)) < 1e-11

# Test with arrays
T_array     =  T*ones(10)'
Cp_array    =  similar(T_array)
P_array     =  similar(T_array)
compute_heatcapacity!(Cp_array, cp1, P_array, T_array)
@test Cp_array[1] ≈ 1.3368075000000002e22

Cp_array    =  similar(T_array)
P_array     =  similar(T_array)
compute_heatcapacity!(Cp_array, cp2, P_array, T_array)
@test sum(Cp_array[:,1]) ≈ 11667.035717418683

T_array     =  T*ones(10)'
Cp_array    =  zeros(size(T_array))
P_array     =  zeros(size(T_array))
compute_heatcapacity!(Cp_array, cp2, T_array)
@test sum(Cp_array[:,1]) ≈ 11667.035717418683

# Check that it works if we give a phase array
MatParam    =   Array{MaterialParams, 1}(undef, 2);
MatParam[1] =   SetMaterialParams(Name="Mantle", Phase=1,
                    HeatCapacity  = ConstantHeatCapacity());

MatParam[2] =   SetMaterialParams(Name="Crust", Phase=2,
                    HeatCapacity  = T_HeatCapacity_Whittacker());

Mat_tup = Tuple(MatParam)

# test computing material properties
n = 100;
Phases              = ones(Int64,n,n,n);
Phases[:,:,20:end] .= 2

Cp = zeros(size(Phases))
T  =  ones(size(Phases))*1500
P  =  zeros(size(Phases))

compute_heatcapacity!(Cp, Mat_tup, Phases, P, T)    # computation routine w/out P (not used in most heat capacity formulations)
num_alloc = @allocated compute_heatcapacity!(Cp, Mat_tup, Phases, P, T)
@test sum(Cp[1,1,:]) ≈ 121399.0486067196
@test num_alloc == 0

compute_heatcapacity!(Cp, Mat_tup, Phases, T)       # computation routine w/out P (not used in most heat capacity formulations)
@test sum(Cp[1,1,:]) ≈ 121399.0486067196

# test if we provide phase ratios
PhaseRatio  = zeros(n,n,n,3);
for i in CartesianIndices(Phases)
    iz = Phases[i]
    I = CartesianIndex(i,iz)
    PhaseRatio[I] = 1.0
end
compute_heatcapacity!(Cp, Mat_tup, PhaseRatio, P, T)
num_alloc = @allocated compute_heatcapacity!(Cp, Mat_tup, PhaseRatio, P, T)
@test sum(Cp[1,1,:]) ≈ 121399.0486067196
@test num_alloc == 0

# -----------------------

# Conductivity ----------

# Constant

# Constant conductivity
cond      =   ConstantConductivity()
@test isbits(cond)
@test NumValue(cond.k) == 3.0
@test cond.k.unit==u"W"/K/m

cond = nondimensionalize(cond,CharUnits_GEO)
@test  NumValue(cond.k) ≈ 3.8194500000000007

@test compute_conductivity(cond, 100.0) ≈ 3.8194500000000007 # compute

# Temperature-dependent conductivity
# dimensional
T        =   Vector{Float64}(250:100:1250);
cond2    =   T_Conductivity_Whittacker()
k        =   compute_conductivity(cond2, T)
@test isbits(cond2)
@test sum(k) ≈ 27.503366436682285

# nondimensional
cond2_nd =   T_Conductivity_Whittacker()
cond2_nd =   nondimensionalize(cond2_nd,CharUnits_GEO)
T_nd     =   Float64.(ustrip.(T/CharUnits_GEO.Temperature))
k_nd     =   compute_conductivity(cond2_nd, T_nd)
@test sum(k_nd) ≈ 35.01591097886205

# Dimensionalize again and double-check the results
@test sum(abs.(ustrip.(k_nd*CharUnits_GEO.conductivity) - k)) < 1e-11

# Check if we use arrays
T_array     =  ustrip.(T)*ones(100)'
k_array     =  copy(T_array)
P_array     =  copy(T_array)

compute_conductivity!(k_array,cond,P_array,T_array)
@test k_array[1] ≈ 3.8194500000000007

compute_conductivity!(k_array,cond2,P_array,T_array)
@test sum(k_array) ≈ 2750.3366436682285

k_TP    =   Set_TP_Conductivity("LowerCrust")
compute_conductivity!(k_array, k_TP, P_array, T_array)
@test sum(k_array) ≈ 2055.7129327367625

# Check that it works if we give a phase array
MatParam    =   Array{MaterialParams, 1}(undef, 3);
MatParam[1] =   SetMaterialParams(Name="Mantle", Phase=1,
                    Conductivity  = ConstantConductivity());

MatParam[2] =   SetMaterialParams(Name="Crust", Phase=2,
                    Conductivity  = T_Conductivity_Whittacker());

MatParam[3] =   SetMaterialParams(Name="MantleLithosphere", Phase=3,
                    Conductivity = Set_TP_Conductivity("Mantle"));

Mat_tup = Tuple(MatParam)
# test computing material properties
n = 100;
Phases              = ones(Int64,n,n,n);
Phases[:,:,20:end] .= 2
Phases[:,:,60:end] .= 3

PhaseRatio  = zeros(n,n,n,3);
for i in CartesianIndices(Phases)
    iz = Phases[i]
    I = CartesianIndex(i,iz)
    PhaseRatio[I] = 1.0
end

k   =  zeros(size(Phases))
T   =  ones(size(Phases))*1500
P   =  zeros(size(Phases))

compute_conductivity!(k, Mat_tup, Phases, P, T)
num_alloc = @allocated compute_conductivity!(k, Mat_tup, Phases, P, T)
@test sum(k) ≈ 1.9216938849389635e6
@test num_alloc == 0

compute_conductivity!(k, Mat_tup, PhaseRatio, P, T)
num_alloc = @allocated compute_conductivity!(k, Mat_tup, PhaseRatio, P, T)
@test num_alloc == 0
@test sum(k) ≈ 1.9216938849389635e6

#

######

# TP-dependent conductivity for different predefines cases
T       =   Vector{Float64}(250:100:1250);
P       = 1e6*ones(size(T))/ustrip(uconvert(Pa,1MPa))  # must be in MPa!
List    = ["LowerCrust"   "Mantle"        "OceanicCrust"  "UpperCrust"]
Sol_kT  = [20.55712932736763 28.700405819019323 20.55712932736763 19.940302462417037]
for i=1:length(List)
    k_TP    =   Set_TP_Conductivity(List[i])
    k       =   compute_conductivity(k_TP,P,T)           # note that P must be in MPa
    @test sum(k) ≈ Sol_kT[i]

    k_TP_nd  =   deepcopy(k_TP)
    k_TP_nd  =   nondimensionalize(k_TP_nd,CharUnits_GEO)
    T_nd     =   Float64.(ustrip.(T/CharUnits_GEO.Temperature))
    P_nd     =   Float64.(ustrip(P/CharUnits_GEO.stress))
    k_nd     =   compute_conductivity(k_TP_nd,P_nd,T_nd)

    @test ustrip(sum(abs.(ustrip.(k_nd*CharUnits_GEO.conductivity) - k))) < 1e-11

end

T = [200. 300.; 400. 500.]
k1        =   compute_conductivity(cond2,T)

# -----------------------

# Latent heat -----------
a = ConstantLatentHeat()
Q_L = compute_latent_heat(a)
@test isbits(a)
@test Q_L == 400

a   = nondimensionalize(a,CharUnits_GEO)
Q_L = compute_latent_heat(a)
@test Q_L ≈ 4e21
# -----------------------

# Radioactive heat ------
a = ConstantRadioactiveHeat()
H_r = compute_radioactive_heat(a)
@test isbits(a)
@test H_r ≈ 1.0e-6

a = nondimensionalize(a,CharUnits_GEO)
H_r = compute_radioactive_heat(a)
@test H_r == 0.1
# -----------------------

# Shear heating -------
Χ       = ConstantShearheating(1.0)
@test isbits(Χ)

# Define parameters as vectors
τ       = [1 2 3 4]*1e6
ε       = [1 0.1 0.1 1]
ε_el    = [0.01 0.01 0.01 0.01]

τ_2D       = [1 2; 3 4]*1e6
ε_2D       = [1 0.1; 0.1 1]
ε_el_2D    = [0.01 0.01; 0.01 0.01]

# With elasticity
H_s1 = compute_shearheating(Χ, τ,   ε,    ε_el   )
H_s2 = compute_shearheating(Χ, τ_2D,ε_2D, ε_el_2D)
@test H_s1 ≈ 5.4e6
@test H_s2 ≈ 5.4e6

# No elasticity
H_s3 = compute_shearheating(Χ, τ,   ε   )
H_s4 = compute_shearheating(Χ, τ_2D,ε_2D)
@test H_s3 ≈ 5.5e6
@test H_s4 ≈ 5.5e6

# Now in non-dimensional units
τ       = [1 2 3 4]
ε       = [1 0.1 0.1 1]
ε_el    = [0.01 0.01 0.01 0.01]

τ_2D       = [1 2; 3 4]
ε_2D       = [1 0.1; 0.1 1]
ε_el_2D    = [0.01 0.01; 0.01 0.01]
Χ       = nondimensionalize(Χ,CharUnits_GEO)
info    = param_info(Χ)

H_s1 = compute_shearheating(Χ, τ,   ε,    ε_el,  )
H_s2 = compute_shearheating(Χ, τ_2D,ε_2D, ε_el_2D)
H_s3 = compute_shearheating(Χ, τ,   ε)
H_s4 = compute_shearheating(Χ, τ_2D,ε_2D)
@test H_s1 ≈ 5.4
@test H_s2 ≈ 5.4
@test H_s3 ≈ 5.5
@test H_s4 ≈ 5.5
# -----------------------

end


"""
Fill volume with spheres of certain radius.

In brief, the model generates a box with spheres in hexagonal packaging, removes the ones outside the volume.
The positions are perturbed by the noise term and finally the system is left to relax by a simple particle model.

# Parameters
 - **f** (Function) Function that returns true if center of sphere is inside the volume
 - **box** (Array{Float,2}) Maximum box where to fill the volumes.
 - **r** (Number) Radius of the spheres

# Optional keyword parameters

 - **N** (Int) Maximum number of particles inside the volume. If NaN (default), there is not upper bound.
 - **noise** (Number) Noise ratio used to perturb the particles from the hexagonal lattice.
 - **platform** (String) Platform in which perform the relaxation step after the noise perturbation.

# Example
```Julia
julia> using AgentBasedModels;
julia> f(x,y,z) = sqrt(x^2+y^2+z^2) < 10;
julia> pos = fillVolumeSpheres(f,[[-10,-10,-10],[10,10,10]],1,noise=0.25);
```
![Figure](assets/FillVolumeSpheres.png)
Figure rendered with [Makie.jl](https://github.com/JuliaPlots/Makie.jl) using meshscatter function.
"""
function fillVolumeSpheres(f,box,rr;N=NaN,noise=0.1,platform="cpu")

    r = rr*0.9
    volumeX, volumeY, volumeZ = latticeCompactHexagonal(box,r,noiseRatio=noise)

    #Extrude
    ext = zeros(Bool,length(volumeX))
    for i in 1:length(volumeX)
        ext[i] = f(volumeX[i],volumeY[i],volumeZ[i])
    end
    volumeX = volumeX[ext]; volumeY = volumeY[ext]; volumeZ = volumeZ[ext];

    #Remove exceding cells
    if N != NaN
        l = length(volumeX)
        while length(volumeX) > N
            rem = rand(1:length(volumeX),l-N)
            nrem = [i for i in 1:l if !(i in rem)]
            volumeX = volumeX[nrem]; volumeY = volumeY[nrem]; volumeZ = volumeZ[nrem];
            l = length(volumeX)
        end
    end

    #Model relax
    relax!(volumeX, volumeY, volumeZ, rr, noise, platform)

    #Remove exceding cells after relaxation
    if N != NaN
        l = length(volumeX)
        while length(volumeX) > N
            rem = rand(1:length(volumeX),l-N)
            nrem = [i for i in 1:l if !(i in rem)]
            volumeX = volumeX[nrem]; volumeY = volumeY[nrem]; volumeZ = volumeZ[nrem];
            l = length(volumeX)
        end
    end

    return volumeX, volumeY, volumeZ
end

function relax!(volumeX, volumeY, volumeZ, r, n, platform="cpu")

    #Community
    if platform == "cpu"
        model = MODEL_BASIC
    else
        model = MODEL_BASIC_GPU
    end

    com = Community(model,N=length(volumeX))
    com[:x] = volumeX
    com[:y] = volumeY
    com[:z] = volumeZ
    com[:D] = 5.
    com[:r] = r
    com[:n] = n
    com[:nn] = 1

    counter = 10

    c = model.evolve(com,dt=0.1,tMax=20.,tSaveStep=19.9)
    com = c[end]
    f = maximum(sqrt.(com[:fx].^2+com[:fy].^2+com[:fz].^2))
    while f > 0.001 && counter > 0
        c = model.evolve(com,dt=0.1,tMax=20.,tSaveStep=19.9)
        com = c[end]
        f = maximum(sqrt.(com[:fx].^2+com[:fy].^2+com[:fz].^2))

        counter -= 1
    end

    volumeX .= c[end][:x]
    volumeY .= c[end][:y]
    volumeZ .= c[end][:z]

    return nothing

end


using Gnuplot
Θ = LinRange(0, 2π, 100) # 50
Φ = LinRange(0, π, 20)
r = 0.8
x = [r * cos(θ) * sin(ϕ)      for θ in Θ, ϕ in Φ]
y = [r * sin(θ) * sin(ϕ)      for θ in Θ, ϕ in Φ]
z = [r * cos(ϕ) for θ in Θ, ϕ in Φ]
@gsp "set pm3d depthorder" :-
@gsp :- "set style fill transparent solid 0.5" "set view equal xyz"
@gsp :-  x y z "w pm3d notit" "set xyplane 0" palette(:viridis)
save(term="pngcairo size 600,600", output="sphere2.png") # hide


include("learn_perceptron.jl")
using DataArrays

# create initial weight vector
w_init = randn(3,1);

# Create initial train data
train_data = @data([0.80857 0.83721;
                    0.35714 0.8505;
                    -0.75143 -0.7309;
                    -0.3 0.12625;
                    0.64286 -0.54485]);

target = @data([-1.0; -1.0; -1.0; -1.0;-1.0]);

train_data_biased = add_bias(train_data);

number_of_errors = eval_perceptron(train_data_biased, w_init, target)
println("The errors before starting training: ", number_of_errors)
while (number_of_errors > 0)
  w_init = learn_perceptron(train_data_biased, target, w_init)
  number_of_errors = eval_perceptron(train_data_biased, w_init, target)
end
println("Desired weight vector for a given train set: ")
println(w_init)


using Distributions
using Turing

x = Float64[1 2]
priors = 0

@model gauss(x) = begin
  priors = TArray{Float64}(2)
  priors[1] ~ InverseGamma(2,3)         # s
  priors[2] ~ Normal(0, sqrt(priors[1])) # m
  for i in 1:length(x)
    x[i] ~ Normal(priors[2], sqrt(priors[1]))
  end
  priors
end

chain = sample(gauss(x), PG(10, 10))
chain = sample(gauss(x), SMC(10))


n = rand(1:10)

r = matrixdepot("deriv2", n, false)
rf32 = matrixdepot("deriv2", Float32, n, false)

r2 = matrixdepot("deriv2", Float32, n, 2, false)
if mod(n, 2) == 0
    r3 = matrixdepot("deriv2", Float32, n, 3, false)
end
try
    matrixdepot("deriv2", Float64, n, 4, false)
catch ArgumentError
    println("deriv2: illegal value of example.")
end

A = matrixdepot("deriv2", n)

@test_approx_eq r.A*r.x  r.b
@test issym(r.A)

r = matrixdepot("shaw", 2*n, false)
A = matrixdepot("shaw", 2*n)

print(r)

@test issym(r.A)

rf32 = matrixdepot("shaw", Float32, 2*n, false)

r = matrixdepot("wing", n, false)
A = matrixdepot("wing", n)

@test r.A == matrixdepot("wing", n, 1/3, 2/3, false).A

r = matrixdepot("foxgood", n, false)
rf32 = matrixdepot("foxgood", Float32, n, false)
A = matrixdepot("foxgood", n)

r = matrixdepot("heat", 2*n, false)
rf32 = matrixdepot("heat", Float32, 2*n)

r = matrixdepot("baart", 2*n, false)
rf32 = matrixdepot("baart", Float32, 2*n, false)
A = matrixdepot("baart", 2*n)

n = rand(3:10)
r = matrixdepot("phillips", 4*n, false)
rf32 = matrixdepot("phillips", Float32, 4*n)

A = matrixdepot("gravity", n)

@test matrixdepot("gravity", n, 1, 0, 1, 0.25) == A

r1 = matrixdepot("gravity", Float32, n, 1, false)
r2 = matrixdepot("gravity", Float32, n, 2, false)
r3 = matrixdepot("gravity", Float32, n, 3, false)

A = matrixdepot("blur", n)

@test matrixdepot("blur", n, 3, 0.7) == A

r1 = matrixdepot("blur", Float32, n, false)

A = matrixdepot("spikes", n)

@test matrixdepot("spikes", n, 5) == A

n = rand(5:10)
r1 = matrixdepot("spikes", Float32, n, false)

A = matrixdepot("ursell", n)
@test matrixdepot("ursell", Float64, n) == A
r1 = matrixdepot("ursell", Float32, n, false)
print(r1)

A = matrixdepot("parallax", n)

m, n = size(A)
@test m == 26
@test matrixdepot("parallax", Float64, n) == A
r1 = matrixdepot("parallax", Float32, n, false)