# ------------------------------------------------------------------------------------------
# # Learning: algorithms, objectives, and assumptions
# (c) Deniz Yuret 2019
#
# In this notebook we will analyze three classic learning algorithms.
# * **Perceptron:** ([Rosenblatt, 1957](https://en.wikipedia.org/wiki/Perceptron)) a neuron
# model trained with a simple algorithm that updates model weights using the input when the
# prediction is wrong.
# * **Adaline:** ([Widrow and Hoff, 1960](https://en.wikipedia.org/wiki/ADALINE)) a neuron
# model trained with a simple algorithm that updates model weights using the error
# multiplied by the input (aka least mean square (LMS), delta learning rule, or the Widrow-
# Hoff rule).
# * **Softmax classification:** ([Cox,
# 1958](https://en.wikipedia.org/wiki/Multinomial_logistic_regression)) a multiclass
# generalization of the logistic regression model from statistics (aka multinomial LR,
# softmax regression, maxent classifier etc.).
#
# We will look at these learners from three different perspectives:
# * **Algorithm:** First we ask only **how** the learner works, i.e. how it changes after
# observing each example.
# * **Objectives:** Next we ask **what** objective guides the algorithm, whether it is
# optimizing a particular objective function, and whether we can use a generic *optimization
# algorithm* instead.
# * **Assumptions:** Finally we ask **why** we think this algorithm makes sense, what prior
# assumptions does this imply and whether we can use *probabilistic inference* for optimal
# learning.
# ------------------------------------------------------------------------------------------

using Knet, Plots, Statistics, LinearAlgebra, Random
Base.argmax(a::KnetArray) = argmax(Array(a))
Base.argmax(a::AutoGrad.Value) = argmax(value(a))
ENV["COLUMNS"] = 72

# ------------------------------------------------------------------------------------------
# ### Data
# ------------------------------------------------------------------------------------------

include(Knet.dir("data/mnist.jl"))
xtrn, ytrn, xtst, ytst = mnist()
ARRAY = Array{Float32}
xtrn, xtst = ARRAY(mat(xtrn)), ARRAY(mat(xtst))
onehot(y) = (m=ARRAY(zeros(maximum(y),length(y))); for i in 1:length(y); m[y[i],i]=1; end; m)
ytrn, ytst = onehot(ytrn), onehot(ytst);

println.(summary.((xtrn, ytrn, xtst, ytst)));

NTRN,NTST,XDIM,YDIM = size(xtrn,2), size(xtst,2), size(xtrn,1), size(ytrn,1)

# ------------------------------------------------------------------------------------------
# ### Prediction
# ------------------------------------------------------------------------------------------

# Model weights
w = ARRAY(randn(YDIM,XDIM))

# Class scores
w * xtrn

# Predictions
[ argmax(w * xtrn[:,i]) for i in 1:NTRN ]'

# Correct answers
[ argmax(ytrn[:,i]) for i in 1:NTRN ]'

# Accuracy
acc(w,x,y) = mean(argmax(w * x, dims=1) .== argmax(y, dims=1))
acc(w,xtrn,ytrn), acc(w,xtst,ytst)

# ------------------------------------------------------------------------------------------
# ## Algorithms
# ------------------------------------------------------------------------------------------

# Training loop
function train(algo,x,y,T=2^20)
    w = ARRAY(zeros(size(y,1),size(x,1)))
    nexamples = size(x,2)
    nextprint = 1
    for t = 1:T
        i = rand(1:nexamples)
        algo(w, x[:,i], y[:,i])  # <== this is where w is updated
        if t == nextprint
            println((iter=t, accuracy=acc(w,x,y), wnorm=norm(w)))
            nextprint = min(2t,T)
        end
    end
    w
end

# ------------------------------------------------------------------------------------------
# ### Perceptron
# ------------------------------------------------------------------------------------------

function perceptron(w,x,y)
    guess = argmax(w * x)
    class = argmax(y)
    if guess != class
        w[class,:] .+= x
        w[guess,:] .-= x
    end
end

# (iter = 1048576, accuracy = 0.8950333333333333, wnorm = 1321.2463f0) in 7 secs
@time wperceptron = train(perceptron,xtrn,ytrn);

# ------------------------------------------------------------------------------------------
# ### Adaline
# ------------------------------------------------------------------------------------------

function adaline(w,x,y; lr=0.0001)
    error = w * x - y
    w .-= lr * error * x'
end

# (iter = 1048576, accuracy = 0.8523, wnorm = 1.2907721f0) in 31 secs with lr=0.0001
@time wadaline = train(adaline,xtrn,ytrn);

# ------------------------------------------------------------------------------------------
# ### Softmax classifier
# ------------------------------------------------------------------------------------------

function softmax(w,x,y; lr=0.01)
    probs = exp.(w * x)
    probs ./= sum(probs)
    error = probs - y
    w .-= lr * error * x'
end

# (iter = 1048576, accuracy = 0.9242166666666667, wnorm = 26.523603f0) in 32 secs with lr=0.01
@time wsoftmax = train(softmax,xtrn,ytrn);

# ------------------------------------------------------------------------------------------
# ## Objectives
# ------------------------------------------------------------------------------------------

# Training via optimization
function optimize(loss,x,y; lr=0.1, iters=2^20)
    w = Param(ARRAY(zeros(size(y,1),size(x,1))))
    nexamples = size(x,2)
    nextprint = 1
    for t = 1:iters
        i = rand(1:nexamples)
        L = @diff loss(w, x[:,i], y[:,i])
        ∇w = grad(L,w)
        w .-= lr * ∇w
        if t == nextprint
            println((iter=t, accuracy=acc(w,x,y), wnorm=norm(w)))
            nextprint = min(2t,iters)
        end
    end
    w
end

# ------------------------------------------------------------------------------------------
# ### Perceptron minimizes the score difference between the correct class and the prediction
# ------------------------------------------------------------------------------------------

function perceptronloss(w,x,y)
    score = w * x
    guess = argmax(score)
    class = argmax(y)
    score[guess] - score[class]
end

# (iter = 1048576, accuracy = 0.8908833333333334, wnorm = 1322.4888f0) in 62 secs with lr=1
@time wperceptron2 = optimize(perceptronloss,xtrn,ytrn,lr=1);

# ------------------------------------------------------------------------------------------
# ### Adaline minimizes the squared error
# ------------------------------------------------------------------------------------------

function quadraticloss(w,x,y)
    0.5 * sum(abs2, w * x - y)
end

# (iter = 1048576, accuracy = 0.8498333333333333, wnorm = 1.2882874f0) in 79 secs with lr=0.0001
@time wadaline2 = optimize(quadraticloss,xtrn,ytrn,lr=0.0001);

# ------------------------------------------------------------------------------------------
# ### Softmax classifier maximizes the probabilities of correct answers
# (or minimizes negative log likelihood, aka cross-entropy or softmax loss)
# ------------------------------------------------------------------------------------------

function negloglik(w,x,y)
    probs = exp.(w * x)
    probs = probs / sum(probs)
    class = argmax(y)
    -log(probs[class])
end

# (iter = 1048576, accuracy = 0.9283833333333333, wnorm = 26.593485f0) in 120 secs with lr=0.01
@time wsoftmax2 = optimize(negloglik,xtrn,ytrn,lr=0.01);


# Compute fast Walsh-Hadamard transforms (in natural, dyadic, or sequency order)
# using FFTW, by interpreting them as 2x2x2...x2x2 DFTs.  We follow Matlab's
# convention in which ifwht is the unnormalized transform and fwht has a 1/N
# normalization (as opposed to using a unitary normalization).

"""
The `Hadamard` module provides functions to compute fast Walsh-Hadamard transforms in Julia,
for arbitrary dimensions and arbitrary power-of-two transform sizes, with the three standard
orderings: natural (Hadamard), dyadic (Paley), and sequency (Walsh) ordering.
See the `fwht`, `fwht_natural`, and `fwht_dyadic` functions, along with their inverses
`ifwht` etc.

There is also a function `hadamard(n)` that returns Hadamard matrices for known sizes
(including non powers of two).
"""
module Hadamard
export fwht, ifwht, fwht_natural, ifwht_natural, fwht_natural!, ifwht_natural!, fwht_dyadic, ifwht_dyadic, hadamard

using FFTW, LinearAlgebra
import FFTW: set_timelimit, dims_howmany, unsafe_execute!, cFFTWPlan, r2rFFTWPlan, PlanPtr, fftwNumber, ESTIMATE, NO_TIMELIMIT, R2HC
import AbstractFFTs: normalization, complexfloat

# A power-of-two dimension to be transformed is interpreted as a
# 2x2x2x....x2x2  multidimensional DFT.  This function transforms
# each individual power-of-two dimension n into the corresponding log2(n)
# DFT dimensions.  If bitreverse is true, then the output is in
# bit-reversed (transposed) order (which may not work in-place).
function hadamardize(dims::Array{Int,2}, bitreverse::Bool)
    ntot = 0
    for i = 1:size(dims,2)
        n = dims[1,i]
        if !ispow2(n)
            throw(ArgumentError("non power-of-two Hadamard-transform length"))
        end
        ntot += trailing_zeros(n)
    end
    hdims = Array{Int}(undef, 3,ntot)
    j = 0
    for i = 1:size(dims,2)
        n = dims[1,i]
        is = dims[2,i]
        os = dims[3,i]
        log2n = trailing_zeros(n)
        krange = j+1:j+log2n
        for k in krange
            hdims[1,k] = 2
            hdims[2,k] = is
            hdims[3,k] = os
            is *= 2
            os *= 2
        end
        if bitreverse
            hdims[3,krange] = reverse(hdims[3,krange])
        end
        j += log2n
    end
    return hdims
end

const libfftw = isdefined(FFTW, :libfftw) ? FFTW.libfftw : FFTW.libfftw3
const libfftwf = isdefined(FFTW, :libfftwf) ? FFTW.libfftwf : FFTW.libfftw3f

for (Tr,Tc,fftw,lib) in ((:Float64,:ComplexF64,"fftw",libfftw),
                         (:Float32,:ComplexF32,"fftwf",libfftwf))
    @eval function Plan_Hadamard(X::StridedArray{$Tc,N}, Y::StridedArray{$Tc,N},
                                 region, flags::Unsigned, timelimit::Real,
                                 bitreverse::Bool) where {N}
        set_timelimit($Tr, timelimit)
        dims, howmany = dims_howmany(X, Y, [size(X)...], region)
        dims = hadamardize(dims, bitreverse)
        plan = ccall(($(string(fftw,"_plan_guru64_dft")),$lib[]),
                     PlanPtr,
                     (Int32, Ptr{Int}, Int32, Ptr{Int},
                      Ptr{$Tc}, Ptr{$Tc}, Int32, UInt32),
                     size(dims,2), dims, size(howmany,2), howmany,
                     X, Y, FFTW.FORWARD, flags)
        set_timelimit($Tr, NO_TIMELIMIT)
        if plan == C_NULL
            if $(FFTW.fftw_provider == "mkl")
                error("MKL is not supported — reconfigure FFTW.jl to use FFTW")
            else
                error("FFTW could not create plan") # shouldn't normally happen
            end
        end
        return cFFTWPlan{$Tc,FFTW.FORWARD,X===Y,N}(plan, flags, region, X, Y)
    end

    @eval function Plan_Hadamard(X::StridedArray{$Tr,N}, Y::StridedArray{$Tr,N},
                                 region, flags::Unsigned, timelimit::Real,
                                 bitreverse::Bool) where {N}
        set_timelimit($Tr, timelimit)
        dims, howmany = dims_howmany(X, Y, [size(X)...], region)
        dims = hadamardize(dims, bitreverse)
        kind = Array{Int32}(undef, size(dims,2))
        kind .= R2HC
        plan = ccall(($(string(fftw,"_plan_guru64_r2r")),$lib[]),
                     PlanPtr,
                     (Int32, Ptr{Int}, Int32, Ptr{Int},
                      Ptr{$Tr}, Ptr{$Tr}, Ptr{Int32}, UInt32),
                     size(dims,2), dims, size(howmany,2), howmany,
                     X, Y, kind, flags)
        set_timelimit($Tr, NO_TIMELIMIT)
        if plan == C_NULL
            if $(FFTW.fftw_provider == "mkl")
                error("MKL is not supported — reconfigure FFTW.jl to use FFTW")
            else
                error("FFTW could not create plan") # shouldn't normally happen
            end
        end
        return r2rFFTWPlan{$Tr,(map(Int,kind)...,),X===Y,N}(plan, flags, region, X, Y)
    end
end

############################################################################
# Define ifwht (inverse/unnormalized) transforms for various orderings

# Natural (Hadamard) ordering:

function ifwht_natural(X::StridedArray{<:fftwNumber}, region)
    Y = similar(X)
    p = Plan_Hadamard(X, Y, region, ESTIMATE, NO_TIMELIMIT, false)
    unsafe_execute!(p)
    return Y
end

function ifwht_natural(X::StridedArray{<:Number}, region)
    Y = float(X)
    p = Plan_Hadamard(Y, Y, region, ESTIMATE, NO_TIMELIMIT, false)
    unsafe_execute!(p)
    return Y
end

# Dyadic (Paley, bit-reversed) ordering:

function ifwht_dyadic(X::StridedArray{<:fftwNumber}, region)
    Y = similar(X)
    p = Plan_Hadamard(X, Y, region, ESTIMATE, NO_TIMELIMIT, true)
    unsafe_execute!(p)
    return Y
end

function ifwht_dyadic(X::StridedArray{<:Number}, region)
    return ifwht_dyadic(float(X), region)
end

############################################################################
# Sequency (Walsh) ordering:

# ifwht along a single dimension d of X
function ifwht(X::Array{T}, region) where {T<:fftwNumber}
    Y = ifwht_dyadic(X, region)

    # Perform Gray-code permutation of Y (TODO: in-place?)
    if isempty(region)
        return Y
    elseif ndims(Y) == 1
        return [ Y[1 + ((i >> 1) ⊻ i)] for i = 0:length(Y)-1 ]
    else
        sz = [size(Y)...]
        tmp = Array{T}(undef, maximum(sz[region])) # storage for out-of-place perm.
        for d in region
            # somewhat ugly loops to do 1d permutations along dimension d
            na = prod(sz[d+1:end])
            n = sz[d]
            nb = prod(sz[1:d-1])
            sa = nb * n
            for ia = 0:na-1
                for ib = 1:nb
                    i0 = ib + sa * ia
                    for i = 0:n-1
                        tmp[i+1] = Y[i0 + nb * ((i >> 1) ⊻ i)]
                    end
                    for i = 0:n-1
                        Y[i0 + nb * i] = tmp[i+1]
                    end
                end
            end
        end
        return Y
    end
end

# handle 1d case of strided arrays (loops in multidim case are too annoying)
function ifwht(X::StridedVector{<:fftwNumber}, region)
    Y = ifwht_dyadic(X, region)

    # Perform Gray-code permutation of Y (TODO: in-place?)
    if isempty(region)
        return Y
    else
        return [ Y[1 + ((i >> 1) ⊻ i)] for i = 0:length(Y)-1 ]
    end
end

# fallback for subarrays
function ifwht(X::StridedArray{<:fftwNumber}, region)
    return ifwht(copy(X), region)
end

# fallback for other types
function ifwht(X::StridedArray{<:Number}, region)
    return ifwht(float(X), region)
end

############################################################################
# in-place transforms, currently for natural ordering only.

"""
    ifwht_natural!(X, dims=1:ndims(X))

Similar to `ifwht_natural`, but works in-place on the input array `X`.
"""
function ifwht_natural!(X::StridedArray{<:fftwNumber}, region=1:ndims(X))
    p = Plan_Hadamard(X, X, region, ESTIMATE, NO_TIMELIMIT, false)
    unsafe_execute!(p)
    return X
end

"""
    fwht_natural!(X, dims=1:ndims(X))

Similar to `fwht_natural`, but works in-place on the input array `X`.
"""
fwht_natural!(X::StridedArray{<:fftwNumber}, region=1:ndims(X)) =
    rmul!(ifwht_natural!(X, region), normalization(X,region))

############################################################################
# Forward transforms (normalized by 1/N as in Matlab) and transforms
# without the region argument:

for f in (:ifwht_natural, :ifwht_dyadic, :ifwht)
    g = Symbol(string(f)[2:end])
    @eval begin
        $f(X) = $f(X, 1:ndims(X))
        $g(X) = rmul!($f(X), normalization(X,1:ndims(X)))
        $g(X,r) = rmul!($f(X,r), normalization(X,r))
    end
end

############################################################################
# Docstrings — since they are all very similar, we
# assign them in a loop.

for inverse in (true, false), ord in (:natural, :dyadic, :sequency)
    name = (inverse ? "i" : "") * "fwht"
    if ord != :sequency
        name = string(name, '_', ord)
    end
    ordstr = ord == :natural ? "natural (Hadamard)" :
             ord == :dyadic ? "dyadic (Paley, bit-reversed)" :
             "sequency"
    docstring = """    $name(X, dims=1:ndims(X))

Return the$(inverse ? " inverse" : "") fast Walsh-Hadamard transform (WHT) of
the array `X` along the dimensions `dims` (an integer,
tuple, or array of dimensions, defaulting to all dimensions).
Only power-of-two sizes (along the transformed dimensions)
are supported.  The result is returned in the $ordstr ordering.

Our WHT is normalized so that the forward transform has a `1/n`
coefficient (where `n` is the product of the transformed dimensions)
and the inverse WHT has no scaling factor.
"""
    @eval @doc $docstring $(Symbol(name))
end

############################################################################
# Utilities to work with a precomputed cache of known Hadamard matrices
# of various sizes, produced by util/fetchhadamard.jl from Sloane's web page
# and stored as BitMatrices

function readcache(cachefile::AbstractString)
    B = BitMatrix[]
    open(cachefile, "r") do io
        while !eof(io)
            k = convert(Int, ntoh(read(io, Int64)))
            b = BitArray(undef, k, k)
            b.chunks .= ntoh.(read!(io, b.chunks))
            push!(B, b)
        end
    end
    sort!(B, by = h -> (isodd(size(h,1)>>2), size(h,1)))
    return B
end
readcache() = readcache(joinpath(dirname(@__FILE__), "..", "src", "cache.dat"))

"""
    printsigns([io, ] A)

Print a table of `+` and `-` characters indicating the
signs of the entries of `A` (with `0` for zero entries).
The output stream `io` defaults to `stdout`.
"""
function printsigns(io::IO, A::AbstractMatrix{<:Real})
    m, n = size(A)
    println(io, m, "×", n, " sign matrix from ", typeof(A))
    for i = 1:m
        for j = 1:n
            print(io, A[i,j] > 0 ? '+' : A[i,j] < 0 ? '-' : '0')
        end
        println(io)
    end
end
printsigns(A) = printsigns(stdout, A)

function frombits(B::BitMatrix)
    A = Matrix{Int8}(B)
    for i = 1:length(A)
        A[i] = A[i] == 0 ? -1 : 1
    end
    return A
end

const hadamards = BitMatrix[] # read lazily below

############################################################################
# Create Int8 order-n Hadamard matrix (in natural order), by factorizing
# n into a product of known Hadamard sizes (if possible)

const H2 = Int8[1 1; 1 -1]

"""
    hadamard(n)

Return a Hadamard matrix of order `n`.  The known Hadamard
matrices up to size 256 are currently supported (via a lookup table),
along with any size that factorizes into products of these known
sizes and/or powers of two.

The return value is a matrix of `Int8` values. If you want to do
further matrix computations with this matrix, you may want to
convert to `Float64` first via `float(hadamard(n))`.

You can pretty-print a Hadamard matrix as a table of `+` and `-`
(indicating the signs of the entries) via `Hadamard.printsigns`,
e.g. `Hadamard.printsigns(hadamard(28))` for the 28×28 Hadamard matrix.
"""
function hadamard(n::Integer)
    n < 1 && throw(ArgumentError("size n=$n should be positive"))
    n0 = n
    H = reshape(Int8[1], 1,1)
    if !ispow2(n)
        isempty(hadamards) && append!(hadamards, readcache())
        for i = length(hadamards):-1:1
            k = size(hadamards[i], 1)
            if rem(n, k) == 0
                Hk = frombits(hadamards[i])
                while true
                    H = kron(H, Hk)
                    n = div(n, k)
                    rem(n, k) != 0 && break
                end
            end
        end
    end
    if !ispow2(n)
        n >>= trailing_zeros(n)
        throw(ArgumentError("unknown Hadamard factor $n of $n0"))
    end
    # Note: it would be faster to do a "power-by-squaring" like algorithm
    # here, where we repeatedly double the order via kron(H, H), but
    # it's not clear to me that we care much about performance here.
    while n > 1
        H = kron(H, H2)
        n >>= 1
    end
    return H
end

############################################################################

end # module


#Figure 2.5
#Run function "Glevel" at bottom of file first
using Plots, SparseArrays, JLD2, LinearAlgebra, MultiscaleGraphSignalTransforms

JLD2.@load "../data/Toronto.jld2"
tmp1 = toronto["G"]
G=GraphSig(tmp1["W"],xy=tmp1["xy"],f=tmp1["f"],name =tmp1["name"],plotspecs = tmp1["plotspecs"])

G = Adj2InvEuc(G)
GP = partition_tree_fiedler(G,:Lrw)
dmatrix = ghwt_analysis!(G, GP=GP)

j = 1
GraphSig_Plot(Glevel(G,GP,1), linewidth = 1., markersize = 4., markercolor = :viridis, markerstrokealpha =0., notitle = true, nocolorbar = true)
plot!(axis = false)
#savefig("G1.pdf")

j = 2
GraphSig_Plot(Glevel(G,GP,2), linewidth = 1., markersize = 4., markercolor = :viridis, markerstrokealpha =0., notitle = true, nocolorbar = true)
plot!(axis = false)
#savefig("G2.pdf")

j = 3
GraphSig_Plot(Glevel(G,GP,3), linewidth = 1., markersize = 4., markercolor = :viridis, markerstrokealpha =0., notitle = true, nocolorbar = true)
plot!(axis = false)
#savefig("G3.pdf")

function Glevel(G::GraphSig, GP::GraphPart, j::Int64)
    f = zeros(size(G.f))
    for k in 1:size(GP.rs,1)
        a = GP.rs[k,j]
        b = GP.rs[k+1,j] - 1
        if b == -1
            break
        end
        f[a:b] .= k*1.0
    end
    Gsub = deepcopy(G)
    Gsub.f[GP.ind] = f
    return Gsub
end


export crelu

doc"""
    crelu(x::Var)

Concatenated Rectified Linear Unit.
The output is twice the size of the input.

```math
f(x) = (\max(0,x), \max(0,-x))
```

# References
* Shang et al., ["Understanding and Improving Convolutional Neural Networks via Concatenated Rectified Linear Units"](https://arxiv.org/abs/1603.05201), arXiv 2016.
"""
crelu(x::Var) = Var(crelu(x.data), (crelu,x))
crelu(x::Node) = Node(crelu, x)

function crelu(x::Array{T}) where T
    y = Array{T}(2size(x,1), Base.tail(size(x))...)
    @inbounds for i = 1:length(x)
        k = (i-1)*2 + 1
        y[k] = max(x[i], zero(T))
        y[k+1] = max(-x[i], zero(T))
    end
    y
end

function addgrad!(y::Var, ::typeof(crelu), x::Var)
    isvoid(x.grad) && return
    ∇crelu!(y.grad, x.data, x.grad)
end

function ∇crelu!(gy::Array{T}, x::Array{T}, gx::Array{T}) where T
    @inbounds for i = 1:length(x)
        k = (i-1)*2 + 1
        gx[i] += x[i] > zero(T) ? gy[k] : -gy[k+1]
    end
end


export Polygon2,
       PolygonWithHoles2

export area,
       bbox,
       bottom_vertex,
       bounded_side,
       edges,
       has_on_boundary,
       has_on_bounded_side,
       has_on_negative_side,
       has_on_positive_side,
       has_on_unbounded_side,
       holes,
       is_clockwise_oriented,
       is_collinear_oriented,
       is_convex,
       is_counterclockwise_oriented,
       is_simple,
       left_vertex,
       orientation,
       oriented_side,
       outer_boundary,
       right_vertex,
       top_vertex,
       transform,
       vertices

"""
    Polygon2

The type [`Polygon2`](@ref) represents polygons.
"""
Polygon2

"""
    Polygon2()

Creates an empty polygon.
"""
Polygon2()

"""
    Polygon2(polygon::Polygon2)

Copy constructor.
"""
Polygon2(polygon::Polygon2)

"""
    Polygon2(ps::AbstractVector{Point2})
    Polygon2(ps::Point2...)

Creates a polygon with vertices from the sequence defined by the vector `ps`.
"""
Polygon2(ps::AbstractVector{Point2})

Polygon2(ps::AbstractVector) = Polygon2(collect(CxxRef{Point2}, CxxRef.(ps)))
Polygon2(ps::reference_type_union(Point2)...) = Polygon2(collect(CxxRef.(ps)))

"""
    ==(p₁::Polygon2, p₂::Polygon2)

Test for equality: two polygons are equal iff there exists a cyclic permutation
of the vertices of `p₂` such that they are equal to the vertices of `p₁`.
"""
==(p₁::Polygon2, p₂::Polygon2)

"""
    transform(t::AffTransformation2, p::Polygon2)

Returns the image of the polygon `p` under the transformation `t`.
"""
transform(t::AffTransformation2, p::Polygon2)

"""
    push!(p::Polygon2, q::Point2)

Inserts the vertex `q` at the end of polygon `p`.
"""
push!(p::Polygon2, x::Point2)

"""
    empty!(p::Polygon2)

Erases the polygon `p`'s vertices.
"""
empty!(p::Polygon2)

"""
    reverse(p::Polygon2)

Reverses the orientation of the polygon.

The first vertex remains the same.
"""
reverse(p::Polygon2)

"""
    reverse!(p::Polygon2)

In-place version of [`reverse!`](@ref).
"""
reverse!(p::Polygon2)

"""
    vertices(p::Polygon2)

Returns the polygon's vertices.
"""
vertices(p::Polygon2)

"""
    edges(p::Polygon2)

Returns the polygon's edges.
"""
edges(p::Polygon2)

"""
    is_simple(p::Polygon2)

Returns whether `p` is a simple polygon.
"""
is_simple(p::Polygon2)

"""
    is_convex(p::Polygon2)

Returns whether `p` is convex.
"""
is_convex(p::Polygon2)

"""
    orientation(p::Polygon2)

Returns the orientation.

If the number of vertices `size(p) < 3` then [`COLLINEAR`](@ref) is returned.

!!! info "Precondition"

    `is_simple(p)`
"""
orientation(p::Polygon2)

"""
    oriented_side(p::Polygon2, q::Point2)

Returns [`ON_POSITIVE_SIDE`](@ref), [`ON_NEGATIVE_SIDE`](@ref), or
[`ON_ORIENTED_BOUNDARY`](@ref), depending on where point `q` is.

!!! info "Precondition"

    `is_simple(p)`
"""
oriented_side(p::Polygon2, q::Point2)

"""
    bounded_side(p::Polygon2, q::Point2)

Returns [`ON_BOUNDED_SIDE`](@ref), [`ON_BOUNDARY`](@ref), or
[`ON_UNBOUNDED_SIDE`](@ref), depending on where point `q` is.

!!! info "Precondition"

    `is_simple(p)`
"""
bounded_side(p::Polygon2, q::Point2)

"""
    bbox(p::Polygon2)

Returns the smallest bounding box containing this polygon.
"""
bbox(p::Polygon2)

"""
    area(p::Polygon2)

Returns the signed area of the polygon.

This means that the area is positive for counter clockwise polygons and negative
for clockwise polygons.
"""
area(p::Polygon2)

"""
    left_vertex(p::Polygon2)

Returns the leftmost vertex of the polygon with the smallest x-coordinate.
"""
left_vertex(p::Polygon2)

"""
    right_vertex(p::Polygon2)

Returns the rightmost vertex of the polygon with the largest x-coordinate.
"""
right_vertex(p::Polygon2)

"""
    top_vertex(p::Polygon2)

Returns the topmost vertex of the polygon with the largest y-coordinate.
"""
top_vertex(p::Polygon2)

"""
    bottom_vertex(p::Polygon2)

Returns the bottommost vertex of the polygon with the smallest y-coordinate.
"""
bottom_vertex(p::Polygon2)

for name ∈ (:counterclockwise, :clockwise, :collinear)
    fname = Symbol("is_$(name)_oriented")
    NAME = uppercase(string(name))
    @eval begin
        """
            $($fname)(p::Polygon2)

        Returns `orientation(p) == $($NAME)`.
        """
        $fname(p::Polygon2)
    end
end

for (name, es) ∈ zip((:oriented, :bounded),
                     ((:positive_side, :negative_side)
                    , (:boundary, :bounded_side, :unbounded_side)))
    fdel = "$(name)_side"
    for e ∈ es
        CNAME = uppercase("on_$e")
        fname = Symbol("has_on_$e")
        @eval begin
            """
                $($fname)(p::Polygon2, q::Point2)

            Returns `$($fdel)(p, q) == $($CNAME)`.
            """
            $fname(p::Polygon2, q::Point2)
        end
    end
end

"""
    size(p::Polygon2)

Returns the number of vertices of the polygon.
"""
size(p::Polygon2)

"""
    isempty(p::Polygon2)

Returns `size(p) == 0`.
"""
isempty(p::Polygon2)

"""
resize!(p::Polygon2, sz::Integer)

Resizes the polygon.
"""
resize!(p::Polygon2, sz)

"""
    PolygonWithHoles2

The type [`PolygonWithHoles2`](@ref) represents a linear polygon with holes.
"""
PolygonWithHoles2

"""
    PolygonWithHoles2(pgn_boundary::Polygon2)

Constructor from a polygon.
"""
PolygonWithHoles2(pgn_boundary::Polygon2)

"""
    PolygonWithHoles2(pgn_boundary::Polygon2, holes::AbstractVector{Polygon2})
    PolygonWithHoles2(pgn_boundary::Polygon2, holes::Polygon2...)

Constructor from a polygon (outer boundary) and hole polygons.
"""
PolygonWithHoles2(pgn_boundary::Polygon2, holes::AbstractVector{Polygon2})

@cxxdereference PolygonWithHoles2(p::Polygon2, holes::AbstractVector) =
    PolygonWithHoles2(p, collect(CxxRef{Polygon2}, CxxRef.(holes)))
@cxxdereference PolygonWithHoles2(p::Polygon2,
                                  holes::reference_type_union(Polygon2)...) =
    PolygonWithHoles2(p, collect(CxxRef.(holes)))

"""
    is_unbounded(p::PolygonWithHoles2)

Returns `true` if the outer boundary is empty, `false` otherwise.
"""
is_unbounded(p::PolygonWithHoles2)

"""
    outer_boundary(p::PolygonWithHoles2)

Returns the polygon that represents the outer boundary.

Note that this polygon is not necessarily a valid (simple) general polygon
because it may be relatively simple.
"""
outer_boundary(p::PolygonWithHoles2)

"""
    holes(p::PolygonWithHoles2)

Returns the polygon `p`'s hole polygons.
"""
holes(p::PolygonWithHoles2)


#
# Discrete Uniform Sampler
#
# parameters:
# 	(1) a (lower bound)
#	(2) b (upper bound)
#
const ONE_EIGTH_INT_RANGE = (typemax(Int)>>3) - (typemin(Int)>>3) # this formulation avoids overflow

type DiscreteUniformSampler <: DiscreteDistributionSampler
	paramranges::Vector{Tuple{Float64,Float64}}
	params::Vector{Float64}
	distribution::Distribution
	function DiscreteUniformSampler(params=Float64[])
		intbounds = (Float64(typemin(Int)), Float64(typemax(Int)))
		s = new([intbounds, intbounds])
		setparams!(s, isempty(params) ? [Float64(typemin(Int)), Float64(typemax(Int))] : params)
		s
	end
end

function setparams!(s::DiscreteUniformSampler, params)
	# note the paramranges check will trap an attempt to sample from the entire UInt range: this cannot be supported
	# since Distributions.Uniform currently only supports Int range (and then only with the workaround below using a Mixture Model)
	# therefore adjust parameters and warn
	adjparams = deepcopy(params)
	for i in 1:length(adjparams)
		if adjparams[i] < Float64(typemin(Int))
			warn("Adjusting parameters of discrete uniform sampler to typemin(Int)")
			adjparams[i] = Float64(typemin(Int))
		end
		if adjparams[i] > Float64(typemax(Int))
			warn("Adjusting parameters of discrete uniform sampler to typemax(Int)")
			adjparams[i] = Float64(typemax(Int))
		end
	end
	checkparamranges(s, adjparams)
	# swap parameters if necessary (as a silent repair during search)
	if adjparams[1] <= adjparams[2]
		a, b = adjparams[[1,2]]
	else
		a, b = adjparams[[2,1]]
	end
	# convert to Int, but with a check to avoid InexactError when at bounds since params will be Float64 and imprecision means
	# that the values can be outside range
	a = a < typemin(Int) ? typemin(Int) : round(Int, a, RoundUp) # round up so that value sampled remain in unrounded range
	b = b > typemax(Int) ? typemax(Int) : round(Int, b, RoundDown) # round down so that value sampled remain in unrounded range
	# if range is too large (more than one quarter of entire Int range), then must use Mixture
	# distribution instead to avoid InexactErrors when sampling from DiscreteUniform
	halfrange = (b>>1) - (a>>1) # >> 1 formulation avoids overflow by calculating half range
	if  halfrange < ONE_EIGTH_INT_RANGE
		s.distribution = DiscreteUniform(a, b)
	else
		# in the worst case (the entire Int range), we must split into four ranges for the
		# mixture model if we are to avoid errors, so let's always do this
		quarterrange = halfrange >> 1  # one quarter desired range
		q1 = a + quarterrange
		q2 = q1 + quarterrange
		q3 = q2 + quarterrange
		prior = [Float64(q1)-Float64(a)+1, Float64(q2)-Float64(q1), Float64(q3)-Float64(q2), Float64(b)-Float64(q3)]
		s.distribution = MixtureModel(
			[DiscreteUniform(a,q1), DiscreteUniform(q1+1,q2), DiscreteUniform(q2+1,q3),  DiscreteUniform(q3+1,b)], prior ./ sum(prior))
	end
	s.params = [Float64(a), Float64(b)]
end

getparams(s::DiscreteUniformSampler) = s.params

estimateparams!(s::DiscreteUniformSampler, traces) = estimateparams!(s, traces, 2)


isweekend(dt::Date) = Dates.dayofweek(dt) ∈ (Dates.Friday, Dates.Saturday, Dates.Sunday)

function hasfiveweekend(month::Integer, year::Integer)
    dmin = Date(year, month, 1)
    dmax = dmin + Dates.Day(Dates.daysinmonth(dmin) - 1)
    return count(isweekend, dmin:dmax) ≥ 15
end

months = collect((y, m) for y in 1900:2100, m in 1:12 if hasfiveweekend(m, y))

println("Number of months with 5 full-weekends: $(length(months))")
println("First five such months:")
for (y, m) in months[1:5] println(" - $y-$m") end
println("Last five such months:")
for (y, m) in months[end-4:end] println(" - $y-$m") end

# extra credit
yrs = getindex.(months, 1)
nyrs = 2100 - 1899 - length(unique(yrs))

println("Number of year with not one 5-full-weekend month: $nyrs")


include("../demo_utilities.jl")
include("../../src/cg_modified.jl")
@info "Demo CG Modified Script running at $(@__DIR__)"
println("Chaging pwd to current directory of the script")
cd(@__DIR__)
@info "New pwd: $(pwd())"

function GetObjectiveVals(A, b, xs)
    return xs .|> x -> dot(x, A, x) - 2*dot(b, x)
end

"""
    Bad eigen value distribution, they are distributed from 0.001 to 1 like a
    geometric series.

    * A lot of small eigenvalues are clustered close to zero, few larger ones are
    far away from the other Eigenvalues.

    Parameters:
        rho: Number:
            An number between 0 and 1 that paramaterize the distribution of the
            eigenvalues for the PSD matrix.
        N=20:
            The size of the PSD matrix.

    returns:
        a diagonal matrix.
"""
function GetNastyPSDMatrix(rho::Number, N=20, inverted=false)
    @assert rho <= 1 && rho >= 0
    EigenValues = zeros(N)
    EigenMin, EigenMax = 0.001, 1    # Min Max Eigenvalues.
    EigenValues[1] = EigenMin
    for IdexI in 2:N
        EigenValues[IdexI] = EigenMin +
            ((IdexI - 1)/(N - 1))*(EigenMax - EigenMin)*rho^(N - IdexI)  # formulas
    end
    if inverted
        return Diagonal(1 + EigenMin - EigenValues)
    end
    return Diagonal(EigenValues)
end

N = 512
A = GetNastyPSDMatrix(0.9, N)
A = A^4
b = rand(N)
cg1 = ConjGradModified(A, b)
cg2 = ConjGradModified(A, b)
cg3 = ConjGradModified(A, b)
cg1Soln = Vector{typeof(b)}()
cg2Soln = Vector{typeof(b)}()
cg3Soln = Vector{typeof(b)}()
push!(cg1Soln, cg1.x)
push!(cg2Soln, cg2.x)
push!(cg3Soln, cg3.x)
TurnOffReorthgonalize(cg2)
ChangeStorageLimit(cg3, 32)
for _ in 1:2*N
    cg1(); cg2(); cg3()
    push!(cg1Soln, cg1.x); push!(cg2Soln, cg2.x); push!(cg3Soln, cg3.x)
end

objectiveVals1 = GetObjectiveVals(A, b, cg1Soln)
objectiveVals2 = GetObjectiveVals(A, b, cg2Soln)
objectiveVals3 = GetObjectiveVals(A, b, cg3Soln)
fig = plot(objectiveVals1, label="With Re-orthonalization", title="Convergence of the Objective")
plot!(fig, objectiveVals2, label="without Re-orthogonalization")
plot!(fig, objectiveVals3, label="partial Re-orthogonalization")
savefig(fig, "objectivevals_convergence.png")

cd(".")


int main () {
printInt(fact(7)) ;
printInt(factr(7)) ;
return 0 ;
}
// iterative factorial
int fact (int n) {
int i,r ;
i = 1 ;
r = 1 ;
while (i <= n) {
r = r * i ;
i++ ;
}
return r ;
}
// recursive factorial
int factr (int n) {
if (n < 2)
return 1 ;
else
return n * factr(n-1) ;
}


# ---
# title: 464. Can I Win
# id: problem464
# author: Tian Jun
# date: 2020-10-31
# difficulty: Medium
# categories: Dynamic Programming, Minimax
# link: <https://leetcode.com/problems/can-i-win/description/>
# hidden: true
# ---
#
# In the "100 game" two players take turns adding, to a running total, any
# integer from `1` to `10`. The player who first causes the running total to
# **reach or exceed** 100 wins.
#
# What if we change the game so that players **cannot** re-use integers?
#
# For example, two players might take turns drawing from a common pool of
# numbers from 1 to 15 without replacement until they reach a total >= 100.
#
# Given two integers maxChoosableInteger and desiredTotal, return `true` if the
# first player to move can force a win, otherwise return `false`. Assume both
# players play **optimally**.
#
#
#
# **Example 1:**
#
#
#
#     Input: maxChoosableInteger = 10, desiredTotal = 11
#     Output: false
#     Explanation:
#     No matter which integer the first player choose, the first player will lose.
#     The first player can choose an integer from 1 up to 10.
#     If the first player choose 1, the second player can only choose integers from 2 up to 10.
#     The second player will win by choosing 10 and get a total = 11, which is >= desiredTotal.
#     Same with other integers chosen by the first player, the second player will always win.
#
#
# **Example 2:**
#
#
#
#     Input: maxChoosableInteger = 10, desiredTotal = 0
#     Output: true
#
#
# **Example 3:**
#
#
#
#     Input: maxChoosableInteger = 10, desiredTotal = 1
#     Output: true
#
#
#
#
# **Constraints:**
#
#   * `1 <= maxChoosableInteger <= 20`
#   * `0 <= desiredTotal <= 300`
#
#
## @lc code=start
using LeetCode

## add your code here:
## @lc code=end


# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     formats: ipynb,jl:hydrogen
#     text_representation:
#       extension: .jl
#       format_name: hydrogen
#       format_version: '1.3'
#       jupytext_version: 1.11.2
#   kernelspec:
#     display_name: Julia 1.6.2
#     language: julia
#     name: julia-1.6
# ---

# %%
using Plots
using Distributions

"""empirical cumulative distribution function"""
ecdf(sample, x) = count(≤(x), sample)/length(sample)

λ = 3
dist_true = Poisson(λ)
n = 2^10
sample = rand(dist_true, n)
m, u² = mean(sample), var(sample)
gamma = Gamma(m^2/u², u²/m)
a, b = 0, 10

P = plot();
histogram!(sample; norm=true, alpha=0.3,
    bin=a-0.5:b+0.5, xtick=a:b,
    label="size-$n sample of Poisson($λ)");
plot!(x -> pdf(gamma, x), a, b;
    label="Gamma distribution approximation");

Q = plot(; legend=:bottomright, xtick=a:b);
plot!(x -> ecdf(sample, x), a-0.5, b+0.5;
    label="ecdf of size-$n sample of Poisson($λ)");
plot!(x -> cdf(gamma, x), a-0.5, b+0.5;
    label="cdf of Gamma distribution approximation");

plot(P, Q; size=(500, 500), layout=(2, 1))

# %%
using Plots
using Distributions
using QuadGK

"""empirical cumulative distribution function"""
ecdf(sample, x) = count(≤(x), sample)/length(sample)

λ = 3
dist_true = Poisson(λ)
n = 2^10
sample = rand(dist_true, n)
m, u² = mean(sample), var(sample)
gamma = Gamma(m^2/u², u²/m)

W_1 = quadgk(x -> abs(ecdf(sample, x) - cdf(gamma, x)), 0, Inf)[1]

a, b = 0, 10
xs = range(a-0.5, b+0.5; length=300)
zs = pdf.(gamma, xs)
Ys = ecdf.(Ref(sample), xs)
Zs = cdf.(gamma, xs)

P = plot();
histogram!(sample; norm=true, alpha=0.3, lw=2,
    bin=a-0.5:b+0.5, xtick=a:b,
    label="size-$n sample of Poisson($λ)");
plot!(xs, zs; lw=2,
    label="Gamma distribution approximation");

Q = plot(; legend=:bottomright, xtick=a:b, ytick=0:0.1:1);
plot!(xs, Ys; linealpha = 0, fillrange = Zs, fillcolor = :yellow,
    label="area = W_1 = Wasserstein distance for p = 1");
plot!(xs, Ys; lw = 2, color = 1,
    label="ecdf of size-$n sample of Poisson($λ)");
plot!(xs, Zs; lw = 2, color = 2,
    label="cdf of Gamma distribution approximation")
title!("W_1 = $(round(W_1; digits=3))")

plot(P, Q; size=(600, 800), layout=(2, 1))

# %%
using Plots
using Distributions
using QuadGK

"""empirical cumulative distribution function"""
ecdf(sample, x) = count(≤(x), sample)/length(sample)

λ = 3
dist_true = Poisson(λ)
n = 2^10
sample = rand(dist_true, n)
normal = fit(Normal, sample)

AIC = -2loglikelihood(normal, sample) + 2
W_1 = quadgk(x -> abs(ecdf(sample, x) - cdf(normal, x)), -Inf, Inf)[1]

a, b = -1, 10
xs = range(a-0.5, b+0.5; length=300)
zs = pdf.(normal, xs)
Ys = ecdf.(Ref(sample), xs)
Zs = cdf.(normal, xs)

P = plot();
histogram!(sample; norm=true, alpha=0.3, lw=2,
    bin=a-0.5:b+0.5, xtick=a:b,
    label="size-$n sample of Poisson($λ)");
plot!(xs, zs; lw=2,
    label="normal distribution approximation");
title!("AIC = $(round(AIC; digits=3))");

Q = plot(; legend=:bottomright, xtick=a:b, ytick=0:0.1:1);
plot!(xs, Ys; linealpha = 0, fillrange = Zs, fillcolor = :yellow,
    label="area = W_1 = Wasserstein distance for p = 1");
plot!(xs, Ys; lw = 2, color = 1,
    label="ecdf of size-$n sample of Poisson($λ)");
plot!(xs, Zs; lw = 2, color = 2,
    label="cdf of normal distribution approximation")
title!("W_1 = $(round(W_1; digits=3))")

plot(P, Q; size=(600, 800), layout=(2, 1))

# %%
#\using StatsFuns
AIC = -2loglikelihood(normal, sample) + 2
#S = -2sum((x = count(==(k), sample)/length(sample); xlogy(x, x)) for k in 0:maximum(sample))*length(sample)
S = -2sum((x = pdf(dist_true, k); xlogy(x, x)) for k in 0:20) * n
@show AIC S AIC-S;

# %%
KL = -2sum((x = pdf(dist_true, k); y = pdf(normal, k); xlogy(x, y/x)) for k in 0:20) * n

# %%

0017/Comparing discrete and continuous distributions.jl


### A Pluto.jl notebook ###
# v0.12.21

using Markdown
using InteractiveUtils

# ╔═╡ 24734878-7d42-11eb-10e7-35421889faaf
using GR

# ╔═╡ 167d66f2-7d42-11eb-1967-3fff5fa301dd
histogram(randn(10000))

# ╔═╡ Cell order:
# ╠═24734878-7d42-11eb-10e7-35421889faaf
# ╠═167d66f2-7d42-11eb-1967-3fff5fa301dd


using Test
using LinearAlgebraicRepresentation
Lar = LinearAlgebraicRepresentation
include("../src/LinearAlgebraicRepresentation.jl")

function generate_perpendicular_lines(steps::Int, minlen, maxlen)
    V = zeros(0,2)

    function rec(o, d, s)
        if s == 0 return end

        a = (maxlen-minlen)*rand() + minlen
        p = o + a*d
        V = [V; o; p]

        b = (a-minlen)*rand() + minlen
        p = o + b*d
        rec(p, d, s-1)

        b = (a-minlen)*rand() + minlen
        p = o + b*d
        rec(p, perpendicular(d), s-1)
    end

    function perpendicular(vec)
        v = zeros(size(vec))
        v[1] = vec[2]
        v[2] = vec[1]
        return v
    end

    rec([0 0], [1 0], steps)
    rec([0 0], [0 1], steps)
    vnum = size(V, 1)
    enum = vnum >> 1
    EV = spzeros(Int8, enum, vnum)
    for i in 1:enum
        EV[i, i*2-1:i*2] = 1
    end
    V, EV
end

function generate_random_lines(n, points_range, alphas_range)
    origins = points_range[1] + (points_range[2]-points_range[1])*rand(n, 2)
    directions = mapslices(normalize, rand(n, 2) - .5*ones(n, 2), dims=2)
    alphas = alphas_range[1] + (alphas_range[2]-alphas_range[1])*rand(n)
    new_points = Array{Float64, 2}(n, 2)
    for i in 1:n
        new_points[i, :] = origins[i, :] + alphas[i]*directions[i, :]
    end
    V = [origins; new_points]
    EV = spzeros(Int8, n, n*2)
    for i in 1:n
        EV[i, i] = 1
        EV[i, n+i] = 1
    end
    V, EV
end

function rubiks_example(ncubes = 3)
    V = Float64[
        0 0 0; 0 1 0;
        1 1 0; 1 0 0;
        0 0 1; 0 1 1;
        1 1 1; 1 0 1
    ]

    EV = sparse(Int8[
        -1  1  0  0  0  0  0  0;
        0 -1  1  0  0  0  0  0;
        0  0 -1  1  0  0  0  0;
        -1  0  0  1  0  0  0  0;
        -1  0  0  0  1  0  0  0;
        0 -1  0  0  0  1  0  0;
        0  0 -1  0  0  0  1  0;
        0  0  0 -1  0  0  0  1;
        0  0  0  0 -1  1  0  0;
        0  0  0  0  0 -1  1  0;
        0  0  0  0  0  0 -1  1;
        0  0  0  0 -1  0  0  1;
    ])

    FE = sparse(Int8[
        1  1  1 -1  0  0  0  0  0  0  0  0;
        0  0  0  0  0  0  0  0 -1 -1 -1  1;
        -1  0  0  0  1 -1  0  0  1  0  0  0;
        0 -1  0  0  0  1 -1  0  0  1  0  0;
        0  0 -1  0  0  0  1 -1  0  0  1  0;
        0  0  0  1 -1  0  0  1  0  0  0 -1;
    ])

    cube = [V, EV, FE]
    cubesRow = (zeros(0,3),spzeros(Int8,0,0),spzeros(Int8,0,0))

    for i in 1:ncubes
        cubesRow = Lar.skel_merge(cubesRow..., cube...)
        cube[1] = cube[1] + [zeros(8) zeros(8) ones(8)]
    end

    cubesRow = collect(cubesRow)
    cubesPlane = cubesRow
    num = size(cubesRow[1], 1)
    for i in 1:ncubes
        cubesPlane = Lar.skel_merge(cubesPlane..., cubesRow...)
        cubesRow[1] = cubesRow[1] + [zeros(num) ones(num) zeros(num)]
    end

    cubesPlane = collect(cubesPlane)
    cubesCube = cubesPlane
    num = size(cubesPlane[1], 1)
    for i in 1:ncubes
        cubesCube = Lar.skel_merge(cubesCube..., cubesPlane...)
        cubesPlane[1] = cubesPlane[1] + [ones(num) zeros(num) zeros(num)]
    end

    println("Arranging a cube of ", ncubes^3," cubes...")
    rubik = Lar.spatial_arrangement(cubesCube...)
    println("DONE")

    rubik = rubik[1] - 1.5, rubik[2:3]...
    c = cos(pi/6); s = sin(pi/6)
    M1 = [1  0 0; 0 c -s; 0 s c]
    M2 = [c -s 0; s c  0; 0 0 1]
    rot_rubik = rubik[1]*M1*M2, rubik[2:3]...

    println("Arranging two rubik cubes...")
    two_rubiks = Lar.skel_merge(rubik..., rot_rubik...)
    println("DONE")

    arranged_rubiks = Lar.spatial_arrangement(two_rubiks...)
end


"""
Mean mutual information of column a (Dunn et. al. 2008).
Summation is over j=1 to N, j ≠ a. Total is N-1.
"""
function _mean_column(mi::Matrix{T}) where {T}
    (dropdims(sum(mi, dims=1), dims=1) .- diag(mi)) ./ (size(mi, 1)-1)
end

"""
Mean mutual information of column a (Dunn et. al. 2008).
Summation is over j=1 to N, j ≠ a. Total is N-1.

Overall mean mutual information (Dunn et. al. 2008).
2/(N*(N-1)) by the sum of MI where the indices run i=1 to N-1, j=i+1 to N (triu).
"""
function _mean_total(mi::Matrix{T}) where T
    values = matrix2list(mi)
    sum(values) / length(values)
end

"APC (Dunn et. al. 2008)"
function APC!(MI::Matrix{T}) where T
    nrow, ncol = size(MI)
    MI_mean = _mean_total(MI)
    #   if MI_mean ==  0.0
    #     return(fill!(MI, zero(T)))
    #   end
    MI_res_mean = _mean_column(MI)
    @inbounds for j in 1:ncol
        for i in 1:nrow
            if i != j
                MI[i,j] -= ((MI_res_mean[i] * MI_res_mean[j]) /  MI_mean)
            else
                MI[i,j] = NaN
            end
        end
    end
    MI
end

function APC!(MI::PairwiseListMatrix{T,true,Vector{T}}) where T <: AbstractFloat
    ncol = MI.nelements
    MI_col_sum = vec(sum_nodiag(MI, dims=1))
    MI_mean = sum(MI_col_sum) / (length(MI) - ncol)
    MI_col_mean = MI_col_sum ./ (ncol-1)
    k = 0
    list = MI.list
    @inbounds for i in 1:ncol
        for j in i:ncol
            k += 1
            if i != j
                list[k] -= ((MI_col_mean[i] * MI_col_mean[j]) /  MI_mean)
            else
                list[k] = NaN
            end
        end
    end
    MI
end

function APC!(MI::PairwiseListMatrix{T,false,Vector{T}}) where T <: AbstractFloat
    ncol = MI.nelements
    MI_col_sum = vec(sum_nodiag(MI, dims=1))
    MI_mean = sum(MI_col_sum) / (length(MI) - ncol)
    MI_col_mean = MI_col_sum ./ (ncol-1)
    k = 0
    list = MI.list
    @inbounds for i in 1:(ncol-1)
        for j in (i+1):ncol
            k += 1
            list[k] -= ((MI_col_mean[i] * MI_col_mean[j]) /  MI_mean)
        end
    end
    MI.diag[:] .= NaN
    MI
end

function APC!(MI::NamedArray{T,2,PairwiseListMatrix{T,D,Vector{T}},NTuple{2,OrderedDict{String,Int}}}) where {T,D}
    plm = getarray(MI)::PairwiseListMatrix{T,D,Vector{T}}
    APC!(plm)
    MI
end


struct polytope
    A;
    b;
    l;
    u;
    model;
    x;
end

function LP(c,P)

    @objective(P.model, Min, c'* P.x)
    optimize!(P.model)
    return value.(P.x)

end

function randomDoublyStochasticMatrix(n; num_perm = n^2)

    M = zeros(n,n);
    α = rand(num_perm)

    α = α / sum(α);

    for i=1:num_perm
        perm = randperm(n);
        for j=1:n
            M[perm[j],j] += α[i];
        end
    end

    return M;
end

function birkhoffPolytope(n)

    M = zeros(n*n,2*n);
    for i=1:n
        M[(i-1)*n*n + (i-1)*n + 1 : (i-1)*n*n + (i-1)*n + n ] = ones(n,1);
    end
    for i=1:n
        for j=1:n
            M[n*n*n + (i-1)*n*n + (j-1)*n + i] = 1;
        end
    end

    A = sparse(M');
    b = ones(2*n);

    model = Model(with_optimizer(Clp.Optimizer))
    set_silent(model)
    @variable(model, 0 <= x[1:n*n] <= 1)
    @constraint(model, A * x .== b)

    return polytope(A,b,0,1,model,x)

end


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/FEMQuad.jl/blob/master/LICENSE

using FEMQuad, Test
using FEMQuad: get_rule, integrate_1d, integrate_2d, integrate_3d

f(i) = x -> sum(sum(x)^j for j=0:i)

@testset "Gauss-Legendre quadratures in one dimension" begin
    # Gaussian quadrature using N points can provide the exact integral if
    # polynomial degree of f is 2N-1 or less => N = (deg(f)+1)/2

    rules = (:GLSEG1, :GLSEG2, :GLSEG3, :GLSEG4, :GLSEG5)
    r(i::Int) = get_rule(i, rules...)

    @test isapprox(integrate_1d(f(0), r(0)), 2.0)
    @test isapprox(integrate_1d(f(1), r(1)), 2.0)
    @test isapprox(integrate_1d(f(2), r(2)), 8/3)
    @test isapprox(integrate_1d(f(3), r(3)), 8/3)
    @test isapprox(integrate_1d(f(4), r(4)), 46/15)
    @test isapprox(integrate_1d(f(5), r(5)), 46/15)
    @test isapprox(integrate_1d(f(6), r(6)), 352/105)
    @test isapprox(integrate_1d(f(7), r(7)), 352/105)
    @test isapprox(integrate_1d(f(8), r(8)), 1126/315)
    @test isapprox(integrate_1d(f(9), r(9)), 1126/315)
end

@testset "Gauss-Legendre quadratures in quadrilaterals" begin
    rules = (:GLQUAD1, :GLQUAD4, :GLQUAD9, :GLQUAD16, :GLQUAD25)
    r(i::Int) = get_rule(i, rules...)

    @test isapprox(integrate_2d(f(0), r(0)), 4.0)
    @test isapprox(integrate_2d(f(1), r(1)), 4.0)
    @test isapprox(integrate_2d(f(2), r(2)), 20/3)
    @test isapprox(integrate_2d(f(3), r(3)), 20/3)
    @test isapprox(integrate_2d(f(4), r(4)), 164/15)
    @test isapprox(integrate_2d(f(5), r(5)), 164/15)
    @test isapprox(integrate_2d(f(6), r(6)), 2108/105)
    @test isapprox(integrate_2d(f(7), r(7)), 2108/105)
    @test isapprox(integrate_2d(f(8), r(8)), 13492/315)
    @test isapprox(integrate_2d(f(9), r(9)), 13492/315)
end

@testset "Gauss-Legendre quadratures in hexahedrons" begin
    rules = (:GLHEX1, :GLHEX8, :GLHEX27, :GLHEX64, :GLHEX125)
    r(i::Int) = get_rule(i, rules...)

    @test isapprox(integrate_3d(f(0), r(0)), 8.0)
    @test isapprox(integrate_3d(f(1), r(1)), 8.0)
    @test isapprox(integrate_3d(f(2), r(2)), 16.0)
    @test isapprox(integrate_3d(f(3), r(3)), 16.0)
    @test isapprox(integrate_3d(f(4), r(4)), 184/5)
    @test isapprox(integrate_3d(f(5), r(5)), 184/5)
    @test isapprox(integrate_3d(f(6), r(6)), 12064/105)
    @test isapprox(integrate_3d(f(7), r(7)), 12064/105)
    @test isapprox(integrate_3d(f(8), r(8)), 9928/21)
    @test isapprox(integrate_3d(f(9), r(9)), 9928/21)
end


import FrankWolfe
import LinearAlgebra

n = Int(1e6);

xpi = rand(n);
total = sum(xpi);
const xp = xpi ./ total;

const f(x) = norm(x - xp)^2

function grad!(storage, x)
    @. storage = 2 * (x - xp)
end

function cf(x, xp)
    return @. norm(x - xp)^2
end

function cgrad(storage, x, xp)
    @. storage = 2 * (x - xp)
end

lmo_prob = FrankWolfe.ProbabilitySimplexOracle(1);
x0 = FrankWolfe.compute_extreme_point(lmo_prob, zeros(n));

FrankWolfe.benchmarkOracles(f, grad!, lmo_prob, n; k=100, T=Float64)

FrankWolfe.benchmarkOracles(
    x -> cf(x, xp),
    (storage, x) -> cgrad(storage, x, xp),
    lmo_prob,
    n;
    k=100,
    T=Float64,
)


using ApproxFun, MultivariateOrthogonalPolynomials, Plots
 import MultivariateOrthogonalPolynomials: DirichletTriangle, Vec

# Neumann
d = Triangle()
S = DirichletTriangle{1,1,1}(d)
Dx = Derivative(S,[1,0])
Dy = Derivative(S,[0,1])
B₁ = I : S → Legendre(Vec(0,0) .. Vec(0,1))
B₂ = I : S → Legendre(Vec(0,0) .. Vec(1,0))
B₃ = I : S → Legendre(Vec(0,1) .. Vec(1,0))

L = [0 B₁ 0;
 0  0  B₂;
 0  B₃ B₃;
 Dx -eye(S) 0;
  Dy 0 -eye(S);
   0.1eye(S) Dx Dy]

f = Fun((x,y) -> exp(x*cos(y)), d)

u = \(L , [0;0; 0; 0;0; f]; tolerance=1E-5)
plot(u[1])

d = Triangle() , Triangle(Vec(1,1),Vec(1,0),Vec(0,1))
S₁,S₂ = S = DirichletTriangle{1,1,1}.(d)
Dx₁ = Derivative(S₁,[1,0])
Dy₁ = Derivative(S₁,[0,1])
Dx₂ = Derivative(S₂,[1,0])
Dy₂ = Derivative(S₂,[0,1])
B₁₁ = I : S[1] → Legendre(Vec(0,0) .. Vec(0,1))
B₁₂ = I : S[1] → Legendre(Vec(0,0) .. Vec(1,0))
B₁₃ = I : S[1] → Legendre(Vec(0,1) .. Vec(1,0))
B₂₁ = I : S[2] → Legendre(Vec(1,1) .. Vec(0,1))
B₂₂ = I : S[2] → Legendre(Vec(1,1) .. Vec(1,0))
B₂₃ = I : S[2] → Legendre(Vec(0,1) .. Vec(1,0))
I₁ = eye(S₁)
I₂ = eye(S₂)

L = [B₁₁  0    0    0    0    0
     B₁₂  0    0    0    0    0
     0    0    0    B₂₁  0    0
     0    0    0    B₂₂  0    0
     B₁₃  0    0   -B₂₃  0    0
     0    B₁₃  B₁₃  0   -B₂₃ -B₂₃
     Dx₁ -I₁   0    0    0    0
     Dy₁  0   -I₁   0    0    0
     0    0    0    Dx₂ -I₂   0
     0    0    0    Dy₂  0   -I₂
     0    Dx₁  Dy₁  0    0    0
     0    0    0    0    Dx₂  Dy₂]

rs = rangespace(L)
f = vcat(map(r->Fun((x,y) -> real(exp(x+im*y)), r,20), rs[1:4])...)

u = \(L, [f; zeros(8)...]; tolerance=1E-5)

plot(u[1])
 plot!(u[4])

plot(u[4])
@which ApproxFun.plotptsvals(u[4])

ncoefficients(u[4])
@which points(u[4].space,210)
points(u[4].space,ncoefficients(u[4]))
pts=points(JacobiTriangle(0,0,0),210)
fromcanonical(S,pts[1])
K = S
n = 210
map(Vec,map(vec,points(ProductTriangle(K),round(Int,sqrt(n)),round(Int,sqrt(n))))...)
S = JacobiTriangle(0,0,0,d[2])
fromcanonical(S, Vec(0.1,0.2))
u[1](0.1,0.2)-real(exp(0.1+0.2im)) # ~ 4.5E-16
u[4](0.6,0.7)-real(exp(0.6+0.7im)) # ~ 6.7E-16


# numerical range
# This test is based on S&P chap. 43
using Pseudospectra
using Test

@testset "Num. range" begin
    η = 0.015
    A = Pseudospectra.advdiff(100,η)
    opts = Dict{Symbol,Any}(:showfov => true, :fov_npts => 200,
                            :ax => [-60,20,-50,50])

    ps_data = new_matrix(A,opts)
    driver!(ps_data,opts,gs)

    @test isapprox(maximum(real(ps_data.ps_dict[:fov])),-η*π^2,atol=1e-6)
end


@testset "custom nnz (number of non-zeros)" begin

    using Calculus

    L = 3
    N = 2*L

    X1 = [n*l for n=1:N, l=1:L]
    X2 = [n*l for n=1:N, l=1:L]
    β1 = range(-1., stop=1., length=L)
    β2 = ones(L)

    # equation depends on β2 only if β1 is positive, so jacobian will have varying number of elements depending on β1
    y = X1*β1 + X2 * ((β1.>0) .* β2)

    function resid(par)
        return y - (X1*par[1:L] + X2 * ((par[1:L].>0) .* par[L+1:end]))
    end
    lower = -500*ones(L*2)
    upper = 500*ones(L*2)
    guess = -300*ones(L*2)

    # number of non-zero elements in Jacobian for negative/positive guess
    nnz1 = PATHSolver.count_nonzeros(Calculus.jacobian(resid, -ones(L*2), :central))
    nnz2 = PATHSolver.count_nonzeros(Calculus.jacobian(resid, ones(L*2), :central))
    @test nnz2>nnz1

    jac(par) = Calculus.jacobian(resid, par, :central)
    # Calculus jacobian used
    status, zero, f_zero = solveMCP(resid, jac, lower, upper, guess; nnz=length(lower)^2)
    @test status == :Solved

    # ForwardDiff jacobian
    status, zero, f_zero = solveMCP(resid, lower, upper, guess; nnz=length(lower)^2)
    @test status == :Solved

end


using LinearRegression
using Test, DataFrames
using StatsModels

include("test_sweep_operator.jl")
include("test_utilities.jl")
include("test_LinearRegression.jl")
include("test_cooksd.jl")
include("test_lessthanfullrank.jl")
include("test_noint.jl")
include("test_heteroscedasticity.jl")
include("test_kfold.jl")


"""
    ' CIq(x::AbstractVector)'

Calculate median and 95% confidence interval around the median
"""

function CIq(x)
    m = median(x)
    (m - quantile(x,0.025), quantile(x,0.975) - m)
    # (quantile(x,[0.025,0.975])...,)
end

"""
    'wilcoxon(df::AbstractDataFrame,x::Symbol, y::Symbol, by = :MouseID; f = mean)'

x and y are paired vector to compare effect of condtion in y vs baseline in x
collect SignedRankTest analysis in a standardize table to plot
"""

function wilcoxon(x::AbstractVector{<:Real})
    t = SignedRankTest(x)
    dd = DataFrame()
    dd[!,:Median] = [t.median]
    dd[!,:CI] = [(t.median - confint(t)[1], confint(t)[2] - t.median)]
    dd[!,:P] = [pvalue(t)]
    dd[!,:Vals] = [t.vals for i in 1:1]
    dd
end

wilcoxon(x::AbstractVector{<:Real}, y::AbstractVector{<:Real}) = wilcoxon(y-x)

function wilcoxon(df::AbstractDataFrame,x::Symbol, by = :MouseID; f = mean)
    df1 = combine(x => f => :Mean,groupby(df,by))
    check = SwitchingAnalysis.complete_vals(df1)
    if any(x -> !x, check)
        println( "got NaN values and dropped them")
        dd = dropnan(df1)
    else
        dd = df1
    end
    wilcoxon(dd[:,:Mean])
end

function wilcoxon(df::AbstractDataFrame,x::Symbol, y::Symbol, by = :MouseID; f = mean)
    df1 = combine([x,y] => (a,b) -> (Mean_x = f(a), Mean_y = f(b)), groupby(df,by))
    check = SwitchingAnalysis.complete_vals(df1)
    if any(x -> !x, check)
        println( "got NaN values and dropped them")
        dd = dropnan(df1)
    else
        dd = df1
    end
    wilcoxon(dd[:,:Mean_x], dd[:,:Mean_y])
end


@doc raw"""
```
t = find_threshold(MinimumError(), histogram, edges)
```

Under the assumption that the histogram is a mixture of two Gaussian
distributions the threshold is chosen such that the expected misclassification
error rate is minimised.

# Output

Returns a real number `t` in `edges`. The `edges` parameter represents an
`AbstractRange` which specifies the intervals associated with the histogram bins.

# Details

Let ``f_i`` ``(i=1 \ldots I)`` denote the number of observations in the
``i``th bin of the histogram. Then the probability that an observation
belongs to the ``i``th bin is given by  ``p_i = \frac{f_i}{N}`` (``i = 1,
\ldots, I``), where ``N = \sum_{i=1}^{I}f_i``.

The minimum error thresholding method assumes that one can find a threshold
``T`` which partitions the data into two categories,  ``C_0`` and ``C_1``, such that
the data can be modelled by a mixture of two Gaussian distribution. Let
```math
P_0(T) = \sum_{i = 1}^T p_i \quad \text{and} \quad P_1(T) = \sum_{i = T+1}^I p_i
```
denote the cumulative probabilities,
```math
\mu_0(T) = \sum_{i = 1}^T i \frac{p_i}{P_0(T)} \quad \text{and} \quad \mu_1(T) = \sum_{i = T+1}^I i \frac{p_i}{P_1(T)}
```
denote the means, and
```math
\sigma_0^2(T) = \sum_{i = 1}^T (i-\mu_0(T))^2 \frac{p_i}{P_0(T)} \quad \text{and} \quad \sigma_1^2(T) = \sum_{i = T+1}^I (i-\mu_1(T))^2 \frac{p_i}{P_1(T)}
```
denote the variances of categories ``C_0`` and ``C_1``, respectively.

Kittler and Illingworth proposed to use the minimum error criterion function
```math
J(T) = 1 + 2 \left[ P_0(T) \ln \sigma_0(T) + P_1(T) \ln \sigma_1(T) \right] - 2 \left[P_0(T) \ln P_0(T) + P_1(T) \ln P_1(T) \right]
```
to assess the discreprancy between the mixture of Gaussians implied by a particular threshold ``T``,
and the piecewise-constant probability density function represented by the histogram.
The discrete value ``T`` which minimizes the function ``J(T)`` produces
the sought-after threshold value (i.e. the bin which determines the threshold).

# Arguments

The function arguments are described in more detail below.

##  `histogram`

An `AbstractArray` storing the frequency distribution.

##  `edges`

An `AbstractRange` specifying how the intervals for the frequency distribution
are divided.

# Example

Compute the threshold for the "cameraman" image in the `TestImages` package.

```julia
using TestImages, ImageContrastAdjustment, HistogramThresholding

img = testimage("cameraman")
edges, counts = build_histogram(img,256)
#=
  The `counts` array stores at index 0 the frequencies that were below the
  first bin edge. Since we are seeking a threshold over the interval
  partitioned by `edges` we need to discard the first bin in `counts`
  so that the dimensions of `edges` and `counts` match.
=#
t = find_threshold(MinimumError(), counts[1:end], edges)
```

# References

1. J. Kittler and J. Illingworth, “Minimum error thresholding,” Pattern Recognition, vol. 19, no. 1, pp. 41–47, Jan. 1986. [doi:10.1016/0031-3203(86)90030-0](https://doi.org/10.1016/0031-3203%2886%2990030-0)
2. Q.-Z. Ye and P.-E. Danielsson, “On minimum error thresholding and its implementations,” Pattern Recognition Letters, vol. 7, no. 4, pp. 201–206, Apr. 1988. [doi:10.1016/0167-8655(88)90103-1](https://doi.org/10.1016/0167-8655%2888%2990103-1)
"""
function find_threshold(algorithm::MinimumError, histogram::AbstractArray, edges::AbstractRange)
    frequencies = cumsum(histogram)
    X = cumsum(edges .* histogram)
    X² = cumsum(edges.^2 .* histogram)
    𝔼X₁, 𝔼X₁², 𝔼X₂, 𝔼X₂² = compute_expectations(frequencies, X, X²)
    minJ = typemax(Float64)
    index = firstindex(frequencies)
    for t =  firstindex(frequencies):lastindex(frequencies)-1
        P₁ = max(frequencies[t] / frequencies[end], eps())
        P₂ = 1 - P₁
        # Var(X) = E[X²] - E[X]² = E[X²] - μ²
        σ₁ = sqrt(max(𝔼X₁²[t] - 𝔼X₁[t]^2, eps()))
        σ₂ = sqrt(max(𝔼X₂²[t] - 𝔼X₂[t]^2, eps()))
        J = 1 + 2*(P₁*log(σ₁) + P₂*log(σ₂)) - 2*(P₁*log(P₁)+ P₂*log(P₂))
        if (J < minJ && isfinite(J))
            minJ = J
            index = t
        end
    end
    edges[index]
end

#=
  If the histogram consists of integers then we risk integer overflow when we
  take the cumulative sums. Hence, we explicitlty convert the histogram to
  Float.
=#
function find_threshold(algorithm::MinimumError, histogram::AbstractArray{T}, edges::AbstractRange) where T <: Int
    find_threshold(algorithm, convert.(Float64, histogram), edges)
end

function compute_expectations(frequencies, X, X²)
    dimensions = axes(frequencies)
    𝔼X₁ = zeros(dimensions)
    𝔼X₁² = zeros(dimensions)
    𝔼X₂ = zeros(dimensions)
    𝔼X₂² = zeros(dimensions)
    for i = firstindex(X):lastindex(X)-1
        𝔼X₁[i] = X[i] / frequencies[i]
        𝔼X₁²[i] = X²[i] / frequencies[i]
        𝔼X₂[i] = (X[end] - X[i]) /  (frequencies[end] - frequencies[i])
        𝔼X₂²[i] = (X²[end] - X²[i]) /  (frequencies[end] - frequencies[i])
    end
    𝔼X₁, 𝔼X₁², 𝔼X₂, 𝔼X₂²
end


"""
---
 Add a white additive circular gaussian noise to input signal
Added noise is real if input signal is real and computedlex is input signal is complex
Noise level is controled by the second input parameter which is the signal to noise ratio (SNR)
Signal power powSig is computed based in the input sequence power (average power in time domain)
although a third parameter (theoretical power of input signal is given). In that case, snr is computed
based on the value powSig
output parameters are the signal with noise, and the noise samples
See the bang methods for non-buffer alloc.
# --- Syntax
[y,n]   = addNoise(x,snr,powSig);
# --- Input parameters
- x= Input signal [Array{Real{Float64}}, Array{Complex{Float64}}] of size N
- snr= Desired signal to noise ratio [Float64]
- powSig= Power of input signal. If not given, power is evaluated based on input signal x
# --- Output parameters
- y= Signal with noise [Array{Real{Float64}}, Array{Complex{Float6464}}] of size N
- n= noise samples [Array{Real{Float64}}, Array{Complex{Float64}}] of size N
# ---
# v 1.0 - Robin Gerzaguet.
"""
function addNoise(x::Array{T},snr::Any,powSig=0) where T
	# --- If powSig is equal to 0, power is computed with x input
	# Expectation is taken with respect to time
	if powSig == 0
		powSig  = sum(abs.(x).^2) / length(x);
	end
	# --- Evaluation of noise power
	powNoise  = sqrt(powSig) *  10^(-snr/20);
	# --- Create a complex random sequence as additive noise
	n		  = randn(T,length(x))* powNoise;
	# --- Create output signal
	y		  = x + n;
	return y,n
end

function addNoise!(y::Array{T},x::Array{T},snr::Any,powSig=0.0) where T
	# --- If powSig is equal to 0, power is computed with x input
	# Expectation is taken with respect to time
	if powSig == 0.0
		powSig  = sum(abs2.(x)) / length(x);
	end
	# --- Evaluation of noise power
	# Additional /sqrt(2) to divide power between I and Q paths
	# No sqrt(2)--> Complex randn is unitary variance in complex plan
	powNoise  = sqrt(powSig) * 10^(-snr/20);
	# --- Create a complex random sequence as additive noise
	y  .= x .+ randn!(y) .* powNoise;
end


"""
    Polylogarithms

Module containing functions to calculate the polylogarithm and associated functions

"""
module Polylogarithms
import SpecialFunctions

# not using MPFR for the moment
# using Base.MPFR: ROUNDING_MODE, big_ln2

export polylog, bernoulli, harmonic, stieltjes, dirichlet_beta
export Diagnostics
export parse

# @compat ComplexOrReal{T} = Union{T,Complex{T}}
# s::ComplexOrReal{Float64}
ComplexOrReal{T} = Union{T,Complex{T}}

# Some extra parsing routines for reading Mathematics output, but also, complex numbers
include("utilities.jl")

# Constants
include("constants.jl")

# Series
include("stieltjes.jl")
include("bernoulli_n.jl")
include("harmonic.jl")
# include("gamma_derivatives.jl") # this just generates a table that we include into the code

# Functions
include("beta.jl")
include("bernoulli_poly.jl")
include("polylog.jl")

end


@doc raw"""
Arnold-Beltrami-Childress (ABC) field in (x,y,z) coordinates with covariant components of the vector
potential given by
```math
A (x,y,z) = \big( a \, \sin(z) + c \, \cos(y) , \, b \, \sin(x) + a \, \cos(z) , \, c \, \sin(y) + b \, \cos(x) \big)^T
```
resulting in the magnetic field ``B(x,y,z) = A(x,y,z)``.

Parameters: `a`, `b`, `c`
"""
module ABC

    using RecipesBase

    import ..ElectromagneticFields
    import ..ElectromagneticFields: CartesianEquilibrium, code
    import ..ElectromagneticFields: A₁, A₂, A₃
    import ..AnalyticCartesianField: X, Y, Z

    export ABCEquilibrium

    const DEFAULT_A = 1
    const DEFAULT_B = 1
    const DEFAULT_C = 1

    struct ABCEquilibrium{T <: Number} <: CartesianEquilibrium
        name::String
        a₀::T
        b₀::T
        c₀::T

        ABCEquilibrium{T}(a::T, b::T, c::T) where T <: Number = new("ABCEquilibrium", a, b, c)
    end

    ABCEquilibrium(a::T=DEFAULT_A, b::T=DEFAULT_B, c::T=DEFAULT_C) where T <: Number = ABCEquilibrium{T}(a, b, c)

    function init(a=DEFAULT_A, b=DEFAULT_B, c=DEFAULT_C)
        ABCEquilibrium(a, b, c)
    end

    macro code(a=DEFAULT_A, b=DEFAULT_B, c=DEFAULT_C)
        code(init(a, b, c); escape=true)
    end

    function Base.show(io::IO, equ::ABCEquilibrium)
        print(io, "ABC Equilibrium with\n")
        print(io, "  A = ", equ.a₀, "\n")
        print(io, "  B = ", equ.b₀, "\n")
        print(io, "  C = ", equ.c₀)
    end

    ElectromagneticFields.A₁(x::AbstractVector, equ::ABCEquilibrium) = equ.a₀ * sin(x[3]) + equ.c₀ * cos(x[2])
    ElectromagneticFields.A₂(x::AbstractVector, equ::ABCEquilibrium) = equ.b₀ * sin(x[1]) + equ.a₀ * cos(x[3])
    ElectromagneticFields.A₃(x::AbstractVector, equ::ABCEquilibrium) = equ.c₀ * sin(x[2]) + equ.b₀ * cos(x[1])
    B(x::AbstractVector, equ::ABCEquilibrium) = A₁(x,equ)^2 + A₂(x,equ)^2 + A₃(x,equ)^2

    ElectromagneticFields.get_functions(::ABCEquilibrium) = (X=X, Y=Y, Z=Z)

    @recipe function f(equ::ABCEquilibrium; nx=99, ni=div(nx,2)+1, nl=12, size=(400,1200))
        xmin = 0
        xmax = 2π
        grid = LinRange(xmin, xmax, nx)
        Bfield = [B([grid[i], grid[j], grid[k]], equ) for i in eachindex(grid), j in eachindex(grid), k in eachindex(grid)]

        seriestype   := :contour
        aspect_ratio := :equal
        layout := (3, 1)
        size   := size
        xlims  := (xmin,xmax)
        ylims  := (xmin,xmax)
        levels := nl
        legend := :none

        @series begin
            subplot := 1
            xguide := "x"
            yguide := "y"
            title  := "|B(x,y,π)|"
            (grid, grid, Bfield[:,:,ni])
        end

        @series begin
            subplot := 2
            xguide := "x"
            yguide := "z"
            title  := "|B(x,π,z)|"
            (grid, grid, Bfield[:,ni,:])
        end

        @series begin
            subplot := 3
            xguide := "y"
            yguide := "z"
            title  := "|B(π,y,z)|"
            (grid, grid, Bfield[ni,:,:])
        end
    end

end


using FastTransforms
using FastSphericalHarmonics
using ForwardDiff
using LinearAlgebra
using SpecialFunctions
using StaticArrays
using Test

bitsign(b::Bool) = b ? -1 : 1
bitsign(n::Integer) = bitsign(isodd(n))

function unit(val::T, ind::CartesianIndex{D}, size::NTuple{D,Int}) where {T,D}
    return setindex!(zeros(T, size), val, ind)
end
function unit(::Type{T}, ind::CartesianIndex{D},
              size::NTuple{D,Int}) where {T,D}
    return unit(one(T), ind, size)
end
function unit(ind::CartesianIndex{D}, size::NTuple{D,Int}) where {D}
    return unit(true, ind, size)
end

chop(x) = abs2(x) < 100eps(x) ? zero(x) : x
chop(x::Complex) = Complex(chop(real(x)), chop(imag(x)))

chop2(x) = chop.(x)

# [Generalized binomial coefficient](https://en.wikipedia.org/wiki/Binomial_coefficient#Generalization_and_connection_to_the_binomial_series)
function Base.binomial(α::Number, k::Integer)
    k == 0 && return one(α)
    return prod((α - i) / (k - i) for i in 0:(k - 1))
end

# [Jacobi
# Polynomials](https://en.wikipedia.org/wiki/Jacobi_polynomials)
function JacobiP(α, β, n, z)
    return gamma(α + n + 1) / (factorial(n) * gamma(α + β + n + 1)) *
           sum(binomial(n, m) * gamma(α + β + n + m + 1) / gamma(α + m + 1) *
               ((z - 1) / 2)^m for m in 0:n)
end

# [Associated Legendre
# Polynomials](https://en.wikipedia.org/wiki/Associated_Legendre_polynomials)
function LegendreP(l, m, x)
    return bitsign(m) *
           2^l *
           sqrt(1 - x^2)^m *
           sum(factorial(k) / factorial(k - m) *
               x^(k - m) *
               binomial(l, k) *
               binomial((l + k - 1) / 2, l) for k in m:l)
end

# [Spherical
# harmonics](https://mikaelslevinsky.github.io/FastTransforms/transforms.html)
# (section "sph2fourier")
function Ylm(l, m, θ, ϕ)
    return bitsign(abs(m)) *
           sqrt((l + 1 / 2) * factorial(l - abs(m)) / factorial(l + abs(m))) *
           LegendreP(l, abs(m), cos(θ)) *
           sqrt((2 - (m == 0)) / 2π) *
           (m ≥ 0 ? cos(abs(m) * ϕ) : sin(abs(m) * ϕ))
end

# [Spin-weighted spherical
# harmonics](https://mikaelslevinsky.github.io/FastTransforms/transforms.html)
# (section "spinsph2fourier")
function sYlm(s, l, m, θ, ϕ)
    l0 = max(abs(m), abs(s))
    l1 = min(abs(m), abs(s))
    return cis(m * ϕ) / sqrt(2π) *
           sqrt((l + 1 / 2) * factorial(l + l0) * factorial(l - l0) /
                (factorial(l + l1) * factorial(l - l1))) *
           sin(θ / 2)^abs(m + s) *
           cos(θ / 2)^abs(m - s) *
           JacobiP(abs(m + s), abs(m - s), l - l0, cos(θ))
end
sYlm(::Type{<:Complex}, s, l, m, θ, ϕ) = sYlm(s, l, m, θ, ϕ)
function sYlm(::Type{<:Real}, s, l, m, θ, ϕ)
    @assert s == 0
    if m == 0
        return real(sYlm(s, l, abs(m), θ, ϕ))
    elseif m > 0
        return sqrt(2) * real(sYlm(s, l, abs(m), θ, ϕ))
    else
        return sqrt(2) * imag(sYlm(s, l, abs(m), θ, ϕ))
    end
end

c2a(c) = SVector(real(c), imag(c))
a2c(a) = Complex(a[1], a[2])

∂θYlm(l, m, θ, ϕ) = a2c(ForwardDiff.derivative(θ -> c2a(Ylm(l, m, θ, ϕ)), θ))
∂ϕYlm(l, m, θ, ϕ) = a2c(ForwardDiff.derivative(ϕ -> c2a(Ylm(l, m, θ, ϕ)), ϕ))
function ∂θsYlm(s, l, m, θ, ϕ)
    return a2c(ForwardDiff.derivative(θ -> c2a(sYlm(s, l, m, θ, ϕ)), θ))
end
function ∂ϕsYlm(s, l, m, θ, ϕ)
    return a2c(ForwardDiff.derivative(ϕ -> c2a(sYlm(s, l, m, θ, ϕ)), ϕ))
end
function ∂θsYlm(::Type{T}, s, l, m, θ, ϕ) where {T}
    return a2c(ForwardDiff.derivative(θ -> c2a(sYlm(T, s, l, m, θ, ϕ)), θ))
end
function ∂ϕsYlm(::Type{T}, s, l, m, θ, ϕ) where {T}
    return a2c(ForwardDiff.derivative(ϕ -> c2a(sYlm(T, s, l, m, θ, ϕ)), ϕ))
end

ðYlm(l, m, θ, ϕ) = -(∂θYlm(l, m, θ, ϕ) + im / sin(θ) * ∂ϕYlm(l, m, θ, ϕ))
ð̄Ylm(l, m, θ, ϕ) = -(∂θYlm(l, m, θ, ϕ) - im / sin(θ) * ∂ϕYlm(l, m, θ, ϕ))
function ðsYlm(s, l, m, θ, ϕ)
    return (-(∂θsYlm(s, l, m, θ, ϕ) + im / sin(θ) * ∂ϕsYlm(s, l, m, θ, ϕ)) +
            s * cot(θ) * sYlm(s, l, m, θ, ϕ))
end
function ð̄sYlm(s, l, m, θ, ϕ)
    return (-(∂θsYlm(s, l, m, θ, ϕ) - im / sin(θ) * ∂ϕsYlm(s, l, m, θ, ϕ)) -
            s * cot(θ) * sYlm(s, l, m, θ, ϕ))
end
function ðsYlm(::Type{T}, s, l, m, θ, ϕ) where {T}
    return (-(∂θsYlm(T, s, l, m, θ, ϕ) + im / sin(θ) * ∂ϕsYlm(T, s, l, m, θ, ϕ)) +
            s * cot(θ) * sYlm(T, s, l, m, θ, ϕ))
end
function ð̄sYlm(::Type{T}, s, l, m, θ, ϕ) where {T}
    return (-(∂θsYlm(T, s, l, m, θ, ϕ) - im / sin(θ) * ∂ϕsYlm(T, s, l, m, θ, ϕ)) -
            s * cot(θ) * sYlm(T, s, l, m, θ, ϕ))
end

# Real spherical harmonics
Y_0_0(θ, ϕ) = sqrt(1 / 4π)

Y_1_0(θ, ϕ) = sqrt(3 / 4π) * cos(θ)
Y_1p1(θ, ϕ) = sqrt(3 / 4π) * sin(θ) * cos(ϕ)
Y_1m1(θ, ϕ) = sqrt(3 / 4π) * sin(θ) * sin(ϕ)

# Complex spin-weighted spherical harmonics

# sYlm[s_, l_, m_, q_, f_] =
#  Module[{l0 = Max[Abs[m], Abs[s]], l1 = Min[Abs[m], Abs[s]]},
#      (Exp[I*m*f]/Sqrt[2*Pi])*
#    Sqrt[(l + 1/2)*(l + l0)!*((l - l0)!/((l + l1)!*(l - l1)!))]*
#        Sin[q/2]^Abs[m + s]*Cos[q/2]^Abs[m - s]*
#    JacobiP[l - l0, Abs[m + s], Abs[m - s],
#          Cos[q]]]

Y_s0_0_0(θ, ϕ) = 1 / (2 * sqrt(π))

Y_s0_1_0(θ, ϕ) = 1 / 2 * sqrt(3 / π) * cos(θ)
Y_s0_1p1(θ, ϕ) = cis(ϕ) * sqrt(3 / (2 * π)) * cos(θ / 2) * sin(θ / 2)
Y_s0_1p1(θ, ϕ) = cis(-f) * sqrt(3 / (2 * π)) * cos(θ / 2) * sin(θ / 2)

Y_s1_1_0(θ, ϕ) = sqrt(3 / (2 * π)) * cos(θ / 2) * sin(θ / 2)
Y_s1_1p1(θ, ϕ) = 1 / 2 * cis(f) * sqrt(3 / π) * sin(θ / 2)^2
Y_s1_1m1(θ, ϕ) = 1 / 2 * cis(-f) * sqrt(3 / π) * cos(θ / 2)^2

Y_s1_2_0(θ, ϕ) = sqrt(15 / (2 * π)) * cos(θ / 2) * cos(θ) * sin(θ / 2)
Y_s1_2p1(θ, ϕ) = 1 / 4 * cis(f) * sqrt(5 / π) * (2 + 4 * cos(θ)) * sin(θ / 2)^2
Y_s1_2p2(θ, ϕ) = cis(2 * f) * sqrt(5 / π) * cos(θ / 2) * sin(θ / 2)^3

# # Real gradient spherical harmonics
# # [∂θ, 1/sin(θ) ∂ϕ]
# grad_Y_1_0_θ(θ, ϕ) = sqrt(3 / 4π) * sin(θ)
# grad_Y_1_0_ϕ(θ, ϕ) = 0.0
# grad_Y_1p1_θ(θ, ϕ) = sqrt(3 / 4π) * cos(θ) * cos(ϕ)
# grad_Y_1p1_ϕ(θ, ϕ) = sqrt(3 / 4π) * cos(ϕ)
# grad_Y_1m1_θ(θ, ϕ) = sqrt(3 / 4π) * cos(θ) * sin(ϕ)
# grad_Y_1m1_ϕ(θ, ϕ) = sqrt(3 / 4π) * sin(ϕ)
#
# grad_Y_2_0_θ(θ, ϕ) = sqrt(45 / 4π) * cos(θ) * sin(θ)
# grad_Y_2_0_ϕ(θ, ϕ) = 0.0
# grad_Y_2p1_θ(θ, ϕ) = sqrt(15 / 4π) * cos(2θ) * cos(ϕ)
# grad_Y_2p1_ϕ(θ, ϕ) = sqrt(15 / 4π) * cos(θ) * cos(ϕ)
#
# grad_Y_3_0_θ(θ, ϕ) = sqrt(63 / 256π) * (5 * sin(3θ) + sin(θ))
# grad_Y_3_0_ϕ(θ, ϕ) = 0.0
# grad_Y_3p1_θ(θ, ϕ) = sqrt(42 / 256π) * (15 * cos(2θ) - 7) * cos(θ) * cos(ϕ)
# grad_Y_3p1_ϕ(θ, ϕ) = sqrt(42 / 256π) * (5 * cos(2θ) + 3) * cos(ϕ)
#
# # Real curl spherical harmonics
# # [1/sin(θ) ∂ϕ, -∂θ]
# curl_Y_1_0_θ(θ, ϕ) = grad_Y_1_0_ϕ(θ, ϕ)
# curl_Y_1_0_ϕ(θ, ϕ) = -grad_Y_1_0_θ(θ, ϕ)
# curl_Y_1p1_θ(θ, ϕ) = grad_Y_1p1_ϕ(θ, ϕ)
# curl_Y_1p1_ϕ(θ, ϕ) = -grad_Y_1p1_θ(θ, ϕ)
# curl_Y_1m1_θ(θ, ϕ) = grad_Y_1m1_ϕ(θ, ϕ)
# curl_Y_1m1_ϕ(θ, ϕ) = -grad_Y_1m1_θ(θ, ϕ)

# Integrate over the sphere
function integrate(u::Array{T1,2}, v::Array{T2,2}) where {T1,T2}
    @assert size(u) == size(v)
    N, M = size(u)
    @assert M > 0 && N > 0
    @assert M == 2 * N - 1
    Θ, Φ = sph_points(N)
    s = zero(conj(one(T1)) * one(T2) * one(eltype(Θ)))
    for j in 1:M, i in 1:N
        s += conj(u[i, j]) * v[i, j] * sin(Θ[i])
    end
    return 2π^2 / (N * M) * s
end

function integrate(uθ::Array{T1,2}, uϕ::Array{T1,2}, vθ::Array{T2,2},
                   vϕ::Array{T2,2}) where {T1,T2}
    @assert size(uθ) == size(uϕ) == size(vθ) == size(vϕ)
    N, M = size(uθ)
    @assert M > 0 && N > 0
    @assert M == 2 * N - 1
    Θ, Φ = sph_points(N)
    s = zero(conj(one(T1)) * one(T2) * one(eltype(Θ)))
    for j in 1:M, i in 1:N
        s += (conj(uθ[i, j]) * vθ[i, j] + conj(uϕ[i, j]) * vϕ[i, j]) * sin(Θ[i])
    end
    return 2π^2 / (N * M) * s
end

################################################################################

include("test_fasttransforms.jl")
include("test_points.jl")
include("test_scalar.jl")
# include("test_vector.jl")
include("test_spin.jl")


#########################################################
# Elementary action/properties of permutation actions

@inline @inbounds function Base.:^(n::Integer, p::Perm)
    if 1 <= n <= length(p.d)
        return oftype(n, p.d[n])
    end
    return n
end

@inline Base.:^(v::Tuple, p::Perm) = ntuple(i->v[i^p], length(v))
@inline function Base.:^(v::AbstractVector, p::Perm)
    res = similar(v);
    @inbounds for i in eachindex(v)
        res[i] = v[i^p]
    end
    return res
end

@inline fixes(p::GroupElem, pt, op=^) = op(pt, p) == pt
@inline fixes(p::Perm, v::AbstractVector, op=^) =
    all( i-> v[i] == v[op(i, p)], eachindex(v))

@inline Base.isone(p::Perm) = all(i->i^p == i, 1:degree(p))

fixedpoints(p::Perm, range=1:degree(p), op=^) = [i for i in range if fixes(p, i, op)]
nfixedpoints(p::Perm, range=1:degree(p), op=^) = count(i->fixes(p, i, op), range)

for (fname, findname) in [(:firstmoved, :findfirst), (:lastmoved, :findlast)]
    @eval begin
        function $fname(p::Generic.Perm{I}, op=^) where I
            k = $findname(i -> i != p.d[i], eachindex(p.d))
            k === nothing && return zero(I)
            return I(k)
        end
    end
end

#########################################################
# Misc functions that should go to AbstractAlgebra

import Base: one, conj
import AbstractAlgebra: degree

AbstractAlgebra.degree(p::Perm{I}) where I = I(length(p.d))

function Generic.emb(p::Generic.Perm{I}, n) where I
    return Generic.emb!(Perm(I(n)), p, 1:degree(p))
end

"""
    conj!(out::Perm, h::Perm, g::Perm)
Computes the conjugation action of `g` on `h` and stores the result in `out`.

The action is understood to be `h → g^-1*h*g`. `out` will be unaliased, if necessary.
"""
function Base.conj!(out::Perm, h::Perm, g::Perm)
    if out === h
        out = deepcopy(out)
    end
    @inbounds for i in 1:degree(g)
        out[g[i]] = g[h[i]]
    end
    return out
end

Base.conj(h::GroupElem, g::GroupElem) = conj!(h, h, g)
Base.:(^)(h::Perm, g::Perm) = conj(h,g)

AbstractAlgebra.degree(S::Generic.SymmetricGroup) = S.n
function AbstractAlgebra.gens(G::Generic.SymmetricGroup{I}) where I
    a = one(G)
    a.d[1], a.d[2] = 2, 1
    b = Perm(circshift(I(1):degree(G), -1))
    return [a, b]
end


const NGSIM_TIMESTEP = 0.1 # [sec]
const SMOOTHING_WIDTH_POS = 0.5 # [s]

include(joinpath(@__DIR__, "trajectory_smoothing.jl"))

# from "Estimating Acceleration and Lane-Changing
#       Dynamics Based on NGSIM Trajectory Data"
function symmetric_exponential_moving_average(
    arr :: Vector{Float64},
    T   :: Float64; # smoothing width [s]
    dt  :: Float64 = 0.1 # sampling period [s]
    )

    Δ = T / dt

    N = length(arr)
    retval = Array{Float64}(undef, N)

    for i = 1 : N

        Z = 0.0
        x = 0.0

        D = min(round(Int, 3Δ), i-1)
        if i+D > N
            D = N-i
        end

        for k in (i-D):(i+D)
            e = exp(-abs(i-k)/Δ)
            Z += e
            x += arr[k] * e
        end

        retval[i] = x / Z
    end

    retval
end

###############

mutable struct FilterTrajectoryResult
    carid::Int
    x_arr::Vector{Float64}
    y_arr::Vector{Float64}
    θ_arr::Vector{Float64}
    v_arr::Vector{Float64}

    function FilterTrajectoryResult(trajdata::NGSIMTrajdata, carid::Int)
        dfstart = trajdata.car2start[carid]
        N = trajdata.df[dfstart, :n_frames_in_dataset]

        # these are our observations
        x_arr = fill(NaN, N)
        y_arr = fill(NaN, N)
        θ_arr = fill(NaN, N)
        v_arr = fill(NaN, N)

        for i in 1 : N
            x_arr[i] = trajdata.df[dfstart + i - 1, :global_x]
            y_arr[i] = trajdata.df[dfstart + i - 1, :global_y]
        end

        # choose an initial belief
        θ_arr[1] = atan(y_arr[5] - y_arr[1], x_arr[5] - x_arr[1])
        v_arr[1] = trajdata.df[dfstart, :speed] #hypot(ftr.y_arr[lookahead] - y₀, ftr.x_arr[lookahead] - x₀)/ν.Δt
        if v_arr[1] < 1.0 # small speed
            # estimate with greater lookahead
            θ_arr[1] = atan(y_arr[end] - y_arr[1], x_arr[end] - x_arr[1])
        end

        new(carid, x_arr, y_arr, θ_arr, v_arr)
    end
end
Base.length(ftr::FilterTrajectoryResult) = length(ftr.x_arr)
function filter_trajectory!(ftr::FilterTrajectoryResult, ν::VehicleSystem = VehicleSystem())

    μ = [ftr.x_arr[1], ftr.y_arr[1], ftr.θ_arr[1], ftr.v_arr[1]]

    σ = 1e-1
    Σ = Matrix(Diagonal([σ*0.01, σ*0.01, σ*0.1, σ]))

    # assume control is centered
    u = [0.0, 0.0]
    z = [NaN, NaN]

    for i in 2 : length(ftr)

        # pull observation
        z[1] = ftr.x_arr[i]
        z[2] = ftr.y_arr[i]

        # apply extended Kalman filter
        μ, Σ = EKF(ν, μ, Σ, u, z)

        # store results
        ftr.x_arr[i] = μ[1]
        ftr.y_arr[i] = μ[2]
        ftr.θ_arr[i] = μ[3]
        ftr.v_arr[i] = μ[4]
    end

    ftr
end
function Base.copy!(trajdata::NGSIMTrajdata, ftr::FilterTrajectoryResult)

    dfstart = trajdata.car2start[ftr.carid]
    N = trajdata.df[dfstart, :n_frames_in_dataset]

    # copy results back to trajdata
    for i in 1 : N
        trajdata.df[dfstart + i - 1, :global_x] = ftr.x_arr[i]
        trajdata.df[dfstart + i - 1, :global_y] = ftr.y_arr[i]
        # trajdata.df[dfstart + i - 1, :speed]   = ftr.v_arr[i]
        if i > 1
            trajdata.df[dfstart + i - 1, :speed]   = hypot(ftr.x_arr[i] - ftr.x_arr[i-1], ftr.y_arr[i] - ftr.y_arr[i-1]) / NGSIM_TIMESTEP
        else
            trajdata.df[dfstart + i - 1, :speed]   = hypot(ftr.x_arr[i+1] - ftr.x_arr[i], ftr.y_arr[i+1] - ftr.y_arr[i]) / NGSIM_TIMESTEP
        end
        trajdata.df[dfstart + i - 1, :global_heading] = ftr.θ_arr[i]
    end

    trajdata
end

function filter_trajectory!(trajdata::NGSIMTrajdata, carid::Int)
    #=
    Filters the given vehicle's trajectory using an Extended Kalman Filter
    =#

    ftr = FilterTrajectoryResult(trajdata, carid)

    # run pre-smoothing
    ftr.x_arr = symmetric_exponential_moving_average(ftr.x_arr, SMOOTHING_WIDTH_POS)
    ftr.y_arr = symmetric_exponential_moving_average(ftr.y_arr, SMOOTHING_WIDTH_POS)

    filter_trajectory!(ftr)

    copy!(trajdata, ftr)
    trajdata
end
function load_ngsim_trajdata(filepath::String; autofilter::Bool=true)

    print("loading from file: ");
    tdraw = NGSIMTrajdata(filepath)

    if autofilter && splitext(filepath)[2] == ".txt" # txt is original
        print("filtering:         ");
        for carid in carid_set(tdraw)
            filter_trajectory!(tdraw, carid)
        end
    end

    tdraw
end

function Base.convert(::Type{Trajdata}, tdraw::NGSIMTrajdata, roadway::Roadway)

    df = tdraw.df

    vehdefs = Dict{Int, VehicleDef}()
    states = Array{RecordState{VehicleState, Int}}(undef, nrow(df))
    frames = Array{RecordFrame}(undef, nframes(tdraw))

    for (id, dfind) in tdraw.car2start
        vehdefs[id] = VehicleDef(df[dfind, :class], df[dfind, :length]*METERS_PER_FOOT, df[dfind, :width]*METERS_PER_FOOT)
    end

    state_ind = 0
    for frame in 1 : nframes(tdraw)

        frame_lo = state_ind+1

        for id in carsinframe(tdraw, frame)
            dfind = car_df_index(tdraw, id, frame)

            posG = VecSE2(df[dfind, :global_x]*METERS_PER_FOOT, df[dfind, :global_y]*METERS_PER_FOOT, df[dfind, :global_heading])
            speed = df[dfind, :speed]*METERS_PER_FOOT

            states[state_ind += 1] = RecordState(VehicleState(posG, roadway, speed), id)
        end

        frame_hi = state_ind
        frames[frame] = RecordFrame(frame_lo, frame_hi)
    end

    Trajdata(NGSIM_TIMESTEP, frames, states, vehdefs)
end

get_corresponding_roadway(filename::String) = occursin("i101", filename) ? ROADWAY_101 : ROADWAY_80

function convert_raw_ngsim_to_trajdatas()
    for filepath in NGSIM_TRAJDATA_PATHS
        filename = splitdir(filepath)[2]
        println("converting ", filename);

        roadway = get_corresponding_roadway(filename)
        tdraw = NGSIM.load_ngsim_trajdata(filepath)
        trajdata = convert(Trajdata, tdraw, roadway)

        outpath = joinpath(@__DIR__, "../data/trajdata_"*filename)
        open(io->write(io, MIME"text/plain"(), trajdata), outpath, "w")
    end
end

const TRAJDATA_PATHS = [
                        joinpath(@__DIR__, "../data/trajdata_i101_trajectories-0750am-0805am.txt"),
                        joinpath(@__DIR__, "../data/trajdata_i101_trajectories-0805am-0820am.txt"),
                        joinpath(@__DIR__, "../data/trajdata_i101_trajectories-0820am-0835am.txt"),
                        joinpath(@__DIR__, "../data/trajdata_i80_trajectories-0400-0415.txt"),
                        joinpath(@__DIR__, "../data/trajdata_i80_trajectories-0500-0515.txt"),
                        joinpath(@__DIR__, "../data/trajdata_i80_trajectories-0515-0530.txt"),
                       ]

function load_trajdata(filepath::String)
    td = open(io->read(io, MIME"text/plain"(), Trajdata), filepath, "r")
    td
end
load_trajdata(i::Int) = load_trajdata(TRAJDATA_PATHS[i])
get_corresponding_roadway(i::Int) = get_corresponding_roadway(TRAJDATA_PATHS[i])


using Chain
using ShiftedArrays

# PART 1
@chain "01/data" begin
    readlines # read into an array, each line is an element
    map(x -> parse(Int32, x), _) # convert strings to integers
    _ .- lag(_) # compute difference with previous value
    _[2:end] # remove first element, which will be missing
    filter(x -> x > 0, _) # filter to the increases in value
    length # now count
end

# PART 2
@chain "01/data" begin
    readlines # read into an array, each line is an element
    map(x -> parse(Int32, x), _) # convert strings to integers
    _ + lag(_) + lag(_, 2) # compute the sum of each value with its previous 2
    _ - lag(_) # now compute the difference with the previous element
    _[4:end] # remove first three elements, since we're summing triples
    filter(x -> x > 0, _) # filter to positive values, which are the increases in value
    length # now count
end


using ForwardDiff
using BenchmarkTools
using StaticArrays
using FiniteDiff
using LinearAlgebra
using SparseArrays

push!(LOAD_PATH, "./src")
using CommonUtils
using ExplicitJacobians
push!(LOAD_PATH, "./examples/EntropyStableEuler")
using EntropyStableEuler

F(x,y) = (x^2+x*y+y^2)/6
# F(x,y) = 1.0/(x+y)
# F(x,y) = logmean(x,y)
dF(x,y) = ForwardDiff.derivative(y->F(x,y),y)

K = 3
Q = randn(K,K)
Q = Q-transpose(Q)

function ff!(out,u,F,QTr)
    for i = 1:size(QTr,1)
        val = zero(eltype(u))
        ui = u[i]
        for j = 1:size(QTr,2)
            uj = u[j]
            val += QTr[j,i]*F(ui,uj)
        end
        out[i] = val
    end
end

# explicit Jacobian
function jac!(dfdu,u,dF,QTr)
    for i = 1:size(QTr,1)
        ui = u[i]
        for j = 1:size(QTr,2)
            uj = u[j]
            dfdu[i,j] = QTr[j,i]*dF(ui,uj)
        end
    end
    sum_df = sum(dfdu,dims=1)
    for i = 1:size(QTr,1)
        dfdu[i,i] += sum_df[i]
    end
end

u = 1 .+ rand(K)
cache = FiniteDiff.JacobianCache(u)
output = zeros(size(Q))
f!(out,u) = ff!(out,u,F,Q)
function ff(u,Q,F)
    uL,uR = meshgrid(u)
    return sum(Q.*F.(uL,uR),dims=2)
end
f(u) = ff(u,Q,F)

# @btime ForwardDiff.jacobian($f,$u) # AD jacobian
# @btime FiniteDiff.finite_difference_jacobian!($output,$f!,$u,$cache) # FiniteDiff.jl
# @btime jac!($output,$u,$dF,$Q) # explicit Jacobian
# out = similar(u); @btime f!($out,$u) # flux eval

function LF(uL,uR)
        return (@. .5*max(abs(uL),abs(uR))*(uL-uR))
        # return (@. (uL-uR))
end
B = randn(K,K)
B = B'*B
# B = [0 1;1 0]
d!(out,u) = ff!(out,u,LF,B)
FiniteDiff.finite_difference_jacobian!(output,d!,u,cache)

uR,uL = meshgrid(u)
dxLF(uL,uR) = ForwardDiff.derivative(uL->LF(uL,uR),uL)
dyLF(uL,uR) = ForwardDiff.derivative(uR->LF(uL,uR),uR)
jacx = -B.*transpose(dxLF.(uL,uR)) + diagm(vec(sum(B.* transpose(dxLF.(uL,uR)),dims=1)))
jacy = B.*dyLF.(uL,uR) - diagm(vec(sum(B.*dyLF.(uL,uR),dims=1)))

dxLFJ(uL,uR) = ForwardDiff.jacobian(uL->LF(uL,uR),uL)
dyLFJ(uL,uR) = ForwardDiff.jacobian(uR->LF(uL,uR),uR)
jacx2 = Matrix(hadamard_jacobian(sparse(B), dxLFJ, [u]))
jacy2 = Matrix(hadamard_jacobian(sparse(B), dyLFJ, [u]))

@show norm(output - jacx), norm(jacx - jacx2)
@show norm(output - jacy), norm(jacy - jacy2)

# F(x,y)  = SVector{2}(x[1]*y[2],y[1]+x[2])
# dF(x,y) = ForwardDiff.jacobian(y->F(x,y),y)
# uL = @SVector [.1 .+ rand(10000),.1 .+ rand(10000)]
# uR = @SVector [.1 .+ rand(10000),.1 .+ rand(10000)]
#
# function feval(uL,uR,F)
#     for i = 1:length(uL)
#         uLi = getindex.(uL,i)
#         uRi = getindex.(uR,i)
#         F(uLi,uRi)
#     end
# end
#
# @btime feval($uL,$uR,$F)
# @btime feval($uL,$uR,$dF)


data = parse.(Int, split(read(joinpath(@__DIR__, "./6.txt"), String), ","))

data = [3,4,3,1,2]

function part1(fish)
  for day in 1:80
    new_fish = fill(8,length(filter(x->x==0,fish)))
    fish = map(x->x==0 ? 6 : x - 1, fish)
    append!(fish,new_fish)
  end

  length(fish)
end

function inc_day(current_counts,days)
  next_counts = Dict{Int,Int}()
  if days > 0
    for i in [1,2,3,4,5,6,7,8,0]
      if i == 0 && haskey(current_counts,i)
        next_counts[8] = current_counts[i]
        if haskey(next_counts,6)
          next_counts[6] += current_counts[i]
        else
          next_counts[6] = current_counts[i]
        end
      elseif i > 0 && haskey(current_counts,i)
        next_counts[i - 1] = current_counts[i]
      end
    end
    return inc_day(next_counts, days - 1)
  else
    return current_counts
  end
end

function part2(fish,days)
  timer_counts = Dict{Int,Int}()
  for n in 1:5
    timer_counts[n] = length(filter(x->x==n,fish))
  end
  return sum(values(inc_day(timer_counts,days)))

end

part1(data)

part2(data,256)