for (A,F) in ((:gamma, :arb_gamma),
              (:lgamma, :arb_lgamma),(:rgamma, :arb_rgamma), (:digamma, :arb_digamma),
              (:zeta, :arb_zeta),

              (:erf, :arb_hypgeom_erf), (:erfc, :arb_hypgeom_erfc), (:erfi, :arb_hypgeom_erfi),
              (:ei, :arb_hypgeom_ei), (:si, :arb_hypgeom_si), (:ci, :arb_hypgeom_ci),
              (:shi, :arb_hypgeom_shi), (:chi, :arb_hypgeom_chi),
             )
    @eval begin
        function ($A)(x::ArbReal{P}, prec::Int=P) where {P}
            z = ArbReal{P}()
            ccall(@libarb($F), Cvoid, (Ref{ArbReal}, Ref{ArbReal}, Clong), z, x, prec)
            return z
         end
    end
end

const Cint0 = zero(Cint)
const FmpzZero = Fmpz(0)

function lambertw(x::ArbReal{P}) where {P}
    z = ArbReal{P}()
    ccall(@libarb(arb_lambertw), Cvoid, (Ref{ArbReal}, Ref{ArbReal}, Cint, Clong), z, x, Cint0, P)
    return z
end

lambertw(x::ArbFloat{P}) where {P} = ArbFloat{P}(lambertw(ArbReal{P}(x)))

function lambertw(x::ArbComplex{P}) where {P}
    z = ArbComplex{P}()
    ccall(@libarb(acb_lambertw), Cvoid, (Ref{ArbComplex}, Ref{ArbComplex}, Ref{Fmpz}, Cint, Clong), z, x, FmpzZero, 0, 128)
    return z
end

for (A,F) in ((:gamma, :acb_gamma),
              (:lgamma, :acb_lgamma),(:rgamma, :acb_rgamma), (:digamma, :acb_digamma),

              (:logsinpi, :acb_log_sin_pi), (:barnesg, :acb_barnes_g), (:logbarnesg, :acb_log_barnes_g),
              (:agm1, :acb_agm1),

              (:erf, :acb_hypgeom_erf), (:erfc, :acb_hypgeom_erfc), (:erfi, :acb_hypgeom_erfi),
              (:ei, :acb_hypgeom_ei), (:si, :acb_hypgeom_si), (:ci, :acb_hypgeom_ci),
              (:shi, :acb_hypgeom_shi), (:chi, :acb_hypgeom_chi),
             )
    @eval begin
        function ($A)(x::ArbComplex{P}, prec::Int=P) where P
            z = ArbComplex{P}()
            ccall(@libarb($F), Cvoid, (Ref{ArbComplex}, Ref{ArbComplex}, Clong), z, x, prec)
            return z
         end
    end
end

for (A,F) in (
              (:eta, :acb_dirichlet_eta), (:xi, :acb_dirichlet_xi)
             )
    @eval begin
        function ($A)(x::ArbReal{P}, prec::Int=P) where {P}
            z = ArbComplex{P}()
            xc = ArbComplex{P}(x)
            ccall(@libarb($F), Cvoid, (Ref{ArbComplex}, Ref{ArbComplex}, Clong), z, xc, prec)
            rea = real(z)
            return rea
         end
    end
end
#=
for (A,F) in ((:elliptick, :elliptic_k), (:elliptice, :elliptic_e))
    @eval begin
        function ($A)(k::ArbReal{P}, prec::Int=P) where {P}
           m = square(k)
           return ($F)(m, prec)
        end
    end
end
=#
for (A,F) in ((:agm, :arb_agm), )
    @eval begin
        function ($A)(x::ArbReal{P}, y::ArbReal{P}, prec::Int=P) where {P}
            z = ArbReal{P}()
            ccall(@libarb($F), Cvoid, (Ref{ArbReal}, Ref{ArbReal}, Ref{ArbReal}, Clong), z, x, y, prec)
            return z
         end
    end
end

function polylog(s::ArbComplex{P}, z::ArbComplex{P}, prec::Int=P) where {P}
    w = ArbComplex{P}()
    ccall(@libarb(acb_polylog), Cvoid, (Ref{ArbComplex}, Ref{ArbComplex}, Ref{ArbComplex}, Cint), w, s, z, P)
    return w
end

function polylog(s::ArbReal{P}, z::ArbReal{P}, prec::Int=P) where {P}
    sc = ArbComplex(s)
    zc = ArbComplex(z)
    wc = polylog(sc, zc, prec)
    return wc
end

function polylog(s::ArbFloat{P}, z::ArbFloat{P}, prec::Int=P) where {P}
    sc = ArbComplex(s)
    zc = ArbComplex(z)
    wc = polylog(sc, zc, prec)
    return wc
end

function polylog(s::Int, z::ArbComplex{P}, prec::Int=P) where {P}
    w = ArbComplex{P}()
    ccall(@libarb(acb_polylog_si), Cvoid, (Ref{ArbComplex}, Cint, Ref{ArbComplex}, Cint), w, s, z, P)
    return w
end

function polylog(s::Int, z::ArbReal{P}, prec::Int=P) where {P}
    zc = ArbComplex(z)
    wc = polylog(s, zc, prec)
    return wc
end

function polylog(s::Int, z::ArbFloat{P}, prec::Int=P) where {P}
    zc = ArbComplex(z)
    wc = polylog(s, zc, prec)
    return wc
end

#=
for (A,F) in ((:ellipticp, :acb_elliptic_p), (:ellipticpi, :acb_elliptic_pi),
              (:ellipticzeta, :acb_elliptic_zeta), (:ellipticsigma, :acb_elliptic_sigma),
             )
    @eval begin
        function ($A)(x::ArbComplex{P}, y::ArbComplex{P}, prec::Int=P) where {P}
            z = ArbComplex{P}()
            ccall(@libarb($F), Cvoid, (Ref{ArbReal}, Ref{ArbReal}, Ref{ArbReal}, Clong), z, x, y, prec)
            return z
         end
    end
end

for (A,F) in ((:ellipticpi, :acb_elliptic_pi), (:ellipticp, :acb_elliptic_p),
              (:ellipticzeta, :acb_elliptic_zeta), (:ellipticsigma, :acb_elliptic_sigma),
             )
    @eval begin
        function ($A)(x::ArbReal{P}, y::ArbReal{P}, prec::Int=P) where {P}
            z = ArbComplex{P}()
            xc = ArbComplex{P}(x)
            yc = ArbComplex{P}(y)
            ccall(@libarb($F), Cvoid, (Ref{ArbComplex}, Ref{ArbComplex}, Ref{ArbComplex}, Clong), z, xc, yc, prec)
            rea = real(z)
            return rea
         end
    end
end
=#
#=
function ellipticpi(n::ArbComplex{P}, k::ArbComplex{P}, prec::Int=P) where {P}
    m = square(k)
    return elliptic_pi(n, m, prec)
end
=#

for (A,F) in ((:gamma, :arb_gamma),
              (:lgamma, :arb_lgamma),(:rgamma, :arb_rgamma), (:digamma, :arb_digamma),
              (:zeta, :arb_zeta),

              (:erf, :arb_hypgeom_erf), (:erfc, :arb_hypgeom_erfc), (:erfi, :arb_hypgeom_erfi),
              (:ei, :arb_hypgeom_ei), (:si, :arb_hypgeom_si), (:ci, :arb_hypgeom_ci),
              (:shi, :arb_hypgeom_shi), (:chi, :arb_hypgeom_chi),
             )
    @eval begin
        function ($A)(x::ArbFloat{P}, prec::Int=P) where {P}
            z = ArbReal{P}()
            xb = ArbReal{P}(x)
            ccall(@libarb($F), Cvoid, (Ref{ArbReal}, Ref{ArbReal}, Clong), z, xb, prec)
            return midpoint_byref(z)
         end
    end
end

for (A,F) in ((:eta, :acb_dirichlet_eta), (:xi, :acb_dirichlet_xi))
    @eval begin
        function ($A)(x::ArbFloat{P}, prec::Int=P) where {P}
            z = ArbComplex{P}()
            xc = ArbComplex{P}(x)
            ccall(@libarb($F), Cvoid, (Ref{ArbComplex}, Ref{ArbComplex}, Clong), z, xc, prec)
            return midpoint_byref(real(z))
         end
    end
end

function agm1(x::ArbReal{P}) where {P}
    x1 = ArbComplex{P}(x)
    w  = agm1(x1)
    z = real(w)
    return z
end

function agm1(x::ArbFloat{P}) where {P}
    x1 = ArbReal{P}(x)
    w  = agm1(x1)
    z = midpoint_byref(w)
    return z
end

function agm(x::ArbFloat{P}, y::ArbFloat{P}) where {P}
    x1 = ArbReal{P}(x)
    y1 = ArbReal{P}(y)
    w  = agm(x1, y1)
    z = midpoint_byref(w)
    return z
end

#=
function erfcx(z::ArbComplex{P}) where {P}
    hiprec = round(Int, P*1.25)
    setprecision(ArbComplex, hiprec)
    w = ArbComplex(real(z), imag(z), bits=hiprec)
    ww = w*w
    a  = exp(ww)
    b  = erfc(w)
    res  = a * b
    setprecision(ArbComplex, P)
    return ArbComplex(real(res), imag(res), bits=P-extrabits())
end

function erfcx(x::ArbReal{P}) where {P}
    hiprec = round(Int, P*1.25)
    setprecision(ArbReal, hiprec)
    w = ArbReal(x, bits=hiprec)
    ww = w*w
    a  = exp(ww)
    b  = erfc(w)
    res  = a * b
    setprecision(ArbReal, P)
    return ArbReal(res, bits=P-extrabits())
end

function erfcx(x::ArbFloat{P}) where {P}
    res = erfcx(ArbReal(x))
    return ArbFloat(res)
end
=#

function erfcx(z::ArbComplex{P}; magnify=2.125) where {P}
    external_precision = P
    external_extrabits = extrabits()
    internal_precision = round(Int, P*magnify)
    internal_extrabits = 0
    setextrabits(internal_extrabits)
    setworkingprecision(ArbReal, internal_precision)

    re = round(real(z), sigdigits=internal_precision, base=2)
    im = round(imag(z), sigdigits=internal_precision, base=2)
    w  = ArbComplex(re, im)
    ww = w*w
    a  = exp(ww)
    b  = erfc(w)
    ab = a * b
    abre = round(real(ab), sigdigits=external_precision, base=2)
    abim = round(imag(ab), sigdigits=external_precision, base=2)
    re = ArbReal{P}(abre)
    im = ArbReal{P}(abim)
    z  = ArbComplex{P}(re, im)
    setextrabits(external_extrabits)
    setworkingprecision(ArbReal, external_precision)
    return z
end

function erfcx(x::ArbReal{P}; magnify=2.125) where {P}
    external_precision = P
    external_extrabits = extrabits()
    internal_precision = round(Int, P*magnify)
    internal_extrabits = 0
    setextrabits(internal_extrabits)
    setworkingprecision(ArbReal, internal_precision)

    w = round(x, sigdigits=internal_precision, base=2)
    ww = w*w
    a  = exp(ww)
    b  = erfc(w)
    ab = a * b
    abre = round(real(ab), sigdigits=external_precision, base=2)
    z = ArbReal{P}(abre)
    setextrabits(external_extrabits)
    setworkingprecision(ArbReal, external_precision)
    return z
end

function erfcx(x::ArbFloat{P}; magnify=2.125) where {P}
    res = erfcx(ArbReal(x); magnify=magnify)
    return ArbFloat(res)
end


import UncertainData:
    resample,
    AbstractUncertainIndexValueDataset,
    SequentialSamplingConstraint,
    RegularGrid

"""
    causality(source::AbstractUncertainIndexValueDataset,
        target::AbstractUncertainIndexValueDataset,
        test::CausalityTest)

Test for a causal influence from `source` to `target` using the provided causality `test`.

The test is performed on a single draw of the values of `source` and `target`, disregarding
the ordering resulting from resampling, but respecting the order of the points in the dataset.

*Note: if the uncertain values furnishing the indices have overlapping supports, you might
mess up the index-ordering (e.g. time-ordering) of the data points*.

!!! note

    This method uses [`UncertainData.jl`](https://github.com/kahaaga/UncertainData.jl) for
    uncertainty handling. To use it, first run `using UncertainData` in the Julia console.

## Example

Generate some example data series x and y, where x influences y:

```julia
using UncertainData, CausalityTools, Distributions
n_pts = 300
a₁, a₂, b₁, b₂, ξ₁, ξ₂, C₁₂ = 0.7, 0.1, 0.75, 0.2, 0.3, 0.3, 0.5

D = rand(n_pts, 2)
for t in 5:n_pts
    D[t,1] = a₁*D[t-1,1] - a₂*D[t-4,1] +                ξ₁*rand(Normal(0, 1))
    D[t,2] = b₁*D[t-1,2] - b₂*D[t-4,2] + C₁₂*D[t-1,1] + ξ₂*rand(Normal(0, 1))
end

x, y = D[:, 1], D[:, 2]
```

Add some uncertainties and gather in an `UncertainIndexValueDataset`

```julia
t = 1:n_pts
tu = UncertainValue.(Normal.(t, rand()))
xu = UncertainValue.(Normal.(x, rand()))
yu = UncertainValue.(Normal.(y, rand()))
X = UncertainIndexValueDataset(tu, xu)
Y = UncertainIndexValueDataset(tu, yu)
```

Define a causality test, for example the predictive asymmetry test:

```julia
# Define causality test
k, l, m = 1, 1, 1
ηs = -8:8
n_subdivs = floor(Int, n_pts^(1/(k+l+m+1)))
bin = RectangularBinning(n_subdivs)
te_test = VisitationFrequencyTest(k = k, l = l, m = m, binning = bin, ηs = ηs)
pa_test = PredictiveAsymmetryTest(predictive_test = te_test)
```

Run the causality test on a single draw of `X` and a single draw of `Y`:

```
pa_XY = causality(X, Y, pa_test)
pa_YX = causality(Y, X, pa_test)
```

Repeat the test on multiple draws:

```julia
pa_XY = [causality(X, Y, pa_test) for i = 1:100]
pa_YX = [causality(Y, X, pa_test) for i = 1:100]
```
"""
function causality(source::AbstractUncertainIndexValueDataset,
            target::AbstractUncertainIndexValueDataset,
            test::CausalityTest)

    # @warn """
    # You're running a causality test by resampling `source` and `target`, which both have
    # uncertain indices, without ensuring proper ordering of the indices. If the
    # distributions/populations furnishing the indices have overlapping supports, you are
    # not guaranteed to have the correct ordering, and the results you get might be meaningless!

    # Use the following method instead:

    # `causality(source::AbstractUncertainIndexValueDataset,
    #     target::AbstractUncertainIndexValueDataset,
    #     constraint::SequentialSamplingConstraint,
    #     grid::RegularGrid)`.

    # For example, `causality(source, target, StrictlyIncreasing(), RegularGrid(start, stop, step))`
    # will first pick a strictly increasing draw of the age model, interpolate to a regular grid
    # with spacing `step`, then run the test on the data interpolated to this grid, and finally
    # perform the causality test.
    # """
    idxs_s, vals_s = resample(source)
    idxs_t, vals_t = resample(target)
    causality(vals_s, vals_t, test)
end

"""
    causality(source::AbstractUncertainIndexValueDataset,
        target::AbstractUncertainIndexValueDataset,
        test::CausalityTest, constraint::SequentialSamplingConstraint, grid::RegularGrid)

Test for a causal influence from `source` to `target` using the provided causality `test` on
single draws of `source` and `target` that have been generated according to the provided
`sequential` sampling constraint. After interpolating both `source` and `target`
to the provided regular `grid`, the causality test is performed on the interpolated data.

!!! note

    This method uses [`UncertainData.jl`](https://github.com/kahaaga/UncertainData.jl) for
    uncertainty handling. To use it, first run `using UncertainData` in the Julia console.

## Example

First, generate some example data.

```julia
using UncertainData, CausalityTools, Distributions
N = 300
a₁, a₂, b₁, b₂, ξ₁, ξ₂, C₁₂ = 0.7, 0.1, 0.75, 0.2, 0.3, 0.3, 0.5

D = rand(N, 2)
for t in 5:N
    D[t,1] = a₁*D[t-1,1] - a₂*D[t-3,1] +                ξ₁*rand(Normal(0, 1))
    D[t,2] = b₁*D[t-1,2] - b₂*D[t-2,2] + C₁₂*D[t-1,1] + ξ₂*rand(Normal(0, 1))
end
```

Gather time series and add some uncertainties to them:

```julia
ts = collect(1:N)
x, y = D[:, 1], D[:, 2]
t = UncertainValue.(Normal.(ts .+ 0.2 .* rand(N), rand(N)))
uvalx = UncertainValue.(Normal.(x, rand()))
uvaly = UncertainValue.(Normal.(y, rand()))

X = UncertainIndexValueDataset(t, uvalx)
Y = UncertainIndexValueDataset(t, uvaly)
```

Now, resample both the indices and values of `X` and `Y` in a strictly
increasing manner according to the indices, interpolate both
to the same regular grid, and perform a causality test.

```julia
n_bins = ceil(Int, N^(1/4))
te_test = VisitationFrequencyTest(binning = RectangularBinning(n_bins), ηs = -5:5)
pa_test = PredictiveAsymmetryTest(predictive_test = te_test)

# Sample a strictly increasing realisation of the indices, interpolate,
# then perform the causality test on the interpolated data.
causality(X, Y, pa_test, StrictlyIncreasing(), RegularGrid(1:1:N))
```
"""
function causality(source::AbstractUncertainIndexValueDataset,
    target::AbstractUncertainIndexValueDataset,
    test::CausalityTest,
    constraint::SequentialSamplingConstraint,
    grid::RegularGrid)

    idxs_s, vals_s = resample(source, constraint, grid)
    idxs_t, vals_t = resample(target, constraint, grid)
    causality(vals_s, vals_t, test)
end

"""
    causality(x, y, test::CausalityTest)

Test for a causal influence from `source` to `target` using the provided causality `test`.

Both `x` and `y` can be a real-valued vector, `Vector{<:AbstractUncertainValue}`
or a `AbstractUncertainValueDataset`. If either `x`, `y` or both are uncertain, then the test
is applied to a single draw from the uncertain data.

## Examples

Generate some example data series x and y, where x influences y:

```julia
n_pts = 300
a₁, a₂, b₁, b₂, ξ₁, ξ₂, C₁₂ = 0.7, 0.1, 0.75, 0.2, 0.3, 0.3, 0.5

D = rand(n_pts, 2)
for t in 5:n_pts
    D[t,1] = a₁*D[t-1,1] - a₂*D[t-3,1] +                ξ₁*rand(Normal(0, 1))
    D[t,2] = b₁*D[t-1,2] - b₂*D[t-2,2] + C₁₂*D[t-1,1] + ξ₂*rand(Normal(0, 1))
end
```

Gather time series and add some uncertainties to them:

```julia
x, y = D[:, 1], D[:, 2]
uvalx = UncertainValue.(Normal.(x, rand()))
uvaly = UncertainValue.(Normal.(y, rand()))

xd = UncertainValueDataset(uvalx)
yd = UncertainValueDataset(uvaly)
```

Any combination of certain and uncertain values will work:

```julia
causality(x, y, pa_test)
causality(x, yd, pa_test)
causality(xd, yd, pa_test)
causality(x, uvaly, pa_test)
causality(uvalx, uvaly, pa_test)
```

On multiple realisations of the uncertain `yd`, but fixing `x`:

```julia
[causality(x, yd, pa_test) for i = 1:100]
```
"""
function causality(source::Union{AbstractVector, Vector{<:AbstractUncertainValue}, AbstractUncertainValueDataset},
        target::Union{AbstractVector, Vector{<:AbstractUncertainValue}, AbstractUncertainValueDataset},
        test::CausalityTest)
end

function causality(source::AbstractVector{T},
    target::Vector{<:AbstractUncertainValue},
    test::CausalityTest) where T

    causality(source, resample(target), test)
end

function causality(source::Vector{<:AbstractUncertainValue},
    target::AbstractVector{T},
    test::CausalityTest) where T

    causality(resample(source), target, test)
end

function causality(source::AbstractVector{T},
    target::AbstractUncertainValueDataset,
    test::CausalityTest) where T

    causality(source, resample(target), test)
end

function causality(source::AbstractUncertainValueDataset,
    target::AbstractVector{T},
    test::CausalityTest) where T

    causality(resample(source), target, test)
end


using Graphs

# Input data
data = map(e -> Tuple(split(e, "-")), readlines("Day12.input"));

# Part 1
mappings = Dict(
    map(
        ((k, v),) -> (v, k),
        enumerate(unique(sort(reduce((acc, p) -> append!(acc, [p...]), data, init = [])))),
    ),
);

g = reduce(
    (g, p) -> (add_edge!(g, mappings[p[1]], mappings[p[2]]); g),
    data,
    init = SimpleGraph(length(mappings)),
);

canrepeat = map(
    v -> isuppercase(first(first(keys(filter(((k, v2),) -> v == v2, mappings))))),
    vertices(g),
);

function all_possible_paths(
    g::AbstractGraph{U},
    canrepeat::Vector{Bool},
    start::Int64,
    curr::Int64,
    exit::Int64;
    visited::Vector{Bool} = map(v -> v == curr, 1:nv(g)),
    smallused::Bool = false,
) where {U<:Real}
    curr == exit && return 1

    total = 0
    for v in outneighbors(g, curr)
        newvisited = map(((i, ok),) -> i == v ? true : ok, enumerate(visited))
        if !visited[v] || canrepeat[v]
            total += all_possible_paths(
                g,
                canrepeat,
                start,
                v,
                exit;
                visited = newvisited,
                smallused = smallused,
            )
        end
        if visited[v] && !canrepeat[v] && !smallused && v != exit && v != start
            total += all_possible_paths(
                g,
                canrepeat,
                start,
                v,
                exit;
                visited = newvisited,
                smallused = true,
            )
        end
    end
    total
end

ans = all_possible_paths(
    g,
    canrepeat,
    mappings["start"],
    mappings["start"],
    mappings["end"];
    smallused = true,
);
println("Part 1: $ans")

# Part 2
ans =
    all_possible_paths(g, canrepeat, mappings["start"], mappings["start"], mappings["end"]);
println("Part 2: $ans")


module CategoricalAlgebra

using Reexport

include("FreeDiagrams.jl")
include("Limits.jl")
include("Sets.jl")
include("FinSets.jl")
include("Matrices.jl")
include("FinRelations.jl")
include("CSets.jl")
include("ACSetViews.jl")
include("GraphCategories.jl")
include("StructuredCospans.jl")
include("CommutativeDiagrams.jl")

@reexport using .FreeDiagrams
@reexport using .CommutativeDiagrams
@reexport using .Limits
@reexport using .CSets
@reexport using .ACSetViews
@reexport using .StructuredCospans

end


# This file is part of the IntervalArithmetic.jl package; MIT licensed

# The order in which files are included is important,
# since certain things need to be defined before others use them

## Interval type

abstract type AbstractInterval{T <: AbstractFloat} end

struct EmptyInterval{T} <: AbstractInterval{T} end

struct NotAnInterval{T} <: AbstractInterval{T} end
nai(::Type{T}) where T = NotAnInterval{T}()
nai() = nai(Float64)

struct Interval{T} <: AbstractInterval{T}
    lo :: T
    hi :: T

    function Interval(lo::T, hi::T, ::Val{CHECK}) where {T <: AbstractFloat, CHECK}
        if CHECK
            @assert !isnan(lo)
            @assert !isnan(hi)
            @assert lo <= hi
            @assert lo < Inf
            @assert hi > -Inf
        end
        return new{T}(lo, hi)
    end
end

# all intervals except NotAnInterval
const ValidInterval{T} = Union{Interval{T}, EmptyInterval{T}}

emptyinterval(::Type{T}) where T = EmptyInterval{T}()
emptyinterval(x::ValidInterval{T}) where T = emptyinterval(T)
emptyinterval() = emptyinterval(Float64)
const ∅ = emptyinterval()

# check boundaries by default
Interval(lo::T, hi::T) where T = Interval(lo, hi, Val(true))

# methods which call unsafe_interval must guarantee validity of inputs!
unsafe_interval(lo::T, hi::T) where T = Interval(lo, hi, Val(false))
unsafe_interval(x::T) where T = unsafe_interval(x, x)

# flexible constructor, methods calling this constructor may produce nai()!
function interval(lo::T, hi::T) where T <: AbstractFloat
    if lo > hi || lo == Inf || hi == -Inf
        return emptyinterval(T)
    end
    if isnan(lo) || isnan(hi)
        return nai(T)
    end
    return unsafe_interval(lo, hi)
end

# thin interval constructor
interval(a::T) where T <: AbstractFloat =
    isfinite(a) ? unsafe_interval(a, a) : nai(T)

interval(a::T, b::T) where T <: Real = interval(float(T), a, b)
interval(a::T) where T <: Real = interval(float(T), a, a)

# type conversion
interval(::Type{T}, lo, hi) where T =
    interval(T(lo, RoundDown), T(hi, RoundUp))
interval(::Type{T}, x::Real) where T = isnan(x) ? nai(T) : unsafe_atomic(T, x)

# automatic type promotion
interval(a::Tuple{T, T}) where T <: Real = interval(a[1], a[2])
interval(a::T, b::S) where {T <: Real, S <: Real} = interval(promote(a, b))

# for b < 0 this produces an empty interval
±(a::Real, b::Real) = interval(a) ± b # type-piracy?
±(a::Interval{T}, b::AbstractFloat) where T = a + interval(T, -b, b)

# common intervals
unit_ball(::Type{T}) where T = unsafe_interval(-one(T), one(T))
nonnegatives(::Type{T}) where T = unsafe_interval(zero(T), T(Inf))

"""`entireinterval`s represent the whole Real line: [-∞, ∞]."""
entireinterval(::Type{T}) where T = Interval(T(-Inf), T(Inf))
entireinterval(::ValidInterval{T}) where T = entireinterval(T)
entireinterval() = entireinterval(Float64)

show(io::IO, x::Interval) = print(io, string("Interval(", x.lo, ",", x.hi, ")"))
show(io::IO, x::EmptyInterval{T}) where T =
    print(io, string("emptyinterval(", T, ")"))
show(io::IO, x::NotAnInterval{T}) where T = print(io, string("nai(", T, ")"))


"""
    OneHotOdds(Y)

Calculates the odds of a one-hot formatted probability matrix. Returns a tuple.
"""
function OneHotOdds(Y)
    rsums = sum(Y, dims = 1);
    return Tuple(rsums ./ sum(rsums))
end

"""
    entropy(v)

Calculates the Shannon-Entropy of a probability vector `v`. Returns a scalar. A common gain function used in tree methods.
"""
entropy(v) = -sum( map( x -> x * (x == 1.0 ? 0.0 : log( x , 2 )), v ) )

"""
    gini(p)

Calculates the GINI coefficient of a probability vector `p`. Returns a scalar. A common gain function used in tree methods.
"""
gini(p) = 1.0 - sum( p .^ 2 )

"""
    ssd(p)

Calculates the sum squared deviations from a decision tree split. Accepts a vector of values, and the mean of that
 vector. Returns a scalar. A common gain function used in tree methods.
"""
ssd(split,mean) = sum( ( split .- mean ) .^ 2 )

#ssd(split) = sum( ( split .- mean( split ) ) .^ 2 )

#The following functions are internal methods.
#I have concerns about some of the performance here...
#Ideas:
#       sortedinds = sortperm(  x[ sortedinds , var ]  )
#       Look for one hot speed ups in X vars?
function StumpOrNode( x, y ; gainfn = entropy )
    maxgain = -Inf
    (decisionbound, decisionvar) = (0.0, 0)
    (Obs, Vars) = size( x )
    beforeinfo = gainfn( sum( y, dims = 1 ) ./ Obs )
    sortedinds = 1:Obs
    for var in 1 : Vars
        sortedinds = sortperm(  x[ : , var ]  )
        y = y[sortedinds,:]
        x = x[sortedinds,:]
        lhsprops = sum( y[1,:]', dims = 1 )
        rhsprops = sum( y[2:end,:], dims = 1 )
        for obs in 2 : ( Obs - 1 )
            lhsprops .+= y[obs,:]'
            rhsprops .-= y[obs,:]'
            LHS = gainfn( lhsprops ./ obs )
            RHS = gainfn( rhsprops ./ (Obs - obs) )
            curgain = beforeinfo - ( (obs/Obs) * LHS + ((Obs - obs)/Obs) * RHS)
            if curgain > maxgain
                maxgain         = curgain
                decisionbound   = (x[sortedinds[obs],var] + x[sortedinds[obs + 1], var]) / 2.0
                decisionvar     = var
            end
        end
    end
    return (decisionbound, decisionvar)
end

#I made this a lot more efficient at the cost of being less generic
#I'd apologize for that, but, I'd rather have this be an end-user function
#then a developer function...
function StumpOrNodeRegress( x, y ; gainfn = ssd )
    maxgain = -Inf
    (decisionbound, decisionvar) = (0.0, 0)
    (Obs, Vars) = size( x )
    totalmean = RunningMean( mean(y), Obs )
    beforeinfo = gainfn( y, totalmean.mu )
    sortedinds = 1:Obs
    for var in 1 : Vars
        sortedinds = sortperm(  x[ : , var ]  )
        y = y[sortedinds]
        x = x[sortedinds,:]
        lhmean = RunningMean(y[1])
        rhmean = Remove(totalmean, y[1])
        for obs in 2 : ( Obs - 1 )
            Update!( lhmean, y[obs] )
            Remove!( rhmean, y[obs] )
            LHS = gainfn( y[1 : obs]        , lhmean.mu )
            RHS = gainfn( y[(obs + 1) : end], rhmean.mu )
            curgain = beforeinfo - ( (obs/Obs) * LHS + ((Obs - obs)/Obs) * RHS)
            if curgain > maxgain
                maxgain         = curgain
                decisionbound   = (x[sortedinds[obs], var] + x[sortedinds[obs + 1], var]) / 2.0
                decisionvar     = var
            end
        end
    end
    return (decisionbound, decisionvar)
end

struct CART
    Tree::Array
    MaxClasses::Int64
    Classification::Bool
end

"""
    ClassificationTree(x, y; gainfn = entropy, maxdepth = 4, minbranchsize = 3)

Builds a CART object using either gini or entropy as a partioning method. Y must be a one hot encoded 2-Array.
Predictions can be formed by calling the following function from the CART object: (M::CART)(x).

*Note: this is a purely nonrecursive decision tree. The julia compiler doesn't like storing structs of nested things.
I wrote it the recursive way in the past and it was quite slow, I think this is true also
of interpretted languages like R/Python...So here it is, nonrecursive tree's!
"""
function ClassificationTree(x, y; gainfn = entropy, maxdepth = 4, minbranchsize = 3, varsmpl = 0)
    curdepth = 1 #Place holder for power of 2 depth of the binary tree
    cursky = 1 #Holds a 1 if branch can grow, 0 if it cannot
    (Obs, Classes) = size(y)
    curmap = [1 : Obs] #Holds indices available to the next split decision
    dt = []#Stores alllll of the decisions we make
    (Obs, Vars) = size(x)
    varsavail = 1 : Vars
    (bound, var) = (1.0,1)
    while (curdepth <= maxdepth) && (cursky >= 1 )
        nextmap = []
        nextsky = 0
        curdt = Dict()
        for sky in 1:cursky
            cmap = curmap[sky]#Get indices from last split for this partition
            if (curdepth == maxdepth) || (length(cmap) <= minbranchsize)#Truncate tree we are at our depth limit
                curdt[sky] = OneHotOdds( y[ cmap, : ] )
            elseif length(cmap) > minbranchsize
                if varsmpl > 0
                    varsavail = unique( rand(1:Vars, varsmpl))
                end
                (bound, var) = StumpOrNode( x[cmap,varsavail], y[cmap,:] ; gainfn = gainfn )
                if varsmpl > 0
                    var = varsavail[var]
                end
                LHS = cmap[findall(x[cmap, var] .< bound)]
                RHS = cmap[findall(x[cmap, var] .>= bound)]
                if (length(LHS) > 0) && (length(RHS) > 0)
                    push!(nextmap, LHS, RHS)
                    nextsky += 2
                    curdt[sky] = (var => bound)
                else
                    curdt[sky] = OneHotOdds( y[ cmap, : ] )
                end
            end
        end

        if length(curdt) > 0
            push!(dt, curdt) #update decision tree
            curmap = nextmap #update our mapped observations
            cursky = nextsky
            curdepth += 1
        else
            break
        end
    end
    return CART(dt, Classes, true)
end

"""
    RegressionTree(x, y; gainfn = ssd, maxdepth = 4, minbranchsize = 3)

Builds a CART object using ssd as a partioning method. Y must be a one column Array.
Predictions can be formed by calling the following function from the CART object: (M::CART)(x).

*Note: this is a purely nonrecursive decision tree. The julia compiler doesn't like storing structs of nested things.
I wrote it the recursive way in the past and it was quite slow, I think this is true also
of interpretted languages like R/Python...So here it is, nonrecursive tree's!
"""
function RegressionTree(x, y; gainfn = ssd, maxdepth = 4, minbranchsize = 3, varsmpl = 0)
    curdepth = 1 #Place holder for power of 2 depth of the binary tree
    cursky = 1 #Holds a 1 if branch can grow, 0 if it cannot
    Obs = size(y)[1]
    curmap = [1 : Obs] #Holds indices available to the next split decision
    dt = []#Stores alllll of the decisions we make
    (Obs, Vars) = size(x)
    varsavail = 1 : Vars
    (bound, var) = (1.0, 1)
    while (curdepth <= maxdepth) && (cursky >= 1 )
        nextmap = []
        nextsky = 0
        curdt = Dict()
        for sky in 1:cursky
            cmap = curmap[sky]#Get indices from last split for this partition
            if (curdepth == maxdepth) || (length(cmap) <= minbranchsize)#Truncate tree we are at our depth limit
                curdt[sky] = mean( y[ cmap ] )
            elseif length(cmap) > minbranchsize
                if varsmpl > 0
                    varsavail = unique( rand(1:Vars, varsmpl) )
                end
                (bound, var) = StumpOrNodeRegress( x[cmap,varsavail], y[cmap] ; gainfn = gainfn )
                if varsmpl > 0
                    var = varsavail[ var ]
                end
                LHS = cmap[findall(x[cmap, var] .< bound)]
                RHS = cmap[findall(x[cmap, var] .>= bound)]
                if (length(LHS) > 0) && (length(RHS) > 0)
                    push!(nextmap, LHS, RHS)
                    nextsky += 2
                    curdt[sky] = (var => bound)
                else
                    curdt[sky] = mean( y[ cmap ] )
                end
            end
        end

        if length(curdt) > 0
            push!(dt, curdt) #update decision tree
            curmap = nextmap #update our mapped observations
            cursky = nextsky
            curdepth += 1
        else
            break
        end
    end
    return CART(dt, 1, false)
end

"""
    (M::CART)(x)

This is a universal CART object predict function.
"""
function (M::CART)(x)
    (Obs, Vars) = size(x) .|> Int
    output = zeros(Obs, M.MaxClasses)
    for obs in 1 : Obs
        Branch = 1
        Offset = 0
        for (i, d) in enumerate(M.Tree)
            if i > 1
                Offset = sum(map( x -> !isa(d[x], Pair), 1 : (Branch) ))
            end
            if haskey(d, Branch)
                if isa(d[Branch], Pair)
                    if x[ obs, first(d[ Branch ]) ] < last(d[ Branch ])
                        Branch = (Branch - Offset) + (Branch - Offset) - 1
                    else
                        Branch = (Branch - Offset) + (Branch - Offset)
                    end
                else
                    output[obs,:] .= d[Branch]
                    break
                end
            end
        end
    end
    return output
end


using StaticArrays
using BenchmarkTools
using Statistics

mutable struct Multidual{N, T}
    val::T
    derivs::SVector{N, T}
end

Base.:+(f::Multidual, g::Multidual) = Multidual(f.val + g.val, f.derivs + g.derivs)
Base.:+(f::Multidual, g::Number) = Multidual(f.val + g, f.derivs)
Base.:+(f::Number, g::Multidual) = g + f

Base.:-(f::Multidual, g::Multidual) = Multidual(f.val - g.val, f.derivs - g.derivs)
Base.:-(f::Multidual, g::Number) = Multidual(f.val - g, f.derivs)
Base.:-(f::Number, g::Multidual) = g - f
Base.:-(f::Multidual) = Multidual(-f.val, -1 .* f.derivs)

Base.:*(f::Multidual, g::Multidual) = Multidual(f.val*g.val, f.val .* g.derivs + g.val .* f.derivs)
Base.:*(α::Number, f::Multidual) = Multidual(f.val * α, α .* f.derivs)
Base.:*(f::Multidual, α::Number) = α * f

function f(u, p)
    α, β, γ, δ = p

    du1 = α*u[1] - β*u[1]*u[2]
    du2 = -γ*u[2] + δ*u[1]*u[2]

    @SVector [du1, du2]
end

function dopri5!(u_vals, p, t_span, f, derivs)
    # Values from the Dormand Prince Tableau
    a21  =  (1.0/5.0)
    a31  = (3.0/40.0)
    a32  = (9.0/40.0)
    a41  = (44.0/45.0)
    a42  = (-56.0/15.0)
    a43  = (32.0/9.0)
    a51  = (19372.0/6561.0)
    a52  = (-25360.0/2187.0)
    a53  = (64448.0/6561.0)
    a54  = (-212.0/729.0)
    a61  = (9017.0/3168.0)
    a62  = (-355.0/33.0)
    a63  = (46732.0/5247.0)
    a64  = (49.0/176.0)
    a65  = (-5103.0/18656.0)
    a71  = (35.0/384.0)
    a72  = (0.0)
    a73  = (500.0/1113.0)
    a74  = (125.0/192.0)
    a75  = (-2187.0/6784.0)
    a76  = (11.0/84.0)

    b1   = (35.0/384.0)
    b2   = (0.0)
    b3   = (500.0/1113.0)
    b4   = (125.0/192.0)
    b5   = (-2187.0/6784.0)
    b6   = (11.0/84.0)
    b7   = (0.0)

    dt = 0.25
    uₚ = SVector{2, Float64}(u_vals[1, :])
    @inbounds for i in 1:Int64(t_span/dt)
        k1 = f(uₚ, p)
        k2 = f(uₚ + dt.*(a21.*k1), p)
        k3 = f(uₚ + dt.*(a31.*k1 + a32.*k2), p)
        k4 = f(uₚ + dt.*(a41.*k1 + a42.*k2 + a43.*k3), p)
        k5 = f(uₚ + dt.*(a51.*k1 + a52.*k2 + a53.*k3 + a54.*k4), p)
        k6 = f(uₚ + dt.*(a61.*k1 + a62.*k2 + a63.*k3 + a64.*k4 + a65.*k5), p)
        u = uₚ + dt.*(b1.*k1 + b3.*k3 + b4.*k4 + b5.*k5 + b6.*k6)
        u_vals[i, 1] = u[1].val
        u_vals[i, 2] = u[2].val
        derivs[i, 1:2] = u[1].derivs
        derivs[i, 3:4] = u[2].derivs
        uₚ = SVector{2, Float64}(u_vals[i, :])
    end
end

# Generating the dataset for parameter estimation
tp1 = Multidual(1.5, SVector(0.0, 0.0))
tp2 = Multidual(1.0, SVector(0.0, 0.0))
tp3 = Multidual(3.0, SVector(0.0, 0.0))
tp4 = Multidual(1.0, SVector(0.0, 0.0))

tp = (tp1, tp2, tp3, tp4)

dataset = Array{Float64}(undef, 40, 2)
dataset[1, :] = [1.0, 1.0]
t_derivs = Array{Float64}(undef, 40, 4)

t_span = 10.0
dopri5!(dataset, tp, t_span, f, t_derivs)

function cost_fun(derivs, ds, up, m)
    diff = abs.(ds .- up)
    dcdu = SVector{2, Float64}((1/m) .* sum(diff, dims=1))
    derivs[:, 1:2] = dcdu[1] .* derivs[:, 1:2] # dx/dp
    derivs[:, 3:4] = dcdu[2] .* derivs[:, 3:4] # dy/dp
    dcdp = SVector{4, Float64}(mean(derivs, dims=1))
    cost = SVector{2, Float64}((1/(2*m)) .* sum((diff .^ 2), dims=1))
    return @SVector[cost, dcdp]
end

function estim_params!(p, ds, t_span, f)
    m = 40
    up = Array{Float64}(undef, 40, 2)
    derivs = Array{Float64}(undef, 40, 4)
    cost = SVector{2, Float64}(1.0, 1.0)
    diff = 1.0
    tol = 10e-6
    lr = 0.001
    count = 0

    while !isapprox(diff, 0.0, atol=tol)
        up[1, :] = [1.0, 1.0]
        dopri5!(up, p, t_span, f, derivs)
        prev = cost
        cost, dcdp = cost_fun(derivs, ds, up, m)
        p[1].val = p[1].val - lr*dcdp[1]
        p[2].val = p[2].val - lr*dcdp[2]
        p[3].val = p[3].val - lr*dcdp[3]
        p[4].val = p[4].val - lr*dcdp[4]
        diff = sum(abs.(cost .- prev))
        count += 1
        if count%250 == 0
            println("Training pass $(count), cost = $(cost), $(p[1].val), $(p[2].val), $(p[3].val), $(p[4].val)")
        end
    end
    println("Finished Training.\nThe estimated parameters are: \nα = $(p[1].val)\ttrue value = 1.5\nβ = $(p[2].val)\ttrue value = 1.0\nγ = $(p[3].val)\ttrue value = 3.0\nδ = $(p[4].val)\ttrue value = 1.0\n")
end

α = Multidual(1.2, SVector(1.0, 0.0))
β = Multidual(0.8, SVector(0.0, 1.0))
γ = Multidual(2.8, SVector(1.0, 0.0))
δ = Multidual(0.8, SVector(0.0, 1.0))

p = (α, β, γ, δ)

estim_params!(p, dataset, t_span, f)


function ndgrid{T}(v1::AbstractVector{T}, v2::AbstractVector{T})
    m, n = length(v1), length(v2)
    v1 = reshape(v1, m, 1)
    v2 = reshape(v2, 1, n)
    (repmat(v1, 1, n), repmat(v2, m, 1))
end

function pe1()
    N = 1e6
    sum = 0
    for i=1:N
        if i % 3 == 0 || i % 5 == 0
            sum += i
        end
    end
end

function pe10()
    N = 2e6
    primes = [1:N]
    for i in 2:sqrt(N)
        j = (2:N/i)*i
        primes[j] = 0
    end
end

function pe73()
    N = 1e3
    di = [1:N]
    ni = [1:N]
    (n, d) = ndgrid(ni, di)

    f = n ./ d

    f = unique(f)
    f = sort(f)
    f = f[f .< 1/2]
    f = f[f .> 1/3]
end

function soft_threshold()
    N = 1e2
    j = linspace(-1.0, 1.0, convert(Int64, N))
    x, y = ndgrid(j, j)
    z = x.^2 + y.^2
    i = abs(z) .< 0.5
    z[i] = 0
    z[!i] -= 0.5
end

function pi_approx()
    N = 1e6
    pie = 1.0
    for k in 1:N
        pie += (-1)^k / (2*k + 1)
    end
end

# timing
pe1()
pe10()
pe73()
soft_threshold()
pi_approx()

print("pe1: ")
println(@elapsed pe1())
print("pe10: ")
println(@elapsed pe10())
print("pe73: ")
println(@elapsed pe73())
print("st_time: ")
println(@elapsed soft_threshold())
print("pi_approx: ")
println(@elapsed pi_approx())


using Animations
using Javis
using NBodySimulator
using StaticArrays

# Define the bodies
body1 = MassBody(SVector(0.0, 1.0, 0.0), SVector(5.775e-6, 0.0, 0.0), 2.0)
body2 = MassBody(SVector(0.0, -1.0, 0.0), SVector(-5.775e-6, 0.0, 0.0), 2.0)

G = 6.673e-11

system = GravitationalSystem([body1, body2], G)
tspan = (0.0, 1111150.0)

# run the simulation
simulation = NBodySimulation(system, tspan)
sim_result = run_simulation(simulation, saveat = 10)
sim_data = sim_result.solution.u

# extract the data
planet1_pos = [(sim_data[x][1, 1], sim_data[x][2, 1]) for x in 1:length(sim_data)]
planet2_pos = [(sim_data[x][1, 2], sim_data[x][2, 2]) for x in 1:length(sim_data)]

#Convert to Points
planet1_point =
    Point.((200 .* getindex.(planet1_pos, 1)), ((200 .* getindex.(planet1_pos, 2) .- 100)))
planet2_point =
    Point.((200 .* getindex.(planet2_pos, 1)), ((200 .* getindex.(planet2_pos, 2) .+ 100)))

# Start the animation
function ground(args...)
    background("black")
    sethue("white")
end

function planet(p, r, color)
    sethue(color)
    return circle(p, r, :fill)
end

function make_animation()
    nframes = 200

    vid = Video(500, 500)
    Background(1:nframes, ground)

    planet1 = Object(1:nframes, (args...) -> planet(planet1_point[1], 10.0, "red"))
    planet2 = Object(1:nframes, (args...) -> planet(planet2_point[1], 10.0, "blue"))

    planet1_anim = Animation(
        collect(range(0, stop = 1, length = length(planet1_point))),
        planet1_point,
        [linear() for _ in 1:(length(planet1_point) - 1)],
    )

    planet2_anim = Animation(
        collect(range(0, stop = 1, length = length(planet2_point))),
        planet2_point,
        [linear() for _ in 1:(length(planet2_point) - 1)],
    )

    act!(planet1, Action(1:nframes, planet1_anim, translate()))
    act!(planet2, Action(1:nframes, planet2_anim, translate()))

    render(vid; pathname = "two_body.gif")
end

make_animation()


# ## Stairstep plot

#md # ![FILE_NAME.png](images/FILE_NAME.png)

x = sin.(0:0.3:2pi)
plot(x, line=:steppre, markersize=20)


# Static Parameters
f_c = 20
x_max = 1.0
num_x = 10*f_c
filter_x = ones(2*f_c+1)

# Dynamic Parameters
K  = 2
tau = 1.0/(K*1.0)
v_max = 0.5
num_v = 10

# Variability of weights
minWeights = 0.9
maxWeights = 1.1

# Variability of number of particles
min_number_part = 20
max_number_part = 30

# Number of generated examples
num_trials = 1000

# test case
test_case = () -> TestCases.cloud_1d_full(x_max, v_max, minWeights, maxWeights, K, tau, rand(min_number_part:max_number_part))
# WARNING: changing anything in the test_case will generate troubles if the rejection_sampling algorithm
# in Utils.js is not either updated for it, or disabled.

# Noise parameters
noises_data = linspace(0,0.1,5)
noises_data = noises_data[2:end]
noises_position = linspace(0,0.06,5)
noises_position = noises_position[2:end]

### Location of data folder
dataFolder = "data/1Dsimulations"
# loaded data for rejection sampling of simulations
bins = np.load(dataFolder*"/separationDistribBins_20particles.npy")
density = np.load(dataFolder*"/separationDistribVal_20particles.npy")

println(" WARNING: The rejection sampling algorithm is being used, with interval of interest: [",bins[1]," ",bins[end],"]. To change it check the separationDistribution.jl file.")

### Cases to be tested

noiseless_dynamic = true
noiseless_static = true
noise_dynamic = false
noise_static = false
curvature_static = false

cases = (noiseless_dynamic, noiseless_static, noise_dynamic, noise_static, curvature_static)


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

@test begin
    promote_rule(SplitBinion{Int}, Float64) == SplitBinion{Float64}
end

@test begin
    promote_rule(SplitBinion{Int}, SplitBinion{Float64}) == SplitBinion{Float64}
end

@test begin
    -(1.0 - SplitBinion(2, 3)) == SplitBinion(1.0, 3.0) == SplitBinion(2, 3) - 1.0
end

@test begin
    1.0 + SplitBinion(2, 3) == SplitBinion(3.0, 3.0) == SplitBinion(2, 3) + 1.0
end

@test begin
    convert(SplitBinion{Float64}, 1) == SplitBinion(1.0, 0)
end

@test begin
    convert(SplitBinion{Float64}, SplitBinion(1,2)) == SplitBinion(1.0, 2.0)
end

@test begin
    convert(SplitBinion, 2) == SplitBinion(2, 0)
end

@test begin
    z = SplitBinion{Int}(1, 2)
    convert(Hyperbolic{Int}, z) == z
end

@test begin
    convert(Int, SplitBinion(3)) == 3
end

@test_throws InexactError begin
    convert(Int, SplitBinion(3,4))
end

@test begin
    a = CayleyDickson.randomBigFloat()
    isreal(SplitBinion(a))
end

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

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

@test begin
    a = 1
    real(SplitBinion(a)) == a
end

@test begin
    a = 1
    l = unreal(SplitBinion(0, a))
    r = [a]
    l == r
end

@test begin
    io = IOBuffer()
    show(io, SplitBinion(1, 2))
    l = String(take!(io))
    r = "[1: 1, s: 2]"
    l == r
end

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

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

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

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

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

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

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

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

@test begin
    x = random(SplitBinion{BigInt})
    conj(conj(x)) == x
end

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

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

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

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

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

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

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

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

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

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

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

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

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

@test begin
    x = random(SplitBinion{BigInt})
    quadrance(x) > big(0) || quadrance(x) < big(0) || quadrance(x) == big(0)
end

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

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

@test_throws ErrorException begin
    inv(SplitBinion(1, 1))
end

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

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

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

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

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

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

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

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

@test begin
    float(SplitBinion{Float64}) == SplitBinion{Float64}
end

@test begin
    x = random(SplitBinion{Rational{BigInt}})
    selfstar(selfstar(x)) == selfstar(x)
end

@test begin
    x = random(SplitBinion{Rational{BigInt}})
    anti_selfstar(anti_selfstar(x)) == anti_selfstar(x)
end


# This shows basic data analysis using the Card data
# on returns to education

##
using CSV, DataFrames, DataFramesMeta, DelimitedFiles, StatsPlots
cd(@__DIR__)
wait_for_key(prompt) = (print(stdout, prompt); read(stdin, 1); nothing)
card = CSV.read("../Data/card.csv", DataFrame) # how to read CSV data into DataFrame
size(card)
names(card)

## see the first 6 lines
show(first(card,6))
# see some statistics using DataFrames functionality
show(describe(card))

## add some variables
card[!,:lnwage] = round.(log.(card[!,:wage]), digits=4) # round to save storage space
card[!,:expsq] = (card[!,:exper].^2)/100
card[!,:agesq] = card[!,:age].^2

## select the ones we want
cooked = @select(card, :lnwage, :educ, :exper, :expsq, :black, :south, :smsa, :age, :agesq, :nearc4 )
display(first(cooked,6))

## write as CSV
CSV.write("cooked.csv", cooked)

## write as plain ASCII, using limited precision to reduce file size
data = Matrix{Float32}(cooked)
writedlm("cooked.txt", data)

## a simple way of looking at geographical and racial discrimination
# select north and white
nw = @subset(card, :south .== 0, :black .== 0)
@df nw scatter(:educ, :wage,label="North-White")
# select south and black
sb = @subset(card, :south .== 1, :black .== 1)
@df sb scatter!(:educ, :wage, label="South-Black",title="Wage vs. Educ",xlabel="Educ", ylabel="Wage")

## the following is similar, but maybe better, as it plots all points
@df card scatter(:educ, :wage, zcolor=:south+:black)

## density plots by groups
@df card density(:wage, group=(:black,:south), fill=true, fillalpha=0.3,legend=:topright, title="wage density by south and\n black categorical variables")

## contours and marginal densities
@df card marginalkde(:wage, :educ)

## violin and box plots
@df card boxplot(:wage) # puts is beside
@df card violin!(:wage)

## violin plot of wage, by education
@df card violin(:educ, :wage)
# add a boxplot on top
@df card boxplot!(:educ, :wage)


# Julia Implementation of Game of Life
# Any live cell with fewer than two live neighbors dies, as if caused by under-population.
# Any live cell with two or three live neighbors lives on to the next generation.
# Any live cell with more than three live neighbors dies, as if by over-population..
# Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

"""
function count_alive :- Calculates the number of alive neighbours present for a cell(x,y) on the board
Input:
    board :- 2D Grid/board
    row :- x coordinate of the current cell
    column :- y coordinate of the current cell

Output:
    count :- Total number of alive neighbours of the current cell
"""
function count_alive(board,row,column)
    check = [[0 1], [1 0], [1 1], [0 -1], [-1 0], [1 -1], [-1 1],[-1,-1]]
    count = 0
    for c in check
        if (row + c[1]) <= 0 || (column + c[2]) <= 0 || (row + c[1]) > size(board)[1] || (column + c[2]) > size(board[1])[1]
            continue
        end
        if board[row + c[1]][column + c[2]] == 1
            count += 1
        end
    end

    return count

end

"""
function gameOfLife :- Generates the next cell structure for the grid
Input:
    board :- Input 2D Grid/board
Output:
    board :- Next generated cell structure according to the rules
"""
function gameOfLife(board,generations)
    new_board = deepcopy(board)
    rows = size(board)
    column = size(board[1])
    for generation in 1:generations
        changed = []
        for i in 1:rows[1]
            for j in 1:column[1]
                neighbours = count_alive(new_board, i, j)
                if new_board[i][j] == 1
                    if neighbours < 2 || neighbours>3
                        append!(changed,[[i,j]])
                    end
                else
                    if neighbours==3
                        append!(changed,[[i,j]])
                    end
                end
            end
        end

        for toggle_index in changed
            x = toggle_index[1]
            y = toggle_index[2]
            new_board[x][y]  = 1 - new_board[x][y]
        end

        #print(new_board)

    end
    return new_board
end

function print_as_grid(input_array,output_array,expected,count,generations,failed)
    println("********************************************************")
    println("Test Case:-",count)
    if output_array==expected
        println("Test Case ", count," has passed")
    else
        println("Test Case ", count," has failed")
        append!(failed,count)
    end

    println("Input Array ===> Output Array ===> Expected Output")
    println("This test case has ",generations," generations")
    println(input_array[1]," ===>",output_array[1]," ===>",expected[1])
    println(input_array[2]," ===>",output_array[2]," ===>",expected[2])
    println(input_array[3]," ===>",output_array[3]," ===>",expected[3])
    println(input_array[4]," ===>",output_array[4]," ===>",expected[4])
    println("********************************************************")
end

function run_test_cases()
    input1 = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]
    expected1 = [[0,0,0],[1,0,1],[0,1,1],[0,1,0]]
    input2 = [[1,1,0,1],[0,0,0,1],[1,0,1,1],[0,1,0,0]]
    expected2 = [[0,0,1,0],[1,0,0,1],[0,1,1,1],[0,1,1,0]]
    input3 = [[1,0,0,1],[1,1,0,1],[1,0,0,1],[0,1,1,0]]
    expected3 = [[1,1,1,0],[1,1,0,1],[1,0,0,1],[0,1,1,0]]
    input4 = [[1,0,0],[0,0,1],[1,0,0],[0,0,1]]
    expected4 = [[0,0,0],[0,1,0],[0,1,0],[0,0,0]]
    input5 = [[1,1,0,0,0],[0,0,0,0,1],[0,0,0,1,1],[0,0,0,0,0]]
    expected5 = [[0,0,0,0,0],[0,0,0,1,1],[0,0,0,1,1],[0,0,0,0,0]]
    input6 = [[0,1,0],[0,1,0],[0,1,0],[0,0,0]]
    expected6 = [[0,1,0],[0,1,0],[0,1,0],[0,0,0]]

    failed = []
    output1 = gameOfLife(input1,1)
    output2 = gameOfLife(input2,1)
    output3 = gameOfLife(input3,1)
    output4 = gameOfLife(input4,1)
    output5 = gameOfLife(input5,1)
    output6 = gameOfLife(input6,2)

    print_as_grid(input1,output1,expected1,1,1,failed)
    print_as_grid(input2,output2,expected2,2,1,failed)
    print_as_grid(input3,output3,expected3,3,1,failed)
    print_as_grid(input4,output4,expected4,4,1,failed)
    print_as_grid(input5,output5,expected5,5,1,failed)
    print_as_grid(input6,output6,expected6,6,2,failed)
    println()
    if size(failed)[1]==0
        println("Congratulations, all test Cases Passed")
    else
        println("Out of 6 test cases the following test cases failed:-")
        println(failed)
    end
end

#testing
run_test_cases()


A+Y -> X+P
X+Y -> 2P
A+X -> 2X + 2Z
2X  -> A + P (note: this has rate kX^2!)
B + Z -> Y

t = 0.0:1.0:30.0
data = [1.0 2.05224 2.11422 2.1857 2.26827 2.3641 2.47618 2.60869 2.7677 2.96232 3.20711 3.52709 3.97005 4.64319 5.86202 9.29322 536.068 82388.9 57868.4 1.00399 1.00169 1.00117 1.00094 1.00082 1.00075 1.0007 1.00068 1.00066 1.00065 1.00065 1.00065
        2.0 1.9494 1.89645 1.84227 1.78727 1.73178 1.67601 1.62008 1.56402 1.50772 1.45094 1.39322 1.33366 1.2705 1.19958 1.10651 0.57194 0.180316 0.431409 251.774 591.754 857.464 1062.78 1219.05 1335.56 1419.88 1478.22 1515.63 1536.25 1543.45 1539.98
        3.0 2.82065 2.68703 2.58974 2.52405 2.48644 2.47449 2.48686 2.52337 2.58526 2.67563 2.80053 2.9713 3.21051 3.5712 4.23706 12.0266 14868.8 24987.8 23453.4 19202.2 15721.6 12872.0 10538.8 8628.66 7064.73 5784.29 4735.96 3877.66 3174.94 2599.6]

t = 0.0:12.0:90.0
data = [100.0 0.246196 0.000597933 0.24547 0.000596251 0.245275 0.000595453 0.245511
        0.0 53.7939 16.8784 58.7789 18.3777 59.1879 18.5003 59.2611]

u0 = Float32[2.; 0.]
datasize = 30
tspan = (0.0f0,1.5f0)

function trueODEfunc(du,u,p,t)
    true_A = [-0.1 2.0; -2.0 -0.1]
    du .= ((u.^3)'true_A)'
end
t = range(tspan[1],tspan[2],length=datasize)
prob = ODEProblem(trueODEfunc,u0,tspan)
ode_data = Array(solve(prob,Tsit5(),saveat=t))

function lotka_volterra(du,u,p,t)
  x, y = u
  α, β, δ, γ = p
  du[1] = dx = α*x - β*x*y
  du[2] = dy = -δ*y + γ*x*y
end
u0 = [1.0,1.0]
tspan = (0.0,10.0)
p = [1.5,1.0,3.0,1.0]
prob = ODEProblem(lotka_volterra,u0,tspan,p)
sol = Array(solve(prob,Tsit5())(0.0:1.0:10.0))

function lotka_volterra(du,u,p,t)
  x, y = u
  α, β, δ, γ = p
  du[1] = dx = α*x - β*x*y
  du[2] = dy = -δ*y + γ*x*y
end
function lv_noise(du,u,p,t)
  du[1] = p[5]*u[1]
  du[2] = p[6]*u[2]
end
u0 = [1.0,1.0]
tspan = (0.0,10.0)
p = [1.5,1.0,3.0,1.0,0.1,0.1]
prob = SDEProblem(lotka_volterra,lv_noise,u0,tspan,p)
sol = [Array(solve(prob,SOSRI())(0.0:1.0:10.0)) for i in 1:20] # 20 solution samples


using SymbolicRegression
using DataDrivenDiffEq

"""
$(TYPEDEF)

Options for using SymbolicRegression.jl within the `solve` function.
Automatically creates [`Options`](https://astroautomata.com/SymbolicRegression.jl/stable/api/#Options) with the given specification.
Sorts the operators stored in `functions` into unary and binary operators on conversion.

# Fields
$(FIELDS)

"""
struct EQSearch <: AbstractSymbolicRegression
    "Operators used for symbolic regression"
    functions::AbstractVector{Function}
    "Additionally keyworded arguments passed to SymbolicRegression.Options"
    kwargs
end

function EQSearch(functions = Function[/, +, *, exp, cos]; kwargs...)
    return EQSearch(functions, kwargs)
end

function to_options(x::EQSearch)
    binops = Tuple([fi for fi in x.functions if is_binary(fi)])
    unaops = Tuple([fi for fi in x.functions if is_unary(fi)])
    fnames = fieldnames(SymbolicRegression.Options)
    ks = Dict(
        [k => v for (k,v) in x.kwargs if k ∈ fnames]
    )
    return SymbolicRegression.Options(;
        binary_operators = binops, unary_operators = unaops,
        ks...
    )
end

function DiffEqBase.solve(prob::AbstractDataDrivenProblem, alg::EQSearch;
    max_iter::Int = 10,
    weights = nothing,
    numprocs = nothing, procs = nothing,
    multithreading = false,
    runtests::Bool = true,
    eval_expression = false
    )

    opt = to_options(alg)

    Y = get_target(prob)

    # Inputs
    X̂, _, t, U = get_oop_args(prob)

    t = iszero(t) ? [] : t
    # Cat the inputs
    X = vcat([x for x in (X̂, U, permutedims(t)) if !isempty(x)]...)
    # Use the eqssearch of symbolic regression
    hof = SymbolicRegression.EquationSearch(X, Y, niterations = max_iter, weights = weights, options = opt,
            numprocs = numprocs, procs = procs, multithreading = multithreading,
            runtests = runtests)
    # Sort the paretofront
    doms = map(1:size(Y, 1)) do i
        calculateParetoFrontier(X, Y[i, :], hof[i], opt)
    end

    build_solution(prob, alg, doms; eval_expression = eval_expression)
end

function build_solution(prob::AbstractDataDrivenProblem, alg::EQSearch, doms; eval_expression = false)

    opt = to_options(alg)
    @variables x[1:size(prob.X, 1)] u[1:size(prob.U,1)] t
    x = Symbolics.scalarize(x)
    u = Symbolics.scalarize(u)
    x_ = [x;u;t]

    # Build a dict
    subs = Dict([SymbolicUtils.Sym{Number}(Symbol("x$(i)")) => x_[i] for i in 1:size(x_, 1)]...)
    # Create a variable
    eqs = vcat(map(x->node_to_symbolic(x[end].tree, opt), doms))
    eqs = map(x->substitute(x, subs), eqs)
    res_ = Basis(
        eqs, x, iv = t, controls = u, eval_expression = eval_expression
    )

    X = get_target(prob)
    Y = res_(get_oop_args(prob)...)

    # Build the metrics
    complexities = map(x->countNodes(x[end].tree), doms)
    complexity = sum(complexities)
    retcode = :success

    error = norm(X-Y, 2)
    k = free_parameters(res_)
    aic = AICC(k, X, Y)
    errors = zeros(eltype(X), size(Y, 1))
    aiccs = zeros(eltype(X), size(Y, 1))
    j = 1
    for i in 1:size(Y,1)
        errors[i] = norm(X[i,:].-Y[i,:],2)
        aiccs[i] = AICC(k, X[i:i, :], Y[i:i,:])
    end

    metrics = (
        Complexity = complexity,
        Error = error,
        AICC = aic,
        complexities = complexities,
        Errors = errors,
        AICCs = aiccs,
    )

    inputs = (
        Problem = prob,
        Algorithm = opt,
    )

    return DataDrivenSolution(
        res_, retcode, [], alg, doms, inputs, metrics
    )
end


Random.seed!(1)
a = mapweight(grid2(20),x->1/(rand(1)[1]));
la = lap(a)
n = size(la)[1]
b = randn(n)
b = b .- mean(b);

d = Array(diag(la))
pre = (x)->(x ./ d)

x = pcg(la,b,pre,maxits=2000,maxtime=1,verbose=true);
x = Laplacians.pcgBLAS(la,b,pre,maxits=2000,maxtime=1,verbose=true);
x = Laplacians.pcgSlow(la,b,pre,maxits=2000,maxtime=1,verbose=true);
x = pcg(la,b,pre,maxits=2000,verbose=true);
x = Laplacians.pcgBLAS(la,b,pre,maxits=2000,verbose=true);
x = Laplacians.pcgBLAS(la,b,pre,maxits=2000,verbose=true,maxtime=1e-10);
x = Laplacians.pcgSlow(la,b,pre,maxits=2000,verbose=true);
x = Laplacians.pcgSlow(la,b,pre,maxits=2000,verbose=true,maxtime=1e-10);
x = Laplacians.pcgBLAS(la,b,pre,tol=1e-2);
x = Laplacians.pcgSlow(la,b,pre,tol=1e-2);
x = pcg(la,b,pre,tol=1e-2);

x = cg(la,b,verbose=true,maxtime=1,maxits=1000);
x = Laplacians.cgBLAS(la,b,verbose=true,maxtime=1,maxits=1000);
x = Laplacians.cgBLAS(la,b,verbose=true,maxtime=1e-10,maxits=1000);
x = Laplacians.cgSlow(la,b,verbose=true,maxtime=1,maxits=1000);
x = Laplacians.cgSlow(la,b,verbose=true,maxtime=1e-10,maxits=1000);
x = cg(la,b,verbose=true,maxits=2000);
x = Laplacians.cgBLAS(la,b,verbose=true,maxits=2000);
x = Laplacians.cgSlow(la,b,verbose=true,maxits=2000);
x = cg(la,b,verbose=true,tol=0.5);
x = Laplacians.cgBLAS(la,b,verbose=true,tol=0.5);
x = Laplacians.cgSlow(la,b,verbose=true,tol=0.5);

bbig = convert(Array{BigFloat,1},b);
f = cgSolver(la)
x = f(bbig);

x = pcg(la,bbig,pre,maxits=200,maxtime=1,verbose=true);
x = cg(la,bbig,maxits=200,maxtime=1,verbose=true);

n = 100
a = wted_chimera(n,3)
la = lap(a)

f = cgSolver(la,verbose=false)
b = randn(n)
b = b .- mean(b)
x = f(b)
norm(la*x-b)

f(zeros(100))

x = f(b,maxits=15,verbose=true, maxtime=1)

d = Diagonal(1 ./ diag(la))
pre = x -> Array(d*x)

x = pcg(la, b, sparse(Diagonal(diag(la))),maxits=10)
x = pcg(la, b, pre, maxits=10, verbose=true)
f = pcgSolver(la, spdiagm(0=>diag(la)) ,maxits=10,verbose=true)
x = f(b)
f = pcgSolver(la,pre,maxits=10,verbose=false)
x = f(b,verbose=true, maxits=1000, maxtime = 2)

f(zeros(100))

t = akpw(a)
lt = lap(t)

f = pcgLapSolver(a, t)
pcgIts = [1]
x = f(b,verbose=true, pcgIts=pcgIts)
bbig = convert(Array{BigFloat,1},b);
x = f(bbig,verbose=true, pcgIts=pcgIts)

sdda = copy(la)
sdda[1,1] += 1
sddt = copy(lt)
sddt[1,1] += 1

f = pcgSolver(sdda, sddt)
x = f(b,verbose=true, pcgIts=pcgIts)
x = f(bbig,verbose=true, pcgIts=pcgIts)


module PartialSvdStoch

using Match

using Logging
using Base.CoreLogging

using Printf
using LinearAlgebra
using LowRankApprox
using Base.CoreLogging
using Statistics

debug_log = stdout
logger = ConsoleLogger(stdout, CoreLogging.Info)
global_logger(logger)

export VrPCA,
        SvdMode,
        LowRankApproxMc,
        reduceToRank,
        reduceToEpsil,
        iterateVrPCA,
        getindex,
        getResidualError,
        SvdApprox,
        isConverged

include("vrpca.jl")
include("lowRankApproxMc.jl")

end


##  Test continuous wavelet transform
##  with classic hare-lynx data set
##
##  Author Bernd Blasius (bernd.blasius@gmail.com)

module W

using CSV, DSP, PyPlot, StatsBase

include("../src/cwt.jl")
include("../src/tools.jl")
include("../src/wcs.jl")

function test_cwt_harelynx()
    # test continuous time wavelet transform (CWT) and
    # wavelet power spectrum

  # read in the data
  df = CSV.read("../data/Hare_Lynx.csv")
  t   = df[:,1]  # time (year)
  h   = df[:,2]  # hare
  l   = df[:,3]  # lynx

  figure("Hare Lynx time series")
  clf()
  plot(t,h)
  plot(t,10*l)
  xlabel("time")
  ylabel("population abundances")

  # do the wavelet transform
  dt = 1    # sampling rate = 1 year
  s0 = 2.0  # starting scale = 1 year
  noctave = 4  # number of octaves
  nvoices = 100 # number of voices per octave

  wave1, period1, power1, global_ws1, variance1, scale1, coi1 =
    cwt(h,dt,noctave = noctave, s0=s0, nvoices = nvoices)
  wave2, period2, power2, global_ws2, variance2, scale2, coi2 =
    cwt(l,dt,noctave = noctave, s0=s0, nvoices = nvoices)

  # Significance levels: (variance=1 for the normalized SST)
  lag1 = 0.72    # lag-1 autocorrelation for red noise background
  signif1,fft_theor1 = wave_signif(variance1,dt,scale1,0,lag1)
  sig95_1 = signif1 .* ones(length(h))'  # expand signif --> (J+1)x(N) array
  sig95_1 = power1 ./ sig95_1

  signif2,fft_theor2 = wave_signif(variance2,dt,scale2,0,lag1)
  sig95_2 = signif2 .* ones(length(l))'  # expand signif --> (J+1)x(N) array
  sig95_2 = power2 ./ sig95_2

  # plotting scale range
  lminp = log2(minimum(period1))
  lmaxp = log2(maximum(period1))
  lrange = (trunc(lminp) :  trunc(lmaxp))

  figure("Hare-lynx wavelet power spectrum")
  clf()
  # plot wavelet power for hare
  subplot(2,1,1)
  # either use pcolormesh ..
     #pcolormesh(t, log2.(period1),power1, cmap=PyPlot.cm.jet, alpha = 0.5)
  # ... or use contours
  levels1 = range(0, stop=maximum(power1), length=200)
  contourf(t, log2.(period1),power1,levels1, cmap=PyPlot.cm.jet )
  plot(t,log2.(coi1),"k")           # cone-of-influence, anything "below" is dubious
  contour(t, log2.(period1),sig95_1, [-99,1], colors="black")   # 95% significance value
  ylabel("period length")
  ylim(lmaxp,lminp)
  yticks(lrange, 2 .^ lrange)

  # plot wavelet power for lynx
  subplot(2,1,2)
  # either use pcolormesh ..
    #pcolormesh(t, log2.(period2),power2, cmap=PyPlot.cm.jet, alpha = 0.5)
  # ... or use contours
  levels2 = range(0, stop=maximum(power2), length=200)
  contourf(t, log2.(period2),power2,levels2, cmap=PyPlot.cm.jet )
  plot(t,log2.(coi2),"k")           # cone-of-influence, anything "below" is dubious
  contour(t, log2.(period2),sig95_2, [-99,1], colors="black")   # 95% significance value
  ylabel("period length")
  xlabel("time")
  ylim(lmaxp,lminp)
  yticks(lrange, 2 .^ lrange)

end

function test_wcs_harelynx()
   # test wavelet cross spectrum  (WCS)

  # read in the data
  df = CSV.read("../data/Hare_Lynx.csv")
  t   = df[:,1]  # time (year)
  h   = df[:,2]  # hare
  l   = df[:,3]  # lynx
  #
  # do the wavelet transform
  dt = 1    # sampling rate = 1 year
  s0 = 2.0  # starting scale = 1 year
  noctave = 4  # number of octaves
  nvoices = 100 # number of voices per octave

  # bi-wavelet analysis: cross-spectrum amplitude, phase, and coherence (wco)
  spower1, spower2, amplitude, phase, wco, period, scale, coi =
    wcs(h,l,dt,noctave = noctave, s0=s0, nvoices = nvoices)

  # calculate global CWS
  global_ws1 = mean(spower1,dims=2)  # global power spectrum hare
  global_ws2 = mean(spower2,dims=2)  # global power spectrum lynx
  global_wco = mean(wco,dims=2)      # global wco
  global_amplitude = mean(amplitude,dims=2)  # global wcs

  gws1 = global_ws1 / maximum(global_ws1)
  gws2 = global_ws2 / maximum(global_ws2)
  gwco = global_wco / maximum(global_wco)
  gwcs = global_amplitude / maximum(global_amplitude)

  # prefixed scale band
  # consider all scales within 0.6 octaves from peaks in global spectrum
  delta_ind = trunc(Int, 0.6*nvoices)
  ind_max   = argmax(gwcs)[1]        # peak scale in global WCS
  ind1 = ind_max - delta_ind  # indices  ...
  ind2 = ind_max + delta_ind
  period_min = period[ind1]  # .. and corresponding scales
  period_max = period[ind2]

  # identifiy phases at scales of maximal corr spectrum
  wcs_max_ind, angle_max, wco_max = get_maxInd(wco,phase,length(t),ind1,ind2)

  # line of maximal wcs
  wco_scale = log2.(period[wcs_max_ind])

  # circular statistics of phase distribution
  phi, var, deviation = circstats(pi*angle_max)
  println("Phase difference between hare and lynx (in pi) ", phi,
          " std ", deviation)

  # calculate histogram of phase differences
  ph_bins  = 0.01 # phase bins = pi/100
  ph_width = 15    # histogram smoothed over 15 bins (=pi/8 = 27 deg)
  h_angle, h_bins = phasehist(angle_max, 1:length(angle_max),ph_bins,ph_width)

  # plotting scale range
  lminp = log2(minimum(period))
  lmaxp = log2(maximum(period))
  lrange = (trunc(lminp) :  trunc(lmaxp))
  # plotting time range
  tmax = maximum(t)
  tmin = minimum(t)

  # ********* Plotting ************
  fig_name = "Hare-lynx cross wavelet analysis"
  nrows = 4
  if nrows == 4 # four columns
      fig = figure("fig_name",figsize=(10,8))   # 4 columns
      clf()
      fig_width_y       = 0.15
      figleft_x         = 0.1
      figleft_width_x   = 0.65
      figright_x        = 0.8
      figrigtht_width_x = 0.15
      fig_y1=0.79
      fig_y2=0.56
      fig_y3=0.33
      fig_y4=0.1
  end

  # Plot time series
  ax = PyPlot.axes([figleft_x, fig_y1, figleft_width_x, fig_width_y])
  label="a"
  plot(t,h/maximum(h),"g",linewidth=2)
  plot(t,l/maximum(l),"r",linewidth=2)
  ylabel("abundance",labelpad=5, fontsize=12)
  xlim(tmin, tmax)
  ylim(0,1)
  yticks([0,0.5,1])
  ax1 = ax.transAxes
  text(-0.04, 1.10, label, fontsize=16, fontweight="bold", transform = ax1)

  # Plot phase plane
  ax = PyPlot.axes([figright_x, fig_y1, figrigtht_width_x, fig_width_y])
  label="b"
  time1 = 1900 - 1845 + 1
  time2 = 1932 - 1845 + 1
  hh = h[time1:time2]  # just for the phase plane ..
  ll = l[time1:time2]  # .. restrict data to nicely oscillating time window
  hh = hh / maximum(hh)
  ll = ll / maximum(ll)
  plot(hh, ll,"k")
  yticks([0,1])
  xticks([0,1])
  xlim(0,1)
  ylim(0,1)
  xlabel("hare", labelpad = -5, fontsize=12)  # labelpad: place the label a bit closer to axis
  ylabel("lynx", labelpad = -2, fontsize=12)
  ax1=ax.transAxes
  text(-0.14,1.10,label,fontsize=16, fontweight="bold",transform = ax1)

  # Plot wcs
  ax = PyPlot.axes([figleft_x, fig_y2, figleft_width_x, fig_width_y])
  label = "c"
  levels1 = range(0, stop=maximum(amplitude), length=200)
  contourf(t, log2.(period),amplitude,levels1, cmap=PyPlot.cm.jet )
  plot(t,log2.(coi),"k")           # cone-of-influence, anything "below" is dubious
  plot(t,wco_scale,"k-",linewidth=2)
  plot(t,ones(length(t))*log2(period_min),"k")    # lower border line
  plot(t,ones(length(t))*log2(period_max),"k")    # upper border line
  ylabel("period length")
  ylim(lmaxp,lminp)
  yticks(lrange, 2 .^ lrange)
  xlim(tmin,tmax)
  ylabel("period length (y)",labelpad=9, fontsize=12)
  ax1 = ax.transAxes
  text(-0.04, 1.10, label, fontsize=16, fontweight="bold", transform = ax1)

  # Plot global wavelet cross spectrum
  ax = PyPlot.axes([figright_x,fig_y2, figrigtht_width_x, fig_width_y])
  label = "d"
  plot(gwcs,log2.(period),"k")
  plot([0.,1], [1,1]*log2.(period_min),"k")
  plot([0.,1], [1,1]*log2.(period_max),"k")
  xlabel("power", fontsize=12)
  ylim(lmaxp,lminp)
  yticks(lrange, 2 .^ lrange)
  xlim(0,1)
  xticks([0, 0.25, 0.5, 0.75, 1], [0., 0.25, 0.5, 0.75, 1.])
  ax1=ax.transAxes
  text(-0.14,1.10,label,fontsize=16, fontweight="bold",transform = ax1)

  # Plot wco
  ax = PyPlot.axes([figleft_x, fig_y3, figleft_width_x, fig_width_y])
  label = "e"
  levels1 = range(0, stop=maximum(wco), length=200)
  contourf(t, log2.(period),wco,levels1, cmap=PyPlot.cm.jet )
  plot(t,log2.(coi),"k")           # cone-of-influence, anything "below" is dubious
  plot(t,wco_scale,"k-",linewidth=2)
  plot(t,ones(length(t))*log2(period_min),"k")    # lower border line
  plot(t,ones(length(t))*log2(period_max),"k")    # upper border line
  ylabel("period length")
  ylim(lmaxp,lminp)
  yticks(lrange, 2 .^ lrange)
  xlim(tmin,tmax)
  ylabel("period length (y)",labelpad=9, fontsize=12)
  ax1 = ax.transAxes
  text(-0.04, 1.10, label, fontsize=16, fontweight="bold", transform = ax1)

  # Plot global wavelet power spectra
  ax = PyPlot.axes([figright_x,fig_y3, figrigtht_width_x, fig_width_y])
  label = "f"
  plot(gws1,log2.(period),"g")
  plot(gws2,log2.(period),"r")
  plot([0.,1], [1,1]*log2.(period_min),"k")
  plot([0.,1], [1,1]*log2.(period_max),"k")
  xlabel("power", fontsize=12)
  ylim(lmaxp,lminp)
  yticks(lrange, 2 .^ lrange)
  xlim(0,1)
  xticks([0, 0.25, 0.5, 0.75, 1], [0., 0.25, 0.5, 0.75, 1.])
  ax1=ax.transAxes
  text(-0.14,1.10,label,fontsize=16, fontweight="bold",transform = ax1)

  # Plot Phase difference
  ax = PyPlot.axes([figleft_x, fig_y4, figleft_width_x, fig_width_y])
  label = "g"
  plot(t,angle_max)
  plot([tmin, tmax],[0.0,0.0],color="lightgrey", "--") #, color="gray")
  plot([tmin, tmax],[0.5,0.5],color="lightgrey", "--") #, color="gray")
  plot([tmin, tmax],[1.0,1.0],color="lightgrey", "--") #, color="gray")
  xlim(tmin,tmax)
  yticks([-0.5, 0.5, 1.0])
  ylim(-0.5,1.0)
  xlabel("time in years", fontsize=12)
  ylabel(L"phase difference in $\pi$",labelpad=4, fontsize=12)
  ax1 = ax.transAxes
  text(-0.04, 1.12, label, fontsize=16,  fontweight="bold",transform = ax1)

  ax = PyPlot.axes([figright_x,fig_y4, figrigtht_width_x, fig_width_y], polar="true") # Create a polar axis
  label = "f"
  plot(h_bins, h_angle,"r")
  fill_between(h_bins,0, h_angle,color="red",alpha=0.5)
  xticks([0, pi/2, pi, 3*pi/2], ["0", L"$0.5\pi$", L"$\pi$", L"$1.5 \pi$" ])
  ax.tick_params(pad=-5)  # place the tick_labels a bit closer
  yticks([0,25,0.5,0.75],[])
  plot([0,0], [0,1], "g" , linewidth=2)   # reference line for algae
  ax1=ax.transAxes
  text(-0.3,1.10,label,fontsize=16, fontweight="bold",transform = ax1)

  savefig("hare_lynx.png")

end

test_wcs_harelynx()

end # module


"""
For shifting the modelled trace to be non-negative.
Shift the modelled trace to ensure no negative values.

Arguments:  modelled = the modelled trace.
Returns:    Array{Float64, 1}, zscored and non-negative fluorescence trace
"""

function shiftFluorescence(modelled::Array{Int})
  z_modelled = zscore(modelled)
  adjustment = abs(minimum([0.0, minimum(z_modelled)]))
  return z_modelled + adjustment
end


import LinearAlgebra.tr

function tr(e::AbstractExpr)
    return sum(diag(e))
end