export
    DiophantineEquationQuadraticXY,
    diophantine_equation_quadratic_xy

# Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0
struct DiophantineEquationQuadraticXY{T<:Integer}
    cx²::T # A
    cxy::T # B
    cy²::T # C
    cx::T  # D
    cy::T  # E
    c0::T  # F
end

diophantine_equation_quadratic_xy(;cx²::T=0, cxy::T=0, cy²::T=0, cx::T=0, cy::T=0, c0::T=0) where {T<:Integer} = DiophantineEquationQuadraticXY(cx², cxy, cy², cx, cy, c0)

Base.show(io::IO, eq::DiophantineEquationQuadraticXY) = print(io, "$(eq.cx²)x² + $(eq.cxy)xy + $(eq.cy²)y² + $(eq.cx)x + $(eq.cy)y + $(eq.c0) = 0")

evaluate(eq::DiophantineEquationQuadraticXY{T}, sol::DiophantineSolutionXY{T}) where {T<:Integer} = evaluate(eq, sol.x, sol.y)
evaluate(eq::DiophantineEquationQuadraticXY{T}, x::T, y::T) where {T<:Integer} = eq.cx² * x^2 + eq.cxy * x * y + eq.cy² * y^2 + eq.cx * x + eq.cy * y + eq.c0

solve(eq::DiophantineEquationQuadraticXY{T}) where {T<:Integer} = begin
    cx², cxy, cy², cx, cy, c0 = eq.cx², eq.cxy, eq.cy², eq.cx, eq.cy, eq.c0

    cx² == cxy == cy² == 0 && return AbstractDiophantineSolutions{DiophantineSolutionXY{T}}[solve(diophantine_equation_linear_xy(cx=cx, cy=cy, c0=c0))]
    cx² == cy² == 0 && return solve_simplehyperbolic(eq)

    discr = cxy^2 - 4*cx²*cy²
    discr < 0 && return solve_elliptical(eq)
    discr == 0 && return solve_parabolic(eq)
    solve_hyperbolic(eq)
end

solve_simplehyperbolic(eq::DiophantineEquationQuadraticXY{T}) where {T<:Integer} = begin
    cxy, cx, cy, c0 = eq.cxy, eq.cx, eq.cy, eq.c0

    solutions = AbstractDiophantineSolutions{DiophantineSolutionXY{T}}[]

    d = cx * cy - cxy * c0
    if d == 0
        cy % cxy == 0 && push!(solutions, diophantine_onex_anyy(-(cy ÷ cxy)))
        cx % cxy == 0 && push!(solutions, diophantine_anyx_oney(-(cx ÷ cxy)))
    else
        xytuples = Tuple{T,T}[]
        for f in factors(abs(d), true)
            x, r = divrem(f - cy, cxy)
            r == 0 || continue
            y, r = divrem(d ÷ f - cx, cxy)
            r == 0 || continue
            push!(xytuples, (x, y))
        end
        push!(solutions, isempty(xytuples) ? diophantine_nonex_noney(T) : diophantine_solutions(xytuples))
    end

    solutions
end

solve_elliptical(eq::DiophantineEquationQuadraticXY{T}) where {T<:Integer} = begin
    cx², cxy, cy², cx, cy, c0 = eq.cx², eq.cxy, eq.cy², eq.cx, eq.cy, eq.c0

    # f(t) = (cxy^2 - 4*cx²*cy²)*t^2 + 2(cxy*cy - 2*cy²*cx)*t + (cy^2 - 4*cy²*c0)
    fa = (cxy^2 - 4*cx²*cy²)
    fb = (cxy*cy - 2*cy²*cx)
    fc = (cy^2 - 4*cy²*c0)
    f(t) = fa*t^2 + 2*fb*t + fc
    D = fb^2 - fa*fc

    D < 0 && return AbstractDiophantineSolutions{DiophantineSolutionXY{T}}[diophantine_nonex_noney(T)]

    z = map(x -> Base.trunc(T, x), Float64[(-fb-sqrt(D)) / fa, (-fb+sqrt(D)) / fa])
    xytuples = Tuple{T,T}[]

    sort!(z)
    for x in z[1]:z[2]
        u = f(x)
        v = isqrt(u)
        v*v == u || continue

        y, r = divrem(-(cxy*x + cy) + v, 2*cy²)
        r == 0 && push!(xytuples, (x, y))

        v != 0 || continue
        y, r = divrem(-(cxy*x + cy) - v, 2*cy²)
        r == 0 && push!(xytuples, (x, y))
    end

    solution = isempty(xytuples) ? diophantine_nonex_noney(T) : diophantine_solutions(xytuples)
    AbstractDiophantineSolutions{DiophantineSolutionXY{T}}[solution]
end

solve_parabolic(eq::DiophantineEquationQuadraticXY{T}) where {T<:Integer} = begin
    cx², cxy, cy², cx, cy, c0 = eq.cx², eq.cxy, eq.cy², eq.cx, eq.cy, eq.c0

    solutions = AbstractDiophantineSolutions{DiophantineSolutionXY{T}}[]

    g = copysign(gcd(cx², cy²), cx²)
    a, b, c = cx² ÷ g, cxy ÷ g, cy² ÷ g
    ra, rc = isqrt(a), copysign(isqrt(c), b)

    fa = ra * g
    fb = cx
    fc = ra * c0
    f(t) = fa * t^2 + fb * t + fc

    d = rc * cx - ra * cy
    if d == 0
        D = fb^2 - 4*fa*fc
        if D < 0
            push!(solutions, diophantine_nonex_noney(T))
        else
            u = map(x -> Base.trunc(T, x), Float64[(-fb-sqrt(D)) / (2*fa), (-fb+sqrt(D)) / (2*fa)])

            for v in unique(u)
                f(v) == 0 && push!(solutions, solve(diophantine_equation_linear_xy(cx=ra, cy=rc, c0=-v)))
            end
        end
    else
        for u in 0:(abs(d) - 1)
            f(u) % d == 0 || continue
            push!(solutions, diophantine_quadraticx_quadraticy(
                rc * g * (-d), -(cy + 2 * rc * g * u), -((rc * g * u^2 + cy * u + rc * c0) ÷ d),
                ra * g *   d,    cx + 2 * ra * g * u,    (ra * g * u^2 + cx * u + ra * c0) ÷ d
            ))
        end
    end

    solutions
end

solve_hyperbolic(eq::DiophantineEquationQuadraticXY{T}) where {T<:Integer} = begin
    (eq.cx == eq.cy == 0) ?
        solve_hyperbolic_homogeneous(eq) :
        solve_hyperbolic_general_quadratic(eq)
end

solve_hyperbolic_homogeneous(eq::DiophantineEquationQuadraticXY{T}) where {T<:Integer} = begin
    cx², cxy, cy², c0 = eq.cx², eq.cxy, eq.cy², eq.c0

    solutions = AbstractDiophantineSolutions{DiophantineSolutionXY{T}}[]

    discr = cxy^2 - 4*cx²*cy²
    k = isqrt(discr)

    if c0 == 0
        push!(solutions, diophantine_solutions((0, 0)))
        if k*k == discr
            push!(solutions, solve(diophantine_equation_linear_xy(cx=2*cx², cy=cxy+k, c0=0)))
            push!(solutions, solve(diophantine_equation_linear_xy(cx=2*cx², cy=cxy-k, c0=0)))
        end
        return solutions
    end

    if k*k == discr
        xytuples = Tuple{T,T}[]
        for f in factors(abs(-4*cx²*c0), true)
            y, r = divrem(f + (4*cx²*c0) ÷ f, 2*k)
            r == 0 || continue
            x, r = divrem(f - (cxy + k)*y, 2*cx²)
            r == 0 || continue
            push!(xytuples, (x, y))
        end
        push!(solutions, isempty(xytuples) ? diophantine_nonex_noney(T) : diophantine_solutions(xytuples))
        return solutions
    end

    # WIP
end


# Based on https://math.stackexchange.com/questions/3176543/intersection-point-of-2-lines-defined-by-2-points-each/3176590
function intersection(e1::E, e2::E, tol = 1e-15)
    nx = e1.q.x - e1.p.x
    ny = e1.q.y - e1.p.y
    mx = e2.p.x - e2.q.x
    my = e2.p.y - e2.q.y
    px = e2.p.x - e1.p.x
    py = e2.p.y - e1.p.y

    D = nx*my - ny*mx
    Qx = my*px - mx*py
    Qy = nx*py - ny*px

    (((D > 0) & (Qx < 0)) | ((D > 0) & (Qy < 0)) | ((D < 0) & (Qx > 0)) | ((D < 0) & (Qy > 0))) && return false, e1.p
    ((abs(D) < abs(Qx)) | (abs(D) < abs(Qy))) && return false, e1.p
    abs(D) < tol  && return false, e1.p
    t = Qx/D
    vx = e1.p.x + t*nx
    vy = e1.p.y + t*ny

    return true, V(vx, vy)
end

function islesswithp(s1::E, s2::E, p::V, tol = 1e-15)
    side = sideof(s1, p, tol)
    if side == 1
        return true
    elseif side == -1
        return false
    end
    side = sideof(s1, s2.q, tol)

    # Warning! If two segments coincide, then this function return false!
    if side == 1
        return true
    else
        return false
    end
end

"""
    insert_node!(tree::RBTree, node::RBTreeNode, p::V, tol = 1e-15)

Inserts `node` at proper location by traversing through the `tree` in a binary-search-tree fashion.
"""
function insert_node!(tree::RBTree, node::RBTreeNode, p::V, tol = 1e-15)
    node_y = nothing
    node_x = tree.root

    while node_x !== tree.nil
        node_y = node_x
        if islesswithp(node_x.data, node.data, p, tol)
        # if node.data < node_x.data
            node_x = node_x.rightChild
        else
            node_x = node_x.leftChild
        end
    end

    node.parent = node_y
    if node_y == nothing
        tree.root = node
    elseif islesswithp(node_y.data, node.data, p, tol)
    # elseif node.data < node_y.data
        node_y.rightChild = node
    else
        node_y.leftChild = node
    end
end

"""
    insert!(tree, key, p, tol = 1e-15)

Inserts `key` in the `tree` if it is not present.
"""
function Base.insert!(tree::RBTree{K}, d::K, p::V, tol = 1e-15) where K
    # if the key exists in the tree, no need to insert
    # haskey(tree, d) && return tree

    # insert, if not present in the tree
    node = RBTreeNode{K}(d)
    node.leftChild = node.rightChild = tree.nil

    insert_node!(tree, node, p, tol)

    if node.parent == nothing
        node.color = false
    elseif node.parent.parent == nothing
        ;
    else
        fix_insert!(tree, node)
    end
    tree.count += 1
    return tree
end

"""
    delete!(tree::RBTree, key, p, tol)

Deletes `key` from `tree`, if present, else returns the unmodified tree.
"""
function Base.delete!(tree::RBTree{K}, d::K, p, tol = 1e-15) where K
    z = tree.nil
    node = tree.root

    while node !== tree.nil
        if node.data == d
            z = node
        end

        if islesswithp(d, node.data, p, tol)
        # if d < node.data
            node = node.leftChild
        else
            node = node.rightChild
        end
    end

    (z === tree.nil) && return tree

    y = z
    y_original_color = y.color
    x = RBTreeNode{K}()
    if z.leftChild === tree.nil
        x = z.rightChild
        rb_transplant(tree, z, z.rightChild)
    elseif z.rightChild === tree.nil
        x = z.leftChild
        rb_transplant(tree, z, z.leftChild)
    else
        y = minimum_node(tree, z.rightChild)
        y_original_color = y.color
        x = y.rightChild

        if y.parent == z
            x.parent = y
        else
            rb_transplant(tree, y, y.rightChild)
            y.rightChild = z.rightChild
            y.rightChild.parent = y
        end

        rb_transplant(tree, z, y)
        y.leftChild = z.leftChild
        y.leftChild.parent = y
        y.color = z.color
    end

    !y_original_color && delete_fix(tree, x)
    tree.count -= 1
    return tree
end

function Base.pop!(tree::RBTree{K}, p::V, tol = 1e-15) where K
    z = search_edge(tree, p, tol)

    (z === tree.nil) && return z

    y = z
    y_original_color = y.color
    x = RBTreeNode{K}()
    if z.leftChild === tree.nil
        x = z.rightChild
        rb_transplant(tree, z, z.rightChild)
    elseif z.rightChild === tree.nil
        x = z.leftChild
        rb_transplant(tree, z, z.leftChild)
    else
        y = minimum_node(tree, z.rightChild)
        y_original_color = y.color
        x = y.rightChild

        if y.parent == z
            x.parent = y
        else
            rb_transplant(tree, y, y.rightChild)
            y.rightChild = z.rightChild
            y.rightChild.parent = y
        end

        rb_transplant(tree, z, y)
        y.leftChild = z.leftChild
        y.leftChild.parent = y
        y.color = z.color
    end

    !y_original_color && delete_fix(tree, x)
    tree.count -= 1
    return z
end

function search_edge(tree::RBTree{K}, p::V, tol = 1e-15) where K
    node = tree.root
    while node !== tree.nil
        side = sideof(node.data, p, tol)
        side == 0 && return node
        if side == -1
            node = node.leftChild
        else
            node = node.rightChild
        end
    end
    return node
end

########################################
# AbstractTrees beautifiers
# WARNING!!! TYPE PIRACY!!! DO SOMETHING!!!
########################################

children(tree::RBTree) = children(tree.root)
function children(node::RBTreeNode)
    if node.leftChild === nothing
        if node.rightChild === nothing
            return ()
        else
            return (node.rightChild, )
        end
    else
        if node.rightChild === nothing
            return (node.leftChild, )
        else
            return (node.leftChild, node.rightChild)
        end
    end
end

## Things that make printing prettier
printnode(io::IO, tree::RBTree) = printnode(io, tree.root)
printnode(io::IO, node::RBTreeNode) = print(io, node.data)

function leftmost(tree)
    node = tree.root
    while node.leftChild !== tree.nil
        node = node.leftChild
    end
    return node
end


using LinearAlgebra
using StaticArrays

function max_distance_for_pbc_dist(traj::Trajectory)
    max_distance_for_pbc_dist(traj.mdbox)
end

function max_distance_for_pbc_dist(mdbox::MDBox)
    max_distance_for_pbc_dist(mdbox.pbc_matrix)
end

function max_distance_for_pbc_dist(pbc::AbstractArray)
    volume = det(pbc)
    a, b, c = eachcol(pbc)
    dist1 = volume / norm(cross(a, b))
    dist2 = volume / norm(cross(b, c))
    dist3 = volume / norm(cross(c, a))
    return min(dist1, dist2, dist3) / 2
end

function pbc_dist(point1, point2, mdbox::OrthorhombicBox)
    for i in 1:3
        mdbox.dist_tmp[i] = (point1[i] - point2[i]) / mdbox.cell_parameters[i]
        mdbox.dist_tmp[i] -= floor(mdbox.dist_tmp[i] + 0.5)
        mdbox.dist_tmp[i] *= mdbox.cell_parameters[i]
    end
    return norm(mdbox.dist_tmp)
end

#function pbc_dist(point1, point2, mdbox::TriclinicBox)
#    pbc_dist(point1, point2, mdbox.pbc_matrix, mdbox.inv_pbc_matrix, mdbox.realspace_tmp, mdbox.inversespace_tmp)
#end

#function pbc_dist(point1, point2, pbc, inv_pbc = inv(pbc), dist_tmp = zeros(MVector{3}), matmul_tmp = zeros(MVector{3}))
#    dist_tmp .= point1 .- point2
#    mul!(matmul_tmp, inv_pbc,dist_tmp)
#    matmul_tmp .-= round.(matmul_tmp)
#    mul!(dist_tmp, pbc, matmul_tmp)
#    return norm(dist_tmp)
#end

# ugliest implementation yet, but also fast
# who needs arrays anyways
function pbc_dist(p1, p2, pbc, inv_pbc)
    # pbc (and inv_pbc by extension) have to be upper-triagonal now!
    # direct distance between the two points
        # d = p1 .- p300
    d1, d2, d3 = p1[1] - p2[1], p1[2] - p2[2], p1[3] - p2[3]
    # convert distance to relative coordinates and save rounded values
        # m = round.(inv_pbc * d)
    m1 = round(inv_pbc[1, 1] * d1 + inv_pbc[1, 2] * d2 + inv_pbc[1, 3] * d3)
    m2 = round(inv_pbc[2, 2] * d2 + inv_pbc[2, 3] * d3)
    m3 = round(inv_pbc[3, 3] * d3)
    # convert rounded relative distance into real distance and subtract it from the real distance; return norm
        # d -= pbc * m
        # n = norm(d)
    n = sqrt(
        (d1 - pbc[1, 1] * m1 - pbc[1, 2] * m2 - pbc[1, 3] * m3)^2 +
        (d2 - pbc[2, 2] * m2 - pbc[2, 3] * m3)^2 +
        (d3 - pbc[3, 3] * m3)^2
        )
    return n
end

function pbc_dist(p1, p2, mdbox::TriclinicBox)
    pbc_dist(p1, p2, mdbox.pbc_matrix, mdbox.inv_pbc_matrix)
end

function minimum_image_vector(point1, point2, mdbox::TriclinicBox)
    minimum_image_vector(point1, point2, mdbox.pbc_matrix, mdbox.inv_pbc_matrix, mdbox.realspace_tmp, mdbox.inversespace_tmp)
end

function minimum_image_vector(point1, point2, pbc, inv_pbc = inv(pbc), dist_tmp = zeros(MVector{3}), matmul_tmp = zeros(MVector{3}))
    dist_tmp .= point1 .- point2
    mul!(matmul_tmp, inv_pbc,dist_tmp)
    matmul_tmp .-= floor.(matmul_tmp .+ 0.5)
    mul!(dist_tmp, pbc, matmul_tmp)
    return dist_tmp
end

function pbc_dist_triclinic(point1, point2, pbc, inv_pbc = inv(pbc), dist_tmp = zeros(MVector{3}), matmul_tmp = zeros(MVector{3}))
    dist_tmp .= abs.(point1 .- point2)
    mul!(matmul_tmp, inv_pbc,dist_tmp)
    matmul_tmp .-= floor.(matmul_tmp .+ 0.5) .+ 1
    distance = pbc[1, 1] + pbc[2, 2] + pbc[3, 3]
    for i in 1:3
        for j in 1:3
            for k in 1:3
                mul!(dist_tmp, pbc, matmul_tmp)
                new_distance = norm(dist_tmp)
                if new_distance < distance
                    distance = new_distance
                end
                matmul_tmp[3] += 1
            end
            matmul_tmp[3] -= 3
            matmul_tmp[2] += 1
        end
        matmul_tmp[2] -= 3
        matmul_tmp[1] += 1
    end
    return distance
end

function next_neighbor(point1, group2, mdbox::MDBox)
    point2 = @view group2[:, 1]
    index, distance = 1, pbc_dist(point1, point2, mdbox)
    for i in 2:size(group2)[2]
        point2 = @view group2[:, i]
        new_distance = pbc_dist(point1, point2, mdbox)
        if new_distance < distance
            distance = new_distance
            index = i
        end
    end
    return index, distance
end


"""
    Sherlock(optimizer, ϵ::Float64)

Sherlock combines local and global search to estimate the range of the output node.

# Problem requirement
1. Network: any depth, ReLU activation, single output
2. Input: hpolytope and hyperrectangle
3. Output: hyperrectangle (1d interval)

# Return
`CounterExampleResult` or `ReachabilityResult`

# Method
Local search: solve a linear program to find local optima on a line segment of the piece-wise linear network.
Global search: solve a feasibilty problem using MILP encoding (default calling NSVerify).
- `optimizer` default `GLPKSolverMIP()`
- `ϵ` is the margin for global search, default `0.1`.

# Property
Sound but not complete.

# Reference
[S. Dutta, S. Jha, S. Sanakaranarayanan, and A. Tiwari,
"Output Range Analysis for Deep Neural Networks,"
*ArXiv Preprint ArXiv:1709.09130*, 2017.](https://arxiv.org/abs/1709.09130)

[https://github.com/souradeep-111/sherlock](https://github.com/souradeep-111/sherlock)
"""
@with_kw struct Sherlock <: Solver
    optimizer = GLPK.Optimizer
    ϵ::Float64 = 0.1
end

function solve(solver::Sherlock, problem::Problem)
    isbounded(problem.input) || UnboundedInputError("Temprarily, Sherlock cannot accept unbounded inputs")

    (x_u, u) = output_bound(solver, problem, :max)
    (x_l, l) = output_bound(solver, problem, :min)
    println("bounds: [", l, ", ", u, "]")
    bound = Hyperrectangle(low = [l], high = [u])
    reach = Hyperrectangle(low = [l - solver.ϵ], high = [u + solver.ϵ])

    output = problem.output
    if reach ⊆ output
        return ReachabilityResult(:holds, [reach])
    end
    high(bound) > high(output) && return CounterExampleResult(:violated, x_u)
    low(bound)  < low(output)  && return CounterExampleResult(:violated, x_l)
    return ReachabilityResult(:unknown, [reach])
end

function output_bound(solver::Sherlock, problem::Problem, type::Symbol)
    opt = solver.optimizer
    x = an_element(problem.input)
    while true
        (x, bound) = local_search(problem, x, opt, type)
        bound_ϵ = bound + ifelse(type == :max, solver.ϵ, -solver.ϵ)
        (x_new, bound_new, feasible) = global_search(problem, bound_ϵ, opt, type)
        feasible || return (x, bound)
        (x, bound) = (x_new, bound_new)
    end
end

function local_search(problem::Problem, x::Vector{Float64}, optimizer, type::Symbol)
    nnet = problem.network

    model = Model(optimizer)
    model[:δ] = get_activation(nnet, x)
    z = init_vars(model, nnet, :z, with_input=true)
    add_set_constraint!(model, problem.input, first(z))
    encode_network!(model, nnet, StandardLP())

    # the gradient wrt to the input is a `1xn` matrix,
    # because the output is 1-d. Therefore o[1] is the
    # only entry in g*z₀
    gradient = get_gradient(nnet, x)
    o = gradient * first(z)
    index = ifelse(type == :max, 1, -1)
    @objective(model, Max, index * o[1])
    optimize!(model)

    x_new = value(first(z))
    bound_new = compute_output(nnet, x_new)
    return (x_new, bound_new[1])
end

function global_search(problem::Problem, bound::Float64, optimizer, type::Symbol)
    index = ifelse(type == :max, 1.0, -1.0)
    h = HalfSpace([index], index * bound)
    output_set = HPolytope([h])
    problem_new = Problem(problem.network, problem.input, output_set)
    solver  = NSVerify(optimizer = optimizer)
    result  = solve(solver, problem_new)
    if result.status == :violated
        x = result.counter_example
        bound = compute_output(problem.network, x)
        return (x, bound[1], true)
    else
        return ([], 0.0, false)
    end
end


using FMM3D
using Test
using Random
using LinearAlgebra

@testset "testing Helmholtz utilities" begin

    n = 1000
    nt = 1100
    sources = randn(3,n)
    targets = randn(3,nt)
    charges = randn(n) + im*randn(n)
    dipvecs = randn(3,n) + im*randn(3,n)
    zk = 2.1

    vals1c = h3ddir(zk,sources,targets,charges=charges,
                    pgt=2,thresh=1e-16)

    vals1cs = h3ddir(zk,sources,sources,charges=charges,
                    pgt=2,thresh=1e-16)

    vals1d = h3ddir(zk,sources,targets,dipvecs=dipvecs,
                    pgt=2,thresh=1e-16)

    vals1ds = h3ddir(zk,sources,sources,dipvecs=dipvecs,
                    pgt=2,thresh=1e-16)

    vals1dc = h3ddir(zk,sources,targets,charges=charges,
                     dipvecs=dipvecs,pgt=2,thresh=1e-16)

    vals1dcs = h3ddir(zk,sources,sources,charges=charges,
                     dipvecs=dipvecs,pgt=2,thresh=1e-16)

    eps = 1e-12
    vals2c = hfmm3d(eps,zk,sources,targets=targets,charges=charges,
                    pg=2,pgt=2)
    vals2d = hfmm3d(eps,zk,sources,targets=targets,dipvecs=dipvecs,
                    pg=2,pgt=2)
    vals2dc = hfmm3d(eps,zk,sources,targets=targets,charges=charges,
                     dipvecs=dipvecs,pg=2,pgt=2)

    @test norm(vals2c.pot-vals1cs.pottarg)/norm(vals1cs.pottarg) < eps
    @test norm(vals2c.grad-vals1cs.gradtarg)/norm(vals1cs.gradtarg) < eps
    @test norm(vals2c.pottarg-vals1c.pottarg)/norm(vals1c.pottarg) < eps
    @test norm(vals2c.gradtarg-vals1c.gradtarg)/norm(vals1c.gradtarg) < eps

    @test norm(vals2d.pot-vals1ds.pottarg)/norm(vals1ds.pottarg) < eps
    @test norm(vals2d.grad-vals1ds.gradtarg)/norm(vals1ds.gradtarg) < eps
    @test norm(vals2d.pottarg-vals1d.pottarg)/norm(vals1d.pottarg) < eps
    @test norm(vals2d.gradtarg-vals1d.gradtarg)/norm(vals1d.gradtarg) < eps

    @test norm(vals2dc.pot-vals1dcs.pottarg)/norm(vals1dcs.pottarg) < eps
    @test norm(vals2dc.grad-vals1dcs.gradtarg)/norm(vals1dcs.gradtarg) < eps
    @test norm(vals2dc.pottarg-vals1dc.pottarg)/norm(vals1dc.pottarg) < eps
    @test norm(vals2dc.gradtarg-vals1dc.gradtarg)/norm(vals1dc.gradtarg) < eps

end

@testset "testing Laplace utilities" begin

    n = 1000
    nt = 1100
    sources = randn(3,n)
    targets = randn(3,nt)
    charges = randn(n)
    dipvecs = randn(3,n)

    vals1c = l3ddir(sources,targets,charges=charges,
                    pgt=2,thresh=1e-16)

    vals1cs = l3ddir(sources,sources,charges=charges,
                    pgt=2,thresh=1e-16)

    vals1d = l3ddir(sources,targets,dipvecs=dipvecs,
                    pgt=2,thresh=1e-16)

    vals1ds = l3ddir(sources,sources,dipvecs=dipvecs,
                    pgt=2,thresh=1e-16)

    vals1dc = l3ddir(sources,targets,charges=charges,
                     dipvecs=dipvecs,pgt=2,thresh=1e-16)

    vals1dcs = l3ddir(sources,sources,charges=charges,
                     dipvecs=dipvecs,pgt=2,thresh=1e-16)

    eps = 1e-12
    vals2c = lfmm3d(eps,sources,targets=targets,charges=charges,
                    pg=2,pgt=2)
    vals2d = lfmm3d(eps,sources,targets=targets,dipvecs=dipvecs,
                    pg=2,pgt=2)
    vals2dc = lfmm3d(eps,sources,targets=targets,charges=charges,
                     dipvecs=dipvecs,pg=2,pgt=2)

    @test norm(vals2c.pot-vals1cs.pottarg)/norm(vals1cs.pottarg) < eps
    @test norm(vals2c.grad-vals1cs.gradtarg)/norm(vals1cs.gradtarg) < eps
    @test norm(vals2c.pottarg-vals1c.pottarg)/norm(vals1c.pottarg) < eps
    @test norm(vals2c.gradtarg-vals1c.gradtarg)/norm(vals1c.gradtarg) < eps

    @test norm(vals2d.pot-vals1ds.pottarg)/norm(vals1ds.pottarg) < eps
    @test norm(vals2d.grad-vals1ds.gradtarg)/norm(vals1ds.gradtarg) < eps
    @test norm(vals2d.pottarg-vals1d.pottarg)/norm(vals1d.pottarg) < eps
    @test norm(vals2d.gradtarg-vals1d.gradtarg)/norm(vals1d.gradtarg) < eps

    @test norm(vals2dc.pot-vals1dcs.pottarg)/norm(vals1dcs.pottarg) < eps
    @test norm(vals2dc.grad-vals1dcs.gradtarg)/norm(vals1dcs.gradtarg) < eps
    @test norm(vals2dc.pottarg-vals1dc.pottarg)/norm(vals1dc.pottarg) < eps
    @test norm(vals2dc.gradtarg-vals1dc.gradtarg)/norm(vals1dc.gradtarg) < eps

end

@testset "testing Stokes utilities" begin

    n = 1000
    nt = 1100
    sources = randn(3,n)
    targets = randn(3,nt)
    stoklet = randn(3,n)
    strslet = randn(3,n)
    strsvec = randn(3,n)

    vals1c = st3ddir(sources,targets,stoklet=stoklet,
                    ppregt=3,thresh=1e-16)

    vals1cs = st3ddir(sources,sources,stoklet=stoklet,
                    ppregt=3,thresh=1e-16)

    vals1d = st3ddir(sources,targets,strslet=strslet,strsvec=strsvec,
                    ppregt=3,thresh=1e-16)

    vals1ds = st3ddir(sources,sources,strslet=strslet,strsvec=strsvec,
                    ppregt=3,thresh=1e-16)

    vals1dc = st3ddir(sources,targets,stoklet=stoklet,
                     strslet=strslet,strsvec=strsvec,ppregt=3,thresh=1e-16)

    vals1dcs = st3ddir(sources,sources,stoklet=stoklet,
                     strslet=strslet,strsvec=strsvec,ppregt=3,thresh=1e-16)

    eps = 1e-12
    vals2c = stfmm3d(eps,sources,targets=targets,stoklet=stoklet,
                    ppreg=3,ppregt=3)
    vals2d = stfmm3d(eps,sources,targets=targets,strslet=strslet,strsvec=strsvec,
                    ppreg=3,ppregt=3)
    vals2dc = stfmm3d(eps,sources,targets=targets,stoklet=stoklet,
                     strslet=strslet,strsvec=strsvec,ppreg=3,ppregt=3)

    @test norm(vals2c.pot-vals1cs.pottarg)/norm(vals1cs.pottarg) < eps
    @test norm(vals2c.pre-vals1cs.pretarg)/norm(vals1cs.pretarg) < eps
    @test norm(vals2c.grad-vals1cs.gradtarg)/norm(vals1cs.gradtarg) < eps
    @test norm(vals2c.pottarg-vals1c.pottarg)/norm(vals1c.pottarg) < eps
    @test norm(vals2c.gradtarg-vals1c.gradtarg)/norm(vals1c.gradtarg) < eps
    @test norm(vals2c.pretarg-vals1c.pretarg)/norm(vals1c.pretarg) < eps

    @test norm(vals2d.pot-vals1ds.pottarg)/norm(vals1ds.pottarg) < eps
    @test norm(vals2d.pre-vals1ds.pretarg)/norm(vals1ds.pretarg) < eps
    @test norm(vals2d.grad-vals1ds.gradtarg)/norm(vals1ds.gradtarg) < eps
    @test norm(vals2d.pottarg-vals1d.pottarg)/norm(vals1d.pottarg) < eps
    @test norm(vals2d.pretarg-vals1d.pretarg)/norm(vals1d.pretarg) < eps
    @test norm(vals2d.gradtarg-vals1d.gradtarg)/norm(vals1d.gradtarg) < eps

    @test norm(vals2dc.pot-vals1dcs.pottarg)/norm(vals1dcs.pottarg) < eps
    @test norm(vals2dc.pre-vals1dcs.pretarg)/norm(vals1dcs.pretarg) < eps
    @test norm(vals2dc.grad-vals1dcs.gradtarg)/norm(vals1dcs.gradtarg) < eps
    @test norm(vals2dc.pottarg-vals1dc.pottarg)/norm(vals1dc.pottarg) < eps
    @test norm(vals2dc.pretarg-vals1dc.pretarg)/norm(vals1dc.pretarg) < eps
    @test norm(vals2dc.gradtarg-vals1dc.gradtarg)/norm(vals1dc.gradtarg) < eps

end

@testset "testing electromagnetics utilities" begin

    n = 10
    nt = 11
    sources = randn(3,n)
    targets = randn(3,nt)
    h_current = randn(3,n) + im*randn(3,n)
    e_current = randn(3,n) + im*randn(3,n)
    e_charge = randn(n) + im*randn(n)
    zk = 2.1

    vals1A = em3ddir(zk,sources,targets,h_current=h_current,
                     ifEtarg=true,ifdivEtarg=true,
                     ifcurlEtarg=true,thresh=1e-16)

    vals1As = em3ddir(zk,sources,sources,h_current=h_current,
                     ifEtarg=true,ifdivEtarg=true,
                     ifcurlEtarg=true,thresh=1e-16)

    vals1B = em3ddir(zk,sources,targets,e_current=e_current,
                     ifEtarg=true,ifdivEtarg=true,
                     ifcurlEtarg=true,thresh=1e-16)

    vals1Bs = em3ddir(zk,sources,sources,e_current=e_current,
                     ifEtarg=true,ifdivEtarg=true,
                     ifcurlEtarg=true,thresh=1e-16)

    vals1lambda = em3ddir(zk,sources,targets,e_charge=e_charge,
                     ifEtarg=true,ifdivEtarg=true,
                     ifcurlEtarg=true,thresh=1e-16)

    vals1lambdas = em3ddir(zk,sources,sources,e_charge=e_charge,
                     ifEtarg=true,ifdivEtarg=true,
                     ifcurlEtarg=true,thresh=1e-16)

    vals1all = em3ddir(zk,sources,targets,h_current=h_current,e_current=e_current,e_charge=e_charge,
                     ifEtarg=true,ifdivEtarg=true,
                     ifcurlEtarg=true,thresh=1e-16)

    vals1alls = em3ddir(zk,sources,sources,h_current=h_current,e_current=e_current,e_charge=e_charge,
                     ifEtarg=true,ifdivEtarg=true,
                     ifcurlEtarg=true,thresh=1e-16)

    eps = 1e-12
    vals2A = emfmm3d(eps,zk,sources,targets=targets,h_current=h_current,
                    ifE=true,ifdivE=true,ifcurlE=true,
                    ifEtarg=true,ifdivEtarg=true,ifcurlEtarg=true)

    vals2B = emfmm3d(eps,zk,sources,targets=targets,e_current=e_current,
                    ifE=true,ifdivE=true,ifcurlE=true,
                    ifEtarg=true,ifdivEtarg=true,ifcurlEtarg=true)

    vals2lambda = emfmm3d(eps,zk,sources,targets=targets,e_charge=e_charge,
                    ifE=true,ifdivE=true,ifcurlE=true,
                    ifEtarg=true,ifdivEtarg=true,ifcurlEtarg=true)

    vals2all = emfmm3d(eps,zk,sources,targets=targets,h_current=h_current,e_current=e_current,e_charge=e_charge,
                    ifE=true,ifdivE=true,ifcurlE=true,
                    ifEtarg=true,ifdivEtarg=true,ifcurlEtarg=true)

    function absrelerr(vex,v)
        return norm(vex-v)/max(norm(vex),1)
    end

    @test absrelerr(vals1As.Etarg,vals2A.E) < eps
    @test absrelerr(vals1A.Etarg,vals2A.Etarg) < eps
    @test absrelerr(vals1As.divEtarg,vals2A.divE) < eps
    @test absrelerr(vals1A.divEtarg,vals2A.divEtarg) < eps
    @test absrelerr(vals1As.curlEtarg,vals2A.curlE) < eps
    @test absrelerr(vals1A.curlEtarg,vals2A.curlEtarg) < eps

    @test absrelerr(vals1Bs.Etarg,vals2B.E) < eps
    @test absrelerr(vals1B.Etarg,vals2B.Etarg) < eps
    @test absrelerr(vals1Bs.divEtarg,vals2B.divE) < eps
    @test absrelerr(vals1B.divEtarg,vals2B.divEtarg) < eps
    @test absrelerr(vals1Bs.curlEtarg,vals2B.curlE) < eps
    @test absrelerr(vals1B.curlEtarg,vals2B.curlEtarg) < eps

    @test absrelerr(vals1lambdas.Etarg,vals2lambda.E) < eps
    @test absrelerr(vals1lambda.Etarg,vals2lambda.Etarg) < eps
    @test absrelerr(vals1lambdas.divEtarg,vals2lambda.divE) < eps
    @test absrelerr(vals1lambda.divEtarg,vals2lambda.divEtarg) < eps
    @test absrelerr(vals1lambdas.curlEtarg,vals2lambda.curlE) < eps
    @test absrelerr(vals1lambda.curlEtarg,vals2lambda.curlEtarg) < eps

    @test absrelerr(vals1alls.Etarg,vals2all.E) < eps
    @test absrelerr(vals1all.Etarg,vals2all.Etarg) < eps
    @test absrelerr(vals1alls.divEtarg,vals2all.divE) < eps
    @test absrelerr(vals1all.divEtarg,vals2all.divEtarg) < eps
    @test absrelerr(vals1alls.curlEtarg,vals2all.curlE) < eps
    @test absrelerr(vals1all.curlEtarg,vals2all.curlEtarg) < eps

end

@testset "testing lower level routines" begin

    # besseljs3d

    nterms = 10
    z = 1.1 + im*1.2
    scale = 1.3
    ifder = 1

    fj10 = (-3.5183264829466616750769540*(1e-9) +
        im*8.88237983492960538163695769*(1e-9))

    fjs, fjder = besseljs3d(nterms,z,scale=scale,ifder=ifder)

    @test (abs(fj10-fjs[11]*(scale^10))/abs(fj10) < 1e-10)

end


"""
    get_dummy(df, cVar, cType. trtRef)

Convert categorical variable to dummy indicators using specified contrast
type. This covers all cases except for treatment contrasts with a specified
reference level.

# Arguments

- df = DataFrame of variables
- cVar = symbol for the categorical variable in df to be converted
- cType = character string indicating the type of contrast to use for `cVar`
- trtRef = nothing

# Value

DataFrame of dummy variables for the specified categorical variable

"""
function get_dummy(df::DataFrames.DataFrame, cVar::Symbol, cType::String,
                   trtRef::Nothing)
    # Obtain the levels to use for the dummy indicators, depending on
    # contrast type
    thisVar = string.(df[:,cVar])
    if cType=="treat"
        levs = unique(thisVar)[2:end]
    elseif (cType=="sum")
        levs = unique(thisVar)[(2:end).-1]
    elseif cType=="noint" || (cType=="sumnoint")
        levs = unique(thisVar)
    else
        error(string("Did not recognize contrast type for ", cVar))
    end

    # Iterate through levels to make dummy indicators
    namedict = Dict(zip(levs, 1:length(levs)))
    dummies = zeros(length(thisVar), length(namedict))
    for i=1:length(thisVar)
        if haskey(namedict, thisVar[i])
            dummies[i, namedict[thisVar[i]]] = 1
        end
    end

    # Some additional modifications for sum contrasts
    if (cType=="sum")
        dummies[thisVar.==(unique(thisVar)[end]),:] .= -1
    end
    if cType=="sumnoint"
        dummies = dummies - (1/length(unique(thisVar)))
    end

    # Convert results to a DataFrame and rename columns
    newDf = convert(DataFrame, dummies)
    rename!(newDf, [Symbol("$(cVar)_$k") for k in levs])
    return newDf
end

"""

    get_dummy(df, cVar, cType. trtRef)

Convert categorical variables to for treatment contrasts with a specified
reference level.

# Arguments

- df = DataFrame of variables
- cVar = symbol for the categorical variable in df to be converted
- cType = character string indicating the type of contrast to use for `cVar`
- trtRef = character string specifying the level in cVar to use as the
reference

# Value

DataFrame of dummy variables for the specified categorical variable

"""
function get_dummy(df::DataFrames.DataFrame, cVar::Symbol, cType::String,
                   trtRef::String)

    # Obtain the levels to use for the dummy indicators.
    thisVar = string.(df[:,cVar])
    if cType=="treat"
        levs = unique(thisVar)[unique(thisVar) .!= trtRef]
    else
        error("Can only specify trtRef for treatment contrasts.")
    end

    # Iterate through levels to make dummy indicators
    namedict = Dict(zip(levs, 1:length(levs)))
    dummies = zeros(length(thisVar), length(namedict))
    for i=1:length(thisVar)
        if haskey(namedict, thisVar[i])
            dummies[i, namedict[thisVar[i]]] = 1
        end
    end

    # Convert results to a DataFrame and rename columns
    newDf = convert(DataFrame, dummies)
    rename!(newDf, [Symbol("$(cVar)_$k") for k in levs])
    return newDf
end

"""
    contr(df, cVars, cTypes, trtRefs)

Converts categorical variables in a DataFrame to specified contrast types.
All other variables are left as-is.

# Arguments

- df = DataFrame of variables
- cVars = 1d array of symbols corresponding to categorical variable names
  in df to be converted
- cTypes = 1d array of character strings of the same length as `cVars`,
  indicating the types of contrasts to use. Defaults to treatment contrasts
  ("treat") for all variables in `cVars`. Other options include "sum" for sum
  contrasts, "noint" for treatment contrasts with no intercept, and
  "sumnoint" for sum contrasts with no intercept. For "treat" `cTypes`, you
  can also specify the level to use as the reference treatment using `trtRefs`.
- trtRefs = optional 1d array of character strings of the same length as
  `cVars`, specifying the level to use as the references for treatment
  contrasts. Defaults to nothing for all variables in `cVars`.

# Value

DataFrame with same variables as the original DataFrame, but categorical
variables converted to dummy contrasts.

# Some notes

If `cVars` consists of only an empty Symbol, i.e. `cVars=[Symbol()]`, this
will signal to the function that no contrasts should be created. The
original DataFrame will be returned.

"""
function contr(df::DataFrames.DataFrame, cVars::AbstractArray{Symbol,1},
               cTypes::AbstractArray{String,1}=repeat(["treat"],
                                                      inner=length(cVars)),
               trtRefs::AbstractArray= repeat([nothing], inner=length(cVars)))

    # If cVars only contains an empty Symbol, stop and return df
    if cVars == [Symbol()]
        return df
    end

    # Ensure that all Symbols in cVars are variables in df
    for cVar in cVars
        if !in(cVar, propertynames(df))
            error(string(cVar, " is not a variable in the DataFrame"))
        end
    end

    # Initialize new DataFrame
    newDf = DataFrame()

    # Iterate through variables in df
    for var in propertynames(df)
        if !in(var, cVars)
            # Add non-categorical variables to the new DataFrame
            newDf[!,var] = df[:,var]
        else
            # Convert categorical variables to specified dummy contrasts
            dummyDf = get_dummy(df, var, cTypes[var.==cVars][1],
                                trtRefs[var.==cVars][1])
            for dummy in propertynames(dummyDf)
                newDf[!,dummy] = dummyDf[:,dummy]
            end
        end
    end

    return newDf
end


module TestECDF

using QuantEcon
using Base.Test
using FactCheck

# set up
srand(42)
obs = rand(40)
e = ECDF(obs)

facts("Testing ecdf.jl") do
    # 1.1 is larger than all obs, so ecdf should be 1
    @fact ecdf(e, 1.1) --> roughly(1.0)

    # -1.0 is small than all obs, so ecdf should be 0
    @fact ecdf(e, -1.0) --> roughly(0.0)

    # larger values should have larger values on ecdf
    let x = rand()
        F_1 = ecdf(e, x)
        F_2 = ecdf(e, x*1.1)
        @fact F_1 <= F_2 --> true
    end

end  # facts
end  # module


#===============================
<-----------Lathe.jl----------->
Programmed by Emmett Boudreau
    <emmett@emmettboudreau.com>
        <http://emmettboudreau.com>
MIT General Open Source License
    (V 3.0.0)
        Free for Modification and
        Redistribution
Thank you for your forks!
<-----------Lathe.jl----------->
38d8eb38-e7b1-11e9-0012-376b6c802672
#[deps]
DataFrames.jl
Random.jl
================================#
module Lathe
using DataFrames
using Random
#================
Stats
    Module
================#
module stats
#<----Mean---->
function mean(array)
    observations = length(array)
    average = sum(array)/observations
    return(average)
end
#<----Mode---->
function mode(array)
    m = findmax(array)
    return(m)
end
#<----Variance---->
function variance(array)
    me = mean(array)
    sq = sum(array) - me
    squared_mean = sq ^ 2
    return(squared_mean)
end
#<----Confidence Intervals---->
function confiints(data, confidence=.95)
    mean = mean(data)
    std = std(data)
    stderr = standarderror(data)
#    interval = stderr * scs.t.ppf((1 + confidence) / 2.0, n-1)
#    return (mean-interval, mean+interval)
end
#<----Standard Error---->
function standarderror(data)
    std = std(data)
    sample = length(data)
    ste = (std/sqrt(sample))
    return(ste)
end
#<----Standard Deviation---->
function std(array3)
    m = mean(array3)
    [i = (i-m) ^ 2 for i in array3]
    m = mean(array3)
    m = sqrt(m)
    return(m)
end
#<---- Correlation Coefficient --->
function correlationcoeff(x,y)
    n = length(x)
    yl = length(y)
    if n != yl
        throw(ArgumentError("The array shape does not match!"))
    end
#    sx = std(x)
#    sy = std(y)
#    x̄ = mean(x)
#    ȳ = mean(x)
#    [i = (i-x̄) / sx for i in x]
#    [i = (i-ȳ) / sy for i in y]
#    n1 = n-1
#    mult = x .* y
#    sq = sum(mult)
#    corrcoff = sq / n1
    xy = x .* y
    sx = sum(x)
    sy = sum(y)
    sxy = sum(xy)
    x2 = x .^ 2
    y2 = y .^ 2
    sx2 = sum(x2)
    sy2 = sum(y2)
    r = ((n*sxy) - (sx * sy)) / (sqrt((((n*sx2)-(sx^2)) * ((n*sy2)-(sy^2)))))
    return(r)
end
#<----Z score---->
function z(array)
    x̄ = mean(array)
    σ = std(array)
    return map(x -> (x - x̄) / σ, array)
end
#<----Quartiles---->
# - First
function firstquar(array)
    m = median(array)
    q15 = array / m
    q1 = array / m
    return(q)
end
# - Second(median)
function secondquar(array)
    m = median(array)
    return(m)
end
# - Third
function thirdquar(array)
    q = median(array)
    q = q * 1.5
end
# <---- Rank ---->
function getranks(array,rev = false)
    sortedar = sort(array,rev=rev)
    num = 1
    list = []
    for i in sortedar
        append!(list,i)
        num = num + 1
    end
    return(list)
end
#-------Inferential-----------__________
#<----Inferential Summary---->
function inf_sum(data,grdata)
    #Doing our calculations
    t = independent_t(data,grdata)
    f = f_test(data,grdata)
#    low,high = confiints(data)
    var = variance(data)
    grvar = variance(grdata)
    avg = mean(data)
    gravg = mean(grdata)
    sampstd = std(data)
    grstd = std(grdata)
    #Printing them out
    println("================")
    println("     Lathe.stats Inferential Summary")
    println("     _______________________________")
    println("N: ",length(grdata))
    println("x̅: ",avg)
    println("μ: ",gravg)
    println("s: ",sampstd)
    println("σ: ",grstd)
    println("var(x): ",var)
    println("σ2: ",grvar)
#    println("Low Confidence interval: ",low)
#    println("High Confidence interval: ",high)
    println("α ",t)
    println("Fp: ",f)
    println("================")
end
#<----T Test---->
# - Independent
function independent_t(sample,general)
    sampmean = mean(sample)
    genmean = mean(general)
    samples = length(sample)
    m = genmean
    [i = (i-m) ^ 2 for i in general]
    m = mean(general)
    m = sqrt(m)
    std = m
    t = (sampmean - genmean) / (std / sqrt(samples))
    return(t)
end
# - Paired
function paired_t(var1,var2)
    d = var1 .- var2
    d̄ = mean(x)
end
#<---- Correlations ---->
# - Spearman
function spearman(var1,var2)
    rgX = getranks(var1)
    rgY = getranks(var2)
    ρ = rgX*rgY / (std(rgX)*std(rgY))
    return(ρ)
end
# - Pearson
function pearson(x,y)
    sx = std(x)
    sy = std(y)
    x̄ = mean(x)
    ȳ = mean(x)
    [i = (i-x̄) / sx for i in x]
    [i = (i-ȳ) / sy for i in y]
    n1 = n-1
    mult = x .* y
    sq = sum(mult)
    corrcoff = sq / n1
    return(corrcoff)
end
# <---- Chi Distribution --->
function chidist(x,e)
# it is tough to calculate -> is it really needed?
    # A little less necessary, as its certainly not the most useful,
    # But this stats library could serve as a foundation for models that
    # Utilize Chi-Distributions, and although I wouldn't say having
    # A function to do so is urgent, It definitely would be cool,
    # Rather than an end user having to add another package just
    # To do one or two things, if you know what I mean.
    # But certainly there are other more important things to get through
    # Before 1.0, and I wouldn't consider any of these statistics incredibly
    # Necessary, but the template is there for what I want to include,
    # So people adding the module now can kindof know what to expect.
    # So hopefully that answers your question!
end
#<---- Chi-Square ---->
function chisq(var1,var2)
    chistat(obs, exp) = (obs - exp)^2/exp
    return chistat.(x, e) |> sum
end
#<---- ANOVA ---->
function anova(var1,var2)

end
#<---- Wilcoxon ---->
# - Wilcoxon Rank-Sum Test
function wilcoxrs(var1,var2)

end
function wilcoxsr(var1,var2)

end
#<---- Binomial Distribution ---->
function binomialdist(positives,size)
    # p = n! / x!(n-x!)*π^x*(1-π)^N-x
    n = size
    x = positives
    factn = factorial(n)
    factx = factorial(x)
    nx = factn / (factx * (n-x))
    return(nx)
end
#<---- Sign Test ---->
function sign(var1,var2)
    sets = var1 .- var2
    positives = []
    negatives = []
    zeros = []
    for i in sets
        if i == 0
            zeros.append(i)
        elseif i > 0
            positives.append(i)
        elseif i < 0
            negatives.append(i)
        end
    end
    totalpos = length(positives)
    totallen = length(sets)
    ans = binomialdist(positives,totallen)
    return(ans)
end
#<---- F-Test---->
function f_test(sample,general)
    totvariance = variance(general)
    sampvar = variance(sample)
    f =  sampvar / totvariance
    return(f)
end
#-------Bayesian--------------___________
#<----Bayes Theorem---->
#P = prob, A = prior, B = Evidence,
function bay_ther(p,a,b)
    psterior = (p*(b|a) * p*(a)) / (p*b)
    return(psterior)
end
function cond_prob(p,a,b)
    psterior = bay_ther(p,a,b)
    cond = p*(a|b)
    return(cond)
end

#---------------------------
end
#================
Model
    Validation
        Module
================#
module validate
#-------Model Metrics--------____________
using Lathe
## <---- Mean Absolute Error ---->
function mae(actual,pred)
    l = length(actual)
    lp = length(pred)
    if l != lp
        throw(ArgumentError("The array shape does not match!"))
    end
    result = actual-pred
    maeunf = Lathe.stats.mean(result)
    if maeunf < 0
        maeunf = (maeunf - maeunf) - maeunf
    end
    return(maeunf)
end
# <---- R Squared ---->
function r2(actual,pred)
    l = length(actual)
    lp = length(pred)
    if l != lp
        throw(ArgumentError("The array shape does not match!"))
    end
    r = Lathe.stats.correlationcoeff(actual,pred)
    rsq = r^2
    rsq = rsq * 100
    return(rsq)
end
function binomialdistribution(actual,pred)
    # p = n! / x!(n-x!)*π^x*(1-π)^N-x
    Lathe.stats.binomialdist(pos,neg,tot)
end
# --- Get Permutation ---
function getPermutation(model)

end
#--------------------------------------------
# End
end
#================
Preprocessing
     Module
================#
module preprocess
using Random
using Lathe
#===============
Generalized
    Data
        Processing
===============#
# Train-Test-Split-----
function TrainTestSplit(df,at = 0.75)
    sample = randsubseq(1:size(df,1), at)
    trainingset = df[sample, :]
    notsample = [i for i in 1:size(df,1) if isempty(searchsorted(sample, i))]
    testset = df[notsample, :]
    return(trainingset,testset)
end
# Array-Split ----------
function ArraySplit(data, at = 0.7)
    n = length(data)
    idx = Random.shuffle(1:n)
    train_idx = view(idx, 1:floor(Int, at*n))
    test_idx = view(idx, (floor(Int, at*n)+1):n)
    data[train_idx,:], data[test_idx,:]
    return(test_idx,train_idx)
end
# Sort-Split -------------
function SortSplit(data, at = 0.25, rev=false)
  n = length(data)
  sort!(data, rev=rev)  # Sort in-place
  train_idx = view(data, 1:floor(Int, at*n))
  test_idx = view(data, (floor(Int, at*n)+1):n)
  return(test_idx,train_idx)
end
# Unshuffled Split ----
function Uniform_Split(data, at = 0.7)
    n = length(data)
    idx = data
    train_idx = view(idx, 1:floor(Int, at*n))
    test_idx = view(idx, (floor(Int, at*n)+1):n)
    data[train_idx,:], data[test_idx,:]
    return(test_idx,train_idx)
end
#=======
Numerical
    Scaling
=======#
# ---- Rescalar (Standard Deviation) ---
function Rescalar(array)
    v = []
    min = minimum(array)
    max = maximum(array)
    for i in array
        x = (i-min) / (max - min)
        append!(v,x)
    end
    return(v)
end
# ---- Arbitrary Rescalar ----
function ArbitraryRescale(array)
    v = []
    a = minimum(array)
    b = maximum(array)
    for i in array
        x = a + ((i-a*i)*(b-a)) / (b-a)
        append!(v,x)
    end
    return(v)
end
# ---- Mean Normalization ----
function MeanNormalization(array)
    avg = Lathe.stats.mean(array)
    v = []
    a = minimum(array)
    b = maximum(array)
    for i in array
        m = (i-avg) / (b-a)
        append!(v,m)
    end
end
# ---- Z Normalization ----
function StandardScalar(array)
    q = Lathe.stats.std(array)
    avg = Lathe.stats.mean(array)
    v = []
    for i in array
        y = (i-avg) / q
        append!(v,y)
    end
    return(v)
end
# ---- Unit L-Scale normalize ----
function Unit_LScale(array)

end
#==========
Categorical
    Encoding
==========#
# <---- One Hot Encoder ---->
function OneHotEncode(array::Number)
    flatarr = Iterators.flatten(array)
    len = size(flatarr, 2)
    poslen = size(unique(flatarr), 2)
    out = Array{Number}(undef, len, poslen)
    for i in 1:len
        el = flatarr[i]
        idx = findall(x -> x == el, flatarr |> unique)[1][2]
        out[i, idx] = 1
    end
    return(out)
end
#-----------------------------
end
#================
Predictive
    Learning
        Models
================#
module models
#==
Baseline
    Model
==#
using Lathe
using Random
#Show models shows all the models that are stable
#And ready for use in the library
function showmodels()
    println("    Lathe.JL    ")
    println("________________")
    println("Current")
    println("    Usable")
    println("       Models")
    println("================")
    println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Continuous Models")
    println("-----------------")
    println("_____Linear_____")
    println("meanBaseline(y)")
    println("LinearLeastSquare(x,y,Type)")
    println("LinearRegression(x,y)")
    println("-----------------")
    println("___Non-Linear___")
    println("FourSquare(x,y)")
    println("ExponentialScalar(x,y)")
end
#Takes model, and X to predict, and returns a y prediction
function predict(m,x)
    if typeof(m) == FourSquare
        y_pred = pred_foursquare(m,x)
    end
    if typeof(m) == majBaseline
        y_pred = pred_catbaseline(m,x)
    end
    if typeof(m) == RegressionTree
        y_pred = pred_regressiontree(m,x)
    end
    if typeof(m) == LinearRegression
        y_pred = pred_LinearRegression(m,x)
    end
    if typeof(m) == meanBaseline
        y_pred = pred_meanbaseline(m,x)
    end
    if typeof(m) == RidgeRegression
        y_pred = pred_ridgeregression(m,x)
    end
    if typeof(m) == LinearLeastSquare
        y_pred = pred_linearleastsquare(m,x)
    end
    if typeof(m) == LogisticRegression
        y_pred = pred_logisticregression(m,x)
    end
    if typeof(m) == ExponentialScalar
        y_pred = pred_exponentialscalar(m,x)
    end
    if typeof(m) == MultipleLinearRegression
        y_pred = pred_multiplelinearregression(m,x)
    end
    return(y_pred)
end
# The help function:
function help(args)
    if typeof(args) == LogisticRegression
        println("Logistic")
    elseif typeof(args) == meanBaseline
        println("MeanBaseline")
    end
end
#======================================================================
=======================================================================
            CONTINUOS MODELS               CONTINUOS MODELS
            CONTINUOS MODELS               CONTINUOS MODELS
======================================================================
======================================================================#
#==
Mean
    Baseline
==#
 # Model Type
mutable struct meanBaseline
    y
end
#----  Callback
function pred_meanbaseline(m,xt)
    e = []
    m = Lathe.stats.mean(m.y)
    print("-Lathe.models Mean Baseline-")
    print("mean: ",m)
    for i in xt
        append!(e,m)
    end
    return(e)
end
#==
Multi-
    Gap
 - A quad range predictor, on steroids. -
==#
# Model Type
mutable struct RegressionTree
    x
    y
    n_divisions
    divisionsize
end
#----  Callback
function pred_regressiontree(m,xt)
    # x = q1(r(floor:q1)) |x2 = q2(r(q1:μ)) |x3 = q3(r(q2:q3)) |x4 q4(r(q3:cieling))
    # y' = q1(x * (a / x)) | μ(x * (a / x2)) | q3(x * (a / x3) | q4(x * (a / x4))
    # Original 4 quartile math ^^
        x = m.x
        y = m.y
        xtcopy = xt
        divs = m.n_divisions
        size = m.divisionsize
        # Go ahead and throw an error for the wrong input shape:
        xlength = length(x)
        ylength = length(y)
        if xlength != ylength
            throw(ArgumentError("The array shape does not match!"))
        end
        # Now we also need an error for when the total output of the
        #    division size and n divisions is > 100 percent
        divisions = size * divs
        if divisions != 1
            throw(ArgumentError("Invalid hyperparameters!: divisions * number of
            divisions must be = to 100 percent!"))
        end
        # Empty list
        e = []
        while divs > 0
            predictorx,x = Lathe.preprocess.SortSplit(x,size)
            predictory,y = Lathe.preprocess.SortSplit(y,size)
            predictorxt,xtcopy = Lathe.preprocess.SortSplit(xtcopy,size)
            currentrange = (minimum(predictorxt):maximum(predictorxt))
            linregmod = LinearRegression(predictorx,predictory)
            # Recursion replacement method:
            [predict(LinearRegression(predictorx,
            predictory),x) for x in currentrange]
            divs = divs - 1
        end
        return(xt)
end
#==
Four
    Square
==#
# Model Type
mutable struct FourSquare
    x
    y
end
#----  Callback
function pred_foursquare(m,xt)
    # x = q1(r(floor:q1)) |x2 = q2(r(q1:μ)) |x3 = q3(r(q2:q3)) |x4 q4(r(q3:cieling))
    # y' = q1(x * (a / x)) | μ(x * (a / x2)) | q3(x * (a / x3) | q4(x * (a / x4))
        x = m.x
        y = m.y
        # Go ahead and throw an error for the wrong input shape:
        xlength = length(x)
        ylength = length(y)
        if xlength != ylength
            throw(ArgumentError("The array shape does not match!"))
        end
        # Our empty Y prediction list==
        e = []
        # Quad Splitting the data ---->
        # Split the Y
        y2,range1 = Lathe.preprocess.SortSplit(y)
        y3,range2 = Lathe.preprocess.SortSplit(y2)
        y4,range3 = Lathe.preprocess.SortSplit(y3)
        y5,range4 = Lathe.preprocess.SortSplit(y4)
        yrange5 = y5
        # Split the x train
        x1,xrange1 = Lathe.preprocess.SortSplit(x)
        x2,xrange2 = Lathe.preprocess.SortSplit(x1)
        x3,xrange3 = Lathe.preprocess.SortSplit(x2)
        x4,xrange4 = Lathe.preprocess.SortSplit(x3)
        xrange5 = y5
        # Fitting the 4 linear regression models ---->
        regone = LinearLeastSquare(xrange1,range1)
        regtwo = LinearLeastSquare(xrange2,range2)
        regthree = LinearLeastSquare(xrange3,range3)
        regfour = LinearLeastSquare(xrange4,range4)
        regfive = LinearLeastSquare(xrange5,yrange5)
        # Split the train Data
        xt1,xtrange1 = Lathe.preprocess.SortSplit(xt)
        xt2,xtrange2 = Lathe.preprocess.SortSplit(xt1)
        xt3,xtrange3 = Lathe.preprocess.SortSplit(xt2)
        xt4,xtrange4 = Lathe.preprocess.SortSplit(xt3)
        xtrange5 = xt4
        # Get min-max
        xtrange1min = minimum(xtrange1)
        xtrange1max = maximum(xtrange1)
        xtrange2min = minimum(xtrange2)
        xtrange2max = maximum(xtrange2)
        xtrange3min = minimum(xtrange3)
        xtrange3max = maximum(xtrange3)
        xtrange4min = minimum(xtrange4)
        xtrange4max = maximum(xtrange4)
        xtrange5min = minimum(xtrange5)
        # Ranges for ifs
        condrange1 = (xtrange1min:xtrange1max)
        condrange2 = (xtrange2min:xtrange2max)
        condrange3 = (xtrange3min:xtrange3max)
        condrange4 = (xtrange4min:xtrange4max)
        # This for loop is where the dimension's are actually used:
        for i in xt
            if i in condrange1
                ypred = predict(regone,i)
            elseif i in condrange2
                ypred = predict(regtwo,i)
            elseif i in condrange3
                ypred = predict(regthree,i)
            elseif i in condrange4
                ypred = predict(regfour,i)
            else
                ypred = predict(regfive,i)
            end

            append!(e,ypred)
        end
        return(e)
end
#==
Isotonic
    Regression
==#
mutable struct IsotonicRegression
    x
    y
end
function pred_isotonicregression(m,xt)
    if length(m.x) != length(m.y)
        throw(ArgumentError("The array shape does not match!"))
    end
end
#==
Multiple
    Linear
        Regression
==#
mutable struct MultipleLinearRegression
    x
    y
end
function pred_multiplelinearregression(m,xt)
    if length(m.x) != length(m.y)
        throw(ArgumentError("The array shape does not match!"))
    end
    if length(m.x) != length(xt)
        throw(ArgumentError("Bad Feature Shape |
        Training Features are not equal!",))
    end
    y_pred = []
    for z in xt
        predavg = []
        for i in matrice
            m = LinearRegression(i,y)
            pred = predict(m,z)
            append!(predavg,pred)
        end
        mn = Lathe.stats.mean(predavg)
        append!(y_pred,mn)
        return(y_pred)
    end
end
#==
Linear
    Regression
==#
mutable struct LinearRegression
    x
    y
end
#----  Callback
function pred_LinearRegression(m,xt)
    # a = ((∑y)(∑x^2)-(∑x)(∑xy)) / (n(∑x^2) - (∑x)^2)
    # b = (x(∑xy) - (∑x)(∑y)) / n(∑x^2) - (∑x)^2
    if length(m.x) != length(m.y)
        throw(ArgumentError("The array shape does not match!"))
    end
    # Get our x and y as easier variables
    x = m.x
    y = m.y
    # Get our Summatations:
    Σx = sum(x)
    Σy = sum(y)
    # dot x and y
    xy = x .* y
    # ∑dot x and y
    Σxy = sum(xy)
    # dotsquare x
    x2 = x .^ 2
    # ∑ dotsquare x
    Σx2 = sum(x2)
    # n = sample size
    n = length(x)
    # Calculate a
    a = (((Σy) * (Σx2)) - ((Σx * (Σxy)))) / ((n * (Σx2))-(Σx^2))
    # Calculate b
    b = ((n*(Σxy)) - (Σx * Σy)) / ((n * (Σx2)) - (Σx ^ 2))
    [i = a+(b*i) for i in xt]
    return(xt)
end
#==
Linear
    Least
     Square
==#
mutable struct LinearLeastSquare
    x
    y
    Type
end
function pred_linearleastsquare(m,xt)
    if length(m.x) != length(m.y)
        throw(ArgumentError("The array shape does not match!"))
    end
    if m.Type == :REG
        x = m.x
        y = m.y
        # Summatation of x*y
        xy = x .* y
        sxy = sum(xy)
        # N
        n = length(x)
        # Summatation of x^2
        x2 = x .^ 2
        sx2 = sum(x2)
        # Summatation of x and y
        sx = sum(x)
        sy = sum(y)
        # Calculate the slope:
        slope = ((n*sxy) - (sx * sy)) / ((n * sx2) - (sx)^2)
        # Calculate the y intercept
        b = (sy - (slope*sx)) / n
        # Empty prediction list:
        y_pred = []
        for i in xt
            pred = (slope*i)+b
            append!(y_pred,pred)
        end
    end
    if m.Type == :OLS

    end
    if m.Type == :WLS

    end
    if m.Type == :GLS

    end
    if m.Type == :GRG

    end
    return(y_pred)
end
#==
Ridge
    Regression
==#
mutable struct RidgeRegression
    x
    y
end
function pred_ridgeregression(m,xt)
    if length(m.x) != length(m.y)
        throw(ArgumentError("The array shape does not match!"))
    end
end
#==
Logistic
    Regression
==#
mutable struct LogisticRegression
    x
    y
end
function pred_logisticregression(m,xt)
    if length(m.x) != length(m.y)
        throw(ArgumentError("The array shape does not match!"))
    end

end
#==
Linear
    Scalar
==#
mutable struct ExponentialScalar
    x
    y
end
function pred_exponentialscalar(m,xt)
    x = m.x
    y = m.y
    at = 0.25
    xdiv1,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv2,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv3,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv4,x = Lathe.preprocess.SortSplit(x,.05)
    ydiv1,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv2,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv3,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv4,y = Lathe.preprocess.SortSplit(y,.05)
    scalarlist1 = ydiv1 ./ xdiv1
    scalarlist2 = ydiv2 ./ xdiv2
    scalarlist3 = ydiv3 ./ xdiv3
    scalarlist4 = ydiv3 ./ xdiv3
    xdiv1,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv2,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv3,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv4,x = Lathe.preprocess.SortSplit(x,.05)
    ydiv1,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv2,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv3,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv4,y = Lathe.preprocess.SortSplit(y,.05)
    scalarlist6 = ydiv1 ./ xdiv1
    scalarlist7 = ydiv2 ./ xdiv2
    scalarlist8 = ydiv3 ./ xdiv3
    scalarlist9 = ydiv3 ./ xdiv3
    xdiv1,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv2,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv3,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv4,x = Lathe.preprocess.SortSplit(x,.05)
    ydiv1,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv2,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv3,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv4,y = Lathe.preprocess.SortSplit(y,.05)
    scalarlist10 = ydiv1 ./ xdiv1
    scalarlist11 = ydiv2 ./ xdiv2
    scalarlist12 = ydiv3 ./ xdiv3
    scalarlist13 = ydiv3 ./ xdiv3
    xdiv1,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv2,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv3,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv4,x = Lathe.preprocess.SortSplit(x,.05)
    ydiv1,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv2,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv3,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv4,y = Lathe.preprocess.SortSplit(y,.05)
    scalarlist14 = ydiv1 ./ xdiv1
    scalarlist15 = ydiv2 ./ xdiv2
    scalarlist16 = ydiv3 ./ xdiv3
    scalarlist17 = ydiv3 ./ xdiv3
    xdiv1,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv2,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv3,x = Lathe.preprocess.SortSplit(x,.05)
    xdiv4,x = Lathe.preprocess.SortSplit(x,.05)
    ydiv1,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv2,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv3,y = Lathe.preprocess.SortSplit(y,.05)
    ydiv4,y = Lathe.preprocess.SortSplit(y,.05)
    scalarlist18 = ydiv1 ./ xdiv1
    scalarlist19 = ydiv2 ./ xdiv2
    scalarlist20 = y ./ x
    # Now we sortsplit the x train
    xtdiv1,xt2 = Lathe.preprocess.SortSplit(xt,.05)
    xtdiv2,xt2 = Lathe.preprocess.SortSplit(xt2,.05)
    xtdiv3,xt2 = Lathe.preprocess.SortSplit(xt2,.05)
    xtdiv4,null = Lathe.preprocess.SortSplit(xt2,.05)
    range1 = minimum(xtdiv1):maximum(xtdiv1)
    range2 = minimum(xtdiv2):maximum(xtdiv2)
    range3 = minimum(xtdiv3):maximum(xtdiv3)
    range4 = minimum(xtdiv4):maximum(xtdiv4)
    range5 = minimum(null):maximum(null)
    returnlist = []
    for i in xt
        if i in range1
            res = i * rand(scalarlist1)
            append!(returnlist,res)
        elseif i in range2
            predlist = []
            res = i * rand(scalarlist2)
            append!(returnlist,res)

        elseif i in range3
            predlist = []
            res = i * rand(scalarlist3)
            append!(returnlist,res)
        elseif i in range4
            predlist = []
            res = i * rand(scalarlist4)
            append!(returnlist,res)
        else
            predlist = []
            res = i * rand(scalarlist20)
            append!(returnlist,res)
        end
    end
    return(returnlist)
end
#======================================================================
=======================================================================
            CATEGORICAL MODELS             CATEGORICAL MODELS
            CATEGORICAL MODELS             CATEGORICAL MODELS
======================================================================
======================================================================#
#==
Majority
    Class
        Baseline
==#
# Model Type
mutable struct majBaseline
    y
end
#----  Callback
function pred_catbaseline(m,xt)
    y = m.y
    e = []
    mode = Lathe.stats.mode(xt)
    for i in xt
        append!(e,i)
    end

end
#
#----------------------------------------------
end
#================
Pipeline
    Module
================#
module Pipelines

# Note to future self, or other programmer:
# It is not necessary to store these as constructors!
# They can just be strings, and use the model's X and Y!
using Lathe
mutable struct Pipeline
    model
    categoricalenc
    contenc
    imputer
end
mutable struct fitpipeline
    pipeline
    catx
    conx
    y
end
function pipe_predict(fitpipeline,xtcats,xtcons)
    """ Takes a fit pipeline, and an X and predicts. """
    fitpipeline.conx = fitpipeline.pipeline.contenc(x)
    ypr = Lathe.models.predict(fitpipeline.pipeline.model(fitpipeline.conx,
    fitpipeline.y),xt)
    return(ypr)
end
function serialize(fitpipeline,filename)
    """Outputs pipeline as sav file."""
end
#----------------------------------------------
end
#==
This is the end of the main
module, nothing is to be written
beyond here
==#
end


# Disjoint sets

############################################################
#
#   A forest of disjoint sets of integers
#
#   Since each element is an integer, we can use arrays
#   instead of dictionary (for efficiency)
#
#   Disjoint sets over other key types can be implemented
#   based on an IntDisjointSets through a map from the key
#   to an integer index
#
############################################################

_intdisjointsets_bounds_err_msg(T) = "the maximum number of elements in IntDisjointSets{$T} is $(typemax(T))"

"""
    IntDisjointSets{T<:Integer}(n::Integer)

A forest of disjoint sets of integers, which is a data structure
(also called a union–find data structure or merge–find set)
that tracks a set of elements partitioned
into a number of disjoint (non-overlapping) subsets.
"""
mutable struct IntDisjointSets{T<:Integer}
    parents::Vector{T}
    ranks::Vector{T}
    ngroups::T

    # creates a disjoint set comprised of n singletons
    # IntDisjointSets(n::T) where {T<:Integer} = new(collect(Base.OneTo(n)), zeros(T, n), n)
end

IntDisjointSets(n::T) where {T<:Integer} = IntDisjointSets{T}(collect(Base.OneTo(n)), zeros(T, n), n)
IntDisjointSets{T}(n::Integer) where {T<:Integer} = IntDisjointSets{T}(collect(Base.OneTo(T(n))), zeros(T, T(n)), T(n))
Base.length(s::IntDisjointSets) = length(s.parents)

"""
    num_groups(s::IntDisjointSets)

Get a number of groups.
"""
num_groups(s::IntDisjointSets) = s.ngroups
Base.eltype(::Type{IntDisjointSets{T}}) where {T<:Integer} = T

# find the root element of the subset that contains x
# path compression is implemented here
function find_root_impl!(parents::Vector{T}, x::Integer) where {T<:Integer}
    p = parents[x]
    @inbounds if parents[p] != p
        parents[x] = p = _find_root_impl!(parents, p)
    end
    return p
end

# unsafe version of the above
function _find_root_impl!(parents::Vector{T}, x::Integer) where {T<:Integer}
    @inbounds p = parents[x]
    @inbounds if parents[p] != p
        parents[x] = p = _find_root_impl!(parents, p)
    end
    return p
end

"""
    find_root!(s::IntDisjointSets{T}, x::T)

Find the root element of the subset that contains an member x.
Path compression is implemented here.
"""
find_root!(s::IntDisjointSets{T}, x::T) where {T<:Integer} = find_root_impl!(s.parents, x)

"""
    in_same_set(s::IntDisjointSets{T}, x::T, y::T)

Returns `true` if `x` and `y` belong to the same subset in `s` and `false` otherwise.
"""
in_same_set(s::IntDisjointSets{T}, x::T, y::T) where {T<:Integer} = find_root!(s, x) == find_root!(s, y)

"""
    union!(s::IntDisjointSets{T}, x::T, y::T)

Merge the subset containing x and that containing y into one
and return the root of the new set.
"""
function Base.union!(s::IntDisjointSets{T}, x::T, y::T) where {T<:Integer}
    parents = s.parents
    xroot = find_root_impl!(parents, x)
    yroot = find_root_impl!(parents, y)
    xroot != yroot ? root_union!(s, xroot, yroot) : xroot
end

"""
    root_union!(s::IntDisjointSets{T}, x::T, y::T)

Form a new set that is the union of the two sets whose root elements are
x and y and return the root of the new set.
Assume x ≠ y (unsafe).
"""
function root_union!(s::IntDisjointSets{T}, x::T, y::T) where {T<:Integer}
    parents = s.parents
    rks = s.ranks
    @inbounds xrank = rks[x]
    @inbounds yrank = rks[y]

    if xrank < yrank
        x, y = y, x
    elseif xrank == yrank
        rks[x] += one(T)
    end
    @inbounds parents[y] = x
    s.ngroups -= one(T)
    return x
end

"""
    push!(s::IntDisjointSets{T})

Make a new subset with an automatically chosen new element x.
Returns the new element. Throw an `ArgumentError` if the
capacity of the set would be exceeded.
"""
function Base.push!(s::IntDisjointSets{T}) where {T<:Integer}
    l = length(s)
    l < typemax(T) || throw(ArgumentError(_intdisjointsets_bounds_err_msg(T)))
    x = l + one(T)
    push!(s.parents, x)
    push!(s.ranks, zero(T))
    s.ngroups += one(T)
    return x
end

"""
    DisjointSets{T}(xs)

A forest of disjoint sets of arbitrary value type T.

It is a wrapper of IntDisjointSets{Int}, which uses a
dictionary to map the input value to an internal index.
"""
mutable struct DisjointSets{T} <: AbstractSet{T}
    intmap::Dict{T,Int}
    revmap::Vector{T}
    internal::IntDisjointSets{Int}

    DisjointSets{T}() where T = new{T}(Dict{T,Int}(), Vector{T}(), IntDisjointSets(0))
    function DisjointSets{T}(xs) where T    # xs must be iterable
        imap = Dict{T,Int}()
        rmap = Vector{T}()
        n = length(xs)
        sizehint!(imap, n)
        sizehint!(rmap, n)
        id = 0
        for x in xs
            imap[x] = (id += 1)
            push!(rmap,x)
        end
        new{T}(imap, rmap, IntDisjointSets(n))
    end
end

DisjointSets() = DisjointSets{Any}()
DisjointSets(xs::T...) where T = DisjointSets{T}(xs)
DisjointSets{T}(xs::T...) where T = DisjointSets{T}(xs)
DisjointSets(xs) = _DisjointSets(xs, Base.IteratorEltype(xs))
_DisjointSets(xs, ::Base.HasEltype) = DisjointSets{eltype(xs)}(xs)
function _DisjointSets(xs, ::Base.EltypeUnknown)
    T = Base.@default_eltype(xs)
    (isconcretetype(T) || T === Union{}) || return Base.grow_to!(DisjointSets{T}(), xs)
    return DisjointSets{T}(xs)
end

Base.iterate(s::DisjointSets) = iterate(s.revmap)
Base.iterate(s::DisjointSets, i) = iterate(s.revmap, i)

Base.length(s::DisjointSets) = length(s.internal)

"""
    num_groups(s::DisjointSets)

Get a number of groups.
"""
num_groups(s::DisjointSets) = num_groups(s.internal)
Base.eltype(::Type{DisjointSets{T}}) where T = T
Base.empty(s::DisjointSets{T}, ::Type{U}=T) where {T,U} = DisjointSets{U}()
function Base.sizehint!(s::DisjointSets, n::Integer)
    sizehint!(s.intmap, n)
    sizehint!(s.revmap, n)
    return s
end

"""
    find_root!{T}(s::DisjointSets{T}, x::T)

Finds the root element of the subset in `s` which has the element `x` as a member.
"""
find_root!(s::DisjointSets{T}, x::T) where {T} = s.revmap[find_root!(s.internal, s.intmap[x])]

"""
    in_same_set(s::DisjointSets{T}, x::T, y::T)

Returns `true` if `x` and `y` belong to the same subset in `s` and `false` otherwise.
"""
in_same_set(s::DisjointSets{T}, x::T, y::T) where {T} = in_same_set(s.internal, s.intmap[x], s.intmap[y])

"""
    union!(s::DisjointSets{T}, x::T, y::T)

Merge the subset containing x and that containing y into one
and return the root of the new set.
"""
Base.union!(s::DisjointSets{T}, x::T, y::T) where {T} = s.revmap[union!(s.internal, s.intmap[x], s.intmap[y])]

"""
    root_union!(s::DisjointSets{T}, x::T, y::T)

Form a new set that is the union of the two sets whose root elements are
x and y and return the root of the new set.
Assume x ≠ y (unsafe).
"""
root_union!(s::DisjointSets{T}, x::T, y::T) where {T} = s.revmap[root_union!(s.internal, s.intmap[x], s.intmap[y])]

"""
    push!(s::DisjointSets{T}, x::T)

Make a new subset with an automatically chosen new element x.
Returns the new element.
"""
function Base.push!(s::DisjointSets{T}, x::T) where T
    id = push!(s.internal)
    s.intmap[x] = id
    push!(s.revmap,x) # Note, this assumes invariant: length(s.revmap) == id
    return x
end


using ESDL
using Test
using Dates
using IterTools
using WeightedOnlineStats

c=Cube()
d = getCubeData(c,variable=["air_temperature_2m", "gross_primary_productivity", "soil_moisture"],longitude=(30,31),latitude=(50,51),
              time=(Date("2002-01-01"),Date("2008-12-31")))
mytable = CubeTable(variable=d,include_axes=("lon", "lat", "time"),fastest="variable")

mytable2 = CubeTable(data=d,include_axes=("lon", "lat", "time", "variable"),fastest="variable")

@testset "cubefittable and WeightedCovMatrix fittable" begin
    covmCube = cubefittable(mytable, WeightedCovMatrix, :variable, weight=(x->cosd(x.lat)),showprog=false)
    newtab = IterTools.partition(mytable,3)
    covmVal = WeightedCovMatrix()
    for row in newtab
        obs = [row[i].variable for i in 1:length(row)]
        obslat = row[1].lat
        if !any(ismissing.(obs))
            fit!(covmVal, obs, cosd(obslat))
        end
    end
    @test all(isapprox.(covmCube.data, value(covmVal)))
end

@testset "remaining functions in tablestats.jl" begin
    meanCube = cubefittable(mytable2, WeightedMean, :data, weight=(x->cosd(x.lat)),
                            by=(:variable,))
    weightedTableMeans = fittable(mytable2, WeightedMean, :data, by=(:variable,),
                                    weight=(x -> cosd(x.lat)))
    foreach(enumerate(["air_temperature_2m", "gross_primary_productivity", "soil_moisture"])) do (i,v)
      @test isapprox(value(weightedTableMeans)[(v,)], meanCube.data[i])
    end
end


import Base.rand

import StatsBase.quantile
import StatsBase.median
import Distributions.support

import Distributions.ecdf
import Base:
    minimum, maximum,
    max, min

"""
    UncertainScalarKDE(d::KernelDensity.UnivariateKDE, values::AbstractVector{T}, range, pdf)

An empirical value represented by a distribution estimated from actual data.

## Fields

- **`distribution`**: The `UnivariateKDE` estimate for the distribution of `values`.
- **`values`**: The values from which `distribution` is estimated.
- **`range`**: The values for which the pdf is estimated.
- **`pdf`**: The values of the pdf at each point in `range`.
"""
struct UncertainScalarKDE{T} <: AbstractUncertainScalarKDE{T}
    distribution::KernelDensity.UnivariateKDE
    values::AbstractVector{T}
    range
    pdf::StatsBase.Weights
end

"""
    TruncatedUncertainScalarKDE

A truncated [`UncertainScalarKDE`](@ref).
"""
struct TruncatedUncertainScalarKDE{T} <: AbstractUncertainScalarKDE{T}
    distribution::KernelDensity.UnivariateKDE
    values::AbstractVector{T}
    range
    pdf::StatsBase.Weights
end

function summarise(uv::AbstractUncertainScalarKDE{T}) where {T}
    dist = uv.distribution
    range = uv.range
    dist = typeof(uv.distribution)
    _type = typeof(uv)
    "$_type($dist, range = $range)"
end

Base.show(io::IO, uv::AbstractUncertainScalarKDE{T}) where {T} = print(io, summarise(uv))

"""
    rand(uv::AbstractUncertainScalarKDE)

Sample a random number from an uncertain value represented by a kernel
density estimate.
"""
function rand(uv::AbstractUncertainScalarKDE)
    # Box width
    δ = step(uv.range)

    # Sample a box
    sampled_val = sample(uv.range, uv.pdf)

    # Sample uniformly from within the box
    rand(Uniform(sampled_val, sampled_val + δ))
end

"""
    rand(uv::AbstractUncertainScalarKDE, n::Int)

Sample a random number from an uncertain value represented by a kernel
density estimate.
"""
function rand(uv::AbstractUncertainScalarKDE, n::Int)
    # Box width
    δ = step(uv.range)

    # Sample n boxes according to estimated pdf
    sampled_vals = Vector{Float64}(undef, n)
    sample!(uv.range, uv.pdf, sampled_vals)

    # Sample uniformly from within each box
    [rand(Uniform(sampled_vals[i], sampled_vals[i] + δ)) for i = 1:n]
end

"""
    ecdf(uv::AbstractUncertainScalarKDE)

Empirical cumulative distribution function for an uncertain value approximated
by kernel density estimation.
"""
ecdf(uv::AbstractUncertainScalarKDE) = cumsum(uv.pdf)

"""
The mode (most likely value) of an uncertain value represented by a
kernel density estimate.
"""
mode(uv::AbstractUncertainScalarKDE) = uv.range(findmax(uv.distribution.density)[2])

"""
    quantile(uv::UncertainScalarKDE, q)

Return the `q`-th quantile of the distribution furnishing the uncertain value.
"""
function quantile(uv::AbstractUncertainScalarKDE{T}, q) where T
    uv.range[findfirst(ecdf(uv) .> q)]
end

median(uv::AbstractUncertainScalarKDE{T}) where T = quantile(uv, 0.5)

"""
    support(uv::UncertainScalarKDE)

Return the support of an uncertain value furnished by a kernel density
estimate.
"""
support(uv::AbstractUncertainScalarKDE{T}) where T =
    interval(minimum(uv.range), maximum(uv.range))

"""
    getquantileindex(uv::AbstractUncertainScalarKDE, q::Float64)

Return the index of the range/density value corresponding to the `q`-th quantile
of an uncertain value furnished by a kernel density estimate.
"""
function getquantileindex(uv::AbstractUncertainScalarKDE{T}, q::Float64) where T
    findfirst(ecdf(uv) .> q)
end

minimum(uv::AbstractUncertainScalarKDE) = minimum(uv.range)
maximum(uv::AbstractUncertainScalarKDE) = maximum(uv.range)

min(uv::AbstractUncertainScalarKDE) = minimum(uv.range)
max(uv::AbstractUncertainScalarKDE) = maximum(uv.range)

export
AbstractUncertainScalarKDE,
UncertainScalarKDE
ecdf,
support,
getquantileindex,
UnivariateKDE,
minimum,
maximum


include("maximin_core.jl")
include("cc_maximin_core.jl")
include("robust_maximin_core.jl")
include("minimax_core.jl")


@reexport using Distributions
import Distributions: mean, var, logpdf, pdf, cdf, maximum, minimum, insupport, quantile
export ContinuousUnivariateCopula

"""
    ContinuousUnivariateCopula(d, c0, c1, c2)

The distribution with density `c * f(x) * (c0 + c1 * x + c2 * x^2)`, where `f`
is the density of the base distribution `d` and `c` is the normalizing constant.
"""
struct ContinuousUnivariateCopula{
    DistT <: ContinuousUnivariateDistribution,
    T     <: Real
    } <: ContinuousUnivariateDistribution
    d  :: DistT
    c0 :: T
    c1 :: T
    c2 :: T
    c  :: T # normalizing constant
end

function ContinuousUnivariateCopula(
    d  :: DistT,
    c0 :: T,
    c1 :: T,
    c2 :: T) where {DistT <: ContinuousUnivariateDistribution, T <: Real}
    c  = inv(c0 + c1 * mean(d) + c2 * (var(d) + abs2(mean(d))))
    Tc = typeof(c)
    ContinuousUnivariateCopula(d, Tc(c0), Tc(c1), Tc(c2), c)
end

minimum(d::ContinuousUnivariateCopula) = minimum(d.d)
maximum(d::ContinuousUnivariateCopula) = maximum(d.d)
insupport(d::ContinuousUnivariateCopula) = insupport(d.d)

"""
    mvsk_to_absm(μ, σ², sk, kt)

Convert mean `μ`, variance `σ²`, skewness `sk`, and kurtosis `kt` to first four
moments about zero. See formula at <https://en.wikipedia.org/wiki/Central_moment#Relation_to_moments_about_the_origin>.
"""
function mvsk_to_absm(μ, σ², sk, kt)
    σ = sqrt(σ²)
    m1 = μ
    m2 = σ² + abs2(μ)
    m3 = sk * σ^3 + 3μ * m2 - 2μ^3
    m4 = kt * abs2(σ²) + 4μ * m3 - 6 * abs2(μ) * m2 + 3μ^4
    m1, m2, m3, m4
end

function mean(d::ContinuousUnivariateCopula)
    μ, σ², sk, kt = mean(d.d), var(d.d), skewness(d.d), kurtosis(d.d, false)
    m1, m2, m3, _ = mvsk_to_absm(μ, σ², sk, kt)
    d.c * (d.c0 * m1 + d.c1 * m2 + d.c2 * m3)
end

function var(d::ContinuousUnivariateCopula)
    μ, σ², sk, kt = mean(d.d), var(d.d), skewness(d.d), kurtosis(d.d, false)
    _, m2, m3, m4 = mvsk_to_absm(μ, σ², sk, kt)
    d.c * (d.c0 * m2 + d.c1 * m3 + d.c2 * m4) - abs2(mean(d))
end

function logpdf(d::ContinuousUnivariateCopula, x::Real)
    log(d.c * (d.c0 + d.c1 * x + d.c2 * abs2(x))) + logpdf(d.d, x)
end

function pdf(d::ContinuousUnivariateCopula, x::Real)
    d.c * pdf(d.d, x) * (d.c0 + d.c1 * x + d.c2 * abs2(x))
end

# this function specialized to Normal base distribution
function cdf(d::ContinuousUnivariateCopula{Normal{T},T}, x::Real) where T <: Real
    μ, σ    = d.d.μ, d.d.σ
    z       = (x - μ) / σ
    result  = (d.c0 + d.c1 * μ + d.c2 * abs2(μ)) * cdf(Normal(), z)
    result += (d.c1 + 2d.c2 * μ) * σ / sqrt(2π) *
        ifelse(z < 0, - ccdf(Chi(2), -z), cdf(Chi(2), z) - 1)
    result += d.c2 * abs2(σ) / 2 * ifelse(z < 0, ccdf(Chi(3), -z), cdf(Chi(3), z) + 1)
    result *= d.c
end

quantile(d::ContinuousUnivariateCopula, p::Real) = Distributions.quantile_newton(d, p, mean(d))


module BayesianIntegral

using LinearAlgebra
using Random

include("integrate_given_hyperparameters.jl")
export gaussian_kernel, marginal_gaussian_kernel, K_matrix
export gaussian_kernel_hyperparameters, bayesian_integral_gaussian_exponential
export marginal_likelihood_gaussian_derivatives, log_likelihood
export correlation_vector_of_a_point
include("calibrate_weights.jl")
export calibrate_by_ML_with_SGD, RProp_params, calibrate_by_ML_with_Rprop

end


function corrcov(C::AbstractMatrix)
    #should check if C is positive semidefinite

    sigma = sqrt.(diag(C))
    return C ./ (sigma*sigma')
end


using ESDL
using Test
using Dates
import Base.Iterators
using Distributed
using Statistics
addprocs(2)
@everywhere using ESDL, Statistics

@everywhere function sub_and_return_mean(xout1,xout2,xin)
    m=mean(skipmissing(xin))
    for i=1:length(xin)
        xout1[i]=xin[i]-m
    end
    xout2[1]=m
end
function sub_and_return_mean(c::ESDL.AbstractCubeData)
  mapCube(sub_and_return_mean,c,
  indims=InDims("Time"),
  outdims=(OutDims("Time"),OutDims()))
end

function doTests()
  # Test simple Stats first
  c=Cube()

  d = subsetcube(c,variable="air_temperature_2m",lon=(10,11),lat=(50,51),
                time=(Date("2002-01-01"),Date("2008-12-31")))

  dmem=readcubedata(d)

  @testset "Simple statistics using mapslices" begin
  # Basic statistics
  m=mapslices(mean∘skipmissing,d,dims = "Time")

  @test isapprox(readcubedata(m).data,[282.04703 281.84494 281.65927 282.2035;
    281.90726 281.59995 281.69135 282.38644;
    281.84158 281.35898 281.66083 282.49042;
    281.92462 281.12613 281.50513 282.4352])

  #Test Spatial meann along laitutde axis
  d1=subsetcube(c,variable="gross_primary_productivity",time=(Date("2002-01-01"),Date("2002-01-01")),lon=(30,30))

  dmem=readcubedata(d1)
  mtime=mapslices(mean∘skipmissing,dmem,dims = ("lon","lat"))

  end
  # Test Mean seasonal cycle retrieval
  @testset "Seasonal cycle statistics and anomalies" begin
  cdata=subsetcube(c,variable="soil_moisture",lon=10,lat=50.75)
  d=readcubedata(cdata)

  x2=getMSC(d)

  x3=getMedSC(d)

  a = d[:]
  a = a[3:46:end]
  @test isapprox(mean(skipmissing(a)),x2[3])
  @test isapprox(median(skipmissing(a)),x3[3])

  # Test gap filling
  cube_filled=readcubedata(gapFillMSC(d))
  imiss=findfirst(i->ismissing(i),d.data)
  @test !ismissing(cube_filled.data[imiss])
  its=mod(imiss-1,46)+1
  @test cube_filled.data[imiss]≈readcubedata(x2).data[its]
  @test !any(ismissing(cube_filled.data))

  # Test removal of MSC

  cube_anomalies=readcubedata(removeMSC(cube_filled))
  @test isapprox(cube_anomalies.data[47:92],(cube_filled.data[47:92].-readcubedata(x2).data[1:46]))

  # Test normalization
  anom_normalized=normalizeTS(cube_anomalies)[:]
  #@show cube_anomalies[:,:,:]
  @test mean(anom_normalized)<1e7
  @test 1.0-1e-6 <= std(anom_normalized) <= 1.0+1e-6
  end

  d1=subsetcube(c,variable=["gross_primary_productivity","net_ecosystem_exchange"],lon=(10,10),lat=(50,50))
  d2=subsetcube(c,variable=["gross_primary_productivity","air_temperature_2m"],lon=(10,10),lat=(50,50))

  @testset "Multiple output cubes" begin
  #Test onvolving multiple output cubes
  c1=subsetcube(c,variable="gross_primary_productivity",lon=(10,11),lat=(50,51),time=Date(2001)..Date(2010))

  c2=readcubedata(c1)

  cube_wo_mean,cube_means=sub_and_return_mean(c2)

  @test isapprox(permutedims(c2[:,:,:].-mean(c2[:,:,:],dims=3),(3,1,2)),readcubedata(cube_wo_mean)[:,:,:])
  @test isapprox(mean(c2[:,:,:],dims=3)[:,:,1],cube_means[:,:])
  end
end

@testset "Parallel processing" begin
doTests()
end
rmprocs(workers())

@testset "Single proc processing" begin
doTests()
end


## SARS-CoV-2 /COVID19
## Affan Shoukat, 2020

using DifferentialEquations, Plots, Parameters, DataFrames, CSV, LinearAlgebra
using StatsPlots, Query, Distributions, Statistics, Random, DelimitedFiles, ProgressMeter

∑(x) = sum(x) # synctactic sugar
heaviside(x) = x <= 0 ? 0 : 1

@with_kw mutable struct ModelParameters
    # default parameter values, (fixed: fixed parameters, sampled: sampled at time of run, input: given as input,i.e. scenarios)
    ## parameters for transmission dynamics.
    β::Float64 = 0.037 # (input) 0.037: R0 ~ 2.5, next generation method
    κ::Float64 = 0.5 # (fixed) reduction in transmission due to mild symptoms.
    σ::Float64 = 1/5.2 # (sampled)incubation period 5.2 days mean, LogNormal
    q::NTuple{4, Float64} = (0.05, 0.05, 0.05, 0.05) # (fixed) proportion of self-quarantine
    h::NTuple{4, Float64} = (0.02075, 0.02140, 0.025, 0.03885) ## (sampled), default values mean of distribution proporition of TOTAL infections going to hospital.
    fₐ::Float64 = 0.05 ##
    fᵢ::Float64 = 0.80 ##
    c::NTuple{4, Float64} = (0.0129, 0.03875, 0.0705, 0.15) ## (sampled), default values mean of distribution, mean is average of Chiense and US CDC.
    τₐ::Float64 = 0.5   ##
    τᵢ::Float64 = 1   ##
    γ::Float64 = 1/4.6 # (fixed) symptom onset to recovery, assumed fixed, based on serial interval... sampling creates a problem negative numbers
    δ::Float64 = 1/3.5 # (sampled), default value mean of distribution, symptom onset to hospitalization
    θ::NTuple{4, Float64} = (0.8, 0.8, 0.4, 0.2) ## (fixed)

    ## recovery and mortality
    mH::Float64 = 0.2296  ## prob of death in hospital
    μH::Float64 = 1/9.7   ## length of hospital stay before death (average of CDC reports)
    ψH::Float64 = 1/10    ## length of hospital stay before recovery (JAMA paper, send PR to CDC GitHub jama paper is median)
    mC::Float64 = 0.1396 ## prob of death in ICU
    μC::Float64 = 1/7     ## length of ICU stay before death Lancet Res "Clinical course and outcomes of critically ill Yang"
    ψC::Float64 = 1/13.25 ## length of ICU before recovery (Chad's ventilation calculation)

    ## internal parameters
    pop::NTuple{4, Float64} = (81982665,129596376,63157200,52431193)

end

function contact_matrix()
    ## contact matrix for general population and in household.
    M = ones(Float64, 4, 4)
    M[1, :] = [9.76190295449833 3.76760818611057 1.50823242972404 0.603972723406184]
    M[2, :] = [3.76760818611057 9.42964041341912 3.05467747979113 0.702942024329397]
    M[3, :] = [1.50823242972404 3.05467747979113 2.95716998311431 0.760587605942274]
    M[4, :] = [0.603972723406184 0.702942024329397 0.760587605942274 1.24948301635913]
    M̃ = ones(Float64, 4, 4)
    M̃[1, :] = [2.03651666746364 1.56405653862347 0.504076940943109 0.380678379808072]
    M̃[2, :] = [1.56405653862347 1.50876370897303 0.445015376096172 0.239252424523331]
    M̃[3, :] = [0.504076940943109 0.445015376096172 1.03746276893954 0.191098354935966]
    M̃[4, :] = [0.380678379808072 0.239252424523331 0.191098354935966 0.641792954732621]
    return M, M̃
end

function Model!(du, u, p, t)
    # model v4, with age structure, feb 20
    # 4 age groups: 0 - 18, 19 - 49, 50 - 65, 65+
    S₁, S₂, S₃, S₄,
    E₁, E₂, E₃, E₄,
    F₁, F₂, F₃, F₄,
    Iₙ₁, Iₙ₂, Iₙ₃, Iₙ₄,
    Qₙ₁, Qₙ₂, Qₙ₃, Qₙ₄,
    Iₕ₁, Iₕ₂, Iₕ₃, Iₕ₄,
    Qₕ₁, Qₕ₂, Qₕ₃, Qₕ₄,
    H₁, H₂, H₃, H₄,
    C₁, C₂, C₃, C₄,
    N₁, N₂, N₃, N₄,

    # internal incidence equations
    Z₁, Z₂, Z₃, Z₄,
    CX₁, CX₂, CX₃, CX₄,
    CY₁, CY₂, CY₃, CY₄,
    DX₁, DX₂, DX₃, DX₄,
    DY₁, DY₂, DY₃, DY₄,

    ## asymptomatic classes
    Aₙ₁, Aₙ₂, Aₙ₃, Aₙ₄,
    Aₛ₁, Aₛ₂, Aₛ₃, Aₛ₄,

    # set up the vectors for syntactic sugar
    S = (S₁, S₂, S₃, S₄)
    E = (E₁, E₂, E₃, E₄)
    F = (F₁, F₂, F₃, F₄)
    Iₙ = (Iₙ₁, Iₙ₂, Iₙ₃, Iₙ₄)
    Qₙ = (Qₙ₁, Qₙ₂, Qₙ₃, Qₙ₄)
    Iₕ = (Iₕ₁, Iₕ₂, Iₕ₃, Iₕ₄)
    Qₕ = (Qₕ₁, Qₕ₂, Qₕ₃, Qₕ₄)
    Aₙ = (Aₙ₁, Aₙ₂, Aₙ₃, Aₙ₄)
    Aₛ = (Aₛ₁, Aₛ₂, Aₛ₃, Aₛ₄)
    H = (H₁, H₂, H₃, H₄)
    C = (C₁, C₂, C₃, C₄)
    N = (N₁, N₂, N₃, N₄)

    # internal incidence euqations
    Z = (Z₁, Z₂, Z₃, Z₄)
    CX = (CX₁, CX₂, CX₃, CX₄)
    CY = (CY₁, CY₂, CY₃, CY₄)
    DX = (DX₁, DX₂, DX₃, DX₄)
    DY = (DY₁, DY₂, DY₃, DY₄)

    #get the contact matrix
    M, M̃ = contact_matrix()

    # constants
    pop = p.pop  ## fixed population
    Nᵥ = p.strat

    ## working in new model

    @unpack β, κ, ξ, ν, σ, q, h, fₐ, fᵢ, τₐ, τᵢ, γ, δ, θ, c, mH, μH, ψH, mC, μC, ψC = p
    for a = 1:4
        # sus S
        du[a+0] = -β*S[a]*(dot(M[a, :], Iₙ./pop) + dot(M[a, :], Iₕ./pop) + κ*dot(M[a, :], Aₙ./pop)  -
                   β*S[a]*(dot(M̃[a, :], Qₙ./pop) + dot(M̃[a, :], Qₕ./pop) + κ*dot(M̃[a, :], Aₛ./pop)
        # exposed E
        du[a+4]  = β*S[a]*(dot(M[a, :], Iₙ./pop) + dot(M[a, :], Iₕ./pop) + κ*dot(M[a, :], Aₙ./pop) +
                   β*S[a]*(dot(M̃[a, :], Qₙ./pop) + dot(M̃[a, :], Qₕ./pop) + κ*dot(M̃[a, :], Aₛ./pop) -
                   σ*E[a]
        # In class
        du[a+8] = (1 - θ[a])*(1 - q[a])*(1 - h[a])*σ*E[a]  - (1 - fᵢ)*γ*Iₙ[a] - fᵢ*τᵢ*Iₙ[a]
        # Qn class
        du[a+12] = (1 - θ[a])*q[a]*(1 - h[a])*σ*E[a] + fᵢ*τᵢ*Iₙ[a] - γ*Qₙ[a]
        # Ih class
        du[a+16] = (1 - θ[a])*(1 - q[a])*h[a]*σ*E[a] - (1 - fᵢ)*δ*Iₕ[a] - fᵢ*τᵢ*Iₕ[a]
        # Qh class
        du[a+20] = (1 - θ[a])*q[a]*h[a]*σ*E[a] + fᵢ*τᵢ*Iₕ[a] - δ*Qₕ[a]
        # Ha class
        du[a+24] = (1 - c[a])*(1 - fᵢ)*δ*Iₕ[a] + (1 - c[a])*δ*Qₕ[a] - (mH*μH + (1 - mH)*ψH)*H[a]
        # Ca class
        du[a+28] = c[a]*(1 - fᵢ)*δ*Iₕ[a] + c[a]*δ*Qₕ[a] - (mC*μC + (1 - mC)*ψC)*C[a]
        # Na class
        du[a+32] = -mC*μC*C[a] - mH*μH*H[a]

        # Z class to collect cumulative incidence (only from suspectibles)
        du[a+36] =  β*S[a]*(dot(M[a, :], Iₙ./pop) + dot(M[a, :], Iₕ./pop) + κ*dot(M[a, :], Aₙ./pop)  +
                    β*S[a]*(dot(M̃[a, :], Qₙ./pop) + dot(M̃[a, :], Qₕ./pop) + κ*dot(M̃[a, :], Aₛ./pop)
        ## collect cumulative incidence of hospitalization and ICU class 69 to 84
        du[a+40] = (1 - c[a])*(1 - fᵢ)*δ*Iₕ[a] + (1 - c[a])*δ*Qₕ[a]
        du[a+44] = c[a]*(1 - fᵢ)*δ*Iₕ[a] + c[a]*δ*Qₕ[a]
        du[a+48] = (mH*μH)*H[a]  ## DX
        du[a+52] = (mC*μC)*C[a]  ## DY

        ## Asymptomatic classes
        # Aₙ₁, Aₙ₂, Aₙ₃, Aₙ₄,
        du[a+56] = θ[a]*σ*(F[a] + E[a]) - (1 - fₐ)*γ*Aₙ[a] - fₐ*τₐ*Aₙ[a]
        # Aₛ₁, Aₛ₂, Aₛ₃, Aₛ₄,
        du[a+60] = fₐ*τₐ*Aₙ[a] - γ*Aₛ[a]
    end
end

function run_model(p::ModelParameters, nsims=1)
    ## set up ODE time and initial conditions
    tspan = (0.0, 1000.0)
    u0 = zeros(Float64, 64) ## total compartments
    sols = []
    for mc = 1:nsims
        ## reset the initial conditions
        u0[1] = u0[33] = p.pop[1]
        u0[2] = u0[34] = p.pop[2]
        u0[3] = u0[35] = p.pop[3]
        u0[4] = u0[36] = p.pop[4]
        # initial infected person
        u0[13] = 1

        # sample the parameters needed per simulation
        p.δ = 1/(rand(Uniform(2, 5)))
        p.σ = 1/(rand(LogNormal(log(5.2), 0.1)))
        p.h = (rand(Uniform(0.0085, 0.033)), rand(Uniform(0.0088, 0.034)), rand(Uniform(0.01, 0.042)), rand(Uniform(0.017, 0.066))) ./ (1 .- p.θ)
        p.c = (rand(Uniform(0.013, 0.015)), rand(Uniform(0.04, 0.044)), rand(Uniform(0.05, 0.1)), rand(Uniform(0.10, 0.20))) ## c dosnt change

        ## solve the ODE model
        #print("...sim: $mc, params: $(p.τ), $(p.f), $(p.nva), $(p.ν) \r")
        prob = ODEProblem(Model!, u0, tspan, p)
        sol = solve(prob, Rodas4(autodiff=false), dt=1.0, adaptive=false, callback=cb)  ## WORKS
        #sol = solve(prob, Rosenbrock23(autodiff=false),  dt=0.1, adaptive=false, callback=cbset)  ## WORKS
        push!(sols, sol)
    end
    #println("\n simulation scenario finished")
    return sols
end

function run_single()
    # A function that runs a single simulation for testing/calibrating purposes.
    ## setup parameters (overwrite some default ones if needed)
    p = ModelParameters()
    p.β = getβ("2.0")
    p.fₐ = 0.20
    p.τₐ = 1
    p.fᵢ = 0.80
    p.τᵢ = 1
    dump(p)
    sol = run_model(p, 1)[1] ## the [1] is required since runsims returns an array of solutions
    return sol
end

## hospital capacity: https://hcup-us.ahrq.gov/reports/statbriefs/sb185-Hospital-Intensive-Care-Units-2011.jsp
## https://www.aha.org/system/files/media/file/2020/01/2020-aha-hospital-fast-facts-new-Jan-2020.pdf
## https://www.sccm.org/Communications/Critical-Care-Statistics


using TargetedLearning: LReg, Qmodels, Parameters
using StatsFuns

import TargetedLearning.Qmodels: Fluctuation, computefluc

facts("Testing Qmodels") do

    srand(412)
    n,p = 100, 10

    w = [ones(n) rand(n,p)]
    a = round(rand(n))
    y = round(rand(n))

    qfit = lreg([w a], y)
    logitQnA1 = linpred(qfit, [w ones(n)])
    logitQnA0 = linpred(qfit, [w zeros(n)])
    logitQnAA = ifelse(a.==1, logitQnA1, logitQnA0)

    q = Qmodel(logitQnA1, logitQnA0)

    context("predict on unfluctuated Qmodel") do
        @fact predict(q, a) --> logistic(ifelse(a.==1, logitQnA1, logitQnA0))
    end

    gn1 = predict(lreg(w,a), w)
    gna = ifelse(a.==1, gn1, 1-gn1)
    hAA = (2a-1)./gna
    hA1 = 1./gn1
    fluc = lreg(hAA, y, offset=logitQnAA)
    param=ATE()
    theirfluc = computefluc(q, param, gn1, a, y)
    fluctuate!(q, param, gn1, a, y)

    context("fluctuation") do
        @fact theirfluc.epsilon.β --> roughly(fluc.β)
        @fact q.flucseq[1].epsilon.β --> roughly(fluc.β)
    end

    context("predict on fluctated Qmodel") do
        @fact predict(q, ones(n)) --> roughly(logistic(logitQnA1 + hA1 * fluc.β[1]))
    end

    context("defluctuate!") do
        @fact defluctuate!(q) --> is_a(Fluctuation{Float64})
        @fact length(q.flucseq) --> 0
    end
end