"""
    metropolis!(model::Ising)

Implementation of the Metropolis algorithm for the Ising model.

Arguments
* `model`: Ising type
"""
function metropolis!(model::Ising)
    t₀ = floor(Int, model.params.cutoff * model.params.n_sweeps)
    ΔE = [-8, -4, 0, 4, 8]
    P = exp(-model.params.β) .^ ΔE

    for t = 1:model.params.n_sweeps
        for j = 1:model.params.L, i = 1:model.params.L
            k = Int(model.σ[i, j] * sum_of_neighbors(model, (i, j)) / 2 + 3)

            (ΔE[k] <= 0 || rand(model.rng) < P[k]) && (model.σ[i, j] *= -1)
        end

        t > t₀ && update_observables!(model)
    end

    compute_observables_statistics!(model)
    return model
end

"""
    metropolis!(model::Potts)

Implementation of the Metropolis algorithm for the Potts model.

Arguments
* `model`: Potts type
"""
function metropolis!(model::Potts)
    t₀ = floor(Int, model.params.cutoff * model.params.n_sweeps)
    ΔE = collect(-4:4)
    P = exp(-model.params.β) .^ ΔE

    for t = 1:model.params.n_sweeps
        for j = 1:model.params.L, i = 1:model.params.L
            counts = compute_counts(model, (i, j))
            old_spin = model.σ[i, j]
            new_spin = rand(model.rng, UnitRange{Int8}(1, model.params.q))
            k = (counts[old_spin] - counts[new_spin]) + 5

            (ΔE[k] <= 0 || rand(model.rng) < P[k]) && (model.σ[i, j] = new_spin)
        end

        t > t₀ && update_observables!(model)
    end

    compute_observables_statistics!(model)
    return model
end

"""
    metropolis!(model::XY)

Implementation of the Metropolis algorithm for the XY model.

Arguments
* `model`: XY type
"""
function metropolis!(model::XY)
    t₀ = floor(Int, model.params.cutoff * model.params.n_sweeps)
    eᵝ = exp(-model.params.β)

    for t = 1:model.params.n_sweeps
        for j = 1:model.params.L, i = 1:model.params.L
            old_spin = model.σ[i, j]
            new_spin = random_XYVector(model.rng)
            ΔE = dot(old_spin .- new_spin, sum_of_neighbors(model, (i, j)))

            (ΔE <= 0 || rand(model.rng) < eᵝ^ΔE) && (model.σ[i, j] = new_spin)
        end

        t > t₀ && update_observables!(model)
    end

    compute_observables_statistics!(model)
    return model
end

"""
    heat_bath!(model::Ising)

Implementation of the heat bath algorithm for the Ising model.

Arguments
* `model`: Ising type
"""
function heat_bath!(model::Ising)
    t₀ = floor(Int, model.params.cutoff * model.params.n_sweeps)
    ΔE = [-8, -4, 0, 4, 8]
    P = exp(-model.params.β) .^ ΔE

    for t = 1:model.params.n_sweeps
        for j = 1:model.params.L, i = 1:model.params.L
            k = Int(model.σ[i, j] * sum_of_neighbors(model, (i, j)) / 2 + 3)

            (rand(model.rng) < P[k] / (P[k] + 1)) && (model.σ[i, j] *= -1)
        end

        t > t₀ && update_observables!(model)
    end

    compute_observables_statistics!(model)
    return model
end

"""
    heat_bath!(model::Potts)

Implementation of the heat bath algorithm for the Potts model.

Arguments
* `model`: Potts type
"""
function heat_bath!(model::Potts)
    t₀ = floor(Int, model.params.cutoff * model.params.n_sweeps)

    for t = 1:model.params.n_sweeps
        for j = 1:model.params.L, i = 1:model.params.L
            counts = compute_counts(model, (i, j))
            old_spin = model.σ[i, j]
            ps = exp.(model.params.β .* counts)
            ps ./= sum(ps)
            R = rand(model.rng)
            P = 0.0

            for (new_spin, p) in enumerate(ps)
                P += p
                R < P && (model.σ[i, j] = new_spin; break)
            end
        end

        t > t₀ && update_observables!(model)
    end

    compute_observables_statistics!(model)
    return model
end

"""
    heat_bath!(model::XY)

Implementation of the heat bath algorithm for the XY model.

Arguments
* `model`: XY type
"""
function heat_bath!(model::XY)
    t₀ = floor(Int, model.params.cutoff * model.params.n_sweeps)
    eᵝ = exp(-model.params.β)

    for t = 1:model.params.n_sweeps
        for j = 1:model.params.L, i = 1:model.params.L
            old_spin = model.σ[i, j]
            new_spin = random_XYVector(model.rng)
            ΔE = dot(old_spin .- new_spin, sum_of_neighbors(model, (i, j)))

            (rand(model.rng) < eᵝ^ΔE / (eᵝ^ΔE + 1)) && (model.σ[i, j] = new_spin)
        end

        t > t₀ && update_observables!(model)
    end

    compute_observables_statistics!(model)
    return model
end


using LineSearches
function bellmanobjective(system::DynamicSystem1D,
                          t,x,a,ω, vt::Function)
    numsamples = length(ω)
    retval = 0.
    @simd for n = 1:numsamples
        retval += system.U(t+1,x,a,ω[n]) + vt(system.f(t+1,x,a,ω[n]))
    end
    return -retval / numsamples # Optim minimizes
end

function bellmanobjective(system::DynamicSystem1D,
                          t,x,a,ω, Iv::AbstractInterpolation)
    vt(x) = Iv[x]
    bellmanobjective(system,t,x,a,ω,vt)
end

function solvebellman!(v, α, system::DynamicSystem1D, xtup, ω)
    K = length(xtup[1])
    T = system.T
    @simd for i = 1:K
        v[i,T+1] = system.Ubar(xtup[1][i])
    end
    ti = T
    t = ti-1
    v[1,ti] = system.Ubar(xtup[1][1]) # Boundary condition
    @simd for i = K:-1:2
        objective(a) = bellmanobjective(system,t,xtup[1][i],a, ω[:,ti], system.Ubar)
        res = optimize(objective, system.amin, system.amax)
        v[i,ti] = -Optim.minimum(res)
        α[i,ti] = Optim.minimizer(res)
    end

    for ti = T-1:-1:1
        Iv = interpolate(xtup, v[:,ti+1], Gridded(Linear()))
        t = ti-1
        v[1,ti] = system.Ubar(xtup[1][1]) # Boundary condition
        @simd for i = K:-1:2
            objective(a) = bellmanobjective(system,t,xtup[1][i],a, ω[:,ti], Iv)
            res = optimize(objective, system.amin, system.amax)
            v[i,ti] = -Optim.minimum(res)
            α[i,ti] = Optim.minimizer(res)
        end
    end
    #α[1,:] = NaN # No policy at x=0
    α[1,:] = system.amax
end

function bellmanobjective(system::DynamicSystem,
                          t,x,a,ω::Tuple, vt::Function)
    numsamples = length(ω[1])
    retval = 0.
    @simd for n = 1:numsamples
        wn = [ωi[n] for ωi in ω]
        retval += system.U(t+1,x,a,wn) + vt(system.f(t+1,x,a,wn))
    end
    return -retval / numsamples
end

function bellmanobjective(system::DynamicSystem,
                          t,x,a,ω::Tuple,Iv::AbstractInterpolation)
    vt(x) = Iv[x...]
    bellmanobjective(system,t,x,a,ω,vt)
end

function solvebellman!(v, α, system::DynamicSystem, xtup, ω::Tuple)
    # TODO: use CartesianIndex stuff to allow any dimension
    #       see http://julialang.org/blog/2016/02/iteration
    @assert system.statedim == 2     # TODO: generalise
    @assert length(system.amin(0,xtup[1])) == 1 # TODO: generalise

    K = [length(xtup[i]) for i = 1:system.statedim]
    T = system.T
    vti = view(v,:,:,T+1)
    for i = 1:K[1]
        @simd for j=1:K[2]
            xij = [xtup[1][i], xtup[2][j]]
            vti[j,i] = system.Ubar(xij)
        end
    end

    ti = T
    vti = view(v,:,:,ti)
    αti = view(α,:,:,ti)
    ωti = tuple([view(ωi,:,ti) for ωi in ω]...)
    t = ti-1
    @show t
    # Boundary condition
    @simd for j = 1:K[2]
        xij = [xtup[1][1], xtup[2][j]] # TODO: CartesianIndex on xtup somehow?
        vti[j,1] = system.Ubar(xij)
    end
    for i = 2:K[1] # TODO: CartesianIndex on vti
        @simd for j = 1:K[2]
            xij = [xtup[1][i], xtup[2][j]] # TODO: CartesianIndex on xtup somehow?
            objective(a) = bellmanobjective(system,t,xij,a, ωti, system.Ubar)
            res = optimize(objective, system.amin(t,xij), system.amax(t,xij))
            #res = optimize(objective, [0.5], LBFGS(linesearch = LineSearches.BackTracking()),
            #               OptimizationOptions(autodiff=true))
            @inbounds vti[j,i] = -Optim.minimum(res)#[1]
            @inbounds αti[j,i] = Optim.minimizer(res)#[1]
        end
    end

    for ti = T-1:-1:1
        # TODO: How to deal with extrapolations?
        Iv = interpolate(xtup, vti', Gridded(Linear()))
        #Iv = extrapolate(Iv,Interpolations.Throw())
        vti = view(v,:,:,ti)
        αti = view(α,:,:,ti)
        ωti = tuple([view(ωi,:,ti) for ωi in ω]...)
        t = ti-1
        @show t
        # Boundary condition
        @simd for j = 1:K[2]
            xij = [xtup[1][1], xtup[2][j]] # TODO: CartesianIndex on xtup somehow?
            vti[j,1] = system.Ubar(xij)
        end
        for i = 2:K[1] # TODO: CartesianIndex on vti
            @simd for j = 1:K[2]
                xij = [xtup[1][i], xtup[2][j]] # TODO: CartesianIndex on xtup somehow?
                objective(a) = bellmanobjective(system,t,xij,a, ωti, Iv)
                res = optimize(objective, system.amin(t,xij), system.amax(t,xij))
                vti[j,i] = -Optim.minimum(res)
                αti[j,i] = Optim.minimizer(res)
                #@show xij
                #@show vti[j,i], αti[j,i]
            end
        end
    end
    @inbounds α[:,1,:] = NaN # No policy at x[1]=0
end


# Wrapper for whatever linear programming we need, in case we don't want to
# use JuMP (e.g., for latency reasons).

module LP

import JuMP
using JuMP: @variable, @objective, @constraint
using MathOptInterface: OPTIMAL
using Clp

mutable struct Model
    LP          :: JuMP.Model
    vars
    base
    shape
    maxval
end
function Model(shape)

    P = JuMP.Model(JuMP.with_optimizer(Clp.Optimizer, LogLevel = 0))
    Y = shape
    n = length(Y)

    @variable(P, x[i=1:n, j=1:Y[i]] >= 0)

    @variable(P, z >= 0)
    @constraint(P, z == sum(x[i, Y[i]] for i=1:n))

    @objective(P, Min, sum(x))

    @constraint(P, [i=1:n, j=1:Y[i]-1], x[i,j] + 1 ≤ x[i,j+1])

    Model(P, x, [], Y, z)

end

set_base!(m::Model, idx) = (m.base = idx)
get_base(m::Model) = m.base

optimize!(m::Model) = JuMP.optimize!(m.LP)

function maybe(func!::Function, m::Model)
    cons = func!(m)
    optimize!(m)
    status = JuMP.termination_status(m.LP)
    for con in cons
        JuMP.delete(m.LP, con)
    end
    return status == OPTIMAL
end

maybe_lt(m::Model, lhs, rhs) = maybe(m->lt!(m, lhs, rhs), m)
maybe_gt(m::Model, lhs, rhs) = maybe(m->gt!(m, lhs, rhs), m)

vars(m::Model, idx) = [m.vars[i, j] for (i,j) in zip(m.base, idx)]

lhs_minus_rhs(m::Model, lhs, rhs) = sum(vars(m, lhs)) - sum(vars(m, rhs))

gt!(m::Model, lhs, rhs) = [@constraint(m.LP, lhs_minus_rhs(m, lhs, rhs) ≥ 1)]
lt!(m::Model, lhs, rhs) = gt!(m, rhs, lhs)
eq!(m::Model, lhs, rhs) = [@constraint(m.LP, lhs_minus_rhs(m, lhs, rhs) == 0)]

value(m::Model, i, j) = JuMP.value(m.vars[i, j])

fix(m::Model, i, j, v) = JuMP.fix(m.vars[i, j], v; force = true)
fixtotal(m::Model, v) = JuMP.fix(m.maxval, v; force = true)

end


@doc raw"""
Axisymmetric tokamak equilibrium in (x,y,z) coordinates with covariant
components of the vector potential given by
```math
A (x,y,z) = \frac{1}{2} \frac{B_0}{q_0} \, \bigg( \frac{q_0 R_0 x z - r^2 y}{R^2} , \, \frac{q_0 R_0 y z + r^2 x}{R^2} , \, - q_0 R_0 \, \ln \bigg( \frac{R}{R_0} \bigg) \bigg)^T ,
```
resulting in the magnetic field with covariant components
```math
B (x,y,z) = \frac{B_0}{q_0} \, \bigg( - \frac{q_0 R_0 y + x z}{R^2} , \, \frac{q_0 R_0 x - y z}{R^2} , \, \frac{R - R_0}{R} \bigg)^T ,
```
where $R = \sqrt{ x^2 + y^2 }$ and $r = \sqrt{ (R - R_0)^2 + z^2 }$.

Parameters:
* `R₀`: position of magnetic axis
* `B₀`: B-field at magnetic axis
* `q₀`: safety factor at magnetic axis
"""
module AxisymmetricTokamakCartesian

    using RecipesBase

    import ..ElectromagneticFields
    import ..ElectromagneticFields: CartesianEquilibrium, code
    import ..AnalyticCartesianField: X, Y, Z

    export  AxisymmetricTokamakCartesianEquilibrium

    const DEFAULT_R₀ = 1
    const DEFAULT_B₀ = 1
    const DEFAULT_q₀ = 2

    const ITER_R₀ = 6.2
    const ITER_B₀ = 5.3
    const ITER_q₀ = √2

    struct AxisymmetricTokamakCartesianEquilibrium{T <: Number} <: CartesianEquilibrium
        name::String
        R₀::T
        B₀::T
        q₀::T

        function AxisymmetricTokamakCartesianEquilibrium{T}(R₀::T, B₀::T, q₀::T) where T <: Number
            new("AxisymmetricTokamakCartesianEquilibrium", R₀, B₀, q₀)
        end
    end

    AxisymmetricTokamakCartesianEquilibrium(R₀::T=DEFAULT_R₀, B₀::T=DEFAULT_B₀, q₀::T=DEFAULT_q₀) where T <: Number = AxisymmetricTokamakCartesianEquilibrium{T}(R₀, B₀, q₀)

    function init(R₀=DEFAULT_R₀, B₀=DEFAULT_B₀, q₀=DEFAULT_q₀)
        AxisymmetricTokamakCartesianEquilibrium(R₀, B₀, q₀)
    end

    function ITER()
        AxisymmetricTokamakCartesianEquilibrium(ITER_R₀, ITER_B₀, ITER_q₀)
    end

    macro code(R₀=DEFAULT_R₀, B₀=DEFAULT_B₀, q₀=DEFAULT_q₀)
        code(init(R₀, B₀, q₀); escape=true)
    end

    macro code_iter()
        code(ITER(); escape=true)
    end

    function Base.show(io::IO, equ::AxisymmetricTokamakCartesianEquilibrium)
        print(io, "Axisymmetric Tokamak Equilibrium in (x,y,z) Coordinates with\n")
        print(io, "  R₀ = ", equ.R₀, "\n")
        print(io, "  B₀ = ", equ.B₀, "\n")
        print(io, "  q₀ = ", equ.q₀)
    end

    R²(x::AbstractVector, equ::AxisymmetricTokamakCartesianEquilibrium) = X(x,equ)^2 + Y(x,equ)^2
    r²(x::AbstractVector, equ::AxisymmetricTokamakCartesianEquilibrium) = (R(x,equ) - equ.R₀)^2 + Z(x,equ)^2
    R(x::AbstractVector, equ::AxisymmetricTokamakCartesianEquilibrium) = sqrt(R²(x,equ))
    r(x::AbstractVector, equ::AxisymmetricTokamakCartesianEquilibrium) = sqrt(r²(x,equ))
    θ(x::AbstractVector, equ::AxisymmetricTokamakCartesianEquilibrium) = atan(Z(x,equ), R(x,equ) - equ.R₀)
    ϕ(x::AbstractVector, equ::AxisymmetricTokamakCartesianEquilibrium) = atan(Y(x,equ), X(x,equ))

    ElectromagneticFields.A₁(x::AbstractVector, equ::AxisymmetricTokamakCartesianEquilibrium) = + equ.B₀ * (equ.R₀ * X(x,equ) * Z(x,equ) - r²(x,equ) * Y(x,equ) / equ.q₀ ) / R²(x,equ) / 2
    ElectromagneticFields.A₂(x::AbstractVector, equ::AxisymmetricTokamakCartesianEquilibrium) = + equ.B₀ * (equ.R₀ * Y(x,equ) * Z(x,equ) + r²(x,equ) * X(x,equ) / equ.q₀ ) / R²(x,equ) / 2
    ElectromagneticFields.A₃(x::AbstractVector, equ::AxisymmetricTokamakCartesianEquilibrium) = - equ.B₀ * equ.R₀ * log(R(x,equ) / equ.R₀) / 2

    ElectromagneticFields.get_functions(::AxisymmetricTokamakCartesianEquilibrium) = (X=X, Y=Y, Z=Z, R=R, r=r, θ=θ, ϕ=ϕ, R²=R², r²=r²)

    @recipe function f(equ::AxisymmetricTokamakCartesianEquilibrium;
                       nx = 100, ny = 120, levels = 50, size = (400,400),
                       xlims = (  0.5 * equ.R₀,   1.5 * equ.R₀),
                       ylims = (- 0.5 * equ.R₀, + 0.5 * equ.R₀))

        xgrid = LinRange(xlims[1], xlims[2], nx)
        zgrid = LinRange(ylims[1], ylims[2], ny)
        pot   = [ElectromagneticFields.A₂([xgrid[i], 0.0, zgrid[j]], equ) for i in eachindex(xgrid), j in eachindex(zgrid)]

        seriestype   := :contour
        aspect_ratio := :equal
        size   := size
        xlims  := xlims
        ylims  := ylims
        levels := levels
        legend := :none

        (xgrid, zgrid, pot')
    end

end


include("Utils.jl")

struct Vertex
    a
    p # x of the vertex
    q # y of the vertex
end

function toexpanded(F::Vertex)
    _validitycheck

    b = -2F.a * F.p
    c = F.a * F.p^2 + F.q

    return Expanded(F.a, b, c)
end

function tofactored(F::Vertex)
    _validitycheck

    under_root = -F.q / F.a

    if (under_root < 0)
        under_root = Complex(under_root)
    end

    x1 = F.p - sqrt(under_root)
    x2 = F.p + sqrt(under_root)

    return Factored(F.a, x1, x2)
end

function calcvalue(F::Vertex, x)
    return F.a * (x - F.p)^2 + F.q
end


"""
    lagrangian_lower_bound(C, c, d, Γ, X, l, dg)

Computes Lagrangian lower bound.

Check section 5.3 _Lagrangian lower bound_ of [publication](https://arxiv.org/abs/1811.06719)
for more information about this algorithm.

# Arguments
- `C`: is a vector of nonnegative first stage costs.
- `c`: is a vector of a nonnegative nominal second stage costs.
- `d`: is a vector of maximal deviations of the costs from their nominal values.
- `Γ`: is a budget, or the amount of uncertainty, which can be allocated to the second stage costs.
- `X`: is a set of feasible solutions represented as a list functions, each of which accepts a list of JuMP variables as an argument and returns a JuMP linear constraint.
- `l`: value of parameter ``l=\\lceil m(1-\\alpha) \\rceil``
- `pd`: an instance of [`ProblemDescriptor`](@ref)
"""
function lagrangianLowerBound(C, c, d, Γ, X, l, dg)
    @assert hasEqualCardinalityProperty(dg)

    tₗ = getProperty("lagrangianLowerBound.overallTimeLimit", parameterType = Int)

    Δt = @elapsed begin
        ϵ = getProperty("lagrangianLowerBound.epsilon", parameterType = Float64)
        invϕ = (sqrt(5) - 1) / 2 # 1/ϕ
        invϕ² = (3 - sqrt(5)) / 2 # 1/ϕ²

        α = 0
        β = 100 # TODO: check this assumption

        h = β - α

        γ = α + invϕ² * h
        fγ, status = relaxedIncrementalProblem(C, c, d, Γ, X, γ, l)
        if status != :Optimal
            @warn "Lagrangian lower bound solved non-optimaly"
            return missing
        end

        δ = α + invϕ * h
        fδ, status = relaxedIncrementalProblem(C, c, d, Γ, X, δ, l)
        if status != :Optimal
            @warn "Lagrangian lower bound solved non-optimaly"
            return missing
        end

        n = ceil(log(ϵ/h)/log(invϕ))
    end
    for i in 0:n-2
        if Δt > tₗ
            @warn "Lagrangian lower bound time limit exceeded (current time is $Δt seconds with time limit $tₗ seconds)"
            break
        end

        Δt += @elapsed begin
            if fγ > fδ
                β, δ, fδ = δ, γ, fγ
                h *=  invϕ
                γ = α + invϕ² * h

                fγ, status = relaxedIncrementalProblem(C, c, d, Γ, X, γ, l)
                if status != :Optimal
                    @warn "Lagrangian lower bound solved non-optimaly"
                    return missing
                end
            else
                α, γ, fγ = γ, δ, fδ
                h *=  invϕ
                δ = α + invϕ * h  # TODO: Why `invphi` here

                fδ, status = relaxedIncrementalProblem(C, c, d, Γ, X, δ, l)
                if status != :Optimal
                    @warn "Lagrangian lower bound solved non-optimaly"
                    return missing
                end
            end
        end
    end

    if (fγ > fδ)
        μ = (α + δ) / 2
        t, status = relaxedIncrementalProblem(C, c, d, Γ, X, μ, l)
        if status != :Optimal
            @warn "Lagrangian lower bound solved non-optimaly"
            return missing
        else
            return t
        end
    else
        μ = (γ + β) / 2
        t, status = relaxedIncrementalProblem(C, c, d, Γ, X, μ, l)
        if status != :Optimal
            @warn "Lagrangian lower bound solved non-optimaly"
            return missing
        else
            return t
        end
    end
end

function relaxedIncrementalProblem(C, c, d, Γ, X, μ, l)
    @assert size(C) == size(c) == size(d)

    n = size(C, 1)

    model = Model(solver = CplexSolver(CPXPARAM_ScreenOutput = getProperty("lagrangianLowerBound.cplexLogging"),
        CPX_PARAM_TILIM = getProperty("lagrangianLowerBound.subproblemTimeLimit")))

    @variable(model, π >= 0)
    if ndims(C) == 1
        @variable(model, x[1:n], Bin)
        @variable(model, 0 <= y[1:n] <= 1)
        @variable(model, u[1:n] >= 0)
        @variable(model, z[1:n] >= 0)
    elseif ndims(C) == 2
        m = size(C, 2)
        @variable(model, x[1:n, 1:m], Bin)
        @variable(model, 0 <= y[1:n, 1:m] <= 1)
        @variable(model, u[1:n, 1:m] >= 0)
        @variable(model, z[1:n, 1:m] >= 0)
    else
        throw(ArgumentError("Unsupported number of dimensions ($(ndims(c)))"))
    end

    @objective(model, Min, vecdot(C, x) + π * Γ + vecdot(y, c) - μ * sum(z) + vecdot(u, d) + μ * l)

    # Constraints defining set of feasible solutions
    for constraint in X
        JuMP.addconstraint(model, constraint(x)) # Assuming X = {Ax=b, x∈{0, 1}ⁿ}
    end

    @constraint(model, -y + π + u .>= 0)

    # Constraints defining set of feasible solutions
    for constraint in X
        JuMP.addconstraint(model, constraint(y)) # Assuming X = {Ax=b, x∈{0, 1}ⁿ}
    end

    @constraint(model, z .<= x)
    @constraint(model, z .<= y)

    status = solve(model)
    if status == :Optimal
        return getobjectivevalue(model), status
    else
        return getobjectivebound(model), status
    end
end


"""
    logbin(x,P)

calculate a log-binned distribution from an evenly space distribution `P(x)`
where bins that start with `bin_width_start` (keyword argument) increase with
`inrement_factor` (keyword argument)

# Remarks
* `P` has to be a probability density
* `x` has to be equi-distant
"""
function logbin(x, P; increment_factor=sqrt(2), bin_width_start=1)
    @assert length(x)==length(P)

    dx = x[2]-x[1]

    index     = 1
    x_bin = zeros(1)
    P_bin = zeros(1)
    w_bin = ones(1)*floor(Int, bin_width_start)

    bin_counter = 0
    bin_width::Float64 = w_bin[end]
    for (i,p) in enumerate(P)
        # bin_counter counts the number of bins that are merged currently; if
        # this coincides with the target size, then create new bin and reset
        # counter.
        if bin_counter == w_bin[end]
            index += 1
            bin_counter = 0
            bin_width *= increment_factor
            push!(x_bin, 0)
            push!(P_bin, 0)
            push!(w_bin, floor(Int, bin_width))
        end
        bin_counter += 1

        # estimate expectation value of x in log bin
        x_bin[index] += x[i]*P[i]
        # estimate probability weight of bin (sum of equally spaced
        # probabilities)
        P_bin[index] += P[i]
    end

    # normalize x bins (end of expectation value)
    x_bin ./= P_bin

    # transform P into proper probability DENSITY such that <x>=\sum xP(x)dx/sum P(x)dx
    P_bin ./= w_bin

    return x_bin, P_bin, w_bin.*dx
end
logbin(dist; increment_factor=sqrt(2), bin_width_start=1) = logbin(collect(dist.edges[1])[1:end-1], dist.weights, increment_factor=increment_factor, bin_width_start=bin_width_start)

function pbc_index(x,y,Lx,Ly)
    _index = (y-1)*Lx + x
    #implement periodic boundary
    while x<1
        _index += Lx
        x += Lx
    end
    while x>Lx
        _index -= Lx
        x -= Lx
    end
    while y<1
        _index += Lx*Ly
        y += Ly
    end
    while y>Ly
        _index -= Lx*Ly
        y -= Ly
    end
    return _index
end

# correlation function for d-dimensional state but only along x-axis
function correlation_x!(C,state,lags)
    Ls = size(state)
    C .= 0
    mean_state = mean(state)
    L_x   = CartesianIndex(Ls[1],zeros(Int,length(Ls)-1)...)
    for (l,lag) in enumerate(lags)
        lag_x = CartesianIndex(lag,zeros(Int,length(Ls)-1)...)
        for r in CartesianIndices(Ls)
            r_ = r + lag_x
            if r_[1] > Ls[1]
                r_ -= L_x
            end
            C[l] += (state[r]-mean_state)*(state[r_]-mean_state)
        end
    end
    C./=length(state)
end
function correlation_x(state,lags)
    C = zeros(Float64, length(lags))
    correlation_x!(C, state,lags)
    return C
end


pomdp = BabyPOMDP()

# testing constructor
b0 = DiscreteBelief(pomdp, [0.5,0.5])
@test pdf(b0,true) == 0.5
@test pdf(b0,false) == 0.5

println("There should be a warning below:")
DiscreteBelief(pomdp, [0.6, 0.5])
println("There should be a warning below:")
DiscreteBelief(pomdp, [-0.1, 1.1])

println("There should NOT be a warning below:")
DiscreteBelief(pomdp, [-0.1, 1.1], check=false)

# testing uniform belief
b1 = uniform_belief(pomdp)
@test pdf(b1,true) == 0.5
@test pdf(b1,false) == 0.5

# testing iterator
@test support(b1) == ordered_states(pomdp)

# testing equality (== function)
b2 = uniform_belief(pomdp)
b3 = DiscreteBelief(pomdp, [0.0,1.0])
@test b2 == b1
@test b2 == b0
@test b2 != b3

# testing hashing
@test hash(b0) == hash(b0)
@test hash(b0) == hash(b1)
@test hash(b1) == hash(b2)
@test hash(b2) != hash(b3)

# testing updater initialization
up = DiscreteUpdater(pomdp)
isd = initialstate_distribution(pomdp) # can be replaced with initialstate when pomdps 0.8 support is dropped
b4 = initialize_belief(up, isd)
@test pdf(b4,true) == pdf(isd,true)
@test pdf(b4,false) == pdf(isd,false)

# testing update function; if we feed baby, it won't be hungry
a = true
o = true
b4p = update(up, b4, a, o)
@test pdf(b4p,true) == 0.0
@test pdf(b4p,false) == 1.0

# if we don't feed the baby and observe crying
a = false
o = true
b4p = update(up, b4, false, true)
@test isapprox(pdf(b4p,true), 0.470588, atol=1e-4)
@test isapprox(pdf(b4p,false), 0.52941, atol=1e-4)

# testing that it works in a solve/simulation loop
# I'm not sure I need this test (could eliminate FIB dependency if not)
# r = test_solver(FIBSolver(), pomdp, max_steps=100)
# @test isapprox(r, -20.414855)

# Some more tests with tiger problem (old tests, but still work)
pomdp = TigerPOMDP()
up = DiscreteUpdater(pomdp)
bold = initialize_belief(up, initialstate_distribution(pomdp))

a = 0
o = true
bnew = update(up, bold, a, o)
@test isapprox(bnew.b, [0.15, 0.85])
@test isapprox(pdf(bnew, false), 0.15)
@test isapprox(pdf(bnew, true), 0.85)

# test mean and mode
b5 = DiscreteBelief(pomdp, [0.4, 0.6])
@test @inferred(mean(b5)) == 0.6
@test @inferred(mode(b5)) == true


# Multidimensional arrays
z = zeros(Float64, 2, 3)
println(typeof(z))

# Declare array of dimension n x m
n, m = 2, 4
arr = Array{Int}(undef, 2, 4)
println(arr)
println(size(arr))
arr2 = Array{Int}(undef, 3, 2, 2)
println(arr2)
println(size(arr2))

s = ones(String, 1, 3)
println(s)
# Note that s is considered a "row matrix / vector".
# The typical array is considered a "column matrix / vector".
println(s ==  ["", "", ""])

a = [1 2 3; 4 5 6]
println(a) # 2 rows, 3 columns
println(a[1, 2])
a[2, 3] = 10
println(a)

u = a[:,2:end]
println(ℯ.^(im*u))


grayencode(n::Integer) = n ⊻ (n >> 1)
function graydecode(n::Integer)
    r = n
    while (n >>= 1) != 0
        r ⊻= n
    end
    return r
end


"""
Provides some useful functions for dealing with tree models and tree approximation using nested distance
Needs more ideas!

"""

function summary(tree0::Tree{A,B,C,D}) where {A,B,C,D}
    println()
    println("Tree with $(maximum(stage(tree0))+1) stages and branching structure $(tree0.name[6:end]) .")
    println("The tree has $(length(tree0.state)) nodes")
end

"""
	@author: Alois Pichler
	Created September 2018
		provides ontoSimplex: function to project onto the simplex
"""

function ontoSimplex!(probabilities::Vector{Float64})   # projects the probabilities onto the simplex
    for i= 1:length(probabilities)
        if probabilities[i] < 0.0   # make them nonnegative
            probabilities[i]= 0.0
        end
    end
	summ= sum(probabilities)
	if summ <= 0.0
		probabilities= fill!(probabilities, 1.0/ length(probabilities))
		summ= sum(probabilities)
		@info "ontoSimplex: created probabilities." maxlog=1
	end
	if summ != 1
		probabilities./= summ
		tmpi= rand(1:length(probabilities)) 	# modify a random index
		summ= -sum(probabilities[1:tmpi-1])+ 1.0- sum(probabilities[tmpi+1:end])
		probabilities[tmpi]= max(0, summ)
		@info "ontoSimplex: modified probabilities." maxlog=1
	end
    nothing     # modifies probabilities
end


"""
    function reliefseq(data::Array{<:Real, 2}, target::Array{<:Integer, 1}, m::Signed=-1,
                       k_min::Integer=5, k_max::Integer=10, dist_func::Any=(e1, e2) -> sum(abs.(e1 .- e2), dims=2);
                       mode::String="discrete", sig::AbstractFloat=1.0, f_type::String="continuous")::Array{Float64,1}

Compute feature weights using ReliefSeq algorithm. The mode argument specifies which type of weights update to perform and can
either have the value of "k_nearest", "diff" or "exp_rank" (see reference paper). The f_type argument specifies whether the features
are continuous or discrete and can either have the value of "continuous" or "discrete". The sig argument is used when mode has the value
of "exp_rank".

---
# Reference:
- Brett A. McKinney, Bill C. White, Diane E. Grill, Peter W. Li, Ri-
chard B. Kennedy, Gregory A. Poland, and Ann L. Oberg. ReliefSeq: a
gene-wise adaptive-k nearest-neighbor feature selection tool for finding
gene-gene interactions and main effects in mRNA-Seq gene expression
data. PloS ONE, 8(12):e81527–e81527, Dec 2013.
"""
function reliefseq(data::Array{<:Real, 2}, target::Array{<:Integer, 1}, m::Signed=-1,
                   k_min::Integer=5, k_max::Integer=10, dist_func::Any=(e1, e2) -> sum(abs.(e1 .- e2), dims=2);
                   mode::String="k_nearest", sig::AbstractFloat=1.0, f_type::String="continuous")::Array{Float64,1}

    # Check if k nearest misses and hits can be found for each class.
    upper_k_lim = minimum(values(countmap(Int64.(target)))) - 1
    if k_min > upper_k_lim
        throw(DomainError(k_min, "Insufficient number of instances to respect lower bound k_min."))
    end

    if k_max > upper_k_lim
        k_max = upper_k_lim
        @warn @sprintf "k_max reduced to %d because of insufficient number of instances." upper_k_lim
    end

    # Allocate array for storing weights for different values of k.
    res_mat = Array{Float64}(undef, size(data, 2), k_max - k_min + 1)

    # Go over interval of k values and compute ReliefF feature weights.
    col_idx = 1
    @inbounds for k = k_min:k_max
        res_mat[:, col_idx] = relieff(data, target, m, k, dist_func, mode=mode, sig=sig, f_type=f_type)
        col_idx += 1
    end

    # For each feature choose highest weight.
    # Return computed feature weights.
    return vec(maximum(res_mat, dims=2))
end


include("../../src/bfs_primitive.jl")
include("../../src/sparse_square_matrix.jl")

@testset "PrimitiveJulia" begin

### algorithm correctness test

    A = spzeros(Bool, 5, 5)
    A[2,1] = true
    A[1,3] = true
    A[4,3] = true
    A[3,4] = true
    A[4,5] = true
    A[4,2] = true

    sol = [1, 4, 2, 3, 4]
    @test bfs_primitive(A, 1) == sol

### exception throw tests

    # Input matrix is not square
    B = spzeros(Bool, 5, 6)
    @test_throws DimensionMismatch bfs_primitive(B, 1)

    # source is a not a positive Integer
    @test_throws ArgumentError bfs_primitive(A, -1)

    # source is not an Integer subtype
    @test_throws MethodError bfs_primitive(A, 1.2)

    # Input matrix is not a SparseMatrixCSC{Bool, U}
    C = spzeros(5, 5)
    @test_throws MethodError bfs_primitive(C, 1)

end


export dot_zero,
       remove_duplicates_sorted!,
       samedir,
       ismultiple,
       nonzero_indices,
       right_turn,
       is_cyclic_permutation,
       is_right_turn,
       to_negative_vector,
       _above,
       _dr,
       _up,
       distance,
       append_zeros,
       prepend_zeros

"""
    dot_zero(x::AbstractVector{N}, y::AbstractVector{N}) where{N<:Real}

Dot product with preference for zero value in the presence of infinity values.

### Input

- `x` -- first vector
- `y` -- second vector

### Output

The dot product of `x` and `y`, but with the rule that `0 * Inf == 0`.
"""
function dot_zero(x::AbstractVector{N}, y::AbstractVector{N}) where{N<:Real}
    res = zero(N)
    for i in 1:length(x)
        if !iszero(x[i]) && !iszero(y[i])
            res += x[i] * y[i]
        end
    end
    return res
end

"""
    remove_duplicates_sorted!(v::AbstractVector)

Remove duplicate entries in a sorted vector.

### Input

- `v` -- sorted vector

### Output

The input vector without duplicates.
"""
function remove_duplicates_sorted!(v::AbstractVector)
    for i in length(v)-1:-1:1
        if v[i] == v[i+1]
            splice!(v, i+1)
        end
    end
    return v
end

"""
    samedir(u::AbstractVector{<:Real}, v::AbstractVector{<:Real})

Check whether two vectors point in the same direction.

### Input

- `u` -- first vector
- `v` -- second vector

### Output

`(true, k)` iff the vectors are identical up to a positive scaling factor `k`
such that `u = k * v`, and `(false, 0)` otherwise.

### Examples

```jldoctest
julia> using LazySets: samedir

julia> samedir([1, 2, 3], [2, 4, 6])
(true, 0.5)

julia> samedir([1, 2, 3], [3, 2, 1])
(false, 0)

julia> samedir([1, 2, 3], [-1, -2, -3])
(false, 0)

```
"""
function samedir(u::AbstractVector{<:Real}, v::AbstractVector{<:Real})
    return _ismultiple(u, v; allow_negative=false)
end

"""
    ismultiple(u::AbstractVector{<:Real}, v::AbstractVector{<:Real})

Check whether two vectors are linearly dependent.

### Input

- `u` -- first vector
- `v` -- second vector

### Output

`(true, k)` iff the vectors are identical up to a scaling factor `k ≠ 0` such
that `u = k * v`, and `(false, 0)` otherwise.

### Examples

```jldoctest
julia> using LazySets: ismultiple

julia> ismultiple([1, 2, 3], [2, 4, 6])
(true, 0.5)

julia> ismultiple([1, 2, 3], [3, 2, 1])
(false, 0)

julia> ismultiple([1, 2, 3], [-1, -2, -3])
(true, -1.0)

```
"""
function ismultiple(u::AbstractVector{<:Real}, v::AbstractVector{<:Real})
    return _ismultiple(u, v; allow_negative=true)
end

function _ismultiple(u::AbstractVector, v::AbstractVector; allow_negative::Bool)
    @assert length(u) == length(v) "wrong dimension"
    no_factor = true
    factor = 0
    @inbounds for i in 1:length(u)
        if isapproxzero(u[i])
            if !isapproxzero(v[i])
                return (false, 0)
            end
            continue
        elseif isapproxzero(v[i])
            return (false, 0)
        end
        if no_factor
            no_factor = false
            factor = u[i] / v[i]
            if !allow_negative && factor < 0
                return (false, 0)
            end
        elseif !_isapprox(factor, u[i] / v[i])
            return (false, 0)
        end
    end
    if no_factor
        # both vectors are zero
        return (true, 0)
    end
    return (true, factor)
end

"""
    nonzero_indices(v::AbstractVector{N}) where {N<:Real}

Return the indices in which a vector is non-zero.

### Input

- `v` -- vector

### Output

A vector of ascending indices `i` such that the vector is non-zero in dimension
`i`.
"""
function nonzero_indices(v::AbstractVector{N}) where {N<:Real}
    n = length(v)
    res = Vector{Int}()
    sizehint!(res, n)
    for i in 1:n
        if v[i] != zero(N)
            push!(res, i)
        end
    end
    return res
end

function nonzero_indices(v::SparseVector{N}) where {N<:Real}
    return v.nzind
end

"""
    is_cyclic_permutation(candidate::AbstractVector, paragon::AbstractVector)

Checks if the elements in `candidate` are a cyclic permutation of the elements
in `paragon`.

### Input

- `candidate` -- candidate vector
- `paragon`   -- paragon vector

### Output

A boolean indicating if the elements of `candidate` are in the same order as in
`paragon` or any of its cyclic permutations.
"""
function is_cyclic_permutation(candidate::AbstractVector,
                               paragon::AbstractVector)
    m = length(candidate)
    if length(paragon) != m
        return false
    end
    return any(candidate == circshift(paragon, i) for i in 0:m-1)
end

"""
    _up(u::AbstractVector, v::AbstractVector)

Checks if the given vector is pointing towards the given direction.

### Input

- `u` -- direction
- `v` -- vector

### Output

A boolean indicating if the vector is pointing towards the direction.
"""
@inline function _up(u::AbstractVector, v::AbstractVector)
    dot(u, v) > 0
end

"""
    _dr(u::AbstractVector, Vi::AbstractVector, Vj::AbstractVector)

Returns the direction of the difference of the given vectors.

### Input

- `u` -- direction
- `Vi` -- first vector
- `Vj` -- second vector

### Output

A number indicating the direction of the difference of the given vectors.
"""
@inline function _dr(u::AbstractVector, Vi::AbstractVector, Vj::AbstractVector)
    dot(u, (Vi) - (Vj))
end

"""
    _above(u::AbstractVector, Vi::AbstractVector, Vj::AbstractVector)

Checks if the difference of the given vectors is pointing towards the given
direction.

### Input

- `u` -- direction
- `Vi` -- first vector
- `Vj` -- second vector

### Output

A boolean indicating if the difference of the given vectors is pointing
towards the given direction.
"""
@inline function _above(u::AbstractVector, Vi::AbstractVector, Vj::AbstractVector)
    _dr(u, Vi, Vj) > 0
end

"""
    to_negative_vector(v::AbstractVector{N}) where {N}

Negate a vector and convert to type `Vector`.

### Input

- `v` -- vector

### Output

A `Vector` equivalent to ``-v``.
"""
@inline function to_negative_vector(v::AbstractVector{N}) where {N}
    u = zeros(N, length(v))
    @inbounds for (i, vi) in enumerate(v)
        u[i] = -vi
    end
    return u
end

@inline function to_negative_vector(v::Vector)
    return -v
end

@inline function to_negative_vector(v::SparseVector{N}) where {N}
    u = zeros(N, length(v))
    @inbounds for (ni, i) in enumerate(v.nzind)
        u[i] = -v.nzval[ni]
    end
    return u
end

"""
    right_turn([O::AbstractVector{N}=[0, 0]], u::AbstractVector{N},
               v::AbstractVector{N}) where {N<:Real}

Compute a scalar that determines whether the acute angle defined by three 2D
points `O`, `u`, `v` in the plane is a right turn (< 180° counter-clockwise)
with respect to the center `O`.

### Input

- `O` -- (optional; default: `[0, 0]`) 2D center point
- `u` -- first 2D point
- `v` -- second 2D point

### Output

A scalar representing the rotation.
If the result is 0, the points are collinear; if it is positive, the points
constitute a positive angle of rotation around `O` from `u` to `v`; otherwise
they constitute a negative angle.

### Algorithm

The [cross product](https://en.wikipedia.org/wiki/Cross_product) is used to
determine the sense of rotation.
"""
@inline function right_turn(O::AbstractVector{N},
                            u::AbstractVector{N},
                            v::AbstractVector{N}) where {N<:Real}
    return (u[1] - O[1]) * (v[2] - O[2]) - (u[2] - O[2]) * (v[1] - O[1])
end

# version for O = origin
@inline function right_turn(u::AbstractVector{N},
                            v::AbstractVector{N}) where {N<:Real}
    return u[1] * v[2] - u[2] * v[1]
end

"""
    is_right_turn([O::AbstractVector{N}=[0, 0]], u::AbstractVector{N},
                  v::AbstractVector{N}) where {N<:Real}

Determine whether the acute angle defined by three 2D points `O`, `u`, `v`
in the plane is a right turn (< 180° counter-clockwise) with
respect to the center `O`.
Determine if the acute angle defined by two 2D vectors is a right turn (< 180°
counter-clockwise) with respect to the center `O`.

### Input

- `O` -- (optional; default: `[0, 0]`) 2D center point
- `u` -- first 2D direction
- `v` -- second 2D direction

### Output

`true` iff the vectors constitute a right turn.
"""
@inline function is_right_turn(O::AbstractVector{N},
                               u::AbstractVector{N},
                               v::AbstractVector{N}) where {N<:Real}
    return _geq(right_turn(O, u, v), zero(N))
end

# version for O = origin
@inline function is_right_turn(u::AbstractVector{N},
                               v::AbstractVector{N}) where {N<:Real}
    return _geq(right_turn(u, v), zero(N))
end

"""
    distance(x::AbstractVector, y::AbstractVector; [p]::Real=2.0)

Compute the distance between two vectors with respect to the given `p`-norm,
computed as

```math
    \\|x - y\\|_p = \\left( \\sum_{i=1}^n | x_i - y_i |^p \\right)^{1/p}
```

### Input

- `x` -- vector
- `y` -- vector
- `p` -- (optional, default: `2.0`) the `p`-norm used; `p = 2.0` corresponds to
         the usual Euclidean norm

### Output

A scalar representing ``‖ x - y ‖_p``.
"""
function distance(x::AbstractVector, y::AbstractVector; p::Real=2.0)
    return norm(x - y, p)
end

"""
    allequal(x)

Check whether all elements in a sequence are equal

### Input

- `x` -- sequence

### Output

`true` iff all elements in `x` are equal.

### Notes

The code is taken from [here](https://stackoverflow.com/a/47578613).
"""
function allequal(x)
    length(x) < 2 && return true
    e1 = @inbounds x[1]
    i = 2
    @inbounds for i=2:length(x)
        x[i] == e1 || return false
    end
    return true
end

# if `vector` has exactly one non-zero entry, return its index
# otherwise return 0
function find_unique_nonzero_entry(vector::AbstractVector{N}) where {N}
    res = 0
    for (i, v) in enumerate(vector)
        if v != zero(N)
            if res != 0
                # at least two non-zero entries
                return 0
            else
                # first non-zero entry so far
                res = i
            end
        end
    end
    return res
end

function append_zeros(v::AbstractVector{N}, n::Int) where {N}
    return vcat(v, zeros(N, n))
end

function append_zeros(v::SparseVector{N}, n::Int) where {N}
    return sparsevec(v.nzind, v.nzval, v.n + n)
end

function prepend_zeros(v::AbstractVector{N}, n::Int) where {N}
    return vcat(zeros(N, n), v)
end

function prepend_zeros(v::SparseVector{N}, n::Int) where {N}
    return sparsevec(v.nzind .+ n, v.nzval, v.n + n)
end


# TODO: make work for other dimensions than 2

"""
    uniform_field(gridwidth, gridheight[, scale=0.2, border=true, normalize=true]) -> Array{Float64,3}

Generate a 2D vector field by placing uniformly random generated
displacement vectors on an equally spaced grid of size `gridheight
× gridwidth`.

The resulting array will have 3 dimensions, for which the first
dimension will always have a size of 2, which denotes the y and x
dimension of the vectors.
"""
function uniform_field(gridheight::Int, gridwidth::Int; scale = .2, border = false, normalize = true)
    uniform_field(gridheight, gridwidth, scale, border, normalize)
end

function uniform_field(gridheight::Int, gridwidth::Int, scale, border, normalize)
    A = if !border
        @assert gridwidth > 2 && gridheight > 2
        _2dborder!(safe_rand(2, gridheight, gridwidth), .5)
    else
        @assert gridwidth > 0 && gridheight > 0
        safe_rand(2, gridheight, gridwidth)
    end::Array{Float64,3}
    broadcast!(*, A, A, 2.)
    broadcast!(-, A, A, 1.)
    if normalize
        for d = 1:2
            Ad = view(A, d, :, :)
            broadcast!(*, Ad, Ad, scale / norm(Ad))
        end
    end
    A
end

"""
    gaussian_field(gridwidth, gridheight[, scale=0.2, sigma=2, iterations=1, border=true, normalize=true]) -> Array{Float64,3}

Generate a 2D vector field by placing uniformly random generated
displacement vectors on an equally spaced grid of size
`gridheight × gridwidth`. Additionally, this vector field is
smoothed using a gaussian filter with parameter `sigma`. This
will result in a less chaotic vector field than
[`uniform_field`](@ref) and be much more similar to an elastic
distortion.

The resulting array will have 3 dimensions, for which the first
dimension will always have a size of 2, which denotes the y and x
dimension of the vectors.
"""
function gaussian_field(gridheight::Int, gridwidth::Int; scale = .2, sigma = 2, iterations = 1, border = false, normalize = true)
    gaussian_field(gridheight, gridwidth, scale, sigma, iterations, border, normalize)
end

function gaussian_field(gridheight::Int, gridwidth::Int, scale, sigma, iterations, border, normalize)
    @assert iterations > 0
    A = uniform_field(gridheight, gridwidth, 1., true, false)
    kern = Kernel.gaussian(sigma)
    if !border
        _2dborder!(A, 0.)
        for iter = 1:iterations
            for d = 1:2
                Ad = view(A, d, :, :)
                imfilter!(Ad, Ad, kern)
            end
            _2dborder!(A, 0.)
        end
        A
    else
        for d = 1:2
            Ad = view(A, d, :, :)
            imfilter!(Ad, Ad, kern)
        end
        A
    end
    if normalize
        for d = 1:2
            Ad = view(A, d, :, :)
            broadcast!(*, Ad, Ad, scale / norm(Ad))
        end
    end
    A
end


using LazySets.Arrays: _rationalize, append_zeros, prepend_zeros

for N in [Float64, Float32, Rational{Int}]
    v1 = N[0, 4, 0]
    v2 = sparsevec(N[2], N[4], 3)
    v3 = SingleEntryVector(2, 3, N(4))
    @test v1 == v2 == v3
    v1a = append_zeros(v1, 2)
    v1p = prepend_zeros(v1, 2)
    @test v1a == append_zeros(v2, 2) == append_zeros(v3, 2) == N[0, 4, 0, 0, 0]
    @test v1p == prepend_zeros(v2, 2) == prepend_zeros(v3, 2) == N[0, 0, 0, 4, 0]
end

for N in [Float32, Float64]
    x = [N(1), N(2), N(3)]
    out = [1//1, 2//1, 3//1]
    @test _rationalize(x) == out
    v = _rationalize(BigInt, x)
    @test v[1] isa Rational{BigInt}
    @test _rationalize(x, tol=2*eps(N)) == out
end


using .Gadfly

"""
    shewhart(
        df::AbstractDataFrame, # DataFrame containing columns :Date & :Value (or below)
        nominal::Real, # Nominal value for :Value column
        onesigma::Real; # Nominal 1σ variation in :Value column
        timestamp::Union{Symbol,AbstractString}=:Date, # Alternative to :Date
        value::Union{Symbol,AbstractString}=:Value, # Alternative to :Value
        result::Union{Missing,Function}=missing,
        color::Bool=true # Use only color vs only black and white
    )

Plot a Shewhart-style control chart from the data in `df`.  The `DataFrame` is evaluated using
the `weco(...)` function to compute the WECO rules based on the specified `nominal` (mean value)
and `onesigma` (standard-deviation).

`color` controls whether the data is plotted in colors (green=>meets, red=>fails, red line=>3σ, yellow line=>2σ)
or black-and-white (white fill=>meets, black fill=>fails, long dashed gray line=>3σ, short dash gray line=>2σ)
"""
function shewhart(
    df::AbstractDataFrame, # DataFrame containing columns :Date & :Value (or below)
    nominal::Real, # Nominal value for :Value column
    onesigma::Real; # Nominal 1σ variation in :Value column
    timestamp::Union{Symbol,AbstractString}=:Date, # Alternative to :Date
    value::Union{Symbol,AbstractString}=:Value, # Alternative to :Value
    result::Union{Missing,Function}=missing,
    color::Bool=true, # Use colors vs only black and white
    title::Union{AbstractString,Nothing}=nothing,
)
    df = sort(df, timestamp)
    res = weco(df, nominal, onesigma, timestamp=timestamp, value=value, result=result)
    gt = isnothing(title) ? [] : [ Guide.title(title) ]
    if color
        plot(
            layer(yintercept=[3*onesigma, -3*onesigma].+nominal, Geom.hline(color="red")),
            layer(yintercept=[2*onesigma, -2*onesigma].+nominal, Geom.hline(color="yellow")),
            layer(res, x=timestamp, y=value, color=:Passes, Geom.point),
            Scale.color_discrete_manual("lime", "firebrick", levels = [ true, false ]), gt...
        )
    else
        plot(
            layer(yintercept=[3*onesigma, -3*onesigma].+nominal, Geom.hline(color="gray50", style=:dash)),
            layer(yintercept=[2*onesigma, -2*onesigma].+nominal, Geom.hline(color="gray50", style=:dot)),
            layer(res, x=timestamp, y=value, color=:Passes, Geom.point, Theme(discrete_highlight_color=c->"gray50")),
            Scale.color_discrete_manual("gray80", "black", levels = [ true, false ]), gt...
        )
    end
end


#RSDeltaSigmaPort SNR calculations
#-------------------------------------------------------------------------------

"""`snr = calculateSNR(hwfft,f,nsig=1)`

Estimate the signal-to-noise ratio, given the in-band bins of a (Hann-windowed)
fft and the location of the input signal (f>0).

 - For nsig=1, the input tone is contained in `hwfft(f:f+2)`;
   this range is appropriate for a Hann-windowed fft.
 - Each increment in `nsig` adds a bin to either side.
 - The SNR is expressed in dB.
"""
function calculateSNR(hwfft, f::Int, nsig::Int = 1)
	signalBins = f-nsig+1:f+nsig+1
	signalBins = signalBins[signalBins .> 0]
	signalBins = signalBins[signalBins .<= length(hwfft)]
	s = norm(hwfft[signalBins]) #*4/(N*sqrt(3)) for true rms value
	noiseBins = collect(1:length(hwfft))
	deleteat!(noiseBins, signalBins)
	n = norm(hwfft[noiseBins])
	snr = (n!=0) ? dbv(s/n) : Inf
	return snr
end

"""`(peak_snr,peak_amp) = peakSNR(snr,amp)`

Estimate the snr peak by threading a line of slope 1 through the (amp,snr) data.
Both amp and snr are expressed in dB.
"""
function peakSNR(snr, amp)
	snr = deepcopy(snr); amp = deepcopy(amp)
	#Delete garbage data
	i = abs.(snr) .== Inf;   deleteat!(snr, i); deleteat!(amp, i)
	i = isnan.(snr);         deleteat!(snr, i); deleteat!(amp, i)
	i = snr .< 3;            deleteat!(snr, i); deleteat!(amp, i)
	n = length(amp)

	#Sort by amplitude
	i = sortperm(amp)
	amp = amp[i]; snr = snr[i]

	i = [true]
	local m = 0.0
	while any(i) && n > 3
		#Draw a 45-degree median line through the data
		tmp = sort(snr .- amp)
		if mod(n,2) == 0
			m = mean( tmp[div(n,2) .+ [1, 0]] )
		else
			m = tmp[div(n+1,2)]
		end
		#Discard data that is more than 6dB off
		i = abs.(amp .- snr .+ m) .> 6
		deleteat!(snr, i); deleteat!(amp, i)
		n = length(amp)
	end

	peak_amp = maximum(amp)
	peak_snr = peak_amp + m
	return (peak_snr, peak_amp)
end

"""`(snr,amp,k0,k1,sigma_e2) = predictSNR(ntf,R=64,amp=...,f0=0)`

Predict the SNR curve of a binary delta-sigma modulator by using
the describing function method of Ardalan and Paulos.

The modulator is specified by a noise transfer function (ntf).
The band of interest is defined by the oversampling ratio (R)
and the center frequency (f0).
The input signal is characterized by the amp vector.
amp defaults to [-120 -110...-20 -15 -10 -9 -8 ... 0]dB, where 0 dB means
a full-scale (peak value = 1) sine wave.

The algorithm assumes that the amp vector is sorted in increasing order;
once instability is detected, the remaining SNR values are set to -Inf.

# Output:
 - snr: a vector of SNR values (in dB)
 - amp: a vector of amplitudes (in dB)
 - k0: the quantizer signal gain
 - k1: the quantizer noise gain
 - sigma_e2: the power of the quantizer noise (not in dB)

The describing function method of A&P assumes that the quantizer processes
signal and noise components separately. The quantizer is modelled as two
(not necessarily equal) linear gains, k0 and k1, and an additive white
gaussian noise source of power sigma_e2. k0, k1 and sigma_e2 are calculated
as functions of the input.
The modulator's loop filter is assumed to have nearly infinite gain at
the test frequency.

Future versions may accommodate STFs.
"""
function predictSNR(ntf, R=64, amp=vcat(-120:10:-20, -15, -10:0); f0::Float64=0.0)
	Nb = 100
	XTAB = range(-2, stop=0, length=21)
	#The following code was used to create the YTAB matrix
	#YTAB = [];
	#for xi=XTAB
	#	YTAB = [YTAB; hypergeo(0.5,1,xi) hypergeo(0.5,2,xi)];
	#end
	YTAB = [0.46575960516930   0.67366999387741;
		0.47904652357101   0.68426650762558;
		0.49316295981407   0.69527947902679;
		0.50817364454269   0.70673173666000;
		0.52414894104004   0.71864765882492;
		0.54116523265839   0.73105299472809;
		0.55930554866791   0.74397552013397;
		0.57866013050079   0.75744456052780;
		0.59932720661163   0.77149158716202;
		0.62141352891922   0.78615015745163;
		0.64503526687622   0.80145609378815;
		0.67031890153885   0.81744754314423;
		0.69740217924118   0.83416539430618;
		0.72643494606018   0.85165339708328;
		0.75758063793182   0.86995816230774;
		0.79101717472076   0.88912981748581;
		0.82693856954575   0.90922164916992;
		0.86555624008179   0.93029111623764;
		0.90710091590881   0.95239937305450;
		0.95182400941849   0.97561222314835;
		1.00000000000000   1.00000000000000;
	]

	if f0==0
		band_of_interest = range(0, stop=pi/R, length=Nb)
	else
		band_of_interest = range(2*pi*(f0-0.25/R), stop=2*pi*(f0+0.25/R), length=Nb)
	end

	num, den = _zp2tf(ntf.z, ntf.p, 1)
	num1 = num - den

	N = length(amp)
	snr = zeros(1,N) .- Inf
	k0 = zeros(1,N)
	k1 = zeros(1,N)
	sigma_e2 = zeros(1,N)

	u = 10 .^ (amp/20)

	Nimp = 100
	unstable = false
	f_prev = 0.0; fprime = 0.0
	m0 = 0.0; rho = 0.0; erfinvu = 0.0
	for n=1:N
		#Calculate sigma_e2
		if f0==0
			erfinvu = erfinv(u[n])
			sigma_e2[n] = 1 - u[n]^2 - 2/pi*exp(-2*erfinvu^2)
		else #Sinusoidal input.
			#Solve sqrt(pi)*u/2 = rho * hypergeo(0.5,2,-rho^2);
			#Formulate as solve f(rho)=0, f = rho*M(0.5,2,-rho^2)-K
			#and use the secant method.
			K = 0.5 * sqrt(pi) * u[n]
			if n==1
				rho = u[n]^2 #Initial guess; otherwise use previous value.
				fprime = 1.0
			end
			drho = 1.0
			for itn = 1:20
				m0 = interp1_cubic(XTAB,YTAB[:,2],-rho^2)
				f = rho*m0 - K
				if( itn >1 )
					fprime = max((f-f_prev)/drho,0.5) #Secant approx.
				end
				if abs(f) < 1e-8; break; end #!Converged
				drho = -f/fprime
				if abs(drho) > 0.2; drho = sign(drho)*0.2; end
				if abs(drho) < 1e-6; break; end #!Converged
				rho = rho + drho
				f_prev = f
			end
			m1 = interp1_cubic(XTAB,YTAB[:,1], -rho^2)
			sigma_e2[n] = 1 - u[n]^2/2 - 2/pi*m1^2
		end #Sinusoidal input

		#Iterate to solve for k1 and sigma_1.
		#Using one of MATLAB's nonlinear equation solvers would be more efficient,
		#but this function code would then require the optimization toolbox.
		#!Future work: put in 2-D BFGS code.
		k1[n] = (n>1) ? k1[n-1] : 1.2 #Use the previous value of k1 as the initial guess.
		k1_prev = 0
		itn = 0
		k1sigma1 = (f0==0) ? sqrt(2/pi)*exp(-erfinvu^2) : sqrt(2/pi)*m1
		local sigma_1 = 0.0
		while abs(k1[n]-k1_prev) > 1e-6*(1+k1[n]) && itn < 100
			#Create the function: H_hat = L1/(1-k1*L1)=(H-1)/(H*(1-k1)+k1).
			den1 = (1-k1[n])*num + den*k1[n]
			#Calculate pGain, the square of the 2-norm of H_hat.
			pGain, Nimp = powerGain(num1, den1, Nimp)
			if isinf(pGain)
				unstable = true
				break
			end

			sigma_1 = sqrt(pGain*sigma_e2[n])
			k1_prev = k1[n]
			k1[n] = k1sigma1/sigma_1
			itn = itn+1
		end
		if unstable
			break
		end

		if f0==0
			y0 = sqrt(2)*erfinvu*sigma_1
			k0[n] = u[n]/y0
		else
			k0[n] = sqrt(2/pi)*m0/sigma_1
		end

		h = _freqz(num, (1-k1[n])*num + k1[n]*den, band_of_interest)
		#For both DC and sine wave inputs, use u^2/2 as the signal
		#power since true DC measurements are usually impossible.
		snr[n] = dbp( 0.5*u[n]^2 / (sum(h.^2)/(R*Nb)*sigma_e2[n]) )
	end

	return (snr, amp, k0, k1, sigma_e2)
end

"""`(snr,amp) = simulateSNR(ΔΣmodel,osr,amp; f0=0,nlev=2,f=1/(4*osr),k=13,quadrature=false)`

Determine the SNR for a delta-sigma modulator by using simulations.

`ΔΣmodel` describes the modulator by a noise transfer function (ntf) and the
number of quantizer levels (`nlev`). Alternatively, `ΔΣmodel` may be a
function taking the input signal as its sole argument:
 - `ΔΣmodel::ZPKData`: z, p, k transfer function
 - `ΔΣmodel::Array` ABCD matrix
 - `ΔΣmodel::Function` funΔΣ(u) generating ΔΣ output.

The band of interest is defined by the oversampling ratio (`osr`)
and the center frequency (`f0`).
The input signal is characterized by the `amp` vector and the `f` variable.

 - `amp` defaults to [-120 -110...-20 -15 -10 -9 -8 ... 0]dB, where 0 dB means
   a full-scale (peak value = nlev-1) sine wave.
 - `f` is the input frequency, normalized such that 1 -> fs;
 - `f` is rounded to an FFT bin.

Using sine waves located in FFT bins, the SNR is calculated as the ratio
of the sine wave power to the power in all in-band bins other than those
associated with the input tone. Due to spectral smearing, the input tone
is not allowed to lie in bins 0 or 1. The length of the FFT is 2^k.

 - If ntf is complex, simulateQDSM (which is slow) is called.
 - If ABCD is complex, simulateDSM is used with the real equivalent of ABCD
   in order to speed up simulations.

Future versions may accommodate STFs.
"""
function simulateSNR(arg1, osr::Int=64, amp=vcat(-120:10:-20, -15, -10:0);
	f0::Float64=0.0, nlev::Int=2, f::Float64=NaN, k::Int=13, quadrature::Bool=false
)
	is_function = isa(arg1, Function)

	#Look at arg1 and decide if the system is quadrature
	quadrature_ntf = false
	if !is_function
		if isa(arg1, ZPKData)
			pz = [arg1.p, arg1.z]
			for i=1:2
				if any( abs.(imag.(poly(pz[i]))) .> 1e-4 )
					quadrature = true
					quadrature_ntf = true
				end
			end
		else #ABCD matrix
			if !all(all(imag.(arg1) .== 0))
				quadrature = true
			end
		end
	end

	osr_mult = 2
	if f0!=0 && !quadrature
   	osr_mult = 2*osr_mult
	end
	if isnan(f)
		f = f0 + 0.5/(osr*osr_mult) #Halfway across the band
	end
	M = nlev-1
	if quadrature && !quadrature_ntf
		#Modify arg1 (ABCD) and nlev so that simulateDSM can be used
		nlev = [nlev; nlev]
		arg1 = mapQtoR(arg1)
	end

	if abs(f-f0) > 1/(osr*osr_mult)
		@warn("The input tone is out-of-band.")
	end

	N = 2^k
	if N < 8*2*osr #Require at least 8 bins to be "in-band"
		@warn("Increasing k to accommodate a large oversampling ratio.")
		k = ceil(Int, log2(8*2*osr))
		N = 2^k
	end
	F = round(Int, f*N)
	if abs(F)<=1
		@warn("Increasing k to accommodate a low input frequency.")
		#Want f*N >= 1
		k = ceil(Int, log2(1/f))
		N = 2^k
		F = 2
	end

	Ntransient = 100
	soft_start = 0.5*(1 .- cos.(2π/Ntransient * (0:div(Ntransient,2)-1)))
	if !quadrature
		tone = M * sin.(2π*F/N * (0:(N+Ntransient-1)))
		tone[1:div(Ntransient,2)] = tone[1:div(Ntransient,2)] .* soft_start
	else
		tone = M * exp.(2π*j*F/N * (0:(N+Ntransient-1)) )
		tone[1:div(Ntransient,2)] = tone[1:div(Ntransient,2)] .* soft_start
		if !quadrature_ntf
			tone = [real(tone); imag(tone)]
		end
	end
	window = .5*(1 .- cos.(2π*(0:N-1)/N) ) #Hann window
	if f0==0
		#Exclude DC and its adjacent bin
		inBandBins = div(N,2) .+ (3:round(Int, N/(osr_mult*osr)))
		F = F-2
	else
		f1 = round(Int, N*(f0-1/(osr_mult*osr)))
		f2 = round(Int, N*(f0+1/(osr_mult*osr)))
		inBandBins = div(N,2) .+ (f1:f2) #Should exclude DC
		F = F-f1+1
	end

	snr = zeros(size(amp))
	i=1
	for A = 10 .^ (amp/20)
		if is_function
			v = arg1(A*tone)
		elseif quadrature_ntf
			v = simulateQDSM(A*tone, arg1, nlev=nlev).v
		else
			v = simulateDSM(A*tone, arg1, nlev=nlev).v
			if quadrature
				v = v[1,:] + im*v[2,:]
			end
		end
		hwfft = fftshift(fft(applywnd(v[1+Ntransient:N+Ntransient], window)))
		snr[i] = calculateSNR(hwfft[inBandBins], F)
		i=i+1
	end

	return (snr,amp)
end

#Not meant to be called externally:
function _calcSNRInfo(NTF, OSR::Int, M::Int, f0::Float64)
	nlev=M+1
	SNR, amp = simulateSNR(NTF, OSR, f0=f0, nlev=nlev)
	pk_snr, pk_amp = peakSNR(SNR, amp) #Single values

	vs_amp_predicted = nothing
	if nlev == 2 #predictSNR() only supports 2 levels
		SNR_pred, amp_pred = predictSNR(NTF, OSR, f0=f0)
		amp_pred = conv2seriesmatrix2D(amp_pred*1.0) #Ensure amplitudes are Float64
		vs_amp_predicted=(amp_pred, SNR_pred)
	end

	return (vs_amp_sim=(amp, SNR), peak=(pk_amp, pk_snr),
		vs_amp_predicted=vs_amp_predicted, OSR=OSR
	)
end

"""`calcSNRInfo(sig, dsm::RealDSM; NTF=nothing)`
"""
function calcSNRInfo(dsm::RealDSM; NTF=nothing)
	isnothing(NTF) && (NTF = synthesizeNTF(dsm))
	return _calcSNRInfo(NTF, dsm.OSR, dsm.M, dsm.f0)
end

#Last line


#=
   algorithms from
   Mioara Joldes, Jean-Michel Muller, Valentina Popescu.
   Tight and rigourous error bounds for basic building blocks of double-word arithmetic.
   ACM Transactions on Mathematical Software, Association for Computing Machinery,
   2017, 44 (2), pp.1 - 27. <10.1145/3121432>. <hal-01351529v3>
   they recommend
   > For adding two double-word numbers, use Algorithm 6.
     Use Algorithm 5 only if both operands have the same sign.
  > For multiplying a double-word number by a floating-point number,
    where FMA is available, use Algorithm 9.
  > For multiplying two double-word numbers, if an FMA instruction
    is available, then Algorithm 12 is to be favored.
  > For dividing a double-word number by a floating-point number,
    use Algorithm 15.
  > For dividing two double-word numbers: Algorithm 17 is suggested.
    If an FMA instruction is available, and accuracy is important,
    prefer Algorithm 18 (it is slower).
=#

# Algorithm 1 in ref: error-free transformation

@inline function Fast2Sum(a::T, b::T) where {T<:AbstractFloat}
    s = a + b
    z = s - a
    t = b - z
    return s, t
end

# Algorithm 2 in ref: error-free transformation

function TwoSum(a::T, b::T) where {T<:AbstractFloat}
    s  = a + b
    a1 = s - b
    b1 = s - a1
    da = a - a1
    db = b - b1
    t  = da + db
    return s, t
end

function TwoDiff(a::T, b::T) where {T<:AbstractFloat}
    s  = a - b
    a1 = s + b
    b1 = s - a1
    da = a - a1
    db = b - b1
    t  = da + db
    return s, t
end

# Algorithm 3 in ref: error-free transformation

@inline function Fast2Mult(a::T, b::T) where {T<:AbstractFloat}
    s = a * b
    t = fma(a, b, -s)
    return s, t
end

# Algorithm 4 in ref: relerr 2u²  [reltime 26]

@inline function DWPlusFP(xₕᵢ::T, xₗₒ::T, y::T) where {T<:AbstractFloat}
    sₕᵢ, sₗₒ = TwoSum(xₕᵢ, y)
    v = xₗₒ + sₗₒ
    zₕᵢ, zₗₒ = TwoSum(sₕᵢ, v)
    return zₕᵢ, zₗₒ
end

@inline function DWMinusFP(xₕᵢ::T, xₗₒ::T, y::T) where {T<:AbstractFloat}
    sₕᵢ, sₗₒ = TwoDiff(xₕᵢ, y)
    v = xₗₒ + sₗₒ
    zₕᵢ, zₗₒ = TwoSum(sₕᵢ, v)
    return zₕᵢ, zₗₒ
end

# Algorithm 6 in ref: relerr  3u² + 13u³  [reltime 35]

function AccurateDWPlusDW(xₕᵢ::T, xₗₒ::T, yₕᵢ::T, yₗₒ::T) where {T<:AbstractFloat}
   sₕᵢ, sₗₒ = TwoSum(xₕᵢ, yₕᵢ)
   tₕᵢ, tₗₒ = TwoSum(xₗₒ, yₗₒ)
   c = sₗₒ + tₕᵢ
   vₕᵢ, vₗₒ = Fast2Sum(sₕᵢ, c)
   w = tₗₒ + vₗₒ
   zₕᵢ, zₗₒ = Fast2Sum(vₕᵢ, w)
   return zₕᵢ, zₗₒ
end

function AccurateDWMinusDW(xₕᵢ::T, xₗₒ::T, yₕᵢ::T, yₗₒ::T) where {T<:AbstractFloat}
   sₕᵢ, sₗₒ = TwoDiff(xₕᵢ, yₕᵢ)
   tₕᵢ, tₗₒ = TwoDiff(xₗₒ, yₗₒ)
   c = sₗₒ + tₕᵢ
   vₕᵢ, vₗₒ = Fast2Sum(sₕᵢ, c)
   w = tₗₒ + vₗₒ
   zₕᵢ, zₗₒ = Fast2Sum(vₕᵢ, w)
   return zₕᵢ, zₗₒ
end

# Algorithm 7 in ref: relerr (³/₂)u² + 4u³  [reltime 18]

@inline function DWTimesFP1(xₕᵢ::T, xₗₒ::T, y::T) where {T<:AbstractFloat}
    cₕᵢ, c1 = Fast2Mult(xₕᵢ, y)
    c2 = xₗₒ * y
    tₕᵢ, t1 = Fast2Sum(cₕᵢ, c2)
    t2 = t1 + c1
    zₕᵢ, zₗₒ = Fast2Sum(tₕᵢ, t2)
    return zₕᵢ, zₗₒ
end

# Algorithm 9 in ref: relerr 2u²  [reltime 15]

@inline function DWTimesFP3(xₕᵢ::T, xₗₒ::T, y::T) where {T<:AbstractFloat}
    cₕᵢ, cₗₒ = Fast2Mult(xₕᵢ, y)
    c = fma(xₗₒ, y, cₗₒ)
    zₕᵢ, zₗₒ = Fast2Sum(cₕᵢ, c)
    return zₕᵢ, zₗₒ
end

# Algorithm 11 in ref: relerr 6u²  [reltime 16]

function DWTimesDW2(xₕᵢ::T, xₗₒ::T, yₕᵢ::T, yₗₒ::T) where {T<:AbstractFloat}
   cₕᵢ, c1 = Fast2Mult(xₕᵢ, yₕᵢ)
   t0 = xₗₒ * yₗₒ
   c2 = fma(xₗₒ, yₕᵢ, t0)
   c3 = c1 + c2
   zₕᵢ, zₗₒ = Fast2Sum(cₕᵢ, c3)
   return zₕᵢ, zₗₒ
end

# Algorithm 12 in ref: relerr 5u²  [reltime 17]

function DWTimesDW3(xₕᵢ::T, xₗₒ::T, yₕᵢ::T, yₗₒ::T) where {T<:AbstractFloat}
   cₕᵢ, c1 = Fast2Mult(xₕᵢ, yₕᵢ)
   t0 = xₗₒ * yₗₒ
   t1 = fma(xₕᵢ, yₗₒ, t0)
   c2 = fma(xₗₒ, yₕᵢ, t1)
   c3 = c1 + c2
   zₕᵢ, zₗₒ = Fast2Sum(cₕᵢ, c3)
   return zₕᵢ, zₗₒ
end

# Algorithm 15 in ref: relerr 3u²  [reltime 27]

function DWDivFP3(xₕᵢ::T, xₗₒ::T, y::T) where {T<:AbstractFloat}
   tₕᵢ = xₕᵢ / y
   pₕᵢ, pₗₒ = Fast2Mult(tₕᵢ, y)
   dₕᵢ = xₕᵢ - pₕᵢ
   dₗₒ = dₕᵢ - pₗₒ
   d = dₗₒ + xₗₒ
   tₗₒ = d / y
   zₕᵢ, zₗₒ = Fast2Sum(tₕᵢ, tₗₒ)
   return zₕᵢ, zₗₒ
end

# Algorithm 17 in ref: relerr 15u² + 56u³ [reltime 50]

function DWDivDW2(xₕᵢ::T, xₗₒ::T, yₕᵢ::T, yₗₒ::T) where {T<:AbstractFloat}
   tₕᵢ = xₕᵢ / yₕᵢ
   rₕᵢ, rₗₒ = DWTimesFP1(yₕᵢ, yₗₒ, tₕᵢ)
   dₕᵢ = xₕᵢ - rₕᵢ
   dₗₒ = xₗₒ - rₗₒ
   d = dₕᵢ + dₗₒ
   tₗₒ = d / yₕᵢ
   zₕᵢ, zₗₒ = Fast2Sum(tₕᵢ, tₗₒ)
   return zₕᵢ, zₗₒ
end

# Algorithm 18 in ref: relerr < 10u² (6u² seen) [reltime 107]
# (note DWTimesDW3 replaces DWTimesDW2 per ref)

function DWDivDW3(xₕᵢ::T, xₗₒ::T, yₕᵢ::T, yₗₒ::T) where {T<:AbstractFloat}
   tₕᵢ = inv(yₕᵢ)
   rₕᵢ = fma(yₕᵢ, -tₕᵢ, one(T))
   rₗₒ = -(yₗₒ * tₕᵢ)
   eₕᵢ, eₗₒ = Fast2Sum(rₕᵢ, rₗₒ)
   dₕᵢ, dₗₒ = DWTimesFP3(eₕᵢ, eₗₒ, tₕᵢ)
   mₕᵢ, mₗₒ = DWPlusFP(dₕᵢ, dₗₒ, tₕᵢ)
   zₕᵢ, zₗₒ = DWTimesDW3(xₕᵢ, xₗₒ, mₕᵢ, mₗₒ)
   return zₕᵢ, zₗₒ
end

# inv(...) using Algorithms 17 and 18

# Algorithm 17 in ref: relerr 15u² + 56u³  [reltime 48]

function DWInvDW2(yₕᵢ::T, yₗₒ::T) where {T<:AbstractFloat}
   tₕᵢ = one(T) / yₕᵢ
   rₕᵢ, rₗₒ = DWTimesFP1(yₕᵢ, yₗₒ, tₕᵢ)
   dₕᵢ = one(T) - rₕᵢ
   dₗₒ = -rₗₒ
   d = dₕᵢ + dₗₒ
   tₗₒ = d / yₕᵢ
   zₕᵢ, zₗₒ = Fast2Sum(tₕᵢ, tₗₒ)
   return zₕᵢ, zₗₒ
end

# Algorithm 18 in ref: relerr < 10u² (6u² seen)  [reltime 72]
# (note DWTimesDW3 replaces DWTimesDW2 per ref)

function DWInvDW3(yₕᵢ::T, yₗₒ::T) where {T<:AbstractFloat}
   tₕᵢ = inv(yₕᵢ)
   rₕᵢ = fma(yₕᵢ, -tₕᵢ, one(T))
   rₗₒ = -(yₗₒ * tₕᵢ)
   eₕᵢ, eₗₒ = Fast2Sum(rₕᵢ, rₗₒ)
   dₕᵢ, dₗₒ = DWTimesFP3(eₕᵢ, eₗₒ, tₕᵢ)
   zₕᵢ, zₗₒ = DWPlusFP(dₕᵢ, dₗₒ, tₕᵢ)
   return zₕᵢ, zₗₒ
end


#=
# A tetrehedral reference element.
# Notation follows that used in Nodal Discontinuous Galerkin Methods
# by Hesthaven and Warburton.
#  https://link.springer.com/book/10.1007/978-0-387-72067-8
#
=#

function build_tetrahedron_refel(refel)
    # refel has already been created, but needs quadrature matrices
    (refel.V, refel.Dr, refel.Ds, refel.Dt) = tetrahedron_vandermonds(refel, refel.r);
    refel.invV = inv(refel.V);

    (refel.Vg, DVgr, DVgs, DVgt) = tetrahedron_vandermonds(refel, refel.g);
    #refel.invVg = inv(refel.Vg);

    refel.Q = refel.Vg*refel.invV;
    refel.Qr = DVgr*refel.invV;
    refel.Qs = DVgs*refel.invV;
    refel.Qt = DVgt*refel.invV;
    refel.Ddr = refel.Dr*refel.invV;
    refel.Dds = refel.Ds*refel.invV;
    refel.Ddt = refel.Dt*refel.invV;

    return refel;
end

function tetrahedron_vandermonds(refel, r)
    Np = refel.Np;      # number of nodal points
    Nrp = size(r,1);    # number of quadrature points may be different
    V = zeros(Nrp, Np);
    gradVr = zeros(Nrp, Np);
    gradVs = zeros(Nrp, Np);
    gradVt = zeros(Nrp, Np);

    # Transfer (r,s,t) to (a,b,c) coordinates
    a = zeros(Nrp);
    b = zeros(Nrp);
    for ni=1:Nrp
        if r[ni,2]+r[ni,3] != 0
            a[ni] = 2*(1+r[ni,1])/(-r[ni,2]-r[ni,3])-1;
        else
            a[ni] = -1;
        end
        if r[ni,3] != 1
            b[ni] = 2*(1+r[ni,2])/(1-r[ni,3])-1;
        else
            b[ni] = -1;
        end
    end
    c = r[:,3];

    # build the Vandermonde and gradVandermond matrix
    sk = 1;
    for i=0:refel.N
        h1 = jacobi_polynomial(a,0,0,i);
        dfa = grad_jacobi_polynomial(a, 0, 0, i);
        for j=0:(refel.N - i)
            h2 = jacobi_polynomial(b,2*i+1,0,j);
            dgb = grad_jacobi_polynomial(b, 2*i+1,0, j);
            for k=0:(refel.N - i - j)
                h3 = jacobi_polynomial(c,2*(i+j)+2,0,k);
                dhc = grad_jacobi_polynomial(b, 2*(i+j)+2,0, k);

                V[:,sk] = 2.8284271247461903 .* h1 .* h2 .* h3 .* (1 .- b).^i .* (1 .- c).^(i+j);

                # r-derivative
                dmodedr = dfa.*h2.*h3;
                if i>0
                    dmodedr = dmodedr.*((0.5 .* (1 .- b)).^(i-1));
                end
                if (i+j)>0
                    dmodedr = dmodedr.*((0.5 .* (1 .- c)).^(i+j-1));
                end

                # s-derivative
                dmodeds = dmodedr .* (0.5 .* (1 .+ a));
                tmp = dgb .* (0.5 .* (1 .- b)).^i;
                if i>0
                    tmp = tmp .+ (-0.5*i) .* (h2 .* (0.5 .* (1 .- b)).^(i-1));
                end
                if i+j>0
                    tmp = tmp .* (0.5 .* (1 .- c)).^(i+j-1);
                end
                tmp = tmp .* h3 .* h1;
                dmodeds = dmodeds .+ tmp;

                # t-derivative
                dmodedt = 0.5 .* (1 .+ a) .* dmodedr .+ 0.5 .* (1 .+ b) .* tmp;
                tmp = dhc .* (0.5 .* (1 .- c)).^(i+j);
                if i+j>0
                    tmp = tmp .- (0.5*(i+j)) .* h3 .* (0.5 .* (1 .- c)).^(i+j-1);
                end
                tmp = h1 .* (h2 .* tmp) .* (0.5 .* (1 .- b)).^i;
                dmodedt = dmodedt .+ tmp;

                # Normalize
                dmodedr = 2^(2*i+j+1.5) .* dmodedr;
                dmodeds = 2^(2*i+j+1.5) .* dmodeds;
                dmodedt = 2^(2*i+j+1.5) .* dmodedt;

                gradVr[:,sk] = dmodedr;
                gradVs[:,sk] = dmodeds;
                gradVt[:,sk] = dmodedt;

                sk = sk+1;
            end
        end
    end

    return (V, gradVr, gradVs, gradVt);
end


"""
    DoubleWell(mass=1, ω=1, γ=1, Δ=1)

Two state double well, also called the one-dimensional spin-boson model.
See: [J. Chem. Phys. 150, 244102 (2019)](https://doi.org/10.1063/1.5096276)
"""
Parameters.@with_kw struct DoubleWell{M,W,Y,D} <: DiabaticModel
    mass::M = 1
    ω::W = 1
    γ::Y = 1
    Δ::D = 1
end

NQCModels.ndofs(::DoubleWell) = 1
NQCModels.nstates(::DoubleWell) = 2

function NQCModels.potential(model::DoubleWell, R::Real)

    V0(R) = 0.5 * model.mass * model.ω^2 * R^2

    V₀ = V0(R)
    v = sqrt(2)*model.γ*R
    V11 = V₀ + v
    V22 = V₀ - v
    V12 = model.Δ/2

    return Hermitian(SMatrix{2,2}(V11, V12, V12, V22))
end

function NQCModels.derivative(model::DoubleWell, R::Real)

    D0(R) = model.mass * model.ω^2 * R

    D₀ = D0(R[1])
    v = sqrt(2)*model.γ
    D11 = D₀ + v
    D22 = D₀ - v
    return Hermitian(SMatrix{2,2}(D11, 0, 0, D22))
end


n = 200
m = 100
p = 25

info("Testing ", MOD.pairwise)
for kernelobj in additive_kernels
    for T in FloatingPointTypes
        x = rand(T,p)
        y = rand(T,p)
        k = convert(Kernel{T}, (kernelobj)())

        @test MOD.pairwise(k, x[1], y[1]) == MOD.phi(k, x[1], y[1])
        @test MOD.pairwise(k, x, y)       == sum(map((x,y) -> MOD.phi(k,x,y), x, y))
    end
end

info("Testing ", MOD.pairwisematrix!)
steps = length(additive_kernels) + 1
counter = 0
info("    Progress:   0.0%")
for kernelobj in (additive_kernels..., MLKTest.PairwiseTestKernel)
    for T in FloatingPointTypes
        Set_X = [rand(T, p) for i = 1:n]
        Set_Y = [rand(T,p)  for i = 1:m]

        X = transpose(hcat(Set_X...))
        Y = transpose(hcat(Set_Y...))
        k = convert(Kernel{T}, (kernelobj)())
        #println("κ:", k, ", X:", typeof(X), ", Y:", typeof(Y))

        P = [MOD.pairwise(k,x,y) for x in Set_X, y in Set_X]
        #@test_approx_eq MOD.pairwisematrix(Val{:row}, k, X)  P
        #@test_approx_eq MOD.pairwisematrix(Val{:col}, k, X') P
        @test_approx_eq MOD.pairwisematrix!(Val{:row}, Array(T,n,n), k, X,  true)  P
        @test_approx_eq MOD.pairwisematrix!(Val{:col}, Array(T,n,n), k, X', true) P

        P = [MOD.pairwise(k,x,y) for x in Set_X, y in Set_Y]
        #@test_approx_eq MOD.pairwisematrix(Val{:row}, k, X,  Y)  P
        #@test_approx_eq MOD.pairwisematrix(Val{:col}, k, X', Y') P
        @test_approx_eq MOD.pairwisematrix!(Val{:row}, Array(T,n,m), k, X,  Y)  P
        @test_approx_eq MOD.pairwisematrix!(Val{:col}, Array(T,n,m), k, X', Y') P

        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:row}, Array(T,n+1,n),   k, X, true)
        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:row}, Array(T,n,n+1),   k, X, true)
        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:row}, Array(T,n+1,n+1), k, X, true)

        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:col}, Array(T,n+1,n),   k, X', true)
        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:col}, Array(T,n,n+1),   k, X', true)
        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:col}, Array(T,n+1,n+1), k, X', true)

        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:row}, Array(T,n+1,m), k, X,  Y)
        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:row}, Array(T,n,m+1), k, X,  Y)
        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:col}, Array(T,n+1,m), k, X', Y')
        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:col}, Array(T,n,m+1), k, X', Y')

        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:row}, Array(T,m+1,n), k, Y,  X)
        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:row}, Array(T,m,n+1), k, Y,  X)
        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:col}, Array(T,m+1,n), k, Y', X')
        @test_throws DimensionMismatch MOD.pairwisematrix!(Val{:col}, Array(T,m,n+1), k, Y', X')

        @test_approx_eq MOD.dotvectors(Val{:row}, X) sum((X.*X),2)
        @test_approx_eq MOD.dotvectors(Val{:col}, X) sum((X.*X),1)

        @test_approx_eq MOD.dotvectors!(Val{:row}, Array(T,n), X) sum((X.*X),2)
        @test_approx_eq MOD.dotvectors!(Val{:col}, Array(T,p), X) sum((X.*X),1)

        @test_throws DimensionMismatch MOD.dotvectors!(Val{:row}, Array(T,2), Array(T,3,2))
        @test_throws DimensionMismatch MOD.dotvectors!(Val{:row}, Array(T,4), Array(T,3,4))
        @test_throws DimensionMismatch MOD.dotvectors!(Val{:col}, Array(T,2), Array(T,2,3))
        @test_throws DimensionMismatch MOD.dotvectors!(Val{:col}, Array(T,4), Array(T,4,3))

        @test_throws DimensionMismatch MOD.squared_distance!(Array(T,3,4), Array(T,3), true)
        @test_throws DimensionMismatch MOD.squared_distance!(Array(T,4,3), Array(T,3), true)

        @test_throws DimensionMismatch MOD.squared_distance!(Array(T,3,4), Array(T,2), Array(T,4))
        @test_throws DimensionMismatch MOD.squared_distance!(Array(T,3,4), Array(T,4), Array(T,4))
        @test_throws DimensionMismatch MOD.squared_distance!(Array(T,3,4), Array(T,3), Array(T,3))
        @test_throws DimensionMismatch MOD.squared_distance!(Array(T,3,4), Array(T,3), Array(T,5))
    end
    counter += 1
    info("    Progress: ", @sprintf("%5.1f", counter/steps*100), "%")
end


using InfiniteLinearAlgebra, InfiniteArrays, BandedMatrices, FillArrays, LazyBandedMatrices, LazyArrays, Test
import BandedMatrices: _BandedMatrix

@testset "∞-banded" begin
    @testset "Diagonal and BandedMatrix" begin
        D = Diagonal(Fill(2,∞))

        B = D[1:∞,2:∞]
        @test B isa BandedMatrix
        @test B[1:10,1:10] == diagm(-1 => Fill(2,9))
        @test B[1:∞,2:∞] isa BandedMatrix

        A = BandedMatrix(0 => 1:∞, 1=> Fill(2.0,∞), -1 => Fill(3.0,∞))
        x = [1; 2; zeros(∞)]
        @test A*x isa Vcat
        @test (A*x)[1:10] == A[1:10,1:10]*x[1:10]

        @test InfBandCartesianIndices(0)[1:5] == CartesianIndex.(1:5,1:5)
        @test InfBandCartesianIndices(1)[1:5] == CartesianIndex.(1:5,2:6)
        @test InfBandCartesianIndices(-1)[1:5] == CartesianIndex.(2:6,1:5)

        @test A[band(0)][2:10] == 2:10
        @test D[band(0)] ≡ Fill(2,∞)
        @test D[band(1)] ≡ Fill(0,∞)

        @test B*A*x isa Vcat
        @test (B*A*x)[1:10] == [0; 10; 14; 12; zeros(6)]

        @test _BandedMatrix((1:∞)', ∞, -1, 1) isa BandedMatrix
    end

    @testset "∞-Toeplitz" begin
        A = BandedMatrix(1 => Fill(2im,∞), 2 => Fill(-1,∞), 3 => Fill(2,∞), -2 => Fill(-4,∞), -3 => Fill(-2im,∞))
        @test A isa InfToeplitz
        @test MemoryLayout(typeof(A.data)) == ConstRows()
        @test MemoryLayout(typeof(A)) == BandedToeplitzLayout()
        V = view(A,:,3:∞)
        @test MemoryLayout(typeof(bandeddata(V))) == ConstRows()
        @test MemoryLayout(typeof(V)) == BandedToeplitzLayout()
        @test BandedMatrix(V) isa InfToeplitz
        @test A[:,3:end] isa InfToeplitz

        @test (A + 2I)[1:10,1:10] == (2I + A)[1:10,1:10] == A[1:10,1:10] + 2I
        @test (A*A)[1:10,1:10] ≈ A[1:10,1:16]A[1:16,1:10]
        @test (A * Fill(2,∞))[1:10] ≈ 2A[1:10,1:16]*ones(16)
        @test (Fill(2,∞,∞)*A)[1:10,1:10] ≈ fill(2,10,13)A[1:13,1:10]

        @test Eye(∞) * A isa BandedMatrix
        @test A * Eye(∞) isa BandedMatrix

        @test A * [1; 2; Zeros(∞)] isa Vcat
        @test A * [1; 2; Zeros(∞)] == [A[1:5,1:2] * [1,2]; Zeros(∞)]

        @test MemoryLayout(Tridiagonal(Fill(1,∞), Fill(2,∞), Fill(3,∞))) isa TridiagonalToeplitzLayout
        @test MemoryLayout(Bidiagonal(Fill(1,∞), Fill(2,∞), :U)) isa BidiagonalToeplitzLayout
        @test MemoryLayout(SymTridiagonal(Fill(1,∞), Fill(2,∞))) isa TridiagonalToeplitzLayout
        @test MemoryLayout(LazyBandedMatrices.Tridiagonal(Fill(1,∞), Zeros(∞), Fill(3,∞))) isa TridiagonalToeplitzLayout
        @test MemoryLayout(LazyBandedMatrices.Bidiagonal(Fill(1,∞), Zeros(∞), :U)) isa BidiagonalToeplitzLayout
        @test MemoryLayout(LazyBandedMatrices.SymTridiagonal(Fill(1,∞), Zeros(∞))) isa TridiagonalToeplitzLayout

        T = LazyBandedMatrices.Tridiagonal(Fill(1,∞), Zeros(∞), Fill(3,∞))
        @test T[2:∞,3:∞] isa SubArray
        @test exp.(T) isa BroadcastMatrix
        @test exp.(T)[2:∞,3:∞] isa SubArray

        B = LazyBandedMatrices.Bidiagonal(Fill(1,∞), Zeros(∞), :U)
        @test B[2:∞,3:∞] isa SubArray
        @test exp.(B) isa BroadcastMatrix
        @test exp.(B)[2:∞,3:∞] isa SubArray

        @testset "algebra" begin
            T = Tridiagonal(Fill(1,∞), Fill(2,∞), Fill(3,∞))
            @test T isa InfiniteLinearAlgebra.TriToeplitz
            @test (T + 2I)[1:10,1:10] == (2I + T)[1:10,1:10] == T[1:10,1:10] + 2I
        end

        @testset "constant data" begin
            A = BandedMatrix(1 => Fill(2im,∞), 2 => Fill(-1,∞), 3 => Fill(2,∞), -2 => Fill(-4,∞), -3 => Fill(-2im,∞))
            B = _BandedMatrix(Fill(2,4,∞), ∞, 1,2)
            @test (B*B)[1:10,1:10] ≈ B[1:10,1:14]B[1:14,1:10]
            @test (A*B)[1:10,1:10] ≈ A[1:10,1:14]B[1:14,1:10]
            @test (B*A)[1:10,1:10] ≈ B[1:10,1:14]A[1:14,1:10]
        end
    end

    @testset "Pert-Toeplitz" begin
        @testset "Inf Pert" begin
            A = BandedMatrix(-2 => Vcat(Float64[], Fill(1/4,∞)), 0 => Vcat([1.0+im,2,3],Fill(0,∞)), 1 => Vcat(Float64[], Fill(1,∞)))
            @test A isa PertToeplitz
            @test MemoryLayout(typeof(A)) == PertToeplitzLayout()
            V = view(A,2:∞,2:∞)
            @test MemoryLayout(typeof(V)) == PertToeplitzLayout()
            @test BandedMatrix(V) isa PertToeplitz
            @test A[2:∞,2:∞] isa PertToeplitz

            @test (A + 2I)[1:10,1:10] == (2I + A)[1:10,1:10] == A[1:10,1:10] + 2I

            @test Eye(∞) * A isa BandedMatrix
            @test A * Eye(∞) isa BandedMatrix
        end

        @testset "TriPert" begin
            A = SymTridiagonal(Vcat([1,2.], Fill(2.,∞)), Vcat([3.,4.], Fill.(0.5,∞)))
            @test A isa InfiniteLinearAlgebra.SymTriPertToeplitz
            @test (A + 2I)[1:10,1:10] == (2I + A)[1:10,1:10] == A[1:10,1:10] + 2I

            A = Tridiagonal(Vcat([3.,4.], Fill.(0.5,∞)), Vcat([1,2.], Fill(2.,∞)), Vcat([3.,4.], Fill.(0.5,∞)))
            @test A isa InfiniteLinearAlgebra.TriPertToeplitz
            @test Adjoint(A) isa InfiniteLinearAlgebra.AdjTriPertToeplitz
            @test (A + 2I)[1:10,1:10] == (2I + A)[1:10,1:10] == A[1:10,1:10] + 2I
            @test (Adjoint(A) + 2I)[1:10,1:10] == (2I + Adjoint(A))[1:10,1:10] == Adjoint(A)[1:10,1:10] + 2I
        end

        @testset "InfBanded" begin
            A = _BandedMatrix(Fill(2,4,∞),ℵ₀,2,1)
            B = _BandedMatrix(Fill(3,2,∞),ℵ₀,-1,2)
            @test mul(A,A) isa PertToeplitz
            @test A*A isa PertToeplitz
            @test (A*A)[1:20,1:20] == A[1:20,1:23]*A[1:23,1:20]
            @test (A*B)[1:20,1:20] == A[1:20,1:23]*B[1:23,1:20]
        end
    end

    @testset "adjortrans" begin
        A = BandedMatrix(0 => 1:∞, 1=> Fill(2.0+im,∞), -1 => Fill(3.0,∞))
        @test copy(A')[1:10,1:10] == (A')[1:10,1:10]
        @test copy(transpose(A))[1:10,1:10] == transpose(A)[1:10,1:10]
    end

    @testset "Eye subindex" begin
        @test Eye(∞)[:,1:3][1:5,:] == Eye(∞)[Base.Slice(oneto(∞)),1:3][1:5,:] == Eye(5,3)
        @test Eye(∞)[1:3,:][:,1:5] == Eye(∞)[1:3,Base.Slice(oneto(∞))][:,1:5] == Eye(3,5)
        @test Eye(∞)[:,:][1:5,1:3] == Eye(∞)[Base.Slice(oneto(∞)),Base.Slice(oneto(∞))][1:5,1:3] == Eye(5,3)
    end

    @testset "band(0) indexing" begin
        D = ApplyArray(*, Diagonal(1:∞), Diagonal(1:∞))
        @test D[band(0)][1:10] == (1:10).^2
    end

    @testset "Fill * Banded" begin
        A = _BandedMatrix(Ones(1,∞), ∞, 1,-1)
        B = _BandedMatrix(Fill(1.0π,1,∞), ∞, 0,0)
        @test (A*B)[1:10,1:10] ≈ BandedMatrix(-1 => Fill(1.0π,9))
    end

    @testset "Diagonal{Fill} * Bidiagonal" begin
        A, B = Diagonal(Fill(2,∞)) , LazyBandedMatrices.Bidiagonal(exp.(1:∞), exp.(1:∞), :L)
        @test (A*B)[1:10,1:10] ≈ (B*A)[1:10,1:10] ≈ 2B[1:10,1:10]
    end

    @testset "concat" begin
        H = ApplyArray(hvcat, 2, 1, [1 Zeros(1,∞)], [1; Zeros(∞)], Diagonal(1:∞))
        @test bandwidths(H) == (1,1)
        H = ApplyArray(hvcat, 2, 1, [0 Zeros(1,∞)], [0; Zeros(∞)], Diagonal(1:∞))
        @test bandwidths(H) == (0,0)
        H = ApplyArray(hvcat, (2,2), 1, [1 Zeros(1,∞)], [1; Zeros(∞)], Diagonal(1:∞))
        @test_broken bandwidths(H) == (1,1)
    end

    @testset "Banded * PaddedMatrix" begin
        A = Eye(∞)[2:∞,:]
        B = PaddedArray(randn(3,3),ℵ₀,ℵ₀)
        @test (A*B)[1:10,1:10] ≈ A[1:10,1:10] * B[1:10,1:10]
    end
end


### Statistics

rank_inverse_transform(dist::Distribution, x) = quantile(dist,  (tiedrank(x) - 0.5) / length(x))

inverse_normal_transform(x::Array) = rank_inverse_transform(Normal(), x)

function inverse_normal_transform(x::DataArray)
	y  = copy(x)
	y[!isna(y)] = rank_inverse_transform(Normal(), dropna(x))
	return y
end

## FIXME: These functions needs cleanup!

function mean_imputation!(data::DataFrame,key::Symbol)
	mean_value = mean(map(Float32, x[find(!isna(x[:,key])),key]))
	data[key] = map(1:nrow(data)) do i
		if isna(data[i,key])
			value = group_mean_dict[data[i,groupby]]
			isnan(value) ? NA : value
		else
			data[i,key]
		end
	end
end

function group_mean_imputation!(data::DataFrame,key::Symbol, groupby::Symbol)
	group_mean = by(data, groupby, x->mean(map(Float32, x[find(!isna(x[:,key])),key])))
	group_mean_dict = Dict(zip(group_mean[groupby], group_mean[:x1]))
	data[key] = map(1:nrow(data)) do i
		if isna(data[i,key])
			value = group_mean_dict[data[i,groupby]]
			isnan(value) ? NA : value
		else
			data[i,key]
		end
	end
end


function _gaussian(E::T, p::T, E0::T, p0::T; dE = 5.0, dthetap = 0.01) where T<:Number
    thetap = acos(p)/(2pi)
    thetap0 = acos(p0)/(2pi)
    p = clamp(p,nextfloat(-1.0),prevfloat(1.0))
    a = ((E - E0)/dE)^2 + ((thetap - thetap0)/dthetap)^2
    J = inv(sqrt(1.0 - p^2))/2pi
    return J*exp(-0.5*a)/(2pi*dE*dthetap)
end

function gaussian(energy::AbstractVector, pitch::AbstractVector, E0, p0; kwargs...)
    nenergy = length(energy)
    npitch = length(pitch)

    f = zeros(nenergy,npitch)
    for j=1:npitch, i=1:nenergy
        f[i,j] = _gaussian(energy[i],pitch[j], E0, p0; kwargs...)
    end
    return f
end

function lnΔ_ee(ne, Te)
    # Thermal electron-electron collisions
    # NRL Plasma Formulary ne in cm^-3, Te in eV
    return 23.5 - log(sqrt(ne)*(Te^(-5/4))) - sqrt(1e-5 + ((log(Te) - 2)^2)/16)
end

function lnΔ_ei(ne, Te, ni, Ti; mu = H2_amu, Z = 1)
    # Electron-ion collisions
    # NRL Plasma Formulary ne/i in cm^-3, Te/i in eV, mu in amu
    mr = e_amu/mu
    if Ti*mr < Te < 10Z^2
        return 23 - log(sqrt(ne)*Z*Te^(-3/2))
    elseif Ti*mr < 10Z^2 < Te
        return 24 - log(sqrt(ne)/Te)
    elseif Te < Ti*mr
        return 16 - log(mu*sqrt(ni)*(Ti^(-3/2))*Z^2)
    end
    return 16.0
end

function lnΔ_ii(ni1, Ti1, ni2, Ti2; mu1=H2_amu, mu2=H2_amu, Z1=1,Z2=1)
    # Mixed ion-ion collisions
    # NRL Plasma Formulary ni in cm^-3 Ti in eV, mu in amu

    return 23 - log(((Z1*Z2*(mu1+mu2))/(mu1*Ti2 + mu2*Ti1))*sqrt((ni1*Z1^2)/Ti1 + (ni2*Z2^2)/Ti2))
end

function lnΔ_ii(ni1, Ti1, ni2, Ti2, ne, Te, beta_D; mu1=H2_amu, mu2=H2_amu, Z1=1, Z2=1)
    # Counter-streaming ions (relative velocity v_D = beta_D*c) in the presence of warm electrons
    # NRL Plasma Formulary ne in cm^-3, Te in eV

    m_i1 = mass_u*mu1
    m_i2 = mass_u*mu2
    m_e = mass_u*e_amu
    L1 = Ti1*e0/m_i1
    L2 = Ti2*e0/m_i2
    U = Te*e0/m_e

    if max(L1, L2) < (beta_D*c0)^2 < U
        return 43 - log((Z1*Z2*(mu1 + mu2)/(mu1*mu2*beta_D^2))*sqrt(ne/Te))
    else
        return lnΔ_ii(ni1, Ti1, ni2, Ti2; mu1=mu1, mu2=mu2, Z1=Z1, Z2=Z2)
    end
end

function legendre(x::T, nterms) where T

    a = one(T)
    b = x

    p = zeros(T, nterms)

    for l=0:min(1,nterms-1)
        p[l+1] = l == 0 ? a : b
    end

    for l in 2:(nterms-1)
        a, p[l+1] = p[l], ((2l-1)*x*p[l] - (l-1)*a)/l
    end

    return p
end

function heaviside(t::T) where T
    t < 0 ? zero(T) : one(T)
end

function _slowing_down_legendre_expansion(u::T, P::Vector, P0::Vector) where T
    L = 0:(length(P)-1)
    S = 0.5*sum((2*l + 1) * P[l+1] * P0[l+1] * u^(l*(l+1)) for l in L)
    return S
end

function _slowing_down_legendre_expansion(xi::T, xi0::T, u::T, nterms) where T
    P = legendre(xi, nterms)
    P0 = legendre(xi0, nterms)
    _slowing_down_legendre_expansion(u, P, P0)
end

function slowing_down_time(ne, Te, Ti, Zeff;
                           Ai = H2_amu, Ab = H2_amu, Aimp=C6_amu,
                           Zi = 1, Zimp = 6, Zb=1)
    # Calculate Slowing down time on electrons
    # Heating of toroidal plasmas by neutral injection: T H Stix 1972 Plasma Phys. 14 367
    # Te in keV, ne in cm^-3

    nimp = Zeff > 1 ? ne*(Zeff - 1)/(Zimp*(Zimp-1)) : zero(ne)
    ni = max(ne - Zimp*nimp,zero(ne))

    lnΔ_e = lnΔ_ei(ne, Te*1e3, ni, Ti*1e3; mu=Ab, Z=Zb)

    tau_s = 6.27e8 * Ab*((Te*1e3)^1.5)/(ne*lnΔ_e*Zb^2)

    return tau_s
end

function _electron_ion_drag_difference(Eb, ne, Te, Ti, Zeff; Ab=H2_amu, Ai=H2_amu, Aimp=C6_amu, Zb=1,Zi=1,Zimp=6)
    # Calculates difference of electron drag - ion drag
    # Energetic ion distribution resulting from neutral beam injection in tokamaks: J Gaffey 1976 J. Plasma Phys. 16 149
    # E0/Te in keV, ne in cm^-3

    m_e = e_amu*mass_u
    m_b = Ab*mass_u
    m_imp = Aimp*mass_u
    m_i = Ai*mass_u

    v_b = sqrt(2*Eb*e0*1e3/m_b)
    v_e = sqrt(2*Te*e0*1e3/m_e)

    nimp = Zeff > 1 ? ne*(Zeff-1)/((Zimp*(Zimp-1))) : zero(ne)
    ni = max(ne - Zimp*nimp,zero(ne))

    lnΔ_be = lnΔ_ei(ne, Te*1e3, ni, Ti*1e3; mu=Ab, Z=Zb)
    Γ_be = (2*pi*ne*(e0^4)*Zb^2*lnΔ_be)/(m_b^2)

    electron_drag = ((8*Γ_be*m_b)/(3*sqrt(pi)*m_e*v_e^3))*v_b^3

    lnΔ_bi = lnΔ_ii(ni, Ti*1e3, ni, Ti*1e3, ne, Te*1e3, v_b/c0; mu1=Ab, Z1=Zb, mu2=Ai, Z2=Zi)
    Γ_bi = (2*pi*ni*(e0^4)*Zi^2*Zb^2*lnΔ_bi)/(m_b^2)

    lnΔ_bimp = lnΔ_ii(ni, Ti*1e3, nimp, Ti*1e3, ne, Te*1e3, v_b/c0; mu1=Ab, Z1=Zb, mu2=Aimp, Z2=Zimp)
    Γ_bimp = (2*pi*nimp*(e0^4)*(Zimp^2)*(Zb^2)*lnΔ_bimp)/(m_b^2)

    ion_drag = 2*m_b*(Γ_bi/m_i + Γ_bimp/m_imp)

    return electron_drag - ion_drag
end

function critical_energy(ne, Te, Ti, Zeff; Emax = 300.0,
                         Ai = H2_amu, Aimp = C6_amu, Ab=H2_amu,
                         Zi = 1, Zimp=6, Zb=1)
    # Calculates critical energy using root finding on drags
    # Energetic ion distribution resulting from neutral beam injection in tokamaks: J Gaffey 1976 J. Plasma Phys. 16 149
    # E0/Te in keV, ne in cm^-3

    Ec = find_zero(x -> _electron_ion_drag_difference(x, ne, Te, Ti, Zeff;
                   Ai=Ai, Aimp=Aimp, Ab=Ab, Zi=Zi,Zimp=Zimp, Zb=Zb),
                  (0, Emax))
    return Ec
end

function approx_critical_energy(ne, Te, Zeff; correction_factor = 1.0,
                                Ai = H2_amu, Aimp = C6_amu, Ab=H2_amu,
                                Zi = 1, Zimp=6, Zb=1)
    # Calculate critical energy assuming lnΔ_be == lnΔ_bi
    # For DIII-D a correction factor of (lnΔ_bi/lnΔ_be)^(2/3) ≈ 1.2 can be used
    # Heating of toroidal plasmas by neutral injection: T H Stix 1972 Plasma Phys. 14 367
    # E0/Te in keV, ne in cm^-3

    nimp = Zeff > 1 ? ne*(Zeff - 1)/(Zimp*(Zimp-1)) : zero(ne)
    ni = max(ne - Zimp*nimp,zero(ne))

    # average charge to mass ratio (avg_cmr) is where approximation comes in
    avg_cmr = (ni*(Zi^2/Ai) + nimp*(Zimp^2/Aimp))/ne
    Ec = 14.8 * Ab * Te * correction_factor*avg_cmr^(2/3)

    return Ec
end

function _thermalization_time(v_b, v_c, tau_s)
    vb3 = v_b^3
    vc3 = v_c^3
    tau_s*log((vb3 + vc3)/vc3)/3
end

function thermalization_time(E0; ne = 1.0, Zeff = 1.0, Te = 1.0, Ti = Te,
                             Ai = H2_amu, Ab = Ai, Aimp = C6_amu,
                             Zi=1, Zimp=6, Zb=Zi, kwargs...) where T<:Number

    m_b = mass_u*Ab
    v_b = sqrt(2*(e0*E0*1e3)/(m_b))
    tau_s = slowing_down_time(ne, Te, Ti, Zeff; Ai = Ai, Aimp=Aimp, Ab = Ab, Zi=Zi, Zimp=Zimp, Zb=Zb)
    Ec = critical_energy(ne, Te, Ti, Zeff; Ai = Ai, Aimp=Aimp, Ab = Ab, Zi=Zi, Zimp=Zimp, Zb=Zb)
    v_c = sqrt(2*(e0*Ec*1e3)/m_b)

    return _thermalization_time(v_b, v_c, tau_s)
end

function _slowing_down(v::T, v_b::T, v_c::T, tau_s::T, Zeff::T, P::Vector, P0::Vector;
                       tau_on = 0.0, tau_off = 1.0, tau = 1.0) where T<:Number

    v >= v_b && return zero(T)

    v3 = v^3
    vb3 = v_b^3
    vc3 = v_c^3

    inv_v3vc3 = inv(v3 + vc3)
    u1 = v3/vb3
    u2 = (vb3 + vc3)*inv_v3vc3
    u = (u1*u2)^(Zeff/6)

    if tau_on > tau_off
        tau_off = Inf
    end
    t_b = _thermalization_time(v_b, v_c, tau_s)
    t_0 = tau_on*t_b
    t_th = tau_s*log(u2)/3 # threshold time
    t_1 = tau_off*t_b - t_0
    t = tau*t_b - t_0

    S = min(t,t_b,t_1)/t_b
    U = heaviside(t - t_th) - heaviside(t - t_1 - t_th)
    if U == 0
        return zero(T)
    end
    lex = _slowing_down_legendre_expansion(u, P, P0)
    g = max(S*tau_s*inv_v3vc3*lex,zero(T))/t_b
end

function _slowing_down(E::T, p::T, E0::T, p0::T, nterms; kwargs...) where T<:Number
    P = legendre(p, nterms)
    P0 = legendre(p0, nterms)
    _slowing_down(E, E0, P, P0; kwargs...)
end

function slowing_down(energy::AbstractVector, pitch::AbstractVector, E0, p0; nterms=50,
                      ne = 1.0, Zeff = 1.0, Te = 1.0, Ti = Te,
                      Ai = H2_amu, Ab = Ai, Aimp = C6_amu,
                      Zi=1, Zimp=6, Zb=Zi, kwargs...) where T<:Number

    nenergy = length(energy)
    npitch = length(pitch)

    # Calculate quantities that do not depend on energy/pitch
    m_b = mass_u*Ab
    v_b = sqrt(2*(e0*E0*1e3)/(m_b))
    tau_s = slowing_down_time(ne, Te, Ti, Zeff; Ai = Ai, Aimp=Aimp, Ab = Ab, Zi=Zi, Zimp=Zimp, Zb=Zb)
    Ec = critical_energy(ne, Te, Ti, Zeff; Ai = Ai, Aimp=Aimp, Ab = Ab, Zi=Zi, Zimp=Zimp, Zb=Zb)
    v_c = sqrt(2*(e0*Ec*1e3)/m_b)

    # Legendre expansion at birth pitch
    P0 = legendre(p0, nterms)

    f_slow = zeros(nenergy,npitch)
    for j=1:npitch
        P = legendre(pitch[j], nterms)
        for i=1:nenergy
            v = sqrt(2*(e0*energy[i]*1e3)/(m_b))
            J = v*((e0*1e3)/m_b)
            f_slow[i,j] = J*_slowing_down(v, v_b, v_c, tau_s, Zeff, P, P0; kwargs...)
        end
    end

    return f_slow
end

function _approx_slowing_down(v, v_b, v_c, p::T, p0::T, Zeff::T, tau_s::T, mi_mb::T;
                             tau = 1.0, tau_off=1.0, tau_on = 0.0) where T<:Number

    v >= v_b && return 0.0

    v3 = v^3
    vb3 = v_b^3
    vc3 = v_c^3

    inv_v3vc3 = inv(v3 + vc3)
    u1 = v3/vb3
    u2 = (vb3 + vc3)*inv_v3vc3
    u = (u1*u2)^(Zeff/6)

    if tau_on > tau_off
        tau_off = Inf
    end
    t_b = _thermalization_time(v_b, v_c, tau_s)
    t_0 = tau_on*t_b
    t_th = tau_s*log(u2)/3 # threshold time
    t_1 = tau_off*t_b - t_0
    t = tau*t_b - t_0

    S = min(t,t_b,t_1)/t_b
    U = heaviside(t - t_th) - heaviside(t - t_1 - t_th)
    if U == 0
        return zero(T)
    end

    beta = mi_mb*Zeff/2
    alpha = (beta/3)*(1 - p0^2)*log((1 + vc3/v3)/(1+vc3/vb3))
    g = tau_s*inv_v3vc3*inv(sqrt(4*pi*alpha))*exp(-((p - p0)^2)/(4*alpha))
    return g*S*U
end

function approx_slowing_down(energy::AbstractVector, pitch::AbstractVector, E0, p0;
                      ne = 1.0, Zeff = 1.0, Te = 1.0, Ti = Te,
                      Ai = H2_amu, Ab = Ai, Aimp = C6_amu,
                      Zi=1, Zimp=6, Zb=Zi, kwargs...) where T<:Number

    nenergy = length(energy)
    npitch = length(pitch)

    # Calculate quantities that do not depend on energy/pitch
    m_b = mass_u*Ab
    v_b = sqrt(2*(e0*E0*1e3)/(m_b))
    tau_s = slowing_down_time(ne, Te, Ti, Zeff; Ai = Ai, Aimp=Aimp, Ab = Ab, Zi=Zi, Zimp=Zimp, Zb=Zb)
    Ec = critical_energy(ne, Te, Ti, Zeff; Ai = Ai, Aimp=Aimp, Ab = Ab, Zi=Zi, Zimp=Zimp, Zb=Zb)
    v_c = sqrt(2*(e0*Ec*1e3)/m_b)

    nenergy = length(energy)
    npitch = length(pitch)

    f_slow = zeros(nenergy,npitch)
    for j=1:npitch, i=1:nenergy
        v = sqrt(2*(e0*energy[i]*1e3)/(m_b))
        J = v*((e0*1e3)/m_b)
        f_slow[i,j] = J*_approx_slowing_down(v, v_b, v_c, pitch[j], p0, Zeff, tau_s, Ai/Ab; kwargs...)
    end

    return f_slow
end

function _bimaxwellian(energy::S, pitch::S, T_perp::S, T_para::S;
                      vd_para=0.0, sigma=-1, Ai = H2_amu) where S<:Number

    m_i = mass_u*Ai

    T_perp = T_perp*1e3*e0
    T_para = T_para*1e3*e0
    Ed_para = (0.5*m_i*vd_para^2)

    E = energy*e0*1e3
    xi = clamp(sigma*pitch,nextfloat(-1.0),prevfloat(1.0))
    J = e0*1e3
    A = sqrt(E/(pi*T_para*T_perp^2))
    T1 = -(E*xi^2 + Ed_para - vd_para*xi*sqrt(2*m_i*E))/T_para
    T2 = -(1-xi^2)*E/T_perp
    return A*exp(T1 + T2)*J
end

function bimaxwellian(energy::AbstractVector, pitch::AbstractVector, T_perp, T_para; kwargs...)
    nenergy = length(energy)
    npitch = length(pitch)
    f_ep = zeros(nenergy, npitch)
    for j=1:npitch, i=1:nenergy
        f_ep[i,j] = _bimaxwellian(energy[i], pitch[j], T_perp, T_para; kwargs...)
    end
    T = (T_para + 2*T_perp)/3
    return f_ep
end

function maxwellian(energy, pitch, T; kwargs...)
    bimaxwellian(energy, pitch, T, T; kwargs...)
end


using SpinMC
using LinearAlgebra

a1 = (3/2, sqrt(3)/2)
a2 = (3/2, -sqrt(3)/2)
uc = UnitCell(a1,a2)

b1 = addBasisSite!(uc, (0.0, 0.0))
b2 = addBasisSite!(uc, (1.0, 0.0))

M = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]
addInteraction!(uc, b1, b2, M, (0, 0))
addInteraction!(uc, b1, b2, M, (0, -1))
addInteraction!(uc, b1, b2, M, (-1, 0))

L = (16, 16)
lattice = Lattice(uc, L)

thermalizationSweeps = 50000
measurementSweeps = 50000
beta = 10.0
m = MonteCarlo(lattice, beta, thermalizationSweeps, measurementSweeps)
run!(m)

# Fourier transform correlations to compute structure factor.
N = 256
correlation = mean(m.observables.correlation) # The correlation is measured with respect to the first spin, i.e. the i-th entry is the correlation dot(S_1,S_i).
kx = collect(range(-2pi,2pi,length=N))
ky = collect(range(-2pi,2pi,length=N))
structurefactor = zeros(N,N)
for i in 1:N
    for j in 1:N
        z = 0.0
        # Compute Fourier transformation at momentum (kx, ky). The real-space position of the i-th spin is obtained via getSitePosition(lattice,i).
        for k in 1:length(lattice)
            z += cos(dot((kx[i],ky[j]),getSitePosition(lattice,k))) * correlation[k]
        end
        structurefactor[j,i] = z / length(lattice)
    end
end

# Plot result
using Plots
heatmap(kx,ky,structurefactor,aspect_ratio=1,xrange=(-2pi,2pi),yrange=(-2pi,2pi),clims=(0,1),xlabel="kx", ylabel="ky")
xs = [4.0pi/3.0, 2.0pi/3.0, -2.0pi/3.0, -4.0pi/3.0, -2.0pi/3.0, 2.0pi/3.0, 4.0pi/3.0]
ys = [0.0, 2.0pi/sqrt(3.0), 2.0pi/sqrt(3.0), 0.0, -2.0pi/sqrt(3.0), -2.0pi/sqrt(3.0), 0.0]
plot!(xs, ys, label="Extended BZ")


import Base.size

immutable NN{n}
    ws::NTuple{n, Matrix{Float64}}   # weight matrices
    bs::NTuple{n, Vector{Float64}}   # bias vectors
    ns::NTuple{n, NodeFunc}          # node function (sigmoid, tanh, etc.)
    ls::NTuple{Int}                  # size vector

    function NN(ws::NTuple{n, Matrix{Float64}},
                bs::NTuple{n, Vector{Float64}},
                ns::NTuple{n, NodeFunc})

        for i in 1:length(ws)-1
            @assert size(ws[i],1) == size(ws[i+1],2) "size of levels $i and $(i+1) do not fit"
        end

        for i in 1:length(ws)
            @assert size(ws[i],1) == size(bs[i],1)   "size of bias and weight matrix do not fit at level $i"
        end

        ls = tuple([ size(w,1) for w in ws ]...)
        new(ws, bs, ns, ls)
    end
end

# NN{n}(ws::NTuple{n, Matrix{FloatingPoint}}, ns::NTuple{n, NodeFunc}) = NN{n}(ws, ns)

size(nn::NN) = nn.ls
inputsize(nn::NN) = size(nn.ws[1],2)
depth{n}(nn::NN{n}) = n

function NN(sz::Vector{Int}, nf=Relu(); seed::Integer=0)
    @assert length(sz) > 1 "Net needs at least 2 layers"
    ws = Matrix{Float64}[]
    bs = Vector{Float64}[]

    (seed != 0) && srand(seed)
    for i in 1:length(sz)-1
        push!(ws, randn(sz[i+1], sz[i]) .* sqrt(6/(sz[i+1]+sz[i])))
        push!(bs, zeros(sz[i+1]))
    end

    if isa(nf, NodeFunc)
        ns = NodeFunc[ nf for i in 1:length(ws)]
    elseif isa(nf, Vector) && length(nf)==1
        ns = NodeFunc[ nf[1] for i in 1:length(ws)]
    elseif isa(nf, Vector) && length(nf)==2
        ns = NodeFunc[ [ nf[1] for i in 1:length(ws)-1] ; nf[2] ]
    elseif isa(nf, Vector) && length(nf)==length(ws)
        ns = NodeFunc[ nf[i] for i in 1:length(ws) ]
    else
        error("net depth and node function length do not match")
    end

    NN{length(ws)}(tuple(ws...), tuple(bs...), tuple(ns...))
end

function show(io::IO, nn::NN)
    print(io, "neural net : ")
    print(io, "layers sizes $( Int[ inputsize(nn) , size(nn)... ]), ")
    print(io, "node functions $( map(typeof, nn.ns) )")
    println(io,"")
end


module AutoARIMA

using LinearAlgebra
using Optim
using Polynomials
using RecipesBase
using StaticArrays
using Statistics
using HypothesisTests

export seriesA,seriesB,seriesB2,seriesC,seriesD,seriesE,seriesF,seriesG,dowj,wine,lake
export autocovariance,autocovariance_matrix,autocorrelation,autocorrelation_matrix,partial_autocorrelation
export isinversible, isstationary
export innovations, levinson_durbin, least_squares, yule_walker, hannan_rissanen
export boxcox, inv_boxcox, guerrero, difference, integrate
export simulate, forecast, fit, toarma, k, residuals
export aic, aicc, bic, mse, rmse, mae, mape
export correlogram,partial_correlogram
export AbstractModel, AbstractParams
export ARParams, MAParams, ARMAParams, ARIMAParams, MSARIMAParams
export ARModel, MAModel, ARMAModel
export MA∞, AR∞
export boxjenkins

include("datasets.jl")
include("stats.jl")
include("transforms.jl")
include("abstract.jl")
include("criteria.jl")
include("simulate.jl")
include("recipes.jl")
include("ls.jl")
include("arma.jl")
include("ar.jl")
include("ma.jl")
include("arima.jl")
include("sarima.jl")
include("auto.jl")

end


using GLMakie
xtmp = 10 .^(range(-1.5, stop=0.3,length=10))
x = vcat(-reverse(xtmp), xtmp)
y = vcat(-reverse(xtmp), xtmp)
f(z) = log(z)
fvals = [f(u+1im*v) for u in x, v in y]
fvalues = real.(fvals)
fargs = angle.(fvals)

set_theme!(theme_dark())
fig = Figure(resolution = (1200,600))
ax1 = Axis3(fig[1,1]; aspect = :data, perspectiveness = 0.5,
    elevation = 0.08, azimuth = -1.02)
ax2 = Axis3(fig[1,2]; aspect = :data, perspectiveness = 0.5,
    elevation = 0.11, azimuth = -0.45)
wireframe!(ax1, x, y, fvalues; color = :white, transparency = true)
surface!(ax2, x, y, fvalues; color = fargs,
    colormap = :diverging_rainbow_bgymr_45_85_c67_n256,
    colorrange = (-π,π), backlight = 1f0, highclip = :black)
save("wireframeSurface.png", fig)
display(fig)


using MyJuliaGEMM

using LinearAlgebra, Printf, Random
BLAS.set_num_threads(1)

function referencegemm!(C, A, B)
  BLAS.gemm!('N', 'N', 1.0, A, B, 1.0, C)
end

Random.seed!(44)

benchsizes = reverse(48:48:1968)

println(" size   refperf (GFLOP/s)   myperf (GFLOP/s)   errorcheck")
for size in benchsizes
  m, n, k = size, size, size

  gflops = 2e-9 * m * n * k

  A    = rand(m, k)
  B    = rand(k, n)
  oldC = rand(m, n)

  refC = Array{Float64}(undef, m, n)
  C = similar(refC)

  referencetime = typemax(Float64)
  for rep = 1:3
    copy!(refC, oldC)
    time = @elapsed referencegemm!(refC, A, B)
    referencetime = min(time, referencetime)
  end

  mytime = typemax(Float64)
  for rep = 1:3
    copy!(C, oldC)
    time = @elapsed mygemm!(C, A, B)
    mytime = min(time, mytime)
  end

  referenceperf = gflops / referencetime
  myperf = gflops / mytime
  diff = maximum(abs.(C .- refC))

  @printf("%5d %19.2f %18.2f %12.2e\n", n, referenceperf, myperf, diff)
end