using LinearAlgebra
using LightGraphs
using NPZ
using SpinGlassPEPS
using Logging
using ArgParse
using CSV
using Test

function M2graph(M::Matrix{Float64}, sgn::Int = 1)
    size(M,1) == size(M,2) || error("matrix not squared")
    L = size(M,1)

    D = Dict{Tuple{Int64,Int64},Float64}()
    for j ∈ 1:size(M, 1)
        for i ∈ 1:j
            if (i == j)
                push!(D, (i,j) => M[j,i])
            elseif M[j,i] != 0.
                push!(D, (i,j) => M[i,j]+M[j,i])
            end
        end
    end
    ising_graph(D, sgn)
end

disable_logging(LogLevel(0))

s = ArgParseSettings("description")
  @add_arg_table! s begin
    "file"
    help = "the file name"
    arg_type = String
  end
file = parse_args(s)["file"]
println(file)

data = npzread(file)
number_of_states = minimum([length(data["energies"][1,:]), 150])
examples = length(data["energies"][:,1])
println("examples = ", examples)

β = 3.

for k in 1:examples

    println(" ..................... SAMPLE =  ", k, "  ...................")
    Mat_of_interactions = data["Js"][k,:,:]
    degeneracy = (0. in diag(Mat_of_interactions))

    states_given = 0
    try
        if !degeneracy
            states_given = data["states"][k,:,:]
        end
    catch
        0
    end

    energies_given = data["energies"][k,:,:]

    g = M2graph(Mat_of_interactions, -1)
    grid_size = isqrt(size(Mat_of_interactions, 1))

    ################ exact method ###################

    fg = factor_graph(
        g,
        spectrum=brute_force,
        cluster_assignment_rule=super_square_lattice((grid_size, grid_size, 1))
    )

    control_params = Dict(
       "bond_dim" => typemax(Int),
       "var_tol" => 1E-12,
       "sweeps" => 4.
   )

    peps = PEPSNetwork(grid_size, grid_size, fg, β, :NW, control_params)
    println("size of peps = ", peps.size)
    print("peps time  = ")

    @time sols = low_energy_spectrum(peps, number_of_states + 2)

    for i in 1:number_of_states

        @test sols.energies[i] ≈ energies_given[i]
        if states_given != 0
            f(i) = (i == 2) ? -1 : 1
            @test map(f, sols.states[i]) == states_given[i,:]
        end
    end

    ############### approximated methods  ################
    χ = 2
    χ = typemax(Int)

    control_params = Dict(
       "bond_dim" => χ,
       "var_tol" => 1E-7,
       "sweeps" => 4.
   )

    print("approx peps  ")

    peps = PEPSNetwork(grid_size, grid_size, fg, β, :NW, control_params)

    @time sols_approx = low_energy_spectrum(peps, number_of_states + 2)

    for i in 1:number_of_states

        @test sols_approx.energies[i] ≈ energies_given[i]
        if states_given != 0
            f(i) = (i == 2) ? -1 : 1
            @test map(f, sols_approx.states[i]) == states_given[i,:]
        end
    end

    print("peps larger T")
    s1 = ceil(Int, grid_size/2)

    if grid_size % 2 == 0
        rule = super_square_lattice((s1, 2, s1, 2, 1))
    else
        rule = Dict{Int, Int}(1 => 1, 2 => 1, 6 => 1, 7 => 1)
        push!(rule, (3 => 2), (4 => 2), (8 => 2), (9 => 2), (5 => 3), (10 => 3))
        push!(rule, (11 => 4), (12 => 4), (16 => 4), (17 => 4), (13 => 5), (14 => 5))
        push!(rule, (18 => 5), (19 => 5), (15 => 6), (20 => 6), (21 => 7), (22 => 7))
        push!(rule, (23 => 8), (24 => 8), (25 => 9))
    end

    fg = factor_graph(
        g,
        cluster_assignment_rule=rule,
        spectrum=brute_force,
    )
    peps = PEPSNetwork(s1, s1, fg, β, :NW, control_params)

    println("size of peps = ", peps.size)

    @time sols_large = low_energy_spectrum(peps, number_of_states + 2)

    for i in 1:number_of_states

        @test sols_large.energies[i] ≈ energies_given[i]
        #TODO we need to know how to read states from clusters
        #if states_given != 0
        #    @test sols_large.states[i] == states_given[i,:]
        #end
    end

    print("peps larger T, limited spectrum")
    D = Dict{Int, Int}()
    for v in vertices(g)
        push!(D, (v => 15))
    end
    fg = factor_graph(
        g,
        D,
        cluster_assignment_rule=rule,
        spectrum=brute_force,
    )

    peps = PEPSNetwork(s1, s1, fg, β, :NW, control_params)

    @time sols_spec = low_energy_spectrum(peps, number_of_states+2)

    for i in 1:minimum([number_of_states, 65])
        @test sols_spec.energies[i] ≈ energies_given[i]
        #TODO we need to know how to read states from clusters
        #if states_given != 0
        #    @test sols_spec.states[i] == states_given[i,:]
        #end
    end
end


"""
"""
struct AffineMap{D,T,L} <:Field
  jacobian::TensorValue{D,D,T,L}
  origin::Point{D,T}
  function AffineMap(jacobian::TensorValue{D,D,T,L}, origin::Point{D,T}) where {D,T,L}
    new{D,T,L}(jacobian,origin)
  end
end

function field_cache(f::AffineMap,x)
  y = copy(x)
  CachedArray(y)
end

@inline function evaluate_field!(cache,f::AffineMap,x)
  setsize!(cache,size(x))
  y = cache.array
  @inbounds for i in eachindex(x)
    xi = x[i]
    yi = _apply_affine_map(f,xi)
    y[i] = yi
  end
  y
end

function _apply_affine_map(h,x)
  t = h.origin
  s = h.jacobian
  (s⋅x)+t
end

struct AffineMapGrad{D,T,L} <: Field
  jacobian::TensorValue{D,D,T,L}
end

function field_gradient(h::AffineMap)
  AffineMapGrad(h.jacobian)
end

function field_cache(f::AffineMapGrad,x)
  xi = testitem(x)
  T = typeof(xi)
  G = gradient_type(T,xi)
  j = similar(x,G)
  CachedArray(j)
end

function evaluate_field!(cache,f::AffineMapGrad,x)
  setsize!(cache,size(x))
  y = cache.array
  G = eltype(y)
  yi = f.jacobian
  @inbounds for i in eachindex(y)
    y[i] = yi
  end
  y
end


"""
    PositionEmbedding(size::Int, max_len::Int = 1024; trainable::Bool = false)

The position embedding layer. `size` is the number of neuron. `max_len` is the maximum acceptable length of input.
If is not `trainable`, `max_len` will dynamically adjust to the longest input length. If `trainable`, use a random init
embedding value, otherwise use a sin/cos position encoding.
"""
mutable struct PositionEmbedding{F, W <: AbstractArray{F}} <: AbstractBroadcastEmbed{F}
    trainable::Bool
    embedding::W
end

Flux.functor(pe::PositionEmbedding) = (pe.embedding,), m -> PositionEmbedding(pe.trainable, m...)

get_value(e::PositionEmbedding, name::Symbol, xs::NamedTuple) = e(first(xs))

function PE(size, pos, i::Int)
    if rem(i, 2) == 0
        sin(pos/1e4^(i/size))
    else
        cos(pos/1e4^((i-1)/size))
    end
end

function PositionEmbedding(size::Int, max_len::Int = 1024; trainable::Bool = false)
    if trainable
        embedding = randn(Float32, size, max_len)
    else
        embedding = Matrix{Float32}(undef, size, max_len)
        for l = 1:max_len
            map!(i->PE(size, l, i), selectdim(embedding, 2, l), 1:size)
        end
    end
    PositionEmbedding(trainable, embedding)
end

using ZygoteRules: @adjoint, AContext, pullback
import ZygoteRules: _pullback

function resize_pe!(pe::PositionEmbedding, len::Int)
    max_len = size(pe.embedding, 2)

    if len > max_len
        if pe.trainable
            error("position embedding length exceeded")
        else
            over = similar(pe.embedding, size(pe.embedding, 1), len)
            copyto!(over, 1, pe.embedding, 1, length(pe.embedding))

            for l = size(pe.embedding, 2)+1:len
                map!(i->PE(size(pe.embedding, 1), l, i), selectdim(over, 2, l), 1:size(pe.embedding, 1))
            end

            pe.embedding = over
        end
    end
    return nothing
end

(pe::PositionEmbedding)(x::AbstractArray{Int}) = pe(size(x, 1))
(pe::PositionEmbedding{F})(x::AbstractArray{F}) where F = pe(size(x, 2))
function (pe::PositionEmbedding)(len::Int)
    resize_pe!(pe, len)
    idx = cumsum(fill!(similar(pe.embedding, Int, len), one(Int)))
    gather(pe.embedding, idx)
end

function _pullback(cx::AContext, pe::PositionEmbedding, len::Int)
    resize_pe!(pe, len)
    idx = cumsum(fill!(similar(pe.embedding, Int, len), one(Int)))
    y, back = _pullback(cx, gather, pe.embedding, idx)
    return y, Δ -> ((trainable=nothing, embedding=back(Δ)[2]), nothing)
end

function Base.show(io::IO, pe::PositionEmbedding)
    s, max_len = size(pe.embedding)
    if pe.trainable
        print(io, "PositionEmbedding($(s), max_len=$(max_len))")
    else
        print(io, "PositionEmbedding($(s))")
    end
end


using DelimitedFiles
ventdata = readdlm("ventdata.txt",'\n')
#@show ventdata

function parse_vents(ventdata)
    ventvector = []
    max_x = max_y = 0
    xyre = r"(\d+),(\d+) -> (\d+),(\d+)"
    for vec in ventdata
        sx1,sy1,sx2,sy2 = match(xyre,vec)
        @show sx1,sy1,sx2,sy2
        x1,y1,x2,y2 = parse(Int,sx1),parse(Int,sy1),parse(Int,sx2),parse(Int,sy2)
        push!(ventvector,[x1,y1,x2,y2])
        if x1 > max_x
            max_x = x1
        end
        if x2 > max_x
            max_x = x2
        end
        if y1 > max_y
            max_y = y1
        end
        if y2 > max_y
            max_y = y2
        end

    end
    ventvector,max_x, max_y
end

function populatemap(ventvect,vmap)
    maxcross = 0
    for vv in ventvect
        x1,y1,x2,y2 = vv

        dx = abs(x1-x2)
        dy = abs(y1-y2)
        #@show dx,dy

        if dx == 0
            if y1 > y2
                # Swap them
                y1,y2 = y2,y1
            end

            # mark the map
            for ypos in y1:y2
                val = vmap[ypos+1,x1+1] += 1
            end
        elseif dy == 0
            if x1 > x2
                # Swap them
                x1,x2 = x2,x1
            end

            # mark the map
            for xpos in x1:x2
                val = vmap[y1+1,xpos+1] += 1
            end
        end
    end

    vmap
end

ventvect, mx, my = parse_vents(ventdata)
println("Size of map: $(my+1) x $(mx+1)")
vmap = zeros(Int,my+1,mx+1)
vmap = populatemap(ventvect,vmap)

#@show vmap
@show count(>=(2),vmap)


# ------------------------------------------------------------------------------------------
# # Functions
#
# Topics:
# 1. How to declare a function
# 2. Duck-typing in Julia
# 3. Mutating vs. non-mutating functions
# 4. Some higher order functions
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## How to declare a function
# Julia gives us a few different ways to write a function. The first requires the `function`
# and `end` keywords
# ------------------------------------------------------------------------------------------

function sayhi(name)
    println("Hi $name, it's great to see you!")
end

function f(x)
    x^2
end

# ------------------------------------------------------------------------------------------
# We can call either of these functions like this:
# ------------------------------------------------------------------------------------------

sayhi("C-3PO")

f(42)

# ------------------------------------------------------------------------------------------
# Alternatively, we could have declared either of these functions in a single line
# ------------------------------------------------------------------------------------------

sayhi2(name) = println("Hi $name, it's great to see you!")

f2(x) = x^2

sayhi2("R2D2")

f2(42)

# ------------------------------------------------------------------------------------------
# Finally, we could have declared these as "anonymous" functions
# ------------------------------------------------------------------------------------------

sayhi3 = name -> println("Hi $name, it's great to see you!")

f3 = x -> x^2

sayhi3("Chewbacca")

f3(42)

# ------------------------------------------------------------------------------------------
# ## Duck-typing in Julia
# *"If it quacks like a duck, it's a duck."*

# Julia functions will just work on whatever inputs make sense.

# For example, `sayhi` works on the name of this minor tv character, written as an
# integer...
# ------------------------------------------------------------------------------------------

sayhi(55595472)

# ------------------------------------------------------------------------------------------
# And `f` will work on a matrix.
# ------------------------------------------------------------------------------------------

A = rand(3, 3)
A

f(A)

# ------------------------------------------------------------------------------------------
# `f` will also work on a string like "hi" because `*` is defined for string inputs as
# string concatenation.
# ------------------------------------------------------------------------------------------

f("hi")

# ------------------------------------------------------------------------------------------
# On the other hand, `f` will not work on a vector. Unlike `A^2`, which is well-defined, the
# meaning of `v^2` for a vector, `v`, is not a well-defined algebraic operation.
# ------------------------------------------------------------------------------------------

v = rand(3)

f(v)

# ------------------------------------------------------------------------------------------
# ## Mutating vs. non-mutating functions
#
# As we mentioned before, the convention is that functions followed by `!` alter their
# contents and functions lacking `!` do not.
#
# For example, let's look at the difference between `sort` and `sort!`.
#
# ------------------------------------------------------------------------------------------

v = [3, 5, 2]

sort(v)

v

# ------------------------------------------------------------------------------------------
# `sort(v)` returns a sorted array that contains the same elements as `v`, but `v` is left
# unchanged.

#
# On the other hand, when we run `sort!(v)`, the contents of v are sorted within the array
# `v`.
# ------------------------------------------------------------------------------------------

sort!(v)

v

# ------------------------------------------------------------------------------------------
# ## Changing names vs. mutation
#
# Recall that variable assignments is just a naming game. Now consider this attempt at a
# mutating function:
# ------------------------------------------------------------------------------------------

function setzero!(x)
    x = 0
    return x
end
y = 1

# ------------------------------------------------------------------------------------------
# Do you think this will be able to set `y` to zero? Try it:
# ------------------------------------------------------------------------------------------

setzero!(y)

y

# ------------------------------------------------------------------------------------------
# `y` is just a name for the number `1`, and so was `x` inside the `setzero!` function until
# we decided it would be better served as a name for `0`! So this function doesn't do any
# mutation at all — it just returns a `0`. If we want to actually mutate the argument, we
# need to use indexed assignment (or use a real `!` function that actually does what it
# claims to do). We also need to use a _mutable_ value as the argument — like an array.
# ------------------------------------------------------------------------------------------

function setzero!(x)
    x[1] = 0
    return x
end
y = [1]

setzero!(y)
y

# ------------------------------------------------------------------------------------------
# ## Some higher order functions
#
# ### map
#
# `map` is a "higher-order" function in Julia that *takes a function* as one of its input
# arguments.
# `map` then applies that function to every element of the data structure you pass it. For
# example, executing
#
# ```julia
# map(f, [1, 2, 3])
# ```
# will give you an output array where the function `f` has been applied to all elements of
# `[1, 2, 3]`
# ```julia
# [f(1), f(2), f(3)]
# ```
# ------------------------------------------------------------------------------------------

map(f, [1, 2, 3])

# ------------------------------------------------------------------------------------------
# Here we've squared all the elements of the vector `[1, 2, 3]`, rather than squaring the
# vector `[1, 2, 3]`.
#
# To do this, we could have passed to `map` an anonymous function rather than a named
# function, such as
# ------------------------------------------------------------------------------------------

x -> x^3

# ------------------------------------------------------------------------------------------
# via
# ------------------------------------------------------------------------------------------

map(x -> x^3, [1, 2, 3])

# ------------------------------------------------------------------------------------------
# and now we've cubed all the elements of `[1, 2, 3]`!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ### broadcast
#
# `broadcast` is another higher-order function like `map`. `broadcast` is a generalization
# of `map`, so it can do every thing `map` can do and more. The syntax for calling
# `broadcast` is the same as for calling `map`
# ------------------------------------------------------------------------------------------

broadcast(f, [1, 2, 3])

# ------------------------------------------------------------------------------------------
# and again, we've applied `f` (squared) to all the elements of `[1, 2, 3]` - this time by
# "broadcasting" `f`!
#
# Some syntactic sugar for calling `broadcast` is to place a `.` between the name of the
# function you want to `broadcast` and its input arguments. For example,
#
# ```julia
# broadcast(f, [1, 2, 3])
# ```
# is the same as
# ```julia
# f.([1, 2, 3])
# ```
# ------------------------------------------------------------------------------------------

f.([1, 2, 3])

# ------------------------------------------------------------------------------------------
# Notice again how different this is from calling
# ```julia
# f([1, 2, 3])
# ```
# We can square every element of a vector, but we can't square a vector!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# To drive home the point, let's look at the difference between
#
# ```julia
# f(A)
# ```
# and
# ```julia
# f.(A)
# ```
# for a matrix `A`:
# ------------------------------------------------------------------------------------------

A = [i + 3*j for j in 0:2, i in 1:3]

f(A)

# ------------------------------------------------------------------------------------------
# As before we see that for a matrix, `A`,
# ```
# f(A) = A^2 = A * A
# ```
#
# On the other hand,
# ------------------------------------------------------------------------------------------

B = f.(A)

# ------------------------------------------------------------------------------------------
# contains the squares of all the entries of `A`.
#
# This dot syntax for broadcasting allows us to write relatively complex compound
# elementwise expressions in a way that looks natural/closer to mathematical notation. For
# example, we can write
# ------------------------------------------------------------------------------------------

A .+ 2 .* f.(A) ./ A

# ------------------------------------------------------------------------------------------
# instead of
# ------------------------------------------------------------------------------------------

broadcast(x -> x + 2 * f(x) / x, A)

# ------------------------------------------------------------------------------------------
# and the two will perform exactly the same.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ### Exercises
#
# #### 6.1
# Write a function `add_one` that adds 1 to its input.
# ------------------------------------------------------------------------------------------

@assert add_one(1) == 2

@assert add_one(11) == 12

# ------------------------------------------------------------------------------------------
# #### 6.2
# Use `map` or `broadcast` to increment every element of matrix `A` by `1` and assign it to
# a variable `A1`.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### 6.3
# Use the broadcast dot syntax to increment every element of matrix `A1` by `1` and store it
# in variable `A2`
# ------------------------------------------------------------------------------------------

@assert A2 == [3 4 5; 6 7 8;9 10 11]

# ------------------------------------------------------------------------------------------
# Please click on `Validate` on the top, once you are done with the exercises.
# ------------------------------------------------------------------------------------------

.nbexports/introductory-tutorials/intro-to-julia/long-version/030 Functions and Broadcasting.jl


# adds a triangle to mesh where a b c d are the relative index coordinates
macro addtri(a,b,c,d)
    return esc(quote
               P2 = center;
               P1 = zeros(3); P1[fixdim[l]]=fixdim_val[l]; P3 = copy(P1);
               P1[freedim[1]]=grid[ii+$a]; P1[freedim[2]]=grid[jj+$b];
               P3[freedim[1]]=grid[ii+$c]; P3[freedim[2]]=grid[jj+$d];
               push!(mesh,Triangle(P1,P2,P3,area=area))
    end)
end
# generates a mesh of unit cube withe a fichera corner. N determines the size.
function gen_ficheramesh(N)

    if (mod(N, 2) != 0)   # Enforce N to be even
	N = N + 1;
    end

    nn = round(Int,N / 2); # Half the size

    # Prepare the mesh: Vector of Triangle
    mesh = Vector{Triangle}(undef,0);

    # compute the area of each triangle
    area = 0.25/N/N;

    # Uniform grid on [0,1]
    grid = Vector(0:N) / N;

    # Metadata for the discretization
    fixdim=[1;2;3;1;1;1;1;2;2;2;2;3;3;3;3]
    fixdim_val=[0;0;0;1;1;1;0.5;1;1;1;0.5;1;1;1;0.5]
    freedims=[2 3; 3 1; 1 2; 2 3; 2 3; 2 3 ; 2 3;3 1;3 1; 3 1; 3 1; 1 2; 1 2; 1 2; 1 2];
    Nvals=[1 N 1 N;
           1 N 1 N;
           1 N 1 N;
           1 nn 1 nn;
           nn+1 N 1 nn;
           1 nn nn+1 N;
           nn+1 N nn+1 N
           1 nn 1 nn
           nn+1 N 1 nn
           1 nn nn+1 N
           nn+1 N nn+1 N
           1 nn 1 nn
           nn+1 N 1 nn
           1 nn nn+1 N
           nn+1 N nn+1 N]
    # Ordering:
    # left face
    # front
    # bottom
    # right
    # (1/2, 1) x (0, 1/2)
    # (0, 1/2) x (1/2, 1)
    # (1/2, 1) x (1/2, 1)
    # back face: (0, 1/2) x (0, 1/2)
    # (1/2, 1) x (0, 1/2)
    # (0, 1/2) x (1/2, 1)
    # (1/2, 1) x (1/2, 1)
    # top face: (0, 1/2) x (0, 1/2)
    # (1/2, 1) x (0, 1/2)
    # (0, 1/2) x (1/2, 1)
    # (1/2, 1) x (1/2, 1)

    for l=1:size(fixdim,1);
        for ii = Nvals[l,1]:Nvals[l,2]
	    for jj = Nvals[l,3]:Nvals[l,4]

                center = zeros(3);
                center[fixdim[l]]=fixdim_val[l];         # Fix dimension
                freedim=freedims[l,:];
                # freedim now contains freedim[1] and freedim[2] which
                # correspond to the non-fixed dimensions
                center[freedim[1]]=(grid[ii]+grid[ii+1])/2;
                center[freedim[2]]=(grid[jj]+grid[jj+1])/2;

                # Now create the four triangles associated with this "center"
                if (l<4)
                    @addtri(0,0,1,0);
                    @addtri(1,0,1,1);
                    @addtri(1,1,0,1);
                    @addtri(0,1,0,0);
                else # For constency with Effenberger
                    @addtri(0,0,0,1);
                    @addtri(0,1,1,1);
                    @addtri(1,1,1,0);
                    @addtri(1,0,0,0);
	        end
            end
        end
    end

    # Auxiliary mesh data
    for tri = mesh
        # determine midpoint of triangle
        tri.aux.midpoint = (tri.P1 + tri.P2 + tri.P3) / 3;

        # normalized tangent vectors
        tri.aux.tau1 = normalize(tri.P2 - tri.P1);
        tri.aux.tau2 = normalize(tri.P3 - tri.P2);
        tri.aux.tau3 = normalize(tri.P1 - tri.P3);

        # exterior (normalized) normal of triangle
        tri.aux.normal = normalize(cross( tri.aux.tau1, tri.aux.tau2 ));

        # exterior (normalized) normals of the edges
        tri.aux.nu1 = normalize(cross(tri.aux.tau1, tri.aux.normal));
        tri.aux.nu2 = normalize(cross(tri.aux.tau2, tri.aux.normal));
        tri.aux.nu3 = normalize(cross(tri.aux.tau3, tri.aux.normal));
    end
    return mesh
end


using LinearAlgebra
BLAS.set_num_threads(1)
using VecchiaFactorization
import VecchiaFactorization as VF
using BlockArrays
using Random
using Test
using LBblocks

@testset "vecchia_approx.jl and sparse_matrix_show.jl" begin

    n = 320

    Σ = @sblock let θtru = 1.5, n
        K = (x,y,θtru) -> exp(- θtru * abs(x - y) ^ 0.8 )
        x = range(0,4,n)
        K.(x, x', θtru)
    end

    block_sizes = [100, 150, 20, 50]
    perm = sortperm(@. sin((1:n)*2*π/n))

    R1, M1, P1 = VF.R_M_P(Σ, block_sizes, perm)
    R2, M2, P2 = VF.R_M_P(Σ[perm, perm], block_sizes)
    R3, M3, P3 = VF.R_M_P((i,j) -> Σ[i,j], block_sizes, perm)

    @test VF.sparse(R1) ≈ VF.sparse(R2) ≈ VF.sparse(R3)
    @test VF.sparse(M1) ≈ VF.sparse(M2) ≈ VF.sparse(M3)
    @test VF.sparse(P1) ≈ VF.sparse(P3)

    @test VF.Matrix(R1) ≈ VF.Matrix(R2) ≈ VF.Matrix(R3)
    @test VF.Matrix(M1) ≈ VF.Matrix(M2) ≈ VF.Matrix(M3)
    @test VF.Matrix(P1) ≈ VF.Matrix(P3)

    @test VF.sparse(R1') ≈ VF.sparse(R1)';
    @test VF.sparse(M1') ≈ VF.sparse(M1)';
    @test VF.sparse(P1') ≈ VF.sparse(P1)';

    VΣ = VF.vecchia(Σ, block_sizes, perm)
    VF.sparse(inv(VΣ))
    VF.sparse(inv(adjoint(VΣ)))
    VF.sparse(adjoint(inv(VΣ)))

    VF.Matrix(VΣ)
    VF.Matrix(inv(VΣ))
    VF.Matrix(adjoint(VΣ))
    VF.Matrix(inv(adjoint(VΣ)))
    VF.Matrix(adjoint(inv(VΣ)))

end

@testset "Ridiagonal and Qidiagonal" begin

    @sblock let

        bs = [2, 4, 3]
        @test VF.sizes_from_blocksides(Ridiagonal, bs) == [(4,2), (3,4)]
        @test VF.sizes_from_blocksides(Qidiagonal, bs) == [(2,4), (4,3)]
        @test VF.sizes_from_blocksides(Midiagonal, bs) == [(2,2), (4,4), (3,3)]

        Ri = rand(Ridiagonal{Float64}, bs)
        Qi = rand(Qidiagonal{Float64}, bs)
        Mi = rand(Midiagonal{Float64}, bs)

        @test Ri isa Ridiagonal
        @test Qi isa Qidiagonal
        @test Mi isa Midiagonal

        @test VF.diag_block_dlengths(Ri) == [2,4,3]
        @test VF.diag_block_dlengths(Qi) == [2,4,3]
        @test VF.diag_block_dlengths(Mi) == [2,4,3]

        @test size(Ri) == (9,9)
        @test size(Qi) == (9,9)
        @test size(Mi) == (9,9)

        @test (size(Ri,1), size(Ri,2)) == size(Ri)
        @test (size(Qi,1), size(Qi,2)) == size(Qi)
        @test (size(Mi,1), size(Mi,2)) == size(Mi)

    end

    @sblock let
        ## bs = vcat(fill(60,7), fill(80,10))
        ## bs = fill(80,20)
        bs = [2, 4, 3, 10]
        ## bs = fill(10,20)
        ## bs = fill(10,200)

        Ri = randn(Ridiagonal{Float64}, bs)
        Qi = randn(Qidiagonal{Float64}, bs)
        Mi = randn(Midiagonal{Float64}, bs)

        v = rand(Float64, sum(bs))

        @inferred Ri * v
        @inferred Ri' * v
        @inferred Ri \ v
        @test (Ri \ v) ≈ ldiv!(Ri, copy(v)) rtol = 1e-10
        @inferred Qi * v
        @inferred Qi' * v
        @inferred Qi \ v
        @test (Qi \ v) ≈ ldiv!(Qi, copy(v)) rtol = 1e-10

        ## -----
        x = 1:size(Ri,1)
        v = zeros(Float64, length(x));
        for i = 1:2  # it is strange how unstable this inversion is
            v[rand(x)] = 1
        end
        ## -----
        #
        ## τ = 10; v = sin.(τ .* 2 .* π .* x ./ x[end])
        ## -----
        ## v = rand(Float64, sum(bs))
        ## -----
        v1 = Ri \ Ri * v
        v2 = Qi \ Qi * v
        w1 = Ri * (Ri \ v)
        w2 = Qi * (Qi \ v)
        @test v ≈ v1 rtol=1e-5
        @test v ≈ v2 rtol=1e-5
        @test v ≈ w1 rtol=1e-5
        @test v ≈ w2 rtol=1e-5

    end

    #=
    using BenchmarkTools

    bs = fill(50,20)
    Ri = randn(Ridiagonal{Float64}, bs)
    Qi = randn(Qidiagonal{Float64}, bs)
    Mi = randn(Midiagonal{Float64}, bs)
    v  = randn(size(Ri,1))
    w  = randn(size(Ri,1))
    M = randn(size(Ri,1), size(Ri,1))
    @benchmark mul!(w, Mi, v, true, false)  # 9 μs
    @benchmark Mi * v  # 9 μs
    @benchmark Ri * v  # 9 μs
    @benchmark Qi * v  # 9 μs

    @benchmark Mi' * v # 9 μs
    @benchmark Ri' * v # 9 μs
    @benchmark Qi' * v # 9 μs

    @benchmark Mi \ v  # 716.999 μs
    @benchmark Ri \ v  # 9 μs
    @benchmark Qi \ v  # 9 μs

    @benchmark Mi' \ v # 734 μs
    @benchmark Ri' \ v # 9 μs
    @benchmark Qi' \ v # 9 μs

    @benchmark M * v # 112 μs
    =#

end

@testset "Vecchia, InvVecchia, VecchiaPivoted, InvVecchiaPivoted" begin

    K(x,y,θ) = exp(- θ * abs(x - y) ^ 0.8 )
    θtru    = 1.5
    nblocks, blocksz = 6, 150
    bsn    = fill(blocksz, nblocks)
    n      = sum(bsn)
    x      = vcat(range(0,4,length=n-100), 4*rand(100))
    prm    = sortperm(x)
    Σ      = PseudoBlockArray(K.(x, x', θtru), bsn, bsn)

    V = Vecchia(;
        diag_blocks=[Σ[Block(i,i)] for i = 1:nblocks],
        subdiag_blocks=[Σ[Block(i+1,i)] for i = 1:nblocks-1],
        )
    V′  = inv(V.R) * V.M * inv(V.R)'
    iV  = inv(V)
    iV′ = inv(V′)

    Vᴾ     = VecchiaPivoted(Vecchia(Σ[prm,prm], bsn), prm)
    Vᴾ′    = Piv(prm)' * inv(Vᴾ.R) * Vᴾ.M * inv(Vᴾ.R)' * Piv(prm)
    iVᴾ    = inv(Vᴾ)
    iVᴾ′   = inv(Vᴾ′)

    matV   = Matrix(V)
    matVᴾ  = Matrix(Vᴾ)
    matiV  = Matrix(iV)
    matiVᴾ = Matrix(iVᴾ)

    v    = randn(size(V,1))

    @test V  * v ≈ matV  * v rtol=1e-5
    @test V  * v ≈ V′    * v rtol=1e-5
    @test Vᴾ * v ≈ matVᴾ * v rtol=1e-5
    @test Vᴾ * v ≈ Vᴾ′   * v rtol=1e-5

    @test iV  * v ≈ matiV  * v rtol=1e-5
    @test iV  * v ≈ iV′    * v rtol=1e-5
    @test iVᴾ * v ≈ matiVᴾ * v rtol=1e-5
    @test iVᴾ * v ≈ iVᴾ′   * v rtol=1e-5

    #=
    using BenchmarkTools

    v  = randn(size(V,1))

    @benchmark V   * v   # 50  μs
    @benchmark V′  * v   # 100 μs
    @benchmark Vᴾ  * v   # 50  μs
    @benchmark Vᴾ′ * v   # 100 μs
    @benchmark iV   * v  # 50  μs
    @benchmark iV′  * v  # 50  μs
    @benchmark iVᴾ  * v  # 50  μs
    @benchmark iVᴾ′ * v  # 50  μs

    R = V.R
    @benchmark R * v          # 15 μs
    @benchmark $(R') * v      # 15 μs
    @benchmark $(inv(R)) * v  # 15 μs
    @benchmark $(inv(R)') * v # 15 μs

    Q = Qidiagonal(map(x->copy(x'), V.R.data))
    @benchmark Q * v          # 15 μs
    @benchmark $(Q') * v      # 15 μs
    @benchmark $(inv(Q)) * v  # 15 μs
    @benchmark $(inv(Q)') * v # 15 ms

    M = V.M
    @benchmark M * v          # 19 μs
    @benchmark $(M') * v      # 20 μs
    @benchmark $(inv(M)) * v  # 20 μs
    @benchmark $(inv(M)') * v # 20 μs
    @benchmark mul!(v, M, $(copy(v))) # 17 μs

    @benchmark Σ * v    # 83 μs
    =#

    #=
    using PyPlot
    fig, ax = subplots(2)
    ax[1].semilogy(eigen(Symmetric(matV)).values,label="matV eigen")
    ax[1].semilogy(eigen(Symmetric(Matrix(Σ))).values,"--",label="Σ eigen")
    ax[1].legend()
    ax[2].semilogy(eigen(Symmetric(matVᴾ)).values,label="matVᴾ eigen")
    ax[2].semilogy(eigen(Symmetric(Matrix(Σ))).values,"--",label="Σ eigen")
    ax[2].legend()
    =#

    #=
    using PyPlot
    fig, ax = subplots(2)
    ax[1].plot(Σv1,label="Σv1")
    ax[1].plot(Σv ,"--",label="Σv")
    ax[1].legend()
    ax[2].plot(Σv3,label="Σv3")
    ax[2].plot(Σv ,"--",label="Σv")
    ax[2].legend()
    =#

    #=
    using PyPlot

    @sblock let V = Vᴾ, Σ = Σ
        mV  = Matrix(V)
        imV = Matrix(inv(V))
        iΣ  = inv(Σ)

        fig,ax = subplots(2,3)
        mV     |> ax[1,1].imshow
        Σ      |> ax[1,2].imshow
        mV - Σ |> ax[1,3].imshow

        imV      .|> abs .|> log |> ax[2,1].imshow
        iΣ       .|> abs .|> log |> ax[2,2].imshow
        imV - iΣ .|> abs .|> log |> ax[2,3].imshow
    end

    @sblock let V
        matRᴴ = VF.Rᴴmat(V)
        matR  = VF.Rmat(V)
        matRᴴ .|> abs .|> log |> matshow; colorbar()
        matR  .|> abs .|> log |> matshow; colorbar()
    end

    =#

    #=
    using PyPlot
    fig, ax = subplots(2)
    ax[1].plot(w1,label="w1")
    ax[1].plot(w ,"--",label="w")
    ax[1].legend()
    ax[2].plot(w3,label="w3")
    ax[2].plot(w ,"--",label="w")
    ax[2].legend()
    =#

    ## invcholΣ = inv(cholesky(Hermitian(Σ,:L)).L)
    ## invcholV = VecchiaFactorization.inv_cholesky(V)
    ## invcholVᴾ = VecchiaFactorization.inv_cholesky(Vecchia(Vᴾ.R,Vᴾ.M, Vᴾ.bsds)) # [invperm(Vᴾ.piv), invperm(Vᴾ.piv)]

    #=
    using PyPlot
    v = randn(size(V,1))

    (invcholV \ v)[prm] |> plot
    (invcholVᴾ \ v[prm]) |> plot
    (invcholΣ \ v)[prm] |> plot
    =#

    #=
    using BenchmarkTools
    ## without threads ...
    @benchmark V * v   # 48 μs
    @benchmark iV * v  # 50 μs

    @benchmark $(Matrix(matV)) * v    # 93 μs
    @benchmark $(Matrix(imatV)) * v   # 90 μs

    @benchmark $matV * v    # 108 μs
    @benchmark $imatV * v   # 108 μs

    (@belapsed $(Matrix(matV)) * v) / (@belapsed V * v)     # 1.7
    Base.summarysize((Matrix(matV))) / Base.summarysize(V)  # 3.27
    =#

end

@testset "Vecchia approx inversion" begin

    K(x,y,θ) = exp(- θ * abs(x - y) ^ 0.8 )
    θtru    = 0.2
    nblocks, blocksz = 50, 50
    bsn    = fill(blocksz, nblocks)
    n      = sum(bsn)
    x      = sort(vcat(range(0,4,length=n-100), 4*rand(100)))

    Σ  = K.(x, x', θtru)
    iΣ = inv(Σ)
    V  = Vecchia(Σ,bsn)
    iV = inv(V)

    IVapx = hcat(map(x->iV*x, eachcol(Σ))...)

    #=
    using PyPlot
    fig, ax = subplots(2)
    ax[1].plot(real.(eigen(IVapx).values))
    ax[1].plot(zeros(size(IVapx,1)),":k")
    ax[2].plot(imag.(eigen(IVapx).values))
    ax[2].plot(zeros(size(IVapx,1)),":k")
    =#

end

## @testset "Inv" begin
##
##     @test VF.Inv(R') == VF.Inv(R)'
##
##
## end

@testset "util.jl" begin

    @test VF.block_split(20, 10) == [10,10]
    @test VF.block_split(20, 5) == [5,5,5,5]
    @test VF.block_split(20, 11) == [11,9]
    @test VF.block_split(11, 11) == [11]
    @test VF.block_split(11, 10) == [10,1]

end


@everywhere function testmodel(ydot, y, p, t)
    """
    Eq. 44-46 in http://onlinelibrary.wiley.com/doi/10.1002/aic.15577/full
    The data sets used for the parameter estimation, involving the time evolution
    of the dimensionless concentrations for species A and Q, are generated
    within the time interval  0 ≤ t ≤ 1 using the dynamic model.
    Subject to the initial conditions

    ya(0)=1
    yq(0)=0

    For the parameter values k₁=12, k₂=8, k₃=2 this is the test cases mentionrd
    in the original paper (ref 80).
    The “experimental” data used and the fit achieved with the method's
    obtained parameters are shown in Figure 8.
    Therefore, this demonstration involves a system of two ODEs, J = 2,
    zero algebraic equations, K = 0, with two sets of accompanying data, L = 2,
    and a total of three parameters to fit, N = 3.
    """
    k₁, k₂, k₃, k₄ = p

    ydot[1] = -k₁*y[1] + k₂*y[2]
    ydot[2] = k₁*y[1] - (k₂+k₃)*y[2] + k₄*y[3]
    ydot[3] = k₃*y[2] - k₄*y[3]
end

@everywhere function obj_func(dataset, model, params::Array{Float64, 1}, lbs, ubs)
    """
    :L: number of dataset
    :timepoints: timepoints of experimental data.
    :params: model parameters
    :eval_idx: indecies of components used for evaluating objective function value.
    """
    params = (lbs.*(ubs./lbs).^(params./10)) # rescaling parameters
    y0 = [1.0, 0.0, 0.0]
    alg = CVODE_BDF(max_hnil_warns = 0, max_error_test_failures = 0)

    L = 1
    rss_per_data = zeros(1)
    timepoints = linspace(0,1,51)
    tspan = (timepoints[1], timepoints[end])
    eval_idx = [1,2,3]
    res = zeros(length(timepoints), length(eval_idx))
    for i in 1:L   # doseの数だけ回す。
        prob = ODEProblem(model, y0, tspan, params)
        sol = solve(prob, alg, saveat=timepoints, maxiters=1e7,
                    abstol = 1e-8, reltol = 1e-8, verbose=false)
        if sol.retcode == :Success
            res[:] = vcat(sol.u'...)
            rss_per_data[i] = mean((dataset[i][:, eval_idx] - res[:, eval_idx]).^2)
        else
            rss_per_data[i] = -1
        end
    end

    if -1 in rss_per_data
        return Inf
    else
        rss = mean(rss_per_data)
        return sqrt(rss)
    end
end

@everywhere function obj_func2(dataset, model, params::Array{Float64, 1}, lbs, ubs)
    """
    :L: number of dataset
    :timepoints: timepoints of experimental data.
    :params: model parameters
    :eval_idx: indecies of components used for evaluating objective function value.
    """
    y0 = [1.0, 0.0, 0.0]
    alg = CVODE_BDF(max_hnil_warns = 0, max_error_test_failures = 0)

    L = 1
    rss_per_data = zeros(1)
    timepoints = linspace(0,1,51)
    tspan = (timepoints[1], timepoints[end])
    eval_idx = [1,2,3]
    res = zeros(length(timepoints), length(eval_idx))
    for i in 1:L   # doseの数だけ回す。
        prob = ODEProblem(model, y0, tspan, params)
        sol = solve(prob, alg, saveat=timepoints, maxiters=1e7,
                    abstol = 1e-8, reltol = 1e-8, verbose=false)
        if sol.retcode == :Success
            res[:] = vcat(sol.u'...)
            rss_per_data[i] = mean((dataset[i][:, eval_idx] - res[:, eval_idx]).^2)
        else
            rss_per_data[i] = -1
        end
    end

    if -1 in rss_per_data
        return Inf
    else
        rss = mean(rss_per_data)
        return sqrt(rss)
    end
end


@info "Start Testing"
using PotentialMap
using SteadyStateDiffEq
using FindSteadyStates
using PyPlot
using Test

include("SampleDE.jl")

plt= PyPlot
sp = SampleDE

@info "Start Testing PotentialMap.LeastAction"
@testset "PotentialMap.jl" begin

    @testset "Generate diffusion matrix" begin
        include("diffusionM.jl")
    end

    @testset "Wrapper of FindSteadyStates" begin
        include("find_fixedpoints.jl")
    end

    @testset "gradient calculation compared to the reference" begin
        include("gradient.jl")
    end

    @info "Start Measuring LeastAction"
    @testset "LeastAction" begin
        #include("LeastAction.jl")
    end
end


using Plexifications
using Base.Test: @test, @test_throws

@test_throws ErrorException begin
    inv(HyperPerplex(0, 0.0))
end

@test begin
    iszerodivisor(HyperPerplex(1,1,3,2))
end

@test begin
    !iszerodivisor(HyperPerplex(1,2,3,5))
end

@test begin
    a = Plexifications.randomBigFloat()
    isreal(HyperPerplex(a))
end

@test begin
    a = 1
    !isreal(HyperPerplex(a, a))
end

@test begin
    z = random(HyperPerplex{Int})
    z == +(z)
end

@test begin
    a = Plexifications.randomBigFloat()
    real(HyperPerplex(a)) == a
end

@test begin
    io = IOBuffer()
    show(io, HyperPerplex(1, 2, 3, 4, 5, 6, 7, 8))
    l = String(take!(io))
    r = "[1: 1, R: 2, A: 3, RA: 4, B: 5, RB: 6, AB: 7, RAB: 8]"
    l == r
end

@test begin
    x = random(HyperPerplex{BigFloat})
    y = random(HyperPerplex{BigFloat})
    x != y
end

@test begin
    a = Plexifications.randomBigInt()
    z = random(HyperPerplex{BigInt})
    l = a + z
    r = z + a
    l == r
end

@test begin
    a = Plexifications.randomBigInt()
    z = random(HyperPerplex{BigInt})
    l = a - z
    r = -(z - a)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = x + y
    r = y + x
    l == r
end

@test begin
    x = random(DualPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = x + y
    r = y + x
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = x * y
    r = y * x
    l == r
end

@test begin
    x = random(DualPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = x * y
    r = y * x
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = x - y
    r = -(y - x)
    l == r
end

@test begin
    x = random(DualPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = x - y
    r = -(y - x)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    z = random(HyperPerplex{BigInt})
    l = (x + y) + z
    r = x + (y + z)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    z = random(HyperPerplex{BigInt})
    l = (x * y) * z
    r = x * (y * z)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    barA(barA(x)) == x
end

@test begin
    x = random(HyperPerplex{BigInt})
    barB(barB(x)) == x
end

@test begin
    x = random(HyperPerplex{BigInt})
    barR(barR(x)) == x
end

@test begin
    x = random(HyperPerplex{BigInt})
    barB(barA(x)) == barA(barB(x))
end

@test begin
    x = random(HyperPerplex{BigInt})
    barR(barA(x)) == barA(barR(x))
end

@test begin
    x = random(HyperPerplex{BigInt})
    barB(barR(x)) == barR(barB(x))
end

@test begin
    x = random(HyperPerplex{Rational{BigInt}})
    inv(inv(x)) == x
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = barA(x * y)
    r = barA(x) * barA(y)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = barA(x + y)
    r = barA(x) + barA(y)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = barA(x - y)
    r = barA(x) - barA(y)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = barB(x * y)
    r = barB(x) * barB(y)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = barB(x + y)
    r = barB(x) + barB(y)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = barB(x - y)
    r = barB(x) - barB(y)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = barR(x * y)
    r = barR(x) * barR(y)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = barR(x + y)
    r = barR(x) + barR(y)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = barR(x - y)
    r = barR(x) - barR(y)
    l == r
end

@test begin
    x = random(HyperPerplex{Rational{BigInt}})
    y = random(HyperPerplex{Rational{BigInt}})
    l = inv(x * y)
    r = inv(y) * inv(x)
    l == r
end

@test begin
    x = random(HyperPerplex{Rational{BigInt}})
    y = random(HyperPerplex{Rational{BigInt}})
    l = inv(x / y)
    r = y / x
    l == r
end

@test begin
    x = random(HyperPerplex{Rational{BigInt}})
    y = random(HyperPerplex{Rational{BigInt}})
    l = inv(x \ y)
    r = y \ x
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    z = random(HyperPerplex{BigInt})
    l = x * (y + z)
    r = (x * y) + (x * z)
    l == r
end

@test begin
    a = Plexifications.randomBigInt()
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = a * (x + y)
    r = (a * x) + (a * y)
    l == r
end

@test begin
    a = Plexifications.randomBigInt()
    x = random(HyperPerplex{BigInt})
    l = a * x
    r = x * a
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    z = random(HyperPerplex{BigInt})
    l = (x + y) * z
    r = (x * z) + (y * z)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    z = random(HyperPerplex{BigInt})
    l = x * (y - z)
    r = (x * y) - (x * z)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    z = random(HyperPerplex{BigInt})
    l = (x - y) * z
    r = (x * z) - (y * z)
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    n = norm(x)
    n > big(0) || n == big(0)
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = norm(x * y)
    r = norm(x) * norm(y)
    l == r
end

@test_throws ErrorException begin
    inv(HyperPerplex(0, 0.0))
end

@test_throws ErrorException begin
    random(HyperPerplex{Int}) / 0
end

@test_throws ErrorException begin
    0 \ random(HyperPerplex{Int})
end

@test begin
    a = Plexifications.randomBigRational()
    z = random(HyperPerplex{Rational{BigInt}})
    l = z / a
    r = a \ z
    l == r
end

@test begin
    a = Plexifications.randomBigRational()
    z = random(HyperPerplex{Rational{BigInt}})
    l = a / z
    r = z \ a
    l == r
end

@test begin
    z = random(HyperPerplex{BigInt})
    z == z + zero(HyperPerplex{BigInt})
end

@test begin
    z = random(HyperPerplex{BigInt})
    z == z * one(HyperPerplex{BigInt})
end

@test begin
    z = random(HyperPerplex{BigInt})
    z == z + zero(z)
end

@test begin
    z = random(HyperPerplex{BigInt})
    z == z * one(z)
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = commutator(x, y)
    r = zero(HyperPerplex{BigInt})
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = alternatorL(x, y)
    r = zero(HyperPerplex{BigInt})
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = alternatorR(x, y)
    r = zero(HyperPerplex{BigInt})
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    l = flexator(x, y)
    r = zero(HyperPerplex{BigInt})
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    z = random(HyperPerplex{BigInt})
    l = associator(x, y, z)
    r = zero(HyperPerplex{BigInt})
    l == r
end

@test begin
    x = random(HyperPerplex{BigInt})
    y = random(HyperPerplex{BigInt})
    z = random(HyperPerplex{BigInt})
    l = jacobiator(x, y, z)
    r = 3 * x * y * z
    l == r
end

@test begin
    l = HyperPerplex(Prplex(1,2))
    r = HyperPerplex(1,2)
    l == r
end

@test begin
    l = HyperPerplex(DualPerplex(1,2))
    r = HyperPerplex(1,2,0)
    l == r
end

@test begin
    l = HyperPerplex(1, 2, 3.0)
    r = HyperPerplex(1.0, 2.0, 3.0)
    l == r
end

@test begin
    l = HyperPerplex(1, 2, 3, 4.0)
    r = HyperPerplex(1.0, 2.0, 3.0, 4.0)
    l == r
end

@test begin
    l = HyperPerplex(1, 2, 3, 4, 5.0)
    r = HyperPerplex(1.0, 2.0, 3.0, 4.0, 5.0)
    l == r
end

@test begin
    l = HyperPerplex(1, 2, 3, 4, 5, 6.0)
    r = HyperPerplex(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
    l == r
end

@test begin
    l = HyperPerplex(1, 2, 3, 4, 5, 6, 7.0)
    r = HyperPerplex(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0)
    l == r
end

@test begin
    l = HyperPerplex(1, 2, 3, 4, 5, 6, 7, 8.0)
    r = HyperPerplex(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0)
    l == r
end


using CSV, DataFrames, Gadfly, StatsBase

df = CSV.read("distances_tiff.csv")

function lincor(x, y)
    n = length(x)
    prod = sum(x .* y)
    ux, sx = mean_and_std(x)
    uy, sy = mean_and_std(y)
    cor = prod / n - ux * uy
    cor = cor / (sx * sy)
    return cor
end
function getKind(df::DataFrame, k::String)
    tmp = filter(e->e[:kind]==k, df)
    deletecols!(tmp, :kind)
    rename!(tmp, Dict(:value => Symbol(k)))
    sort!(tmp, Symbol(k))
    return tmp
end

symbolsStr = ["c2c_full_eucli", "c2c_proj_eucli", "c2c_maha",
              "s2s_full_eucli", "s2s_proj_eucli", "s2s_maha"]
println("\nDistance-Hist_Div correlations:")
for str in symbolsStr
    sym = Symbol(str)
    div_df = join(getKind(df, str), getKind(df, "hist_divergence"), on = [:index1, :index2])
    div_df[:id] = 1:(size(div_df)[1])
    s_cor = corspearman(div_df[sym], div_df[:hist_divergence])
    p_cor = lincor(div_df[sym], div_df[:hist_divergence])
    println("[$(str)]\tspearman: $(s_cor)\tp_cor: $(p_cor) .")
end

df_divs = join(getKind(df, "divergence"), getKind(df, "mle_divergence"),
                   on = [:index1, :index2])
println(size(df_divs))
df_divs = join(df_divs, getKind(df, "hist_divergence"), on = [:index1, :index2])
println(size(df_divs))

s_cor = corspearman(df_divs[:divergence], df_divs[:hist_divergence])
p_cor = lincor(df_divs[:divergence], df_divs[:hist_divergence])
println("[Normal-Hist divergences correlation]\tSpearman: $(s_cor)\tPearson: $(p_cor) .")

s_cor = corspearman(df_divs[:mle_divergence], df_divs[:hist_divergence])
p_cor = lincor(df_divs[:mle_divergence], df_divs[:hist_divergence])
println("[MLE-Hist divergences correlation]\tSpearman: $(s_cor)\tPearson: $(p_cor) .")

s_cor = corspearman(df_divs[:mle_divergence], df_divs[:divergence])
p_cor = lincor(df_divs[:mle_divergence], df_divs[:divergence])
println("[MLE-Normal divergences correlation]\tSpearman: $(s_cor)\tPearson: $(p_cor) .")


# These are attempts to write code in a future iteration API

function copy!(dest, src)
    for (idest, s) in couple(index(dest), src)
        dest[idest] = s
    end
    dest
end

# Dot product of two vectors (either/both may be sparse)
# Based on `vecdot(x, y)` for arbitrary iterables in linalg/generic
# This implementation has the same problem as reported in #15690.
function vecdot(x::AbstractVector, y::AbstractVector)
    length(x) == length(y) || throw(DimensionMismatch("x and y are of different lengths!"))
    citer = couple(stored(x), stored(y))
    cstate = iterate(citer)
    cstate === nothing && return dot(zero(eltype(x)), zero(eltype(y)))
    # Otherwise we avoid calling zero
    (vx, vy), cstate = cstate
    s = dot(vx, vy)
    while (cstate = iterate(citer, cstate)) !== nothing
        (vx, vy), cstate = cstate
        s += dot(vx, vy)
    end
    s
end

# Adding two vectors, either/both of which might be sparse.
#
# Note that `similar` is likely just not good enough for this kind of task.
# This is more likely to be a problem with matrices, e.g., knowing that
#    ::Bidiagonal + ::Tridiagonal -> ::Tridiagonal
#    ::Bidiagonal * ::Tridiagonal -> ::Banded
# We may need more promote_op magic.
#
# Here I punt on the "inference problem" and just indicate the output type
# as the first argument.
for op in (:+, :.*)
    @eval begin
        function ($op){Tout<:SparseVector}(::Type{Tout}, x::AbstractVector{Tx}, y::AbstractVector{Ty})
            length(x) == length(y) || throw(DimensionMismatch("x and y are of different lengths!"))
            z = Tout(length(x))  # doesn't exist yet, I think
            for (ix, iy) in couple(index(x), index(y))
                push!(z.nzind, Int(ix))
                push!(z.nzval, $op(x[ix], y[iy]))
            end
            z
        end
    end
end


import Sobol.next!

"""
    A NumberGenerator must always return a vector of uniforms when called with the function next!(::NumberGenerator).
    It can contain whatever it wants as long as it does that method.
"""
abstract type NumberGenerator end

"""
    A NumberGenerator wrapper for SobolSeq.
"""
struct SobolGen <: NumberGenerator
    seq_::SobolSeq
end
function next!(number_generator::SobolGen)
    return next!(number_generator.seq_)
end

"""
    A NumberGenerator wrapper for MersenneTwister.
"""
struct Mersenne <: NumberGenerator
    twister_::MersenneTwister
    number_of_itos_::UInt
end
function next!(number_generator::Mersenne)
    return rand(number_generator.twister_, number_generator.number_of_itos_)
end

"""
    ImportanceSampler(underlying_generator_::NumberGenerator, conversion_function_::Function)
    The importance sampler takes in a NumberGenerator that is used to get a random vector of uniforms,  [0,1]^d.
    Then it inputs this vector into a function (the conversion_function): [0,1]^d -> [0,1]^d.
    Note that if the identity function is input then the numbers generated are the same as the input
    number generator. If on the other hand a convex function (in some dimension) is input it is less likely to
    get a high number for that dimension.
    If you are doing Importance Sampling remember to multiply the results by the inverse of the pdf later. This
    package doesn't do that for you.
"""
struct ImportanceSampler <: NumberGenerator
    underlying_generator_::NumberGenerator
    conversion_function_::Function
end
function next!(number_generator::ImportanceSampler)
    array_of_uniforms = next!(number_generator.underlying_generator_)
    return number_generator.conversion_function_(array_of_uniforms)
end


using FountainCodes
using SparseArrays
using DelimitedFiles
using Random, StatsBase, Distributions

function load_matrix(filename="./examples/H0.txt")
    M = readdlm(filename)
    Is = M[:, 2] .+ 1
    Js = M[:, 1] .+ 1
    Vs = ones(GF256, length(Is))
    # Vs = FountainCodes.rand_nonzero(Random.GLOBAL_RNG, GF256, length(Is))
    # Vs = randn(length(Is))
    println("Loaded matrix $filename with $(eltype(Vs)) entries")

    # set the first entry of each constraint to 1
    A = sparse(Is, Js, Vs, 1536, 512)
    vals = nonzeros(A)
    for j in 1:size(A, 2)
        i = 1
        k = nzrange(A, j)[i]
        vals[k] = one(eltype(vals))
    end

    A
end

"""

Write a `AbstractSparseMatrixCSC` to file, with each line corresponding to a non-zero entry.
"""
function write_matrix(A::SparseArrays.AbstractSparseMatrixCSC, filename="R10.txt"; sep="\t")
    Is, Js, Vs = findnz(A)
    open(filename, "w") do io
        for i in 1:length(Is)
            println(io, "$(Is[i]-1)$(sep)$(Js[i]-1)\n")
        end
    end
    return
end

function simulate_ldpc(Ht, enc, erasurerate, erasures)
    @assert erasures == "fixed" || erasures == "random"
    enc .= zero(eltype(enc))
    nenc, nconstraints = size(Ht)
    Xs = collect(1:nenc)
    if erasures == "fixed"
        shuffle!(Xs)
        nerasures = round(Int, erasurerate*nenc)
        Xs_rec = sort(Xs[1:nenc-nerasures])
        Xs_era = sort(Xs[end-nerasures+1:end])
    elseif erasures == "random"
        mask = rand(nenc) .< erasurerate
        Xs_rec = Xs[.!mask]
        Xs_era = Xs[mask]
    end
    Ht_rec = Ht[Xs_rec, :] # constraints corresponding to received symbols
    Ht_era = Ht[Xs_era, :] # constraints corresponding to erased symbols
    enc_rec = enc[Xs_rec]
    enc_era = enc[Xs_era]

    # attempt to create the decoder
    nrowops, ninactivated = 0, 0
    decoding_success = false
    decoder = Decoder(Ht_era)

    # attempt decoding
    try
        rhs = Ht_rec'*enc_rec
        enc_era_dec = decode(Ht_era, rhs; decoder)
        # if eltype(enc_era_dec) == Float64
        #     # @assert
        #     if !(enc_era_dec ≈ enc_era)
        #         println(enc_era_dec)
        #     end
        # else
        #     @assert enc_era_dec == enc_era
        # end
        decoding_success = true
    catch e
        if e isa RankDeficiencyException
            # if eltype(enc_era) == Float64
            #     @assert rank(Matrix(Ht_era)) < length(enc_era)
            # end
        else
            rethrow()
        end
    end
    nrowops = decoder.num_rowops
    ninactivated = decoder.num_inactivated
    nrowops, ninactivated, decoding_success
end

function main_ldpc(;erasurerate, minsamples=10000, maxsamples=100000, minfails=100, erasures)
    Ht = load_matrix()
    H = sparse(Ht')
    nconstraints, nenc = size(H)
    enc = zeros(eltype(H), nenc)

    # run simulations
    samples = Vector{Any}()
    nfails = 0
    while length(samples) < minsamples || (length(samples) < maxsamples && (nfails < minfails || length(samples) - nfails < minfails))
        sample = simulate_ldpc(Ht, enc, erasurerate, erasures)
        nfails += 1 - sample[3]
        push!(samples, sample)
    end

    # compute and return means
    nops = [sample[1] for sample in samples]
    nina = [sample[2] for sample in samples]
    fails = 1 - mean([sample[3] for sample in samples])
    length(samples), mean(nops), mean(nina), fails
end

function simulate_raptor(code, int, nenc, erasurerate, erasures)
    @assert erasures == "fixed" || erasures == "random"

    # randomly selected nenc unique encodes symbol identifiers, corresponding to the nenc encoded
    # symbols received by the decoder (the upper limit is chosen arbitrarily)
    Xs = collect(0:nenc-1)
    if erasures == "fixed"
        shuffle!(Xs)
        nerasures = round(Int, erasurerate * nenc)
        Xs = sort!(Xs[1:nenc-nerasures])
    elseif erasures == "random"
        mask = rand(nenc) .< erasurerate
        Xs = Xs[.!mask]
    end

    # compute the encoded symbols
    # G = generator_matrix(code, Xs)
    # enc = G'*int

    # decode the intermediate symbols from the encoded symbols
    A = constraint_matrix(code, Xs)
    pre = zeros(GF256, size(A, 2))
    # pre[end-length(enc)+1:end] .= enc

    # attempt to create the decoder
    nrowops, ninactivated = 0, 0
    decoding_success = false
    decoder = Decoder(A)

    # attempt decoding
    try

        # permanent inactivations
        if code isa RQ
            for cpi in (code.L-code.P+1):code.L
                FountainCodes.mark_inactive!(decoder, A, cpi)
            end
        end

        # decode
        dec_int = decode(A, pre; decoder)
        decoding_success = true
    catch e
        if e isa RankDeficiencyException
        else
            rethrow()
        end
    end

    # only count non-permanent inactivations
    ninactivated = decoder.num_inactivated
    if code isa RQ
        ninactivated -= code.P
    end
    nrowops = decoder.num_rowops
    nrowops, ninactivated, decoding_success
end

function main_raptor(;nsrc=1024, rate=2/3, erasurerate, minsamples=10000, maxsamples=100000, minfails=100, style, erasures)
    @assert style == "R10" || style == "RQ"

    code = style == "R10" ? R10(nsrc) : RQ(nsrc)
    nsrc = style == "R10" ? nsrc : code.Kp

    nenc = round(Int, nsrc / rate)
    src = rand(GF256, nsrc)

    # compute the intermediate symbols
    A = constraint_matrix(code, 0:nsrc-1)
    pre = zeros(GF256, size(A, 2))
    pre[end-nsrc+1:end] .= src
    int = decode(A, pre)

    # run simulations
    samples = Vector{Any}()
    nfails = 0
    while length(samples) < minsamples || (length(samples) < maxsamples && (nfails < minfails || length(samples) - nfails < minfails))
        sample = simulate_raptor(code, int, nenc, erasurerate, erasures)
        nfails += 1 - sample[3]
        push!(samples, sample)
    end

    # compute and return means
    nops = [sample[1] for sample in samples]
    nina = [sample[2] for sample in samples]
    fails = 1 - mean([sample[3] for sample in samples])
    length(samples), mean(nops), mean(nina), fails
end

function simulate_lt(code, nenc, erasurerate, erasures)
    @assert erasures == "fixed" || erasures == "random"

    # randomly selected nenc unique encodes symbol identifiers, corresponding to the nenc encoded
    # symbols received by the decoder (the upper limit is chosen arbitrarily)
    Xs = collect(0:nenc-1)
    if erasures == "fixed"
        shuffle!(Xs)
        nerasures = round(Int, erasurerate * nenc)
        Xs = sort!(Xs[1:nenc-nerasures])
    elseif erasures == "random"
        mask = rand(nenc) .< erasurerate
        Xs = Xs[.!mask]
    end

    # compute the encoded symbols
    G = generator_matrix(code, Xs)
    enc = zeros(eltype(G), length(Xs))

    # attempt to create the decoder
    nrowops, ninactivated = 0, 0
    decoding_success = false
    decoder = Decoder(G)

    # attempt decoding
    try
        # decode
        decode(G, enc; decoder)
        decoding_success = true
    catch e
        if e isa RankDeficiencyException
        else
            rethrow()
        end
    end
    nrowops = decoder.num_rowops
    ninactivated = decoder.num_inactivated
    nrowops, ninactivated, decoding_success
end

function main_lt(;nsrc=1024, rate=2/3, erasurerate, minsamples=10000, maxsamples=100000, minfails=100, erasures)
    dd = Soliton(nsrc, 2, 1e-6, 1e-3)
    code = LT{GF256}(nsrc, dd) # w. generator matrix coefficients of type GF256
    nenc = round(Int, nsrc / rate)

    # run simulations
    samples = Vector{Any}()
    nfails = 0
    while length(samples) < minsamples || (length(samples) < maxsamples && (nfails < minfails || length(samples) - nfails < minfails))
        sample = simulate_lt(code, nenc, erasurerate, erasures)
        nfails += 1 - sample[3]
        push!(samples, sample)
    end

    # compute and return means
    nops = [sample[1] for sample in samples]
    nina = [sample[2] for sample in samples]
    fails = 1 - mean([sample[3] for sample in samples])
    length(samples), mean(nops), mean(nina), fails
end

function main(erasures="fixed")
    erasurerates = 0.28:0.01:0.33

    # LT simulations
    nsamples_lt = zeros(0)
    rowops_lt = zeros(0)
    inactivations_lt = zeros(0)
    fails_lt = zeros(0)
    for erasurerate in erasurerates
        println("Simulating lt at $erasurerate")
        nsamples, rowops, inactivations, fails = main_lt(;erasurerate, erasures)
        push!(nsamples_lt, nsamples)
        push!(rowops_lt, rowops)
        push!(inactivations_lt, inactivations)
        push!(fails_lt, fails)
    end
    println("LT")
    println(nsamples_lt)
    println(rowops_lt)
    println(inactivations_lt)
    println(fails_lt)

    # R10 simulations
    println("Simulating R10")
    nsamples_r10 = zeros(0)
    rowops_r10 = zeros(0)
    inactivations_r10 = zeros(0)
    fails_r10 = zeros(0)
    for erasurerate in erasurerates
        println("Simulating R10 at $erasurerate")
        nsamples, rowops, inactivations, fails = main_raptor(;erasurerate, style="R10", erasures)
        push!(nsamples_r10, nsamples)
        push!(rowops_r10, rowops)
        push!(inactivations_r10, inactivations)
        push!(fails_r10, fails)
    end
    println("R10")
    println(nsamples_r10)
    println(rowops_r10)
    println(inactivations_r10)
    println(fails_r10)

    # RQ simulations
    println("Simulating RQ")
    nsamples_rq = zeros(0)
    rowops_rq = zeros(0)
    inactivations_rq = zeros(0)
    fails_rq = zeros(0)
    for erasurerate in erasurerates
        println("Simulating RQ at $erasurerate")
        nsamples, rowops, inactivations, fails = main_raptor(;erasurerate, style="RQ", erasures)
        push!(nsamples_rq, nsamples)
        push!(rowops_rq, rowops)
        push!(inactivations_rq, inactivations)
        push!(fails_rq, fails)
    end
    println("RQ")
    println(nsamples_rq)
    println(rowops_rq)
    println(inactivations_rq)
    println(fails_rq)

    # LDPC simulations
    nsamples_ldpc = zeros(0)
    rowops_ldpc = zeros(0)
    inactivations_ldpc = zeros(0)
    fails_ldpc = zeros(0)
    for erasurerate in erasurerates
        println("Simulating LDPC at $erasurerate")
        nsamples, rowops, inactivations, fails = main_ldpc(;erasurerate, erasures)
        push!(nsamples_ldpc, nsamples)
        push!(rowops_ldpc, rowops)
        push!(inactivations_ldpc, inactivations)
        push!(fails_ldpc, fails)
    end
    println("LDPC")
    println(nsamples_ldpc)
    println(rowops_ldpc)
    println(inactivations_ldpc)
    println(fails_ldpc)
    return
end


struct BinaryConfusionMatrix{T<:Number} <: AbstractArray{T,2}
    true_positives::T
    false_positives::T
    false_negatives::T
    true_negatives::T
end

(::Type{BinaryConfusionMatrix{T}})() where {T} = BinaryConfusionMatrix{T}(0,0,0,0)
BinaryConfusionMatrix() = BinaryConfusionMatrix{Int}()
function BinaryConfusionMatrix(A::AbstractArray)
    @assert length(A) == 4
    BinaryConfusionMatrix{Int}(Tuple(A)...)
end

function Base.:(+)(a::BinaryConfusionMatrix{T},
                   b::BinaryConfusionMatrix{S}) where {T,S}
    BinaryConfusionMatrix{promote_type(T,S)}(
        a.true_positives  + b.true_positives,
        a.false_positives + b.false_positives,
        a.false_negatives + b.false_negatives,
        a.true_negatives  + b.true_negatives,
    )
end

@inline Core.Tuple(c::BinaryConfusionMatrix) = (c.true_positives, c.false_positives, c.false_negatives, c.true_negatives)
@inline Base.size(c::BinaryConfusionMatrix) = (2, 2)
function Base.getindex(c::BinaryConfusionMatrix, i::Int, j::Int)
    ifelse((i == 1) & (j == 1), c.true_positives,
    ifelse((i == 2) & (j == 1), c.false_positives,
    ifelse((i == 1) & (j == 2), c.false_negatives, c.true_negatives)))
end

true_positives(c::BinaryConfusionMatrix)  = c.true_positives
false_positives(c::BinaryConfusionMatrix) = c.false_positives
true_negatives(c::BinaryConfusionMatrix)  = c.true_negatives
false_negatives(c::BinaryConfusionMatrix) = c.false_negatives
condition_positive(c::BinaryConfusionMatrix) =
    true_positives(c) + false_negatives(c)
condition_negative(c::BinaryConfusionMatrix) =
    true_negatives(c) + false_positives(c)
LearnBase.nobs(c::BinaryConfusionMatrix) =
    condition_positive(c) + condition_negative(c)
predicted_positive(c::BinaryConfusionMatrix) =
    true_positives(c) + false_positives(c)
predicted_negative(c::BinaryConfusionMatrix) =
    true_negatives(c) + false_negatives(c)
correctly_classified(c::BinaryConfusionMatrix) =
    true_positives(c) + true_negatives(c)
incorrectly_classified(c::BinaryConfusionMatrix) =
    false_positives(c) + false_negatives(c)
prevalence(c::BinaryConfusionMatrix) =
    condition_positive(c) / nobs(c)

function Base.show(io::IO, ::MIME"text/plain", c::BinaryConfusionMatrix)
    println(io, summary(c), ":")
    tp = string(true_positives(c))
    fp = string(false_positives(c))
    fn = string(false_negatives(c))
    tn = string(true_negatives(c))
    len_p = max(maximum(map(length, (tp, fp))), 3)
    len_n = max(maximum(map(length, (fn, tn))), 3)
    tp = lpad(tp, len_p); fp = lpad(fp, len_p)
    fn = lpad(fn, len_n); tn = lpad(tn, len_n)
    pad = "  "
    println(io, pad, " ", "Predicted")
    println(io, pad, "  ", lpad("+",len_p), "   ", lpad("-",len_n))
    println(io, pad, "┌", repeat("─",len_p+2), "┬", repeat("─",len_n+2), "┐")
    println(io, pad, "│ ", tp, " │ ", fn, " │ +")
    println(io, pad, "├", repeat("─",len_p+2), "┼", repeat("─",len_n+2), "┤   Actual")
    println(io, pad, "│ ", fp, " │ ", tn, " │ -")
    println(io, pad, "└", repeat("─",len_p+2), "┴", repeat("─",len_n+2), "┘")
    println(io, pad, "   f1_score: ", round(f1_score(c),digits=4))
    println(io, pad, "   accuracy: ", round(accuracy(c),digits=4))
    println(io, pad, "  precision: ", round(precision_score(c),digits=4))
    println(io, pad, "sensitivity: ", round(sensitivity(c),digits=4))
    print(io, pad,   "specificity: ", round(specificity(c),digits=4))
end

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

function confusions(targets::AbstractArray{T},
                    outputs::AbstractArray,
                    encoding::AbstractVector) where T
    length(encoding) == 2 || error("Multiclass not yet implemented")
    confusions(targets, outputs, LabelEnc.NativeLabels{T,2}(encoding))
end

# Generic fallback. Tries to infer label encoding
function confusions(targets, outputs)
    encoding = _labelenc(targets, outputs)
    nlabel(encoding) == 2 || error("Multiclass not yet implemented")
    confusions(targets, outputs, encoding)
end

function confusions(targets::AbstractArray,
                    outputs::AbstractArray,
                    encoding::BinaryLabelEncoding)
    @_dimcheck length(targets) == length(outputs)
    tp::Int = 0; tn::Int = 0;
    fp::Int = 0; fn::Int = 0;
    @inbounds for I in eachindex(targets, outputs)
        target = targets[I]
        output = outputs[I]
        tp +=  true_positives(target, output, encoding)
        fp += false_positives(target, output, encoding)
        fn += false_negatives(target, output, encoding)
        tn +=  true_negatives(target, output, encoding)
    end
    BinaryConfusionMatrix{Int}(tp, fp, fn, tn)
end

function confusions(targets::AbstractArray,
                    outputs::AbstractArray,
                    encoding::BinaryLabelEncoding,
                    thresholds::AbstractVector)
    @_dimcheck length(targets) == length(outputs)
    curve = fill(BinaryConfusionMatrix{Int}(), length(thresholds))
    @inbounds for I in eachindex(targets, outputs)
        target = targets[I]
        output = outputs[I]
        for (j, th) in enumerate(thresholds)
            pred = convertlabel(encoding, classify(output, th), LabelEnc.ZeroOne())
            curve[j] += BinaryConfusionMatrix{Int}(
                 true_positives(target, pred, encoding),
                false_positives(target, pred, encoding),
                false_negatives(target, pred, encoding),
                 true_negatives(target, pred, encoding),
            )
        end
    end
    curve
end

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

struct ROCCurve{T<:Number,F<:Number,V<:AbstractVector{F}} <: AbstractArray{Tuple{F,BinaryConfusionMatrix{T}},1}
    thresholds::V
    confusions::Vector{BinaryConfusionMatrix{T}}
end

function ROCCurve(thresholds::AbstractVector, confusions::Vector{BinaryConfusionMatrix{T}}) where T
    @_dimcheck length(thresholds) == length(confusions)
    ROCCurve{T,eltype(thresholds),typeof(thresholds)}(thresholds, confusions)
end

function Base.:(+)(r1::ROCCurve, r2::ROCCurve)
    @assert r1.thresholds == r2.thresholds
    nm = r1.confusions .+ r2.confusions
    ROCCurve(r1.thresholds, nm)
end

Base.size(r::ROCCurve) = size(r.confusions)
Base.@propagate_inbounds function Base.getindex(r::ROCCurve, I::Int...)
    (r.thresholds[I...], r.confusions[I...])
end
Base.@propagate_inbounds function Base.setindex!(r::ROCCurve, tup::Tuple, I::Int...)
    r.thresholds[I...] = tup[1]
    r.confusions[I...] = tup[2]
end
Base.@propagate_inbounds function Base.setindex!(r::ROCCurve, v::BinaryConfusionMatrix, I::Int...)
    r.confusions[I...] = v
end

for fun in (:true_positives,  :true_negatives,
            :false_positives, :false_negatives,
            :condition_positive, :condition_negative,
            :correctly_classified, :incorrectly_classified,
            :prevalence,
            :predicted_positive, :predicted_negative,
            :true_positive_rate, :false_positive_rate,
            :true_negative_rate, :false_negative_rate,
            :positive_predictive_value, :negative_predictive_value,
            :false_discovery_rate, :false_omission_rate,
            :accuracy, :f_score, :f1_score,
            :positive_likelihood_ratio, :negative_likelihood_ratio,
            :diagnostic_odds_ratio)
    @eval ($fun)(r::ROCCurve) = Base.broadcast(($fun), r.confusions)
end

confusions(r::ROCCurve) = r.confusions

function confusions_at_sensitivity(r::ROCCurve, at::Number)
    0 <= at <= 1 || throw(ArgumentError("parameter \"at\" has to be in [0, 1]"))
    idx = findfirst(s -> s >= at, sensitivity(r))
    r[idx][2]
end

function specificity_at_sensitivity(r::ROCCurve, at::Number)
    specificity(confusions_at_sensitivity(r, at))
end

function precision_at_sensitivity(r::ROCCurve, at::Number)
    precision_score(confusions_at_sensitivity(r, at))
end

function accuracy_at_sensitivity(r::ROCCurve, at::Number)
    accuracy(confusions_at_sensitivity(r, at))
end

function Base.show(io::IO, r::ROCCurve)
    print(io, length(r), "-element ", typeof(r).name, " (auc: ", round(auc(r),digits=5), ")")
end

function Base.show(io::IO, ::MIME"text/plain", r::ROCCurve)
    println(io, summary(r), ":")
    fpr = false_positive_rate(r)
    tpr = true_positive_rate(r)
    p = lineplot(fpr, tpr, height=10, width=21, xlim=[0,1], ylim=[0,1], canvas=DotCanvas)
    annotate!(p, :r, 1, "auc: $(round(auc_from_rates(fpr, tpr),digits=5))")
    lineplot!(p, 0, 1, color=:white)
    xlabel!(p, "FPR")
    ylabel!(p, "TPR")
    print(io, p)
end

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

auc(r::ROCCurve) = auc_from_rates(false_positive_rate(r), true_positive_rate(r))
auc(args...; kw...) = auc(roc(args...; kw...))

function auc_from_rates(fpr::AbstractVector{T}, tpr::AbstractVector{R}) where {T,R}
    O = promote_type(T,R)
    @_dimcheck length(fpr) == length(tpr)
    dfpr = diff(fpr)
    dtpr = diff(tpr)
    a = zero(O); b = zero(O)
    @inbounds for i in 1:length(fpr)-1
        a += tpr[i] * dfpr[i]
        b += dtpr[i] * dfpr[i]
    end
    a + b / 2
end

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

@inline _thresholds(th::AbstractVector) = th
@inline _thresholds(th::Number) = range(1, stop=0, length=th)

function roc(targets::AbstractArray,
             outputs::AbstractArray,
             labels::AbstractVector;
             thresholds = 100)
    encoding = LabelEnc.NativeLabels(labels)
    nlabel(encoding) == 2 || error("Multiclass not yet implemented")
    roc(targets, outputs, encoding; thresholds=thresholds)
end

function roc(targets::AbstractArray,
             outputs::AbstractArray,
             encoding::BinaryLabelEncoding = labelenc(targets);
             thresholds = 100)
    th = _thresholds(thresholds)
    curve = confusions(targets, outputs, encoding, th)
    ROCCurve(th, curve)
end


function populationrate(spikes::Array{Spike,1}, windowdur, hopsize)
	ts = [s.t for s ∈ spikes]
	t_min = minimum(ts)
	t_max = maximum(ts)
	ts_bases = t_min:hopsize:t_max-windowdur
	rates = zeros(length(ts_bases))
	for (k, t_base) ∈ enumerate(ts_bases)
		# i1 = searchsortedfirst(ts, t_base)
		# i2 = searchsortedlast(ts, t_base+windowdur)
		# rates[k] = length(ts[i1:i2])/windowdur
		spikes = filter(t -> t_base <= t < t_base + windowdur, ts)
		rates[k] = length(spikes)/windowdur
	end
	(ts_bases, rates)
end

gaussian(x, x0, σ) = 1.0/(σ*√(2*π)) * exp(-0.5 * (x - x0)^2 / σ^2 )

function imagefromspikes(spikes::Array{Spike,1}, t1, t2, n1, n2, fs=1000.0, σ=0.002)
	spikes = filter( s -> t1 <= s.t < t2, spikes )
	image = zeros(n2-n1+1, Int( round((t2-t1)*fs )) )
	if length(spikes) == 0
		return image
	end
	@assert minimum(s.n+1 for s ∈ spikes) >= n1 && maximum(s.n for s ∈ spikes) <= n2
	sigmalen = Int(round(3σ*fs))
	ts = range(-3σ, 3σ, length=2*sigmalen+1)
	bump = gaussian.(ts, 0.0, σ)
	bumplen = length(bump)
	halfbumplen = Int(floor(bumplen/2))
	centeridx = Int(ceil(bumplen/2))
	for s ∈ spikes
		n_idx = s.n - n1 + 1 # + 2 to also account for 0 indexing in C
		t_idx = Int(round((s.t - t1)*fs)) + 1
		t_lo = max(1, t_idx - halfbumplen)
		t_hi = min(size(image, 2), t_idx + halfbumplen)
		offset_lo = t_idx - t_lo
		offset_hi = t_hi - t_idx
		image[n_idx, t_lo:t_hi] .+= bump[centeridx-offset_lo:centeridx+offset_hi]
	end
	image
end

function populationpst(spikes::Array{Spike,1}, inputtimes, dur, n1, n2,
							fs, σ=0.002)
	cumimage = imagefromspikes(spikes, inputtimes[1], inputtimes[1]+dur,
							   n1, n2, fs, σ)
	count = 0
	for time ∈ inputtimes[2:end]
		image = imagefromspikes(spikes, time, time+dur, n1, n2, fs, σ)
		if size(image) == size(cumimage) # in case at end of data
			cumimage .+= image
			count += 1
		end
	end
	cumimage ./ maximum(cumimage)#count
end

function channelsort(spikes::Array{Spike,1}, n::Int)
	times = [Array{Float64,1}(undef,0) for _ ∈ 1:n]
	for s ∈ spikes
		push!(times[s.n], s.t)
	end
	times
end

function sparseac(spiketimes::Array{Float64,1})
	actimes = zeros(length(spiketimes)^2)
	c = 1
	for τ ∈ spiketimes
		for t ∈ spiketimes
			actimes[c] = t - τ
			c += 1
		end
	end
	return actimes |> sort
end

function sparseac(spikes::Array{Spike,1})
	spiketimes = [s.t for s ∈ spikes]
	sparseac(spiketimes)
end

function densehalfac(ts::Array{Float64,1}, fs; norm=true)
	maxdelay = abs(maximum(ts) - minimum(ts))
	maxsamples = Int(round(maxdelay * fs))
	n = maxsamples
	ac = zeros(Float64, n+1)
	for i ∈ 1:length(ts)
		for j ∈ i:length(ts)
			ac[Int(round((ts[j]-ts[i])*fs))+1] += 1.0
		end
	end
	norm && ac ./= ac[1]
	times = (0:n) .* (1/fs)
	return times, ac
end

function densehalfac(spikes::Array{Spike,1}, fs; norm=true)
	ts = [s.t for s ∈ spikes]
	densehalfac(ts, fs; norm=norm)
end

function denseac(spiketimes::Array{Float64,1}, fs; norm=true)
	maxdelay = abs(maximum(spiketimes) - minimum(spiketimes))
	maxsamples = Int(round(maxdelay * fs))
	ac = zeros(2*maxsamples+1)
	mid = maxsamples+1
	for τ ∈ spiketimes
		for t ∈ spiketimes
			ac[Int(round((t-τ)*fs)) + mid] += 1.0
		end
	end
	norm && ac ./= ac[mid]
	times = (-maxsamples:maxsamples) .* (1/fs)
	return times, ac
end

function denseac(spikes::Array{Spike,1}, fs; norm=true)
	spiketimes = [s.t for s ∈ spikes]
	denseac(spiketimes, fs)
end

function spiketodense(spikes::Array{Spike,1}, t1, t2, fs)
	ts = t1:(1.0/fs):t2
	densetrain = zeros(length(ts))
	for spike ∈ spikes
		densetrain[ Int(round((spike.t -t1)*fs)) + 1 ] = 1.0
	end
	return ts, densetrain
end

function corranalysis(spikes::Array{Spike,1}, input::Array{Spike,1},
					  intimes, windur, steps, fs)
	ts, in_ac = densehalfac(input, fs)
	corrvalues = [Array{Float64,1}(undef, length(steps)) for _ ∈ 1:length(intimes)]
	for (i, intime) ∈ enumerate(intimes)
		for (j, step) ∈ enumerate(steps)
			ss = filter(s -> intime+step <= s.t < intime+step+windur, spikes)
			ts, out_ac = densehalfac(ss, fs)
			len = min(length(out_ac), length(in_ac)) # calculate outside loop
			# corrvalues[i][j] = sum(in_ac[1:len] .* out_ac[1:len])
			corrvalues[i][j] = cor([in_ac[1:len]'; out_ac[1:len]']')[1,2]
			println(corrvalues)
		end
	end
	return corrvalues
end

function pstspikes(spikes::Array{Spike,1}, intimes, windur; offset=0.0)
	spikeblocks = [ Array{Spike,1}(undef,0) for _ ∈ 1:length(intimes) ]
	for (k, time) ∈ enumerate(intimes)
		spikeblocks[k] = filter(s -> time+offset <= s.t < time+offset+windur,
							    spikes)
	end
	spikeblocks
end

function averagedelaylength(synapses)
	delays = unique([s.delay for s ∈ synapses]) |> sort
	delay_counts = Dict{UInt16,Float64}()
	delay_vals = Dict{UInt16,Float64}()
	for d ∈ delays
		delay_counts[d] = 0.0
		delay_vals[d] = 0.0
	end

	for s ∈ synapses
		delay_counts[s.delay] += 1.0
		delay_vals[s.delay] += s.strength
	end

	avg_vals = zeros(Float64, length(delays))
	for (k,d) ∈ enumerate(delays)
		avg_vals[k] = delay_vals[d] / delay_counts[d]
	end
	return Int64.(delays), avg_vals
end

## PG search prototyping -- now in C/MPI
# struct PolychronousGroup
# 	nodes::Array{Int64,1}
# 	target::Int64
# 	times::Array{Float64,1}
# 	weights::Array{Float64,1}
# end
#
#
# """
# 	Finds all polychronous groups associated with a set of "base" neurons.
# 	Loops should be fused/optimized (annoying but doable and necessary)
# """
# function testbasegroup(group, graph::DenseGraph{Int64}, weights::DenseGraph{Float64}; threshold=20.0)
# 	s = length(group)
# 	pgs = Array{PolychronousGroup,1}(undef,0)
# 	contacts = Array{Array{Int64,1},1}(undef,s)
# 	for i ∈ 1:s
# 		contacts[i] = graph.contacts[graph.startidcs[group[i]]:graph.startidcs[group[i]]+graph.counts[group[i]]-1]
# 	end
# 	commoncontacts = copy(contacts[1])
# 	for i ∈ 2:s
# 		commoncontacts = filter!(x -> x ∈ contacts[i], commoncontacts)
# 	end
# 	idcs = zeros(Int64,s)
# 	for commoncontact ∈ commoncontacts
# 		for i ∈ 1:s
# 			idcs[i] = findfirst(x -> x == commoncontact, contacts[i])
# 		end
# 		ws = [weights.vals[weights.startidcs[group[k]]+idcs[k]-1] for k ∈ 1:s]
# 		if sum(ws) >= threshold
# 			delays  = [graph.vals[graph.startidcs[group[k]]+idcs[k]-1] for k ∈ 1:s]
# 			delays = maximum(delays) .- delays
# 			push!(pgs, PolychronousGroup(copy(group), commoncontact, delays, ws))
# 		end
# 	end
# 	return pgs
# end
#
#
#
# """
#
# Generate appropriate loop structure with a macro...
# """
# function findpgs(graph, synapses, s=3; threshold=18.0, maxpgs = 10000)
# 	n = size(graph)[1]
# 	group = zeros(Int64, s)
# 	graph = densegraph(graph) 	# remove after resolving graph in C code
# 	#weights = densegraph(weights)
# 	weights = densifysynapses(synapses, n)
# 	pgs = Array{PolychronousGroup,1}(undef,maxpgs)
# 	numpgs = 0
# 	if s == 1
# 		for i ∈ 1:n
# 			group[1] = i
# 			pgnew = testbasegroup(group, graph, weights; threshold=threshold)
# 			if length(pgnew) > 0
# 				push!(pgs, pgnew...)
# 			end
# 		end
# 	elseif s == 2
# 		for i ∈ 1:n
# 			for j ∈ i+1:n
# 				group[1], group[2] = i, j
# 				pgnew = pgingroup(group, graph, weights; threshold=threshold)
# 				if length(pgnew) > 0
# 					push!(pgs, pgnew...)
# 				end
# 			end
# 		end
# 	elseif s == 3
# 		for i ∈ 1:n
# 			println("Finished $i")
# 			for j ∈ i+1:n
# 				for k ∈ j+1:n
# 					group[1],group[2],group[3] = i,j,k
# 					pgnew = pgingroup(group, graph, weights; threshold=threshold)
# 					if length(pgnew) > 0
# 						for a ∈ 1:length(pgnew)
# 							numpgs += 1
# 							pgs[numpgs] = pgnew[a]
# 							if numpgs == maxpgs
# 								return pgs
# 							end
# 						end
# 					end
# 				end
# 			end
# 		end
# 	elseif s == 4
# 		for i ∈ 1:n
# 			for j ∈ i+1:n
# 				for k ∈ j+1:n
# 					for l ∈ k+1:n
# 						group[1],group[2],group[3],group[4] = i,j,k,l
# 						pgnew = pgingroup(group, graph, weights; threshold=threshold)
# 						if length(pgnew) > 0
# 							push!(pgs, pgnew...)
# 						end
# 					end
# 				end
# 			end
# 		end
# 	end
# 	resize!(pgs,numpgs)
# end
#
#
# """
# 	pginspikes(pg::Array{Spike,1},spikes::Array{Spike,1})
#
# Spikes must be time-sorted
# """
# # export pginspikes
# function pginspikes(pg::Array{Spike,1},spikes::Array{Spike,1};
# 					timetol=0.001,hitthreshold=1.0)
# 	baseidcs = findall(x -> x.n == pg[1].n, spikes)
# 	groupinstances = Array{Array{Spike,1},1}(undef,0)
# 	for baseidx ∈ baseidcs
# 		numfound = 0
# 		t1 = spikes[baseidx].t
# 		t2 = t1 + pg[end].t + timetol
# 		k = baseidx
# 		while k < length(spikes)
# 			k += 1
# 			if spikes[k].t > t2
# 				break
# 			end
# 		end
# 		endidx = k
# 		subspikes = @view spikes[baseidx:endidx]
# 		# subspikes = @view spikes[:]
# 		# println(length(subspikes))
# 		localinstance = Array{Spike,1}(undef,length(pg))
# 		localinstance[1] = spikes[baseidx]
# 		numhits = 1
# 		for pgspike ∈ pg[2:end]
# 			for spike ∈ subspikes
# 				if pgspike.n == spike.n
# 					if abs((pgspike.t+t1)-spike.t) <= timetol
# 						numhits += 1
# 						localinstance[numhits] = spike
# 					end
# 				end
# 			end
# 		end
# 		if numhits/length(pg) >= hitthreshold
# 			localinstance = resize!(localinstance,numhits)
# 			push!(groupinstances,localinstance)
# 		end
# 	end
#
# 	return groupinstances
# end
#
# """
# 	resultsintochunks(results::ExperimentOutput, dur=0.35)
#
# Optimize this later.
# """
# function resultsintochunks(results::ExperimentOutput, dur=0.35)
#
# end
#
#
# """
# """
# # export showfoundgroup
# function showfoundgroup(foundgroup, spikes)
# 	t1 = foundgroup[1].t-0.1
# 	t2 = foundgroup[end].t+0.1
# 	p = spikeraster(filter(s->t1<=s.t<=t2, spikes); reuse=false)
# 	spikeraster(p, foundgroup; markersize=7, markeralpha=0.3,markercolor=:red)
# end
#
#


# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     formats: ipynb,jl:hydrogen
#     text_representation:
#       extension: .jl
#       format_name: hydrogen
#       format_version: '1.2'
#       jupytext_version: 1.2.1
#   kernelspec:
#     display_name: Julia 1.6.0-beta1 depwarn -O3
#     language: julia
#     name: julia-1.6-depwarn-o3
# ---

# %% [markdown]
# # Ising2D
#
# * Copyright (c) 2020 Gen Kuroki
# * License: MIT
# * Repository: https://github.com/genkuroki/Ising2D.jl

# %% [markdown] {"toc": true}
#
Table of Contents

#

1  Benchmarks

2  Examples

3  Documents

# %%
if isfile("Project.toml")
    using Pkg
    Pkg.activate(".")
    using Revise
end

# %%
using Ising2D, Plots, Random

# %% [markdown]
# ## Benchmarks

# %%
using Ising2D, Random
Random.seed!(464937337564)
print("IfElse() on v$VERSION:")
@time s = ising2d!(rand_ising2d(), β_ising2d, 10^5; algorithm=IfElse())
plot_ising2d(s)

# %%
using Ising2D, Random
Random.seed!(464937337564)
print("MultiFor() on v$VERSION:")
@time s = ising2d!(rand_ising2d(), β_ising2d, 10^5; algorithm=MultiFor())
plot_ising2d(s)

# %%
using Ising2D, BenchmarkTools, Random
Random.seed!(464937337564)
s = rand_ising2d()
print("IfElse() on v$VERSION:")
@btime ising2d!($s; algorithm=IfElse());

# %%
using Ising2D, BenchmarkTools, Random
Random.seed!(464937337564)
s = rand_ising2d()
print("MultiFor() on v$VERSION:")
@btime ising2d!($s; algorithm=MultiFor());

# %% [markdown]
# ## Examples

# %%
Random.seed!(464937337564);

# %%
s = rand_ising2d(200)
P0 = plot_ising2d(s)

# %%
ising2d!(s, β_ising2d, 500)
P1 = plot_ising2d(s)

# %%
png(P0, "images/s0.png")
png(P1, "images/s1.png")

# %%
s = rand_ising2d(200)
gif_ising2d(s=s, k=1.0, nwarmups=0, nskips=1, niters=500, fps=15,
    gifname="images/ising2d.gif")

# %%
s₀ = rand_ising2d()
@show energy_density_ising2d(s₀)
@show magnetization_ising2d(s₀)
s = copy(s₀)
S = mcmc_ising2d!(s=s, k=1.0, nwarmups=1000, nskips=100, niters=5000)
E = energy_density_ising2d.(S)
M = magnetization_ising2d.(S)
@show mean(E)
@show mean(M)

histogram_mcmc_ising2d(S)

# %%
plot_mcmc_ising2d(S)

# %%
plot_mcmc_ising2d(S; t=4000)

# %%
gif_mcmc_ising2d(S[10:10:end]; gifname="images/ising2d_mcmc.gif")

# %% [markdown]
# ## Documents

# %%
?Ising2D

# %%
?Ising2D.β_ising2d

# %%
?Ising2D.rand_ising2d

# %%
?Ising2D.ones_ising2d

# %%
?Ising2D.ising2d!

# %%
?Ising2D.plot_ising2d

# %%
?Ising2D.gif_ising2d

# %%
?Ising2D.mcmc_ising2d!

# %%
?Ising2D.energy_density_ising2d

# %%
?Ising2D.magnetization_ising2d

# %%
?Ising2D.plot_mcmc_ising2d

# %%
?Ising2D.histogram_mcmc_ising2d

# %%
?Ising2D.gif_mcmc_ising2d

# %%


# # Modal analysis of a free-floating steel circle: three-dimensional solid model

# Source code: [`circle_3d_modal_conv_tut.jl`](circle_3d_modal_conv_tut.jl)

# ## Description

# Vibration analysis of a free-floating steel ring. This is a
# benchmark from the NAFEMS Selected Benchmarks for Natural Frequency Analysis,
# publication: Test VM09: Circular Ring --  In-plane and Out-of-plane
# Vibration.

# The results can be compared with analytical expressions, but the main purpose
# is to compute data for extrapolation to the limit to predict the true natural
# frequencies.

# ## Reference frequencies

# There will be 6 rigid body modes (zero natural frequencies).

# The numerical results are due to the publication:
# NAFEMS Finite Element Methods & Standards, Abbassian, F., Dawswell, D. J., and
# Knowles, N. C. Selected Benchmarks for Natural Frequency Analysis, Test No.
# 6. Glasgow: NAFEMS, Nov., 1987.

# The reference values were analytically determined (Blevins, FORMULAS FOR
# DYNAMICS, ACOUSTICS AND VIBRATION, Table 4.16). Note that shear flexibility
# was neglected when computing the reference values.

# | Mode       |         Reference Value (Hz)  |  NAFEMS Target Value (Hz) |
# | -------   |     -------  |  ------- |
# | 7, 8 | (out of plane)   |        51.85          |         52.29  |
# | 9, 10 |  (in plane)       |       53.38         |          53.97  |
# | 11, 12 |  (out of plane)   |     148.8          |         149.7  |
# | 13, 14 |  (in plane)       |     151.0          |         152.4  |
# | 15, 16 |  (out of plane)   |     287.0          |         288.3  |
# |  17, 18 |  (in plane)      |      289.5         |          288.3  |

# ## Goals

# - Show convergence relative to reference values.
# - Compute data for extrapolation to the limit to predict the true natural
#   frequencies.

# ## Definition of the basic inputs

# The finite element code realize on the basic functionality implemented in this
# package.
using FinEtools
using FinEtoolsDeforLinear
using SymRCM
using Arpack
using LinearAlgebra

# The material parameters may be defined with the specification of the units.
# The elastic properties are:
E = 200.0 * phun("GPa")
nu = 0.3;

# The mass density is
rho = 8000 * phun("kg/m^3")

# The geometry consists of the radius of the circle, and the diameter of the
# circular cross-section.
radius = 1.0 * phun("m"); diameter = 0.1 * phun("m");

# We shall calculate these eigenvalues, but we are mostly interested in the
# first three  natural frequencies.
neigvs = 18;

# The mass shift needs to be applied since the structure is free-floating.
oshift = (2*pi*15)^2

# We will generate this many elements per radius of the cross-section, and along
# the length of the circular ring.
results = let
    results = []
    for i in 1:3
        nperradius, nL = 2^i, 20*2^i
        tolerance = diameter/nperradius/100
        # Generate the mesh in a straight cylinder.
        fens, fes = H8cylindern(diameter/2, 2*pi, nperradius, nL)
        # Select the nodes in the bases of the cylinder for future merging of the nodes.
        z0l = selectnode(fens, plane = [0, 0, 1, 0], inflate = tolerance)
        z2l = selectnode(fens, plane = [0, 0, 1, 2*pi], inflate = tolerance)
        # Twist the straight cylinder into a ring.
        for i in 1:count(fens)
            a = fens.xyz[i, 3]
            x = fens.xyz[i, 1]
            y = fens.xyz[i, 2] + radius
            fens.xyz[i, :] .= (x, y*cos(a), y*sin(a))
        end
        # Merge the nodes of the bases, which involves renumbering the connectivity.
        fens, fes = mergenodes(fens, fes, tolerance, vcat(z0l, z2l))

        # If desired, visualize the mesh.
        # File = "ring.vtk"
        # vtkexportmesh(File, fens, fes)
        # @async run(`"paraview.exe" $File`)

        # Generate the material and the FEM machine.
        MR = DeforModelRed3D
        material = MatDeforElastIso(MR, rho, E, nu, 0.0)
        femm = FEMMDeforLinearESNICEH8(MR, IntegDomain(fes, NodalTensorProductRule(3)), material)
        # Set up the nodal fields for the geometry in the displacements.
        geom = NodalField(fens.xyz)
        u = NodalField(zeros(size(fens.xyz,1),3)) # displacement field
        # Renumber the nodes to produce quicker linear equation solves.
        p = let
            C = connectionmatrix(femm, count(fens))
            p = symrcm(C)
        end
        numberdofs!(u, p)
        # Now set up the discrete model.
        associategeometry!(femm,  geom)
        K  = stiffness(femm, geom, u)
        M = mass(femm, geom, u)

        # Solve the free vibration problem.
        evals, evecs, nconv = eigs(Symmetric(K+oshift*M), Symmetric(M); nev=neigvs, which=:SM, explicittransform = :none)
        # Correct for the mass shift.
        evals = evals .- oshift;
        sigdig(n) = round(n * 10000) / 10000
        fs = real(sqrt.(complex(evals)))/(2*pi)
        println("Eigenvalues: $(sigdig.(fs)) [Hz]")
        push!(results, (fs[6+1], fs[6+3], fs[6+5]))
    end
    results # return these values from the block
end

@show results

# ## Richardson extrapolation

# Here we will use Richardson extrapolation from the three sets of data. This
# will allow us to predict the convergence rate and the true solution for each
# of the three frequencies (or rather the pairs of frequencies, 7 and 8, 9 and
# 10, and 11 and 12).

# We will immediately set up the convergence plots. We will extrapolate and then
# compute from that the normalized error to be plotted with respect to the
# refinement factors (which in this case are 4, 2, and 1). We use the
# refinement factor as a convenience: we will calculate the element size by
# dividing the circumference of the ring with a number of elements generated
# circumferentially.

using FinEtools.AlgoBaseModule: richextrapol

using Gnuplot
@gp  "set terminal windows 0 "  :-

# Modes 7 and 8
sols = [r[1] for r in results]
resextrap = richextrapol(sols, [4.0, 2.0, 1.0])
print("Predicted frequency 7 and 8: $(resextrap[1])\n")
errs = abs.(sols .- resextrap[1])./resextrap[1]
@gp  :- 2*pi*radius./[80, 160, 320] errs " lw 2 lc rgb 'red' with lp title 'Mode 7, 8' "  :-

# Modes 9 and 10
sols = [r[2] for r in results]
resextrap = richextrapol(sols, [4.0, 2.0, 1.0])
print("Predicted frequency 9 and 10: $(resextrap[1])\n")
errs = abs.(sols .- resextrap[1])./resextrap[1]
@gp  :- 2*pi*radius./[80, 160, 320] errs " lw 2 lc rgb 'green' with lp title 'Mode 9, 10' "  :-

# Modes 11 and 12
sols = [r[3] for r in results]
resextrap = richextrapol(sols, [4.0, 2.0, 1.0])
print("Predicted frequency 11 and 12: $(resextrap[1])\n")
errs = abs.(sols .- resextrap[1])./resextrap[1]
@gp  :- 2*pi*radius./[80, 160, 320] errs " lw 2 lc rgb 'blue' with lp title 'Mode 11, 12' "  :-

@gp  :- "set xrange [0.01:0.1]" "set logscale x" :-
@gp  :- "set logscale y" :-
@gp  :- "set xlabel 'Element size'" :-
@gp  :- "set ylabel 'Normalized error [ND]'" :-
@gp  :- "set title '3D: Convergence of modes 7, ..., 12'"


#
# To plot all cutoff functions, uncomment the code at the end of th e
# file and then execute the entire file.
#

using Test, NBodyIPs, BenchmarkTools
using NBodyIPs.Cutoffs
using NBodyIPs: fcut, fcut_d
import JuLIP

r0 = 0.712
rn = 1.234
rc = 2.890

cutoffs = [ CosCut(rc-1.0, rc),
            CosCut2s(r0, r0+0.4, rc-0.9, rc),
            PolyCut(2, rc),
            PolyCutSym(2, rc),
            PolyCut2sA(r0, rn, rc) ]
cnames = ["CosCut", "CosCut2s", "PolyCut", "PolyCutSym", "PolyCut2sA"]

##
@info("Testing Correctness of Derivative Implementations")
xt = [range(r0-0.5, rc+0.5, length=50); r0; rc]
for (C, name) in zip(cutoffs, cnames)
   println("Testing Derivatives of $name")
   f = r -> fcut(C, r)
   df = r -> fcut_d(C, r)
   println(@test JuLIP.Testing.fdtest_R2R(f, df, xt))
   Cd = Dict(C)
   C1 = JuLIP.decode_dict(Cd)
   Cd1 = Dict(Cd)
   println((@test C1 == C))
   println((@test Cd == Cd1))
end

##
@info("Testing Performance of fcut and fcut_d")
@info("One test includes 1_000 fcut/fcut_d calls")
function runN(f, x, dx, N)
   s = 0.0
   for n = 1:N
      x += dx
      s += f(x)
   end
   return s
end

N = 1_000
x0 = r0
dx = (rc+0.3-r0)/N

for (C, name) in zip(cutoffs, cnames)
   println(name)
   print("   fcut  : ")
   @btime runN($(r->fcut(C,r)), $x0, $dx, $N)
   print("   fcut_d: ")
   @btime runN($(r->fcut_d(C,r)), $x0, $dx, $N)
end

##
# using Plots
# xx = range(r0-0.5, rc+0.5, length=200)
# P = plot(; ylims = [-0.2, 1.2])
# for (C, name) in zip(cutoffs, cnames)
#    plot!(xx, fcut.(Ref(C), xx), label=name)
# end
# P = scatter!([r0, rc], [0, 0], label = "r0, rcut")
# P = vline!([rn], c=:black, ls=:dash, label = "rn")
# display(P)


"""
    ismeanfeasible(m::Model; [toler::Number = 1e-5, solver = Clp.ClpSolver()])
Returns a `Bool` indicating if the mean stored in `FlexibilityData.RVmeans` is feasible, meaning that it
lies inside the feasible region. This check is done using the so-called feasibility function.

**Arguments**
- `m::Model` The flexibility model.

**Keyword Arguments**
- `toler::Number = 1e-5` The numerical tolerance for checking the feasibility.
- `solver = Clp.ClpSolver()` The solver, any LP or NLP solver shoudl work.

```julia
julia> ismeanfeasible(m)
true

julia> ismeanfeasible(m, solver = GurobiSolver(OutputFlag = 0))
Academic license - for non-commercial use only
true
```
"""
function ismeanfeasible(m::Model; toler::Number = 1e-5, solver = Clp.ClpSolver())
    # Make the input dictionary
    input_dict = MakeInputDict(m)

    # Parse the necessary data dimensions
    n_z = input_dict["dims"][1]
    n_θ = input_dict["dims"][2]
    n_x = input_dict["dims"][3]
    n_f = input_dict["dims"][4]
    n_h = input_dict["dims"][5]

    # Prepare the JuMP model
    m_solve = Model(solver = solver)

    # Initialize variables
    @variable(m_solve, θ[1:n_θ])
    @variable(m_solve, u)
    if n_z != 0
        @variable(m_solve, z[1:n_z])
    end
    if n_x != 0
        @variable(m_solve, x[1:n_x])
    end

    # Set the objective
    @objective(m_solve, Min, u)

    # Fix θ and set values
    for i = 1:n_θ
        JuMP.fix(θ[i], input_dict["theta_nom"][i])
    end

    # Add the constraints
    exprs = AddSystemExpressions(m_solve, input_dict)
    @constraint(m_solve, f[j = 1:n_f], exprs[1][j] <= u)
    if n_h != 0
        @constraint(m_solve, h[i = 1:n_h], exprs[2][i] == 0)
    end

    # Solve and return
    status = solve(m_solve)
    return getvalue(u) <= toler
end

"""
    ComputeCenter(m::Model, center::Symbol, solver, toler::Number, only_positive::Bool)
Returns a center point that can be used to replace the mean if desired.

**Arguments**
- `m::Model` The flexibility model.
- `center::Symbol` Indicates the type of center, accepted arguments are `:feasible` and `:analytic`.
- `solver` The solver which must be an NLP solver for the analytic center.
- `toler::Number` The tolerance to check solution validity.
- `only_positive::Bool` Indicates if the center need by strictly positive.
"""
function ComputeCenter(m::Model, center::Symbol, solver, toler::Number, only_positive::Bool)
    # Check input
    if center != :analytic && center != :feasible
        error("Invalid argument center = $center, must be :analytic or :feasible")
    end

    # Make the input dictionary
    input_dict = MakeInputDict(m)

    # Parse the necessary data dimensions
    n_z = input_dict["dims"][1]
    n_θ = input_dict["dims"][2]
    n_x = input_dict["dims"][3]
    n_f = input_dict["dims"][4]
    n_h = input_dict["dims"][5]

    # Prepare the JuMP model
    m_solve = Model(solver = solver)

    # Initialize variables
    if only_positive
        @variable(m_solve, θ[1:n_θ] >= 0)
    else
        @variable(m_solve, θ[1:n_θ])
    end
    if center == :feasible
        @variable(m_solve, u)
    else
        @variable(m_solve, s[1:n_f] >= 0)
    end
    if n_z != 0
        @variable(m_solve, z[1:n_z])
    end
    if n_x != 0
        @variable(m_solve, x[1:n_x])
    end

    # Set objective
    if center == :feasible
        @objective(m_solve, Min, u)
    else
        @NLobjective(m_solve, Min, -sum(log(s[i]) for i = 1:n_f))
    end

    # Add constraints
    exprs = AddSystemExpressions(m_solve, input_dict)
    if center == :feasible
        @constraint(m_solve, f[j = 1:n_f], exprs[1][j] <= u)
    else
        @constraint(m_solve, f[j = 1:n_f], exprs[1][j] + s[j] == 0)
    end
    if n_h != 0
        @constraint(m_solve, h[i = 1:n_h], exprs[2][i] == 0)
    end

    # Solve and return
    status = solve(m_solve)
    if center == :feasible && getvalue(u) > toler
        upper_b = getvalue(u)
        @warn "Optimized mean is not feasible, can only achieve inequalities with upper bound u = $upper_b"
    end
    return getvalue(θ)
end

"""
    findcenteredmean(m::Model; [center::Symbol = :feasible, solver = Ipopt.IpoptSolver(print_level = 0), toler::Number = 1e-5, update_mean::Bool = false, only_positive::Bool = false])
Returns a center point based on the analytic or feasible center. The result can overwrite the mean stored in `FlexibilityData.RVmeans`
if desired. This is a wrapper function for [`ComputeCenter`](@ref).

**Arguments**
- `m::Model` The flexibility model.

**Keyword Arguments**
- `center::Symbol = :feasible` Indicates the type of center, accepted arguments are `:feasible` and `:analytic`.
- `solver = Ipopt.IpoptSolver(print_level = 0)` The solver which must be an NLP solver for the analytic center.
- `toler::Number = 1e-5` The tolerance to check solution validity.
- `update_mean::Bool = false` Indicates if the computed center should overwrite `FlexibilityData.RVmeans`.
- `only_positive::Bool = false` Indicates if the center need by strictly positive.

```julia
julia> findcenteredmean(m, only_positive = true)
4-element Array{Float64,1}:
 1684.74
   79.0718
  195.073
    0.0

julia> findcenteredmean(m, center = :analytic, update_mean = true)
4-element Array{Float64,1}:
  898.125
 -507.214
  594.544
  317.23
```
"""
function findcenteredmean(m::Model; center::Symbol = :feasible, solver = Ipopt.IpoptSolver(print_level = 0), toler::Number = 1e-5, update_mean::Bool = false, only_positive::Bool = false)
    center = ComputeCenter(m, center, solver, toler, only_positive) # included in functions.jl
    if update_mean
        flex_data = getflexibilitydata(m)
        flex_data.RVmeans = center
    end
    return center
end


"""
    is_leap_year(year)

Return `true` if `year` is a leap year in the gregorian calendar.

"""
function is_leap_year(year)
    return year % 4 == 0 && year % 100 != 0 || year % 400 == 0
end