"""
    function expintegrator(A, t::Number, u₀, u₁, …; kwargs...)
    function expintegrator(A, t::Number, (u₀, u₁, …); kwargs...)
    function expintegrator(A, t::Number, (u₀, u₁, …), algorithm)

Compute ``y = ϕ₀(t*A)*u₀ + t*ϕ₁(t*A)*u₁ + t^2*ϕ₂(t*A)*u₂ + …``, where `A` is a general linear map, i.e. a `AbstractMatrix` or just a general function or callable object and `u₀`, `u₁` are of any Julia type with vector like behavior. Here, ``ϕ₀(z) = exp(z)`` and ``ϕⱼ₊₁ = (ϕⱼ(z) - 1/j!)/z``. In particular, ``y = x(t)`` represents the solution of the ODE
``x' = A*x + ∑ⱼ t^j/j! uⱼ₊₁`` with ``x(0) = u₀``.

!!! note
    When there are only input vectors `u₀` and `u₁`, `t` can equal `Inf`, in which the
    algorithm tries to evolve all the way to the fixed point `y = - A \\ u₁ + P₀ u₀` with
    `P₀` the projector onto the eigenspace of eigenvalue zero (if any) of `A`. If `A` has
    any eigenvalues with real part larger than zero, however, the solution to the ODE will
    diverge, i.e. the fixed point is not stable.

!!! warning
    The returned solution might be the solution of the ODE integrated up to a smaller time ``t̃ = sign(t) * |t̃|`` with ``|t̃| < |t|``, when the required precision could not be attained. Always check `info.converged > 0` or `info.residual == 0` (see below).

### Arguments:
The linear map `A` can be an `AbstractMatrix` (dense or sparse) or a general function or
callable object that implements the action of the linear map on a vector. If `A` is an
`AbstractMatrix`, `x` is expected to be an `AbstractVector`, otherwise `x` can be of any
type that behaves as a vector and supports the required methods (see KrylovKit docs).

The time parameter `t` can be real or complex, and it is better to choose `t` e.g. imaginary
and `A` hermitian, then to absorb the imaginary unit in an antihermitian `A`. For the
former, the Lanczos scheme is used to built a Krylov subspace, in which an approximation to
the exponential action of the linear map is obtained. The arguments `u₀`, `u₁`, … can be
of any type and should be in the domain of `A`.

### Return values:
The return value is always of the form `y, info = expintegrator(...)` with
*   `y`: the result of the computation, i.e.
    ``y = ϕ₀(t̃*A)*u₀ + t̃*ϕ₁(t̃*A)*u₁ + t̃^2*ϕ₂(t̃*A)*u₂ + …``
    with ``t̃ = sign(t) * |t̃|`` with ``|t̃| <= |t|``, such that the accumulated error in
    `y` per unit time is at most equal to the keyword argument `tol`
*   `info`: an object of type [`ConvergenceInfo`], which has the following fields
    -   `info.converged::Int`: 0 or 1 if the solution `y` was evolved all the way up to the
        requested time `t`.
    -   `info.residual`: there is no residual in the conventional sense, however, this
        value equals the residual time `t - t̃`, i.e. it is zero if `info.converged == 1`
    -   `info.normres::Real`: a (rough) estimate of the total error accumulated in the
        solution, should be smaller than `tol * |t̃|`
    -   `info.numops::Int`: number of times the linear map was applied, i.e. number of times
        `f` was called, or a vector was multiplied with `A`
    -   `info.numiter::Int`: number of times the Krylov subspace was restarted (see below)

### Keyword arguments:
Keyword arguments and their default values are given by:
*   `verbosity::Int = 0`: verbosity level, i.e. 0 (no messages), 1 (single message
    at the end), 2 (information after every iteration), 3 (information per Krylov step)
*   `krylovdim = 30`: the maximum dimension of the Krylov subspace that will be constructed.
    Note that the dimension of the vector space is not known or checked, e.g. `x₀` should
    not necessarily support the `Base.length` function. If you know the actual problem
    dimension is smaller than the default value, it is useful to reduce the value of
    `krylovdim`, though in principle this should be detected.
*   `tol = 1e-12`: the requested accuracy per unit time, i.e. if you want a certain
    precision `ϵ` on the final result, set `tol = ϵ/abs(t)`. If you work in e.g. single
    precision (`Float32`), you should definitely change the default value.
*   `maxiter::Int = 100`: the number of times the Krylov subspace can be rebuilt; see below
    for further details on the algorithms.
*   `issymmetric`: if the linear map is symmetric, only meaningful if `T<:Real`
*   `ishermitian`: if the linear map is hermitian
The default value for the last two depends on the method. If an `AbstractMatrix` is used,
`issymmetric` and `ishermitian` are checked for that matrix, ortherwise the default values
are `issymmetric = false` and `ishermitian = T <: Real && issymmetric`.

### Algorithm
The last method, without keyword arguments and the different vectors `u₀`, `u₁`, … in a
tuple, is the one that is finally called, and can also be used directly. Here, one
specifies the algorithm explicitly as either [`Lanczos`](@ref), for real symmetric or
complex hermitian linear maps, or [`Arnoldi`](@ref), for general linear maps. Note that
these names refer to the process for building the Krylov subspace, and that one can still
use complex time steps in combination with e.g. a real symmetric map.
"""
function expintegrator end

function expintegrator(A, t::Number, u₀, us...; kwargs...)
    alg = eigselector(A, promote_type(typeof(t), eltype(u₀), eltype.(us)...); kwargs...)
    expintegrator(A, t, (u₀, us...), alg)
end

function expintegrator(A, t::Number, u::Tuple, alg::Union{Lanczos,Arnoldi})
    length(u) == 1 && return expintegrator(A, t, (u[1], rmul!(similar(u[1]), false)), alg)

    p = length(u) - 1

    # process initial vector and determine result type
    u₀ = first(u)
    β₀ = norm(u₀)
    Au₀ = apply(A, u₀) # used to determine return type
    numops = 1
    T = promote_type(promote_type(eltype(Au₀), typeof(β₀), typeof(t)),
                        promote_type(eltype.(u)...))
    S = real(T)
    w₀ = copyto!(similar(u₀, T), u₀)

    # krylovdim and related allocations
    krylovdim = alg.krylovdim
    K = krylovdim
    HH = zeros(T, (krylovdim+p+1, krylovdim+p+1))

    # time step parameters
    η::S = alg.tol # tol is per unit time
    totalerr = zero(η)
    sgn = sign(t)
    τ::S = abs(t)
    Δτ::S = one(τ) # don't try any clever initial guesses, rely on correction mechanism
    τ₀ = zero(τ)

    # safety factors
    δ::S = 1.2
    γ::S = 0.8

    # initial vectors
    w = Vector{typeof(w₀)}(undef, p+1)
    w[1] = w₀
    for j = 1:p
        w[j+1] = apply(A, w[j])
        numops += 1
        lfac = 1
        for l = 0:p-j
            w[j+1] = axpy!((sgn*τ₀)^l/lfac, u[j+l+1], w[j+1])
            lfac *= l+1
        end
    end
    v = similar(w₀)
    β = norm(w[p+1])
    if β < alg.tol && p == 1
        if alg.verbosity > 0
            @info """expintegrate finished after 0 iterations, converged to fixed point up to error = $β"""
        end
        return w₀, ConvergenceInfo(1, zero(τ), β, 0, numops)
    end
    mul!(v, w[p+1], 1/β)

    # initialize iterator
    if alg isa Lanczos
        iter = LanczosIterator(A, w[p+1], alg.orth)
    else
        iter = ArnoldiIterator(A, w[p+1], alg.orth)
    end
    fact = initialize(iter; verbosity = alg.verbosity - 2)
    numops += 1
    sizehint!(fact, krylovdim)

    # start outer iteration loop
    maxiter = alg.maxiter
    numiter = 0
    while true
        if β < alg.tol && p == 1 # w₀ is fixed point of ODE
            if alg.verbosity > 0
                @info """expintegrate finished after $numiter iterations, converged to fixed point up to error = $β"""
            end
            return w₀, ConvergenceInfo(1, zero(τ), β, numiter, numops)
        end

        numiter += 1
        Δτ = min(Δτ, τ-τ₀)

        # Lanczos or Arnoldi factorization
        while normres(fact) > eps() && length(fact) < krylovdim
            fact = expand!(iter, fact; verbosity = alg.verbosity-2)
            numops += 1
        end
        K = fact.k # current Krylov dimension
        V = basis(fact)

        # Small matrix exponential and error estimation
        H = fill!(view(HH, 1:K+p+1, 1:K+p+1), zero(T))
        mul!(view(H, 1:K, 1:K), rayleighquotient(fact), sgn*Δτ)
        H[1, K+1] = 1
        for i = 1:p
            H[K+i, K+i+1] = 1
        end
        expH = LinearAlgebra.exp!(H)
        ϵ = abs(Δτ^p * β * normres(fact) * expH[K,K+p+1])
        ω = ϵ / (Δτ * η)

        q = K/2
        while ω > one(ω)
            ϵ_prev = ϵ
            Δτ_prev = Δτ
            Δτ *= (γ/ω)^(1/(q+1))
            H = fill!(view(HH, 1:K+p+1, 1:K+p+1), zero(T))
            mul!(view(H, 1:K, 1:K), rayleighquotient(fact), sgn*Δτ)
            H[1, K+1] = 1
            for i = 1:p
                H[K+i, K+i+1] = 1
            end
            expH = LinearAlgebra.exp!(H)
            ϵ = abs(Δτ^p * β * normres(fact) * expH[K,K+p+1])
            ω = ϵ / (Δτ * η)
            q = max(zero(q),  log(ϵ / ϵ_prev)/log(Δτ / Δτ_prev)-1)
        end

        # take time step
        totalerr += ϵ
        τ₀ += Δτ
        jfac = 1
        for j = 1:p-1
            w₀ = axpy!((sgn*Δτ)^j/jfac, w[j+1], w₀)
            jfac *= (j+1)
        end
        w[p+1] = mul!(w[p+1], basis(fact), view(expH, 1:K, K+p))
        # add first correction
        w[p+1] = axpy!(expH[K,K+p+1], residual(fact), w[p+1])
        w₀ = axpy!(β*(sgn*Δτ)^p, w[p+1], w₀)

        # increase time step for next iteration:
        if ω < γ
            Δτ *= (γ/ω)^(1/(q+1))
        end

        if alg.verbosity > 1
            msg = "expintegrate in iteration $numiter: "
            msg *= "reached time " * @sprintf("%.2e", τ₀)
            msg *= ", total error = " * @sprintf("%.4e", totalerr)
            @info msg
        end

        if τ₀ >= τ
            if alg.verbosity > 0
                @info """expintegrate finished after $numiter iterations: total error = $totalerr"""
            end
            return w₀, ConvergenceInfo(1, zero(τ), totalerr, numiter, numops)
        elseif numiter == maxiter
            if alg.verbosity > 0
                @warn """expintegrate finished without convergence after $numiter iterations:
                total error = $totalerr, residual time = $(τ - τ₀)"""
            end
            return w₀, ConvergenceInfo(0, τ-τ₀, totalerr, numiter, numops)
        else
            for j = 1:p
                w[j+1] = apply(A, w[j])
                numops += 1
                lfac = 1
                for l = 0:p-j
                    w[j+1] = axpy!((sgn*τ₀)^l/lfac, u[j+l+1], w[j+1])
                    lfac *= l+1
                end
            end
            β = norm(w[p+1])
            mul!(v, w[p+1], 1/β)
            if alg isa Lanczos
                iter = LanczosIterator(A, w[p+1], alg.orth)
            else
                iter = ArnoldiIterator(A, w[p+1], alg.orth)
            end
            fact = initialize!(iter, fact; verbosity = alg.verbosity-2)
            numops += 1
        end
    end
end


export NeedPiecewiseCorrection

# Computes samplex of real or complex Haar matrices of size nxn
#
# For beta=1,2,4, generates random orthogonal, unitary and symplectic matrices
# of uniform Haar measure.
# These matrices are distributed with uniform Haar measure over the
# classical orthogonal, unitary and symplectic groups O(N), U(N) and
# Sp(N)~USp(2N) respectively.
#
# The last parameter specifies whether or not the piecewise correction
# is applied to ensure that it truly of Haar measure
# This addresses an inconsistency in the Householder reflections as
# implemented in most versions of LAPACK
# Method 0: No correction
# Method 1: Multiply rows by uniform random phases
# Method 2: Multiply rows by phases of diag(R)
# References:
#    Edelman and Rao, 2005
#    Mezzadri, 2006, math-ph/0609050
#TODO implement O(n^2) method
function rand(W::Haar, n::Integer, doCorrection::Integer)
    beta = W.beta
    M=rand(Ginibre(beta,n))
    q,r=qr(M)
    if doCorrection==0
        q
    elseif doCorrection==1
        if beta==1
            L = sign(rand(n).-0.5)
        elseif beta==2
            L = exp(im*rand(n)*2pi)
        elseif beta==4
            L = exp(im*rand(2n)*2pi)
        else
            error(string("beta = ",beta, " not implemented."))
        end
        q*diagm(L)
    elseif doCorrection==2
        if beta==1
            L=sign(diag(r))
        elseif (beta==2 || beta==4)
            L=diag(r)
            L=L./abs(L)
        else
            error(string("beta = ",beta, " not implemented."))
        end
        q*diagm(L)
    end
end

#By default, always do piecewise correction
#For most applications where you use the HaarMatrix as a similarity transform
#it doesn't matter, but better safe than sorry... let the user choose else
rand(W::Haar,n::Integer) = rand(W,n, 1)

#A utility method to check if the piecewise correction is needed
#This checks the R part of the QR factorization; if correctly done,
#the diagonals are all chi variables so are non-negative
function NeedPiecewiseCorrection()
    n=20
    R=qr(randn(Ginibre(2,n)))[2]
    return any([x<0 for x in diag(R)])
end

#TODO maybe, someday
#Haar measure on U(N) in terms of local coordinates
#Zyczkowski and Kus, Random unitary matrices, J. Phys. A: Math. Gen. 27,
#4235–4245 (1994).


import GreyDecision.GreyNumbers.GreyNumber
import GreyDecision.GreyNumbers.kernel

@testset "Grey Numbers" begin

    @testset "Kernel" begin
        @test kernel(GreyNumber(0.0, 10.0)) == 5.0
    end

    @testset "Constructor with single argument" begin
        @test GreyNumber(1.0) == GreyNumber{Float64}(1.0, 1.0)
        @test GreyNumber(0.0) == GreyNumber{Float64}(0.0, 0.0)
        @test GreyNumber(1)   == GreyNumber{Int64}(1, 1)
        @test GreyNumber(0)   == GreyNumber{Int64}(0, 0)
    end

    @testset "Data structure - Zero Constructor" begin
        g = GreyNumber()
        @test g.a == 0.0
        @test g.b == 0.0
    end

    @testset "Data structure - Constructor" begin
        g = GreyNumber(2.0, 5.0)
        @test g.a == 2.0
        @test g.b == 5.0

        h = GreyNumber(5.0, 2.0)
        @test g.a == 2.0
        @test g.b == 5.0
    end

    @testset "Whitenization" begin
        g = GreyNumber(10.0, 20.0)
        w = GreyNumbers.whitenizate(g)
        @test w == 15.0

        w2 = GreyNumbers.whitenizate(g, t = 1.0)
        @test w2 == 10.0

        w3 = GreyNumbers.whitenizate(g, t = 0.0)
        @test w3 == 20.0
    end

    @testset "+" begin
        @testset "Grey + Grey" begin
            g1 = GreyNumber(1, 2)
            g2 = GreyNumber(5, 10)
            greysum = g1 + g2
            greysum2 = g2 + g1
            @test greysum isa GreyNumber
            @test greysum2 isa GreyNumber
            @test greysum == greysum2
            @test greysum.a == 6
            @test greysum.b == 12
        end
        @testset "Grey + scalar" begin
            g1 = GreyNumber(1, 2)
            k = 3
            result  = g1 + k
            result2 = k  + g1
            @test result isa GreyNumber
            @test result2 isa GreyNumber
            @test result2 == result
            @test result.a == 4
            @test result.b == 5
        end
    end

    @testset "Additive inverse (negatition)" begin
        g = GreyNumber(10, 20)
        ginv = -g
        @test ginv isa GreyNumber
        @test ginv.a <= ginv.b
        @test ginv.a == -20
        @test ginv.b == -10
    end

    @testset "Substraction" begin
        @testset "Grey - Grey" begin
            g1 = GreyNumber(10, 20)
            g2 = GreyNumber(1, 2)
            gdiff1 = g1 - g2
            gdiff2 = g2 - g1
            @test gdiff1 isa GreyNumber
            @test gdiff2 isa GreyNumber
            @test gdiff1.a <= gdiff1.b
            @test gdiff2.a <= gdiff2.b
            @test gdiff1.a == 8
            @test gdiff1.b == 19
            @test gdiff2.a == -19
            @test gdiff2.b == -8
        end
        @testset "Grey - scalar" begin
            g = GreyNumber(10, 20)
            k = 3
            result  = g - k
            @test result isa GreyNumber
            @test result.a == 7
            @test result.b == 17
        end
        @testset "Scalar - Grey" begin
            g1 = GreyNumber(100.0, 100.0)
            k = 30.0
            g2 = GreyNumber(k, k)
            @test g1 - k  == g1 - g2
            @test k - g1  == g2 - g1
        end
    end

    @testset "Multiplication" begin
        g1 = GreyNumber(3, 4)
        g2 = GreyNumber(5, 10)
        mult = g1 * g2
        mult2 = g2 * g1
        @test mult isa GreyNumber
        @test mult2 isa GreyNumber
        @test mult == mult2
        @test mult.a == 15
        @test mult.b == 40
    end

    @testset "Multiplication by scalar" begin
        @testset "Scalar on the right" begin
            g1 = GreyNumber(10, 20)
            result = g1 * 10
            @test result isa GreyNumber
            @test result.a == 100
            @test result.b == 200
        end
        @testset "Scalar on the left" begin
            g1 = GreyNumber(10, 20)
            result = 10 * g1
            @test result isa GreyNumber
            @test result.a == 100
            @test result.b == 200
        end
        @testset "Negative scalar" begin
            g1 = GreyNumber(10, 20)
            result = -10 * g1
            @test result isa GreyNumber
            @test result.a <= result.b
            @test result.a == -200
            @test result.b == -100
        end
    end

    @testset "Reciprocal (inverse)" begin
        g = GreyNumber(10, 20)
        repic = inv(g)
        @test repic isa GreyNumber
        @test repic.a <= repic.b
        @test repic.a == 1 / 20
        @test repic.b == 1 / 10
    end

    @testset "Division" begin
        @testset "Division of two numbers" begin
            g1 = GreyNumber(3, 4)
            g2 = GreyNumber(5, 10)
            division = g1 / g2
            @test division isa GreyNumber
            @test division.a <= division.b
            @test division.a == 0.3
            @test division.b == 0.8
        end

        @testset "Division by scalar" begin
            @testset "Positive scalar" begin
                g1 = GreyNumber(30, 40)
                result = g1 / 10
                @test result isa GreyNumber
                @test result.a <= result.b
                @test result.a == 3
                @test result.b == 4
            end

            @testset "Negative scalar" begin
                g1 = GreyNumber(30, 40)
                result = g1 / -10
                @test result isa GreyNumber
                @test result.a <= result.b
                @test result.a == -4
                @test result.b == -3
            end
        end
    end

    @testset "Power" begin
        @testset "All positive" begin
            g = GreyNumber(2, 4)
            powered = g^5
            @test powered isa GreyNumber
            @test powered.a == 32
            @test powered.b == 1024
        end

        @testset "Some negative" begin
            g = GreyNumber(-5, -4)
            powered = g^3
            @test powered isa GreyNumber
            @test powered.a <= powered.b
            @test powered.a == -5 * 5 * 5
            @test powered.b == -4 * 4 * 4
        end
    end

    @testset "sqrt" begin
        g = GreyNumber(36, 100)
        gs = sqrt(g)
        @test gs isa GreyNumber
        @test gs.a == 6.0
        @test gs.b == 10.0
    end

    @testset "cbrt" begin
        k = 5.0
        p = 3.0
        g = GreyNumber(k * k * k, p * p * p)
        result = cbrt(g)

        @test result isa GreyNumber
        @test result.a == p
        @test result.b == k
    end

    @testset "abs" begin
        @testset "all positive" begin
            g = GreyNumber(4, 5)
            gabs = abs(g)
            @test gabs isa GreyNumber
            @test gabs == g
        end

        @testset "all negative" begin
            g = GreyNumber(-4, -5)
            gabs = abs(g)
            @test gabs isa GreyNumber
            @test gabs.a == 4
            @test gabs.b == 5
        end
    end

    @testset "< and >" begin
        @test GreyNumber(4, 5) < GreyNumber(7, 8)
        @test GreyNumber(4, 5) < GreyNumber(7, 10)
        @test GreyNumber(4, 5) > GreyNumber(1, 2)
        @test GreyNumber(5, 40) > GreyNumber(4, 5)
        @test GreyNumber(1, 2) < GreyNumber(1,3)
        @test GreyNumber(1, 5) > GreyNumber(1,3)
    end

    @testset "<= and >=" begin
        @test GreyNumber(4, 5) <= GreyNumber(4, 8)
        @test GreyNumber(4, 5) <= GreyNumber(4, 10)
        @test GreyNumber(4, 5) >= GreyNumber(1, 5)
        @test GreyNumber(5, 40) >= GreyNumber(4, 39)
        @test GreyNumber(1, 2) <= GreyNumber(1,3)
        @test GreyNumber(1, 20) >= GreyNumber(1,3)
    end

    @testset "==" begin
        @test GreyNumber(4, 5) == GreyNumber(4, 5)
        @test GreyNumber(4, 5) != GreyNumber(4, 2)
    end

    @testset "min" begin
        nums = [GreyNumber(1, 20), GreyNumber(2, 20), GreyNumber(3, 20)]
        @test minimum(nums) == GreyNumber(1, 20)
    end

    @testset "max" begin
        nums = [GreyNumber(1, 20), GreyNumber(1, 40), GreyNumber(1, 60)]
        @test maximum(nums) == GreyNumber(1, 60)
    end

    @testset "isinteger" begin
        @test isinteger(GreyNumber(5, 6))
    end

    @testset "isreal" begin
        @test isreal(GreyNumber(5.0, 6.0))
    end

    @testset "isinf" begin
        @test isinf(GreyNumber(0.0, Inf64))
    end

    @testset "iszero" begin
        @test iszero(GreyNumber())
        @test iszero(GreyNumber(0, 0))
        @test iszero(GreyNumber(0.0, 0.0))
    end

    @testset "isone" begin
        @test isone(GreyNumber(1, 1))
        @test isone(GreyNumber(1.0, 1.0))
        @test !isone(GreyNumber())
    end

    @testset "iseven" begin
        # Base.iseven is defined for integers
        @test_throws AssertionError iseven(GreyNumber(2.0, 4.0))
        @test iseven(GreyNumber(2, 4))
    end

    @testset "isodd" begin
        # Base.isodd is defined for integers
        @test_throws AssertionError isodd(GreyNumber(1.0, 3.0))
        @test isodd(GreyNumber(1, 3))
    end

    @testset "elttype" begin
        @test eltype(GreyNumber(0.0, 0.0)) == Float64
        @test eltype(GreyNumber(0, 0)) == Int64
    end

    @testset "sum" begin
        @test sum(GreyNumber()) == 0
        @test sum(GreyNumber(0, 0)) == 0
        @test sum(GreyNumber(0.0, 0.0)) == 0.0
        @test sum(GreyNumber(3.0, 4.0)) == 7.0
    end

    @testset "getindex (g[i])" begin
        @testset "Integers" begin
            g = GreyNumber(7, 8)
            @test g[1] isa Int64
            @test g[2] isa Int64
            @test g[1] == 7
            @test g[2] == 8
        end

        @testset "Floats" begin
            g = GreyNumber(7.0, 8.0)
            @test g[1] isa Float64
            @test g[2] isa Float64
            @test g[1] == 7.0
            @test g[2] == 8.0
        end
    end

    @testset "first" begin
        @testset "Integer" begin
            g = GreyNumber(7, 8)
            @test g[1] isa Int64
            @test first(g) == 7
        end

        @testset "Float" begin
            g = GreyNumber(7.0, 8.0)
            @test g[1] isa Float64
            @test first(g) == 7.0
        end
    end

    @testset "last" begin
        @testset "Integer" begin
            g = GreyNumber(7, 9)
            @test last(g) == 9
        end

        @testset "Floats" begin
            g = GreyNumber(7.0, 9.0)
            @test last(g) == 9.0
        end
    end

end # End of testset GreyNumbers


module EnsembleAnalysis

using SciMLBase, Statistics, RecursiveArrayTools

# Getters
get_timestep(sim,i) = (getindex(sol,i) for sol in sim)
get_timepoint(sim,t) = (sol(t) for sol in sim)
function componentwise_vectors_timestep(sim,i)
  arr = [get_timestep(sim,i)...]
  if typeof(arr[1]) <: AbstractArray
   return vecarr_to_vectors(VectorOfArray(arr))
  else
   return arr
  end
 end
 function componentwise_vectors_timepoint(sim,t)
   arr = [get_timepoint(sim,t)...]
   if typeof(arr[1]) <: AbstractArray
    return vecarr_to_vectors(VectorOfArray(arr))
   else
    return arr
   end
  end

# Timestep statistics
timestep_mean(sim,i) = componentwise_mean(get_timestep(sim,i))
timestep_mean(sim,::Colon) = timeseries_steps_mean(sim)
function timestep_median(sim,i)
  arr = componentwise_vectors_timestep(sim,i)
  if typeof(first(arr)) <: AbstractArray
    return reshape([median(x) for x in arr],size(sim[1][i])...)
  else
    return median(arr)
  end
end
timestep_median(sim,::Colon) = timeseries_steps_median(sim)
function timestep_quantile(sim,q,i)
  arr = componentwise_vectors_timestep(sim,i)
  if typeof(first(arr)) <: AbstractArray
    return reshape([quantile(x,q) for x in arr],size(sim[1][i])...)
  else
    return quantile(arr,q)
  end
end
timestep_quantile(sim,q,::Colon) = timeseries_steps_quantile(sim,q)
timestep_meanvar(sim,i) = componentwise_meanvar(get_timestep(sim,i))
timestep_meanvar(sim,::Colon) = timeseries_steps_meanvar(sim)
timestep_meancov(sim,i,j) = componentwise_meancov(get_timestep(sim,i),get_timestep(sim,j))
timestep_meancov(sim,::Colon,::Colon) = timeseries_steps_meancov(sim)
timestep_meancor(sim,i,j) = componentwise_meancor(get_timestep(sim,i),get_timestep(sim,j))
timestep_meancor(sim,::Colon,::Colon) = timeseries_steps_meancor(sim)
timestep_weighted_meancov(sim,W,i,j) = componentwise_weighted_meancov(get_timestep(sim,i),get_timestep(sim,j),W)
timestep_weighted_meancov(sim,W,::Colon,::Colon) = timeseries_steps_weighted_meancov(sim,W)

function timeseries_steps_mean(sim)
  DiffEqArray([timestep_mean(sim,i) for i in 1:length(sim[1])],sim[1].t)
end
function timeseries_steps_median(sim)
  DiffEqArray([timestep_median(sim,i) for i in 1:length(sim[1])],sim[1].t)
end
function timeseries_steps_quantile(sim,q)
  DiffEqArray([timestep_quantile(sim,q,i) for i in 1:length(sim[1])],sim[1].t)
end
function timeseries_steps_meanvar(sim)
  m,v = timestep_meanvar(sim,1)
  means = [m]
  vars = [v]
  for i in 2:length(sim[1])
    m,v = timestep_meanvar(sim,i)
    push!(means,m)
    push!(vars,v)
  end
  DiffEqArray(means,sim[1].t),DiffEqArray(vars,sim[1].t)
end
function timeseries_steps_meancov(sim)
  reshape([timestep_meancov(sim,i,j) for i in 1:length(sim[1]) for j in 1:length(sim[1])],length(sim[1]),length(sim[1]))
end
function timeseries_steps_meancor(sim)
  reshape([timestep_meancor(sim,i,j) for i in 1:length(sim[1]) for j in 1:length(sim[1])],length(sim[1]),length(sim[1]))
end
function timeseries_steps_weighted_meancov(sim,W)
  reshape([timestep_meancov(sim,W,i,j) for i in 1:length(sim[1]) for j in 1:length(sim[1])],length(sim[1]),length(sim[1]))
end

timepoint_mean(sim,t) = componentwise_mean(get_timepoint(sim,t))
function timepoint_median(sim,t)
  arr = componentwise_vectors_timepoint(sim,t)
  if typeof(first(arr)) <: AbstractArray
    return reshape([median(x) for x in arr],size(sim[1][1])...)
  else
    return median(arr)
  end
end
function timepoint_quantile(sim,q,t)
  arr = componentwise_vectors_timepoint(sim,t)
  if typeof(first(arr)) <: AbstractArray
    return reshape([quantile(x,q) for x in arr],size(sim[1][1])...)
  else
    return quantile(arr,q)
  end
end
timepoint_meanvar(sim,t) = componentwise_meanvar(get_timepoint(sim,t))
timepoint_meancov(sim,t1,t2) = componentwise_meancov(get_timepoint(sim,t1),get_timepoint(sim,t2))
timepoint_meancor(sim,t1,t2) = componentwise_meancor(get_timepoint(sim,t1),get_timepoint(sim,t2))
timepoint_weighted_meancov(sim,W,t1,t2) = componentwise_weighted_meancov(get_timepoint(sim,t1),get_timepoint(sim,t2),W)

function EnsembleSummary(sim::SciMLBase.AbstractEnsembleSolution{T,N},
                         t=sim[1].t;quantiles=[0.05,0.95]) where {T,N}
  if typeof(sim[1]) <: DESolution
    m,v = timeseries_point_meanvar(sim,t)
    qlow = timeseries_point_quantile(sim,quantiles[1],t)
    qhigh = timeseries_point_quantile(sim,quantiles[2],t)
  else
    m,v = timeseries_steps_meanvar(sim)
    qlow = timeseries_steps_quantile(sim,quantiles[1])
    qhigh = timeseries_steps_quantile(sim,quantiles[2])
  end

  trajectories = length(sim)
  EnsembleSummary{T,N,typeof(t),typeof(m),typeof(v),typeof(qlow),typeof(qhigh)}(t,m,v,qlow,qhigh,trajectories,sim.elapsedTime,sim.converged)
end

function timeseries_point_mean(sim,ts)
  DiffEqArray([timepoint_mean(sim,t) for t in ts],ts)
end
function timeseries_point_median(sim,ts)
  DiffEqArray([timepoint_median(sim,t) for t in ts],ts)
end
function timeseries_point_quantile(sim,q,ts)
  DiffEqArray([timepoint_quantile(sim,q,t) for t in ts],ts)
end
function timeseries_point_meanvar(sim,ts)
  m,v = timepoint_meanvar(sim,first(ts))
  means = [m]
  vars = [v]
  for t in Iterators.drop(ts,1)
    m,v = timepoint_meanvar(sim,t)
    push!(means,m)
    push!(vars,v)
  end
  DiffEqArray(means,ts),DiffEqArray(vars,ts)
end
function timeseries_point_meancov(sim,ts1,ts2)
  reshape([timepoint_meancov(sim,t1,t2) for t1 in ts1 for t2 in ts2],length(ts1),length(ts2))
end
function timeseries_point_meancor(sim,ts1,ts2)
  reshape([timepoint_meancor(sim,t1,t2) for t1 in ts1 for t2 in ts2],length(ts1),length(ts2))
end
function timeseries_point_weighted_meancov(sim,W,ts1,ts2)
  reshape([timepoint_meancov(sim,W,t1,t2) for t1 in ts1 for t2 in ts2],length(ts1),length(ts2))
end

function componentwise_mean(A)
  x0 = first(A)
  n = 0
  mean = zero(x0)
  for x in A
    n += 1
    if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
      mean .+= x
    else
      mean += x
    end
  end
  if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
    mean ./= n
  else
    mean /= n
  end
  mean
end

# Welford algorithm
# https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
function componentwise_meanvar(A;bessel=true)
  x0 = first(A)
  n = 0
  mean = zero(x0)
  M2 = zero(x0)
  delta = zero(x0)
  delta2 = zero(x0)
  for x in A
    n += 1
    if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
      delta .= x .- mean
      mean .+= delta./n
      delta2 .= x .- mean
      M2 .+= delta.*delta2
    else
      delta = x .- mean
      mean += delta./n
      delta2 = x .- mean
      M2 += delta.*delta2
    end
  end
  if n < 2
    return NaN
  else
    if bessel
      if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
        M2 .= M2 ./ (n .- 1)
      else
        M2 = M2 ./ (n .- 1)
      end
    else
      if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
        M2 .= M2 ./ n
      else
        M2 = M2 ./ n
      end
    end
    return mean,M2
  end
end

function componentwise_meancov(A,B;bessel=true)
  x0 = first(A)
  y0 = first(B)
  n = 0
  meanx = zero(x0)
  meany = zero(y0)
  C = zero(x0)
  dx = zero(x0)
  for (x,y) in zip(A,B)
    n += 1
    if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
      dx .= x .- meanx
      meanx .+= dx./n
      meany .+= (y.-meany)./n
      C .+= dx .* (y .- meany)
    else
      dx = x .- meanx
      meanx += dx./n
      meany += (y.-meany)./n
      C += dx .* (y .- meany)
    end
  end
  if n < 2
    return NaN
  else
    if bessel
      if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
        C .= C ./ (n .- 1)
      else
        C = C ./ (n .- 1)
      end
    else
      if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
        C .= C ./ n
      else
        C = C ./ n
      end
    end
    return meanx,meany,C
  end
end

function componentwise_meancor(A,B;bessel=true)
  mx,my,cov = componentwise_meancov(A,B;bessel=bessel)
  mx,vx = componentwise_meanvar(A;bessel=bessel)
  my,vy = componentwise_meanvar(B;bessel=bessel)
  if typeof(vx) <: AbstractArray
    vx .= sqrt.(vx)
    vy .= sqrt.(vy)
  else
    vx = sqrt.(vx)
    vy = sqrt.(vy)
  end
  mx,my,cov./(vx.*vy)
end

function componentwise_weighted_meancov(A,B,W;weight_type=:reliability)
  x0 = first(A)
  y0 = first(B)
  w0 = first(W)
  n = 0
  meanx = zero(x0)
  meany = zero(y0)
  wsum = zero(w0)
  wsum2 = zero(w0)
  C = zero(x0)
  dx = zero(x0)
  for (x,y,w) in zip(A,B,W)
    n += 1
    if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
      wsum .+= w
      wsum2 .+= w.*w
      dx .= x .- meanx
      meanx .+= (w ./ wsum) .* dx
      meany .+= (w ./ wsum) .* (y .- meany)
      C .+= w .* dx .* (y .- meany)
    else
      wsum += w
      wsum2 += w.*w
      dx = x .- meanx
      meanx += (w ./ wsum) .* dx
      meany += (w ./ wsum) .* (y .- meany)
      C += w .* dx .* (y .- meany)
    end
  end
  if n < 2
    return NaN
  else
    if weight_type == :population
      if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
        C .= C ./ wsum
      else
        C = C ./ wsum
      end
    elseif weight_type == :reliability
      if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
        C .= C ./ (wsum .- wsum2 ./ wsum)
      else
        C = C ./ (wsum .- wsum2 ./ wsum)
      end
    elseif weight_type == :frequency
      if typeof(x0) <: AbstractArray && !(typeof(x0) <: SArray)
        C .= C ./ (wsum .- 1)
      else
        C = C ./ (wsum .- 1)
      end
    else
      error("The weight_type which was chosen is not allowed.")
    end
    return meanx,meany,C
  end
end

export get_timestep, get_timepoint, apply_timestep, apply_timepoint,
       componentwise_vectors_timestep, componentwise_vectors_timepoint

export componentwise_mean, componentwise_meanvar

export timestep_mean, timestep_median, timestep_quantile, timestep_meanvar,
       timestep_meancov, timestep_meancor, timestep_weighted_meancov

export timeseries_steps_mean, timeseries_steps_median, timeseries_steps_quantile,
       timeseries_steps_meanvar, timeseries_steps_meancov,
       timeseries_steps_meancor, timeseries_steps_weighted_meancov

export timepoint_mean, timepoint_median, timepoint_quantile,
       timepoint_meanvar, timepoint_meancov,
       timepoint_meancor, timepoint_weighted_meancov

export timeseries_point_mean, timeseries_point_median, timeseries_point_quantile,
       timeseries_point_meanvar, timeseries_point_meancov,
       timeseries_point_meancor, timeseries_point_weighted_meancov

end


"""
tupling of two or more discrete functions
if we have
    f1 : D → R1
    f2 : D → R2
        …
    fk : D → Rk
then FunTupling((f1, …, fk)) is F
    F : D → R1 × … × Rk
"""
struct FunTupling{N, T, D, R} <: CompositeFunction
    factors :: NTuple{N, T}
    dom :: D
    rng :: R
end

const FTuple = FunTupling

function FTuple(factors)
    dom = domain(first(factors))
    rng = DirectProduct(map(codomain, factors))
    return FTuple(factors, dom, rng)
end

# creating functional tupling from two tuples
# not necessarily equal length !

function FTuple(from :: NTuple{N, Int}, to :: NTuple{M, Int}) where N where M
    funs = tuple(map(x -> ExtendedResidueFunction(from, x), to)...)
    return FTuple(funs)
end

# creating functional product from dom and codom

function FTuple(dom :: AbstractProduct, codom :: AbstractProduct)
    #fromto = zip(from, to)
    #funs = tuple(map(ResidueFunction, fromto)...)
    #return FProd(funs)
    from = size(dom)
    to = size(codom)
    return FTuple(from, to)
end

function (ft :: FTuple{N, T, D, R})(v) where {N,T,D,R}
    funs = factors(ft)
    n = length(funs)
    return NTuple{N}(funs[i](v) for i in 1 : n)
end

function Base.setindex!(ft :: FTuple, v :: NTuple, k)
    fs = factors(ft)
    for (i, val) in enumerate(v)
        fs[i][k] = val
    end
end

(fs :: FTuple)(v...) = fs(tuple(v...))

FTuple(factors...) = FTuple(tuple(factors...))


module Day12
export day12

parseAngle(arg) = div(arg, 90)

# char => (:name, factor, parsefunction)
const COMMANDS = Dict('R' => (:rotate, -1, parseAngle),
                      'L' => (:rotate,  1, parseAngle),
                      'F' => (:forward, 1, identity),
                      'N' => (:go, im, identity),
                      'S' => (:go, -im, identity),
                      'W' => (:go, -1, identity),
                      'E' => (:go, 1, identity))

function parseLine(line)
    command = line[1]
    arg = parse(Int, line[2:end])
    name, factor, func = COMMANDS[command]
    name, factor*func(arg)
end

function readInput(inputfile="inputs/input12.txt")
    [parseLine(line) for line in eachline(inputfile)]
end

function day12(inputfile="inputs/input12.txt")
    instructions = readInput(inputfile)
    (day12Part1(instructions), day12Part2(instructions))
end

function day12Part1(instructions)
    at = complex(0)
    facing = complex(1)
    for (command, value) in instructions
        if command === :go
            at += value
        elseif command === :rotate
            facing *= (sign(value)*im)^abs(value)
        elseif command === :forward
            at += facing*value
        else
            error("Unknown instruction: $command")
        end
    end
    abs(real(at)) + abs(imag(at))
end

function day12Part2(instructions)
    waypoint = 10+1im
    at = complex(0)
    for (command, value) in instructions
        if command === :go
            waypoint += value
        elseif command === :rotate
            waypoint *= (sign(value)*im)^abs(value)
        elseif command === :forward
            at += waypoint*value
        else
            error("Unknown instruction: $command")
        end
    end
    abs(real(at)) + abs(imag(at))
end

end #module


"""
**Adadelta constructor**

```julia
    Adadelta(; ρ::Float64=0.9, ϵ::Float64=1e-8)
```

Algorithm :

```math
\\begin{align*}
E[g^2]_t =& \\rho  E[g^2]_{t-1}+(1-\\rho)g^2_t\\\\
\\text{RMS}[g]_t =& \\sqrt{E[g^2]_t + \\epsilon}\\\\
\\text{RMS}[\\Delta x]_{t-1} =& \\sqrt{E[\\Delta x^2]_{t-1} + \\epsilon}\\\\
\\Delta x_t =& \\text{RMS}[Δx]_{t-1} * g_t / \\text{RMS}[g]_t\\\\
E[\\Delta x^2]_{t} =& \\rho E[\\Delta x^2]_{t} + (1 - \\rho) \\Delta x^2_t\\\\
\\end{align*}
```

[Algorithm Reference](https://arxiv.org/abs/1212.5701)
"""
mutable struct Adadelta <: Optimizer
    opt_type::String
    t::Int64
    ϵ::Float64
    ρ::Float64
    E_g²_t::AbstractArray
    E_Δx²_t_1::AbstractArray
    Δx²_t_1::AbstractArray
end

function Adadelta(; ρ::Real=0.9, ϵ::Real=1e-8)
    @assert ρ > 0.0 "ρ must be greater than 0"
    @assert ϵ > 0.0 "ϵ must be greater than 0"

    Adadelta("Adadelta", 0, ϵ, ρ, [], [], [])
end

params(opt::Adadelta) = "ϵ=$(opt.ϵ), ρ=$(opt.ρ)"

function update(opt::Adadelta, g_t::AbstractArray{T}) where {T<:Real}
    # resize accumulated and squared updates
    if opt.t == 0
        opt.E_g²_t = zero(g_t)
        opt.E_Δx²_t_1  = zero(g_t)
        opt.Δx²_t_1 = zero(g_t)
    end

    # accumulate gradient
    opt.E_g²_t = opt.ρ * opt.E_g²_t + (one(T) - opt.ρ) * (g_t .^ 2)

    # compute update
    RMS_g_t = sqrt.(opt.E_g²_t .+ opt.ϵ)
    RMS_Δx_t_1 = sqrt.(opt.E_Δx²_t_1 .+ opt.ϵ)
    Δx_t = RMS_Δx_t_1 .* g_t ./ RMS_g_t

    # accumulate updates
    opt.E_Δx²_t_1 = opt.ρ * opt.E_Δx²_t_1 + (one(T) - opt.ρ) * (Δx_t .^ 2)

    return Δx_t
end

"""

"""


import FourierTransforms
import FourierTransforms: fft_kernel_sizes
using Test, LinearAlgebra, OffsetArrays

include("symbolic.jl")

function testdft(x::AbstractVector{T}) where T<:Union{Real,Complex}
    Tr = real(float(T))
    Tc = complex(float(T))
    y = similar(x, Tc)
    N = length(x)
    for (k1, yind) in enumerate(eachindex(y))
        fct = zero(Tr)
        yk  = zero(Tc)
        for xi in x
            yk  += xi*complex(cos(fct), -sin(fct))
            fct += one(Tr)/N*(k1 - 1)*2*π
        end
        y[yind] = yk
    end
    return y
end

@testset "Multiplication of size: $sz" for sz in [collect(fft_kernel_sizes); 511; 512; 513]
  @testset "Real plan" begin
      xr = rand(sz)
      @test FourierTransforms.fft(xr) ≈ testdft(xr)
      p = FourierTransforms.plan_fft(xr)
      @test p*xr == FourierTransforms.fft(xr)
      @test p\(p*xr) ≈ xr
  end

  @testset "Complex plan applied to $atype" for (atype, x) in (
      ("Vector", rand(Complex{Float64}, sz)),
      ("OffsetVector", OffsetArray(rand(Complex{Float64}, sz), 0:(sz - 1)))
  )
      p = FourierTransforms.plan_fft(x)
      @test mul!(similar(x), p, x) == p*x
      @test p*x ≈ testdft(x)
      @test p\(p*x) ≈ x
  end
end


using MultipleViewGeometry
using Test

@testset "Operators Tests" begin include("operators_tests.jl") end
@testset "Data Normalization Tests" begin include("data_normalization_tests.jl") end
@testset "Transform Tests" begin include("transform_tests.jl") end
@testset "Fundamental Matrix Construction Tests" begin include("construct_fundamentalmatrix_tests.jl") end
@testset "Homography Matrix Construction Tests" begin include("construct_homographymatrix_tests.jl") end
@testset "Essential Matrix Construction Tests" begin include("construct_essentialmatrix_tests.jl") end
@testset "Estimate Fundamental Matrix Tests" begin include("estimate_fundamental_tests.jl") end
@testset "Estimate Homography Matrix Tests" begin include("estimate_homography_tests.jl") end
@testset "Projection Matrix Construction Tests" begin include("construct_projectionmatrix_tests.jl") end
@testset "Projection Matrices Construction Tests" begin include("construct_projectionmatrices_tests.jl") end
@testset "Rotations Tests" begin include("rotations_tests.jl") end
@testset "Cost Function Tests" begin include("cost_functions_tests.jl") end
#@testset "Fundamental Matrix Covariance Test" begin include("fundamental_matrix_covariance_test.jl") end
@testset "Satisfy Epipolar Constraints Test" begin include("satisfy_epipolar_constraints_tests.jl") end
@testset "Triangulate Test" begin include("triangulate_tests.jl") end
@testset "Planar Triangulate Test" begin include("planar_triangulate_tests.jl") end
@testset "Noise Test" begin include("perturb_tests.jl") end


# adapted from https://github.com/chaaland/EllipseFit.jl by Casey Haaland

using LinearAlgebra

export ParametricFormEllipse, ConicFormEllipse, parametric2conic, conic2parametric

abstract type EllipseForm end

struct ParametricFormEllipse{T<:Real, U<:Real, V<:Real}
    axis_lengths::Array{T}
    center::Array{U}
    ccw_angle::V

    function ParametricFormEllipse(axis_lengths::Array{T}, center::Array{U}, ccw_angle::V) where {T<:Real, U<:Real, V<:Real}
        axis_lengths = vec(axis_lengths)
        center = vec(center)

        if size(center)[1] != 2
            error("Parameter 'center' must be vector of length 2")
        elseif size(axis_lengths)[1] != 2
            error("Parameter 'axis_lengths' must be vector of length 2")
        elseif sum(axis_lengths .< 0) > 0
            error("Parameter 'axis_lengths' must be nonnegative")
        else
            new{T,U,V}(axis_lengths, center, ccw_angle)
        end
    end
end

function parametric2conic(ellipse::ParametricFormEllipse)
    # inputs
    a = ellipse.axis_lengths[1] / 2
    b = ellipse.axis_lengths[2] / 2
    x₀ = ellipse.center[1]
    y₀ = ellipse.center[2]
    θ = deg2rad(ellipse.ccw_angle)

    # outputs
    A = a^2 * sin(θ)^2 + b^2 * cos(θ)^2
    B = 2 * (b^2 - a^2) * sin(θ) * cos(θ)
    C = a^2 * cos(θ)^2 + b^2 * sin(θ)^2
    D = -2 * A * x₀ - B * y₀
    E = -B * x₀ - 2 * C * y₀
    F = A * x₀^2 + B * x₀ * y₀ + C * y₀^2 - a^2 * b^2

    return ConicFormEllipse(A, B, C, D, E, F)
end

struct ConicFormEllipse{T<:Real} <: EllipseForm
    A::T
    B::T
    C::T
    D::T
    E::T
    F::T
    function ConicFormEllipse(A::T, B::T, C::T, D::T, E::T, F::T) where {T<:Real}
        if B ^2 - 4 * A * C >= 0
            error("Discriminant is non-negative. Input does not denote an ellipse")
        end
        new{T}(A, B, C, D, E, F)
    end
end

ConicFormEllipse(A::Real, B::Real, C::Real, D::Real, E::Real, F::Real) = ConicFormEllipse(promote(A, B, C, D, E, F)...)

function conic2parametric(ellipse::ConicFormEllipse)
    # TODO reference the different equations properly

    # mostly from here: https://en.wikipedia.org/wiki/Ellipse#General_ellipse

    #=
    Helper for converting from conic form to parametric form of ellipse
    Given an ellipse in standard conic section form
            A * x^2 + B * x * y + C * y^2 + D * x + E * y + F = 0
    convert it to parametric form
            [x_c y_c] + rot_mat2d(ccw_angle) * [a*cos(theta) b*sin(theta)]
    Args :

    Returns :
        axis_lengths : array of the form [2*a 2*b] where a is half the width
                          and b is half the height (before rotation)
        center : array of the x and y coordinates of the center of the ellipse
        ccw_angle : The counter clockwise angle (in degrees) to rotate the ellipse wrt
                    the positive x-axis
    =#

    A = ellipse.A; B = ellipse.B; C = ellipse.C; D = ellipse.D; E = ellipse.E; F = ellipse.F

    A_Q = [A B/2 D/2; B/2 C E/2; D/2 E/2 F]
    Δ = det(A_Q) # -1/4 * (AE² + CD² + FB² -BDE - 4 * ACF)

    A_33 = [A B/2; B/2 C]
    J = det(A_33) # -1/4 * (B² - 4 * AC)

    I = tr(A_33)

    sq = sqrt((A - C)^2 + B^2)

    #=
    now write the equations (21) and (22) at http://mathworld.wolfram.com/Ellipse.html
    in terms of Δ, J, I and sq --> semiaxes a and b
    =#

    a = sqrt(-2 * Δ * (I + sq)) / (2*J)
    b = sqrt(-2 * Δ * (I - sq)) / (2*J)

    axis_lengths = [2*a 2*b]

    x₀, y₀ = inv(A_33) * [-D/2; -E/2]

    center = [x₀ y₀]

    # all of the angles are mapped onto [-90,90]
    # as the system has a period of 180°
    # i.e. [-280, -100, 80, 260, etc] → 80

    if B == 0
        if A <= C
            θ = 0
        else
            θ = π/2
        end
    else
        θ = atan(1/B * (C - A - sq))
    end

    ccw_angle = rad2deg(θ)

    return ParametricFormEllipse(axis_lengths, center, ccw_angle)
end


# ---
# title: 313. Super Ugly Number
# id: problem313
# author: Indigo
# date: 2021-06-10
# difficulty: Medium
# categories: Math, Heap
# link: <https://leetcode.com/problems/super-ugly-number/description/>
# hidden: true
# ---
#
# Write a program to find the `nth` super ugly number.
#
# Super ugly numbers are positive numbers whose all prime factors are in the
# given prime list `primes` of size `k`.
#
# **Example:**
#
#
#
#     Input: n = 12, primes = [2,7,13,19]
#     Output: 32
#     Explanation:[1,2,4,7,8,13,14,16,19,26,28,32] is the sequence of the first 12
#                  super ugly numbers given primes = [2,7,13,19] of size 4.
#
# **Note:**
#
#   * `1` is a super ugly number for any given `primes`.
#   * The given numbers in `primes` are in ascending order.
#   * 0 < `k` ≤ 100, 0 < `n` ≤ 106, 0 < `primes[i]` < 1000.
#   * The nth super ugly number is guaranteed to fit in a 32-bit signed integer.
#
#
## @lc code=start
using LeetCode

function nth_super_ugly_number(n::Int, primes::Vector{Int})
    dp = fill(1, n)
    len = length(primes)
    cur_min = primes[:]
    ptrs = fill(1, len)

    @inbounds for i in 2:n
        minn = minimum(cur_min)
        dp[i] = minn
        @simd for pidx in 1:len
            if minn == cur_min[pidx]
                ptrs[pidx] += 1
                cur_min[pidx] = dp[ptrs[pidx]] * primes[pidx]
            end
        end
    end
    return dp[end]
end
## @lc code=end


function is_hermitian(M; atol = ATOL)
	isapprox(M, M', atol = atol)
end

function is_positive_semidefinite(M; atol = ATOL)
	!any(eigvals(M) .< - atol)
end

function is_a_gram_matrix(M; atol = ATOL)
	is_hermitian(M, atol = atol) && is_positive_semidefinite(M, atol = atol) && all([isapprox(M[i,i], one(eltype(M)), atol = atol) for i in 1:size(M)[1]])
end

function is_unitary(U; atol = ATOL)
    isapprox(U' * U, Matrix(I, size(U)), atol = atol)
end

function is_orthonormal(U; atol = ATOL)
	for i = 1:size(U)[2]
		isapprox(abs(norm(U[:, i])), 1., atol = atol) ? nothing : return false
		for j = 1:size(U)[2]
			if j < i
				isapprox(abs(dot(U[:, i], U[:, j])), 0., atol = atol) ? nothing : return false
			end
		end
	end
end

function is_column_normalized(M; atol = ATOL)

	for i = 1:size(M)[2]
		@test  abs(norm(M[:, i])) ≈ 1. atol = atol
	end
end


### A Pluto.jl notebook ###
# v0.12.16

using Markdown
using InteractiveUtils

# ╔═╡ e077ab16-22e5-11eb-15ed-5f0654bf7328
using Pkg, DrWatson

# ╔═╡ e75a9a06-22e5-11eb-2e36-8d4f62b830ed
begin
	@quickactivate "StatisticsWithJuliaPlutoNotebooks"
	using Distributions, Plots
end;

# ╔═╡ ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
md"## Listing6.11"

# ╔═╡ 1aa41898-3afa-11eb-2f49-570853ce6435
begin
	mu, sig = 2, 3
	eta = sqrt(3)*sig/pi
	n, N = 15, 10^7
	dNormal   = Normal(mu, sig)
	dLogistic = Logistic(mu, eta)
	xGrid = -8:0.1:12

	sNormal   = [var(rand(dNormal,n)) for _ in 1:N]
	sLogistic = [var(rand(dLogistic,n)) for _ in 1:N]
end;

# ╔═╡ 2eb0846a-3b2b-11eb-1154-ff7ca71f3c27
begin
	p1 = plot(xGrid, pdf.(dNormal,xGrid), c=:blue, label="Normal")
	p1 = plot!(xGrid, pdf.(dLogistic,xGrid), c=:red, label="Logistic",
		xlabel="x",ylabel="Density", xlims= (-8,12), ylims=(0,0.16))

	p2 = stephist(sNormal, bins=200, c=:blue, normed=true, label="Normal")
	p2 = stephist!(sLogistic, bins=200, c=:red, normed=true, label="Logistic",
		xlabel="Sample Variance", ylabel="Density", xlims=(0,30), ylims=(0,0.14))

	plot(p1, p2, size=(800, 400))
end

# ╔═╡ 475ff888-22e6-11eb-2354-09f8f40a8e12
md"## End of listing6.11"

# ╔═╡ Cell order:
# ╟─ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
# ╠═e077ab16-22e5-11eb-15ed-5f0654bf7328
# ╠═e75a9a06-22e5-11eb-2e36-8d4f62b830ed
# ╠═1aa41898-3afa-11eb-2f49-570853ce6435
# ╠═2eb0846a-3b2b-11eb-1154-ff7ca71f3c27
# ╟─475ff888-22e6-11eb-2354-09f8f40a8e12


#==============================================================================
    Code for solving the Hamiltonian Jacboi Bellman for
	   a problem with non-convexities as in Skiba 1978

	Translated Julia code from Matlab code by Ben Moll:
        http://www.princeton.edu/~moll/HACTproject.htm
==============================================================================#

using Plots, SparseArrays, LinearAlgebra

γ= 2.0 #gamma parameter for CRRA utility
ρ = 0.05 #the discount rate
α = 0.3 # the curvature of the production function (cobb-douglas)
δ = 0.05 # the depreciation rate
AH = 0.6 #high prodictivity, costs κ
AL = 0.4 #low prodictivity, free
κ = 2 #fixed cost

kssH = (α*AH/(ρ+δ))^(1/(1-α))+κ
k_st = κ./(1-(AL/AH).^(1/α))

# create the grid for k
H = 1000 #number of points on grid
k_min = 0.001*kssH # min value
k_max = 1.3*kssH # max value
k = LinRange(k_min, k_max, H)
dk = (k_max-k_min)/(H-1)

#Create the production function
yH = AH*max.(k.-κ,0).^α #high productivity
yL = AL*k.^α #low prodictivity
y = max.(yH,yL)

plot(k,y, ylabel="\$f(k)\$", xlabel="\$k\$",
		xlims=(k_min,k_max), ylims=(0,0.9),legend=false,color=:black)
plot!(k,yH, line=:dash, color=:red)
plot!(k,yL, line=:dashdot, color=:orange)

# use Ito's lemma to find the drift and variance of our optimization equation

maxit = 150 #needed to increase this for same results in julia
ε = 10^(-6)
Δ = 1000
# set up all of these empty matrices
dVf, dVb, dV0, c, If, Ib, I0, V= [zeros(H,1) for i in 1:8]

# Now it's time to solve the model, first put in a guess for the value function
v0 = (k.^α).^(1-γ)/(1-γ)/ρ
v=v0

dist = [] # set up empty array for the convergence criteria

for n = 1:maxit
    global V=v

    #Now set up the forward difference

    dVf[1:H-1] = (V[2:H] - V[1:H-1])/dk
    dVf[H] = (y[H]-δ.*k_max).^(-γ) # imposes a constraint

    #backward difference
    dVb[2:H] = (V[2:H] - V[1:H-1])/dk
    dVb[1] = (y[1]-δ.*k_min).^(-γ)

    #I_concave = Vab .> Vaf # indicator for whether the value function is concave

    # Consumption and savings functions
    cf = max.(dVf,10^(-10)).^(-1/γ)
	μf = y -δ.*k -cf
    Hf = cf.^(1-γ)/(1-γ) + dVf.*μf

    # consumption and saving backwards difference

	cb = max.(dVb,10^(-10)).^(-1/γ)
	μb = y -δ.*k -cb
    Hb = cb.^(1-γ)/(1-γ) + dVb.*μb

    #consumption and derivative of the value function at the steady state

    c0 = y- δ.*k
    dV0 = max.(c0, 10^(-10)).^(-γ)
	H0 = c0.^(1-γ)/(1-γ)

    # Upwind scheme chooses between the forward or backward difference
	Ieither = (1 .- (μf.>0)) .* (1 .- (μb.<0))
	Iunique = (μb.<0).*(1 .- (μf.>0)) + (1 .- (μb.<0)).*(μf.>0)
	Iboth = (μb.<0).*(μf.>0)
    If= Iunique.*(μf .> 0) + Iboth.*(Hf.>=Hb)  #positive drift → forward difference
    Ib = Iunique.*(μb .< 0) + Iboth.*(Hb.>=Hf) #negative drift → backward difference
    I0 = Ieither  #at steady state

    global c = cf.*If + cb.*Ib + c0.*I0
    u = (c.^(1-γ))/(1-γ)

	# CONSTRUCT MATRIX
    global X = -μb.*Ib/dk
    global Y = - μf.*If/dk  + μb.*Ib/dk
    global Z = μf.*If/dk

	global A = spdiagm(0=>Y[:], -1=>X[2:end], 1=>Z[1:end-1])

  	B = (1/Δ + ρ)*sparse(I, H, H) - A

  	u_stacked= reshape(u, H, 1)
  	V_stacked = reshape(V,H, 1)

  	b = u_stacked + (V_stacked/Δ)

  	V_stacked = B\b

  	global V = reshape(V_stacked, H, 1)

  	V_change = V-v

  	global v= V

  	# need push function to add to an already existing array
  	push!(dist, findmax(abs.(V_change))[1])
  	if dist[n].< ε
      	println("Value Function Converged Iteration=")
      	println(n)
      	break
  	end

end

# calculate the savings for kk
kdot = y -δ.*k -c
dV_upwind = dVf.*If + dVb.*Ib+dV0.*I0
Verr = c.^(1-γ)/(1-γ) + dV_upwind.*kdot - ρ.*V

#plot for consumption
plot(k, c, ylabel="\$c(k)\$", xlabel="\$k\$", label="Consumption")
plot!(k,y-δ.*k, label="Production net of depreciation", legend=:bottomright)

#plot for savings
plot(k, kdot, ylabel="\$s(k)\$", xlabel="\$k\$", legend=false)
plot!(k,zeros(H,1), line=:dash)

#Plot the value function
plot(k, V,ylabel="\$V(k)\$", xlabel="\$k\$", legend=false)

#Plot the error in HJB equation
plot(k,Verr,ylabel="Value Function error", xlabel="\$k\$", legend=false)


#!/usr/bin/julia

using DataFrames

srand(46)

SAMPLES = 100
ITERS=200
ITERS_p=200

values = readdlm("SDvalues.dat") |> vec
# A = 5 + randn(10000)
# B = -5 + randn(10000)
# values = vcat(A,B)
info("Real mean: $(mean(values))")

function bootstrap(array, frac; N=1000)
    L = length(array)
    chunksize = floor(Int64,frac*L)
    0 < chunksize ≤ L || error("chunksize = $chunksize ∉ (0,L)")
    means = zeros(N)
    for i ∈ 1:N
        chunk = rand(array,chunksize)
        means[i] = mean(chunk)
    end
    return mean(means), std(means)
end

function jacknife(array)
    L = length(array)
    means = zeros(L)
    for i ∈ 1:L
        means[i] = mean(array[j] for j in 1:L if j≠i)
    end
    return mean(means), std(means)
end

jdf = DataFrame()
bdf = DataFrame()

fr = 1/e
for i in 1:ITERS
    smallsample = rand(values,SAMPLES)
    jackμ, jackσ = jacknife(smallsample)
    bootμ, bootσ = bootstrap(smallsample, fr)
    jdf = vcat(jdf, DataFrame(i=i,
                              μ=jackμ,
                              σ=jackσ))
    bdf = vcat(bdf, DataFrame(i=i,
                              fraction=fr,
                              μ=bootμ,
                              σ=bootσ))
end

writetable("data_jack.csv", jdf)
writetable("data_boot.csv", bdf)


# Linear Interpolation
function (A::LinearInterpolation{<:AbstractVector{<:Number}})(t::Number)
  idx = findfirst(x->x>=t,A.t)-1
  idx == 0 ? idx += 1 : nothing
  θ = (t - A.t[idx])/ (A.t[idx+1] - A.t[idx])
  (1-θ)*A.u[idx] + θ*A.u[idx+1]
end

function (A::LinearInterpolation{<:AbstractMatrix{<:Number}})(t::Number)
  idx = findfirst(x->x>=t,A.t)-1
  idx == 0 ? idx += 1 : nothing
  θ = (t - A.t[idx])/ (A.t[idx+1] - A.t[idx])
  (1-θ)*A.u[:,idx] + θ*A.u[:,idx+1]
end

# Quadratic Interpolation
function (A::QuadraticInterpolation{<:AbstractVector{<:Number}})(t::Number)
  idx = findfirst(x->x>=t,A.t)-1
  idx == 0 ? idx += 1 : nothing
  if idx == length(A.t) - 1
    i₀ = idx - 1; i₁ = idx; i₂ = i₁ + 1;
  else
    i₀ = idx; i₁ = i₀ + 1; i₂ = i₁ + 1;
  end
  l₀ = ((t-A.t[i₁])*(t-A.t[i₂]))/((A.t[i₀]-A.t[i₁])*(A.t[i₀]-A.t[i₂]))
  l₁ = ((t-A.t[i₀])*(t-A.t[i₂]))/((A.t[i₁]-A.t[i₀])*(A.t[i₁]-A.t[i₂]))
  l₂ = ((t-A.t[i₀])*(t-A.t[i₁]))/((A.t[i₂]-A.t[i₀])*(A.t[i₂]-A.t[i₁]))
  A.u[i₀]*l₀ + A.u[i₁]*l₁ + A.u[i₂]*l₂
end

function (A::QuadraticInterpolation{<:AbstractMatrix{<:Number}})(t::Number)
  idx = findfirst(x->x>=t,A.t)-1
  idx == 0 ? idx += 1 : nothing
  if idx == length(A.t) - 1
    i₀ = idx - 1; i₁ = idx; i₂ = i₁ + 1;
  else
    i₀ = idx; i₁ = i₀ + 1; i₂ = i₁ + 1;
  end
  l₀ = ((t-A.t[i₁])*(t-A.t[i₂]))/((A.t[i₀]-A.t[i₁])*(A.t[i₀]-A.t[i₂]))
  l₁ = ((t-A.t[i₀])*(t-A.t[i₂]))/((A.t[i₁]-A.t[i₀])*(A.t[i₁]-A.t[i₂]))
  l₂ = ((t-A.t[i₀])*(t-A.t[i₁]))/((A.t[i₂]-A.t[i₀])*(A.t[i₂]-A.t[i₁]))
  A.u[:,i₀]*l₀ + A.u[:,i₁]*l₁ + A.u[:,i₂]*l₂
end

# Lagrange Interpolation
function (A::LagrangeInterpolation{<:AbstractVector{<:Number}})(t::Number)
  idxs = findRequiredIdxs(A,t)
  if A.t[idxs[1]] == t
    return A.u[idxs[1]]
  end
  N = zero(A.u[1]); D = zero(A.t[1]); tmp = N
  for i = 1:length(idxs)
    mult = one(A.t[1])
    for j = 1:(i-1)
      mult *= (A.t[idxs[i]] - A.t[idxs[j]])
    end
    for j = (i+1):length(idxs)
      mult *= (A.t[idxs[i]] - A.t[idxs[j]])
    end
    tmp = inv((t - A.t[idxs[i]]) * mult)
    D += tmp
    N += (tmp * A.u[idxs[i]])
  end
  N/D
end

function (A::LagrangeInterpolation{<:AbstractMatrix{<:Number}})(t::Number)
  idxs = findRequiredIdxs(A,t)
  if A.t[idxs[1]] == t
    return A.u[:,idxs[1]]
  end
  N = zero(A.u[:,1]); D = zero(A.t[1]); tmp = D
  for i = 1:length(idxs)
    mult = one(A.t[1])
    for j = 1:(i-1)
      mult *= (A.t[idxs[i]] - A.t[idxs[j]])
    end
    for j = (i+1):length(idxs)
      mult *= (A.t[idxs[i]] - A.t[idxs[j]])
    end
    tmp = inv((t - A.t[idxs[i]]) * mult)
    D += tmp
    @. N += (tmp * A.u[:,idxs[i]])
  end
  N/D
end

function (A::AkimaInterpolation{<:AbstractVector{<:Number}})(t::Number)
  i = searchsortedlast(A.t, t)
  i == 0 && return A.u[1]
  i == length(A.t) && return A.u[end]
  wj = t - A.t[i]
  @evalpoly wj A.u[i] A.b[i] A.c[i] A.d[i]
end

# ConstantInterpolation Interpolation
function (A::ConstantInterpolation{<:AbstractVector})(t::Number)
  if A.dir === :left
    # :left means that value to the left is used for interpolation
    i = searchsortedlast(A.t, t)
    return A.u[max(1, i)]
  else
    # :right means that value to the right is used for interpolation
    i = searchsortedfirst(A.t, t)
    return A.u[min(length(A.t), i)]
  end
end
 function (A::ConstantInterpolation{<:AbstractMatrix})(t::Number)
  if A.dir === :left
    # :left means that value to the left is used for interpolation
    i = searchsortedlast(A.t, t)
    return A.u[:, max(1, i)]
  else
    # :right means that value to the right is used for interpolation
    i = searchsortedfirst(A.t, t)
    return A.u[:, min(length(A.t), i)]
  end
end

# QuadraticSpline Interpolation
function (A::QuadraticSpline{<:AbstractVector{<:Number}})(t::Number)
  i = findfirst(x->x>=t,A.t)
  i == 1 ? i += 1 : nothing
  Cᵢ = A.u[i-1]
  σ = 1//2 * (A.z[i] - A.z[i-1])/(A.t[i] - A.t[i-1])
  A.z[i-1] * (t - A.t[i-1]) + σ * (t - A.t[i-1])^2 + Cᵢ
end

# CubicSpline Interpolation
function (A::CubicSpline{<:AbstractVector{<:Number}})(t::Number)
  i = findfirst(x->x>=t,A.t)
  i == nothing ? i = length(A.t) - 1 : i -= 1
  i == 0 ? i += 1 : nothing
  I = A.z[i] * (A.t[i+1] - t)^3 / (6A.h[i+1]) + A.z[i+1] * (t - A.t[i])^3 / (6A.h[i+1])
  C = (A.u[i+1]/A.h[i+1] - A.z[i+1]*A.h[i+1]/6)*(t - A.t[i])
  D = (A.u[i]/A.h[i+1] - A.z[i]*A.h[i+1]/6)*(A.t[i+1] - t)
  I + C + D
end

# BSpline Curve Interpolation
function (A::BSplineInterpolation{<:AbstractVector{<:Number}})(t::Number)
  # change t into param [0 1]
  idx = searchsortedlast(A.t,t)
  idx == length(A.t) ? idx -= 1 : nothing
  t = A.p[idx] + (t - A.t[idx])/(A.t[idx+1] - A.t[idx]) * (A.p[idx+1] - A.p[idx])
  n = length(A.t)
  N = spline_coefficients(n,A.d,A.k,t)
  ucum = zero(eltype(A.u))
  for i = 1:n
    ucum += N[i] * A.c[i]
  end
  ucum
end

# BSpline Curve Approx
function (A::BSplineApprox{<:AbstractVector{<:Number}})(t::Number)
  # change t into param [0 1]
  idx = searchsortedlast(A.t,t)
  idx == length(A.t) ? idx -= 1 : nothing
  t = A.p[idx] + (t - A.t[idx])/(A.t[idx+1] - A.t[idx]) * (A.p[idx+1] - A.p[idx])
  n = length(A.t)
  N = spline_coefficients(A.h,A.d,A.k,t)
  ucum = zero(eltype(A.u))
  for i = 1:A.h
    ucum += N[i] * A.c[i]
  end
  ucum
end

# Curvefit
function (A::CurvefitCache{<:AbstractVector{<:Number}})(t::Union{AbstractVector{<:Number},Number})
  A.m(t,A.pmin)
end


### A Pluto.jl notebook ###
# v0.17.4

using Markdown
using InteractiveUtils

# ╔═╡ 303488a5-8102-4077-8cad-12655d8c5777
begin
	using ScikitLearn
	using Statistics
	using StatsBase: sample
	using DataFrames
	using CSV
	using Bootstrap
end

# ╔═╡ 6790c593-f5d3-4e45-be83-c974e581bbf2
md"""
# Bootstrap Notes pt. 2

This document will work through Jason Brownlee's article [A Gentle Introduction to the Bootstrap Method](https://machinelearningmastery.com/a-gentle-introduction-to-the-bootstrap-method/). This is my second document on this method, so I'll try to be more concise. I'll be translating the python in the article to Julia.

Further down in this document, I'll work on another of Brownlee's articles, [How to Calculate Bootstrap Confidence Intervals For Machine Learning Results in Python
](https://machinelearningmastery.com/calculate-bootstrap-confidence-intervals-machine-learning-results-python/).

Date: 2022 April 23
"""

# ╔═╡ 8b12df14-0a1f-4fc4-8677-02f26f16e1d6
md"""
---
The bootstrap method is a **resampling technique** used to estimate statistics on a population. It can be used to estimate **summary statistics** on a population such as the mean or standard deviation. It is used to estimate the skill of a model when making predictions on data not in the training set.

This method provides us confidence intervals which is something cross-validation does not.

This article will teach us:

 - The bootstrap method involves iteratively resampling a dataset with replacement.
 - The bootstrap method has two parameters, the size of the sample and the number of repeats.
 - Scikit-learn provides a resampling funciton. (Note, Julia provides a wrapper for ScikitLearn)
"""

# ╔═╡ 1023b0ba-fc8e-4d49-b795-ed3d75004b69
md"""
## Bootstrap Method

First, get one sample from the data set. This is done by choosing a random set of observations to add to the sample. I.e. this is done *without* replacement.

Then, we draw bootstrap samples from this original sample, but this time *with* replacement. We calculate the statistic on each sample, then calculate the mean of those statistics.

How would we use this method in the case of statistical inference?

For each bootstrap sample:
1. Fit a model on this sample
2. Estimate the skill on the model on the out-of-bag sample

Then calculate the mean of the sample of model skill estimates.

*Note*: The model is evaluated against the samples that weren't selected, *not* against the test dataset, which we can't use until we want to fully evluate the model. The data in the original sample but not the bootstrap sample are called out-of-bag (oob).

*Note*: A useful feature of the bootstrap method is that the resulting sample often forms a normal distribution.

"""

# ╔═╡ 66d8365c-cb3a-4a85-8da3-00eab0e1d1eb
md"""
## Example

In this example, we have a dataset with 6 elements. We will be using the entire dataset to create our EDF, meaning we can sample from the whole thing to create our bootstrap samples.

"""

# ╔═╡ 1a0f56fa-94a9-420f-b318-b6a0f197b140
dataset = 0.1:0.1:0.6

# ╔═╡ 8b712ac8-37fe-4c0e-98c1-495f632ed305
begin
	sampleset = [0.2, 0.1, 0.2, 0.6] # we can have repeats
	oob    = [0.3, 0.4, 0.5] # everything not in sample
end

# ╔═╡ ef6a30bf-2f90-486d-879d-a1a089a89c84

# ╔═╡ 68ad9943-a5a8-492f-8c5b-93bc7b49d36f
md"""
Note that above, we have a single sample (that we made by hand instead of actually using random sampling). This would be sufficient for a single iteration of the bootstrap procedure.

The rest of this iteration would look as follows:
1. Fit the model on `sample`
2. Evaluate the model on `oob`

## ScikitLearn

We can use ScikitLearn's `resample` method to generate the sample for us, but I'm not quite sure how to use the interface so I'll leave this one here.
"""

# ╔═╡ 4006c742-dc11-403d-b921-459b64ddd3c2
md"""
# Bootstrap Confidence Intervals

Now we'll get to the second article, as promised.

Step 1 is to do our bootstrap sampling:

```julia
B = 1000
stats = [calculate_stat(sample(data,N,replace=True)) for _ in 1:B]
```

We can get an **Empirical Confidence Interval** by simply sorting this list and taking the values at the percentile we are interested in. For intance if we want a 95% confidence interval, we take our interval to be the range starting at the 2.5 percentil and ending at the 97.5 percentile.

```julia
alpha=0.95
ordered = sort(stats)
lower = quantile(ordered, (1-alpha)/2)
upper = quantile9ordered, (alpha+((1-alpha)/2))
```
"""

# ╔═╡ e74d5d8f-7d69-4e32-a036-c711194d216e
md"""
## Calculate Classification Accuracy Confidence Interval

We'll read our dataset into a dataframe first.
"""

# ╔═╡ 54890bac-8050-4141-8e7e-2fb603bfe3b8
data = CSV.read("./datasets/pima-indians-diabetes.data.csv", DataFrame;
				header=false)

# ╔═╡ b8ccebb7-b303-4315-94fb-8ff253a90271
begin
	n_iterations = 1
	n_size = trunc(Int64, nrow(data)*0.5)
	stats = Array{Int64, n_iterations}
end

# ╔═╡ bde70fbd-d415-4d6b-b1d0-6eaba93012b9
#for i in 1:1
#	train = bootstrap(data, )
#	train = sample(std, data, BasicSampling(n_size))
#	#test = setdiff(data, train)
#end

# ╔═╡ 0148021c-b8a8-4c47-bcbe-3eae9ef6e75d
bootstrap(std, data[!, "Column6"], BasicSampling(n_iterations))

# ╔═╡ 00000000-0000-0000-0000-000000000001
PLUTO_PROJECT_TOML_CONTENTS = """
[deps]
Bootstrap = "e28b5b4c-05e8-5b66-bc03-6f0c0a0a06e0"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
ScikitLearn = "3646fa90-6ef7-5e7e-9f22-8aca16db6324"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"

[compat]
Bootstrap = "~2.3.3"
CSV = "~0.10.4"
DataFrames = "~1.3.3"
ScikitLearn = "~0.6.4"
StatsBase = "~0.33.16"
"""

# ╔═╡ 00000000-0000-0000-0000-000000000002
PLUTO_MANIFEST_TOML_CONTENTS = """
# This file is machine-generated - editing it directly is not advised

julia_version = "1.7.1"
manifest_format = "2.0"

[[deps.ArgTools]]
uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"

[[deps.Artifacts]]
uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"

[[deps.Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

[[deps.Bootstrap]]
deps = ["DataFrames", "Distributions", "Random", "Statistics", "StatsBase", "StatsModels"]
git-tree-sha1 = "fb4bdaa6c24fea362cc54965c3f645f7cfac659f"
uuid = "e28b5b4c-05e8-5b66-bc03-6f0c0a0a06e0"
version = "2.3.3"

[[deps.CSV]]
deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings"]
git-tree-sha1 = "873fb188a4b9d76549b81465b1f75c82aaf59238"
uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
version = "0.10.4"

[[deps.ChainRulesCore]]
deps = ["Compat", "LinearAlgebra", "SparseArrays"]
git-tree-sha1 = "9950387274246d08af38f6eef8cb5480862a435f"
uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
version = "1.14.0"

[[deps.ChangesOfVariables]]
deps = ["ChainRulesCore", "LinearAlgebra", "Test"]
git-tree-sha1 = "bf98fa45a0a4cee295de98d4c1462be26345b9a1"
uuid = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0"
version = "0.1.2"

[[deps.CodecZlib]]
deps = ["TranscodingStreams", "Zlib_jll"]
git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da"
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
version = "0.7.0"

[[deps.Compat]]
deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "SHA", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"]
git-tree-sha1 = "b153278a25dd42c65abbf4e62344f9d22e59191b"
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
version = "3.43.0"

[[deps.CompilerSupportLibraries_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"

[[deps.Conda]]
deps = ["Downloads", "JSON", "VersionParsing"]
git-tree-sha1 = "6e47d11ea2776bc5627421d59cdcc1296c058071"
uuid = "8f4d0f93-b110-5947-807f-2305c1781a2d"
version = "1.7.0"

[[deps.Crayons]]
git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15"
uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
version = "4.1.1"

[[deps.DataAPI]]
git-tree-sha1 = "cc70b17275652eb47bc9e5f81635981f13cea5c8"
uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
version = "1.9.0"

[[deps.DataFrames]]
deps = ["Compat", "DataAPI", "Future", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrettyTables", "Printf", "REPL", "Reexport", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"]
git-tree-sha1 = "6c19003824cbebd804a51211fd3bbd81bf1ecad5"
uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
version = "1.3.3"

[[deps.DataStructures]]
deps = ["Compat", "InteractiveUtils", "OrderedCollections"]
git-tree-sha1 = "3daef5523dd2e769dad2365274f760ff5f282c7d"
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
version = "0.18.11"

[[deps.DataValueInterfaces]]
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464"
version = "1.0.0"

[[deps.Dates]]
deps = ["Printf"]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"

[[deps.DelimitedFiles]]
deps = ["Mmap"]
uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab"

[[deps.DensityInterface]]
deps = ["InverseFunctions", "Test"]
git-tree-sha1 = "80c3e8639e3353e5d2912fb3a1916b8455e2494b"
uuid = "b429d917-457f-4dbc-8f4c-0cc954292b1d"
version = "0.4.0"

[[deps.Distributed]]
deps = ["Random", "Serialization", "Sockets"]
uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"

[[deps.Distributions]]
deps = ["ChainRulesCore", "DensityInterface", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SparseArrays", "SpecialFunctions", "Statistics", "StatsBase", "StatsFuns", "Test"]
git-tree-sha1 = "5a4168170ede913a2cd679e53c2123cb4b889795"
uuid = "31c24e10-a181-5473-b8eb-7969acd0382f"
version = "0.25.53"

[[deps.DocStringExtensions]]
deps = ["LibGit2"]
git-tree-sha1 = "b19534d1895d702889b219c382a6e18010797f0b"
uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
version = "0.8.6"

[[deps.Downloads]]
deps = ["ArgTools", "LibCURL", "NetworkOptions"]
uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"

[[deps.FilePathsBase]]
deps = ["Compat", "Dates", "Mmap", "Printf", "Test", "UUIDs"]
git-tree-sha1 = "129b104185df66e408edd6625d480b7f9e9823a0"
uuid = "48062228-2e41-5def-b9a4-89aafe57970f"
version = "0.9.18"

[[deps.FillArrays]]
deps = ["LinearAlgebra", "Random", "SparseArrays", "Statistics"]
git-tree-sha1 = "246621d23d1f43e3b9c368bf3b72b2331a27c286"
uuid = "1a297f60-69ca-5386-bcde-b61e274b549b"
version = "0.13.2"

[[deps.Formatting]]
deps = ["Printf"]
git-tree-sha1 = "8339d61043228fdd3eb658d86c926cb282ae72a8"
uuid = "59287772-0a20-5a39-b81b-1366585eb4c0"
version = "0.4.2"

[[deps.Future]]
deps = ["Random"]
uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820"

[[deps.InlineStrings]]
deps = ["Parsers"]
git-tree-sha1 = "61feba885fac3a407465726d0c330b3055df897f"
uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48"
version = "1.1.2"

[[deps.InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"

[[deps.InverseFunctions]]
deps = ["Test"]
git-tree-sha1 = "91b5dcf362c5add98049e6c29ee756910b03051d"
uuid = "3587e190-3f89-42d0-90ee-14403ec27112"
version = "0.1.3"

[[deps.InvertedIndices]]
git-tree-sha1 = "bee5f1ef5bf65df56bdd2e40447590b272a5471f"
uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f"
version = "1.1.0"

[[deps.IrrationalConstants]]
git-tree-sha1 = "7fd44fd4ff43fc60815f8e764c0f352b83c49151"
uuid = "92d709cd-6900-40b7-9082-c6be49f344b6"
version = "0.1.1"

[[deps.IterTools]]
git-tree-sha1 = "fa6287a4469f5e048d763df38279ee729fbd44e5"
uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
version = "1.4.0"

[[deps.IteratorInterfaceExtensions]]
git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856"
uuid = "82899510-4779-5014-852e-03e436cf321d"
version = "1.0.0"

[[deps.JLLWrappers]]
deps = ["Preferences"]
git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1"
uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
version = "1.4.1"

[[deps.JSON]]
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "0.21.3"

[[deps.LibCURL]]
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"

[[deps.LibCURL_jll]]
deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"

[[deps.LibGit2]]
deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"

[[deps.LibSSH2_jll]]
deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"

[[deps.Libdl]]
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"

[[deps.LinearAlgebra]]
deps = ["Libdl", "libblastrampoline_jll"]
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

[[deps.LogExpFunctions]]
deps = ["ChainRulesCore", "ChangesOfVariables", "DocStringExtensions", "InverseFunctions", "IrrationalConstants", "LinearAlgebra"]
git-tree-sha1 = "a970d55c2ad8084ca317a4658ba6ce99b7523571"
uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688"
version = "0.3.12"

[[deps.Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"

[[deps.MacroTools]]
deps = ["Markdown", "Random"]
git-tree-sha1 = "3d3e902b31198a27340d0bf00d6ac452866021cf"
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
version = "0.5.9"

[[deps.Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"

[[deps.MbedTLS_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"

[[deps.Missings]]
deps = ["DataAPI"]
git-tree-sha1 = "bf210ce90b6c9eed32d25dbcae1ebc565df2687f"
uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28"
version = "1.0.2"

[[deps.Mmap]]
uuid = "a63ad114-7e13-5084-954f-fe012c677804"

[[deps.MozillaCACerts_jll]]
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"

[[deps.NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"

[[deps.OpenBLAS_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"

[[deps.OpenLibm_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "05823500-19ac-5b8b-9628-191a04bc5112"

[[deps.OpenSpecFun_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1"
uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
version = "0.5.5+0"

[[deps.OrderedCollections]]
git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c"
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
version = "1.4.1"

[[deps.PDMats]]
deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
git-tree-sha1 = "e8185b83b9fc56eb6456200e873ce598ebc7f262"
uuid = "90014a1f-27ba-587c-ab20-58faa44d9150"
version = "0.11.7"

[[deps.Parameters]]
deps = ["OrderedCollections", "UnPack"]
git-tree-sha1 = "34c0e9ad262e5f7fc75b10a9952ca7692cfc5fbe"
uuid = "d96e819e-fc66-5662-9728-84c9c7592b0a"
version = "0.12.3"

[[deps.Parsers]]
deps = ["Dates"]
git-tree-sha1 = "3b429f37de37f1fc603cc1de4a799dc7fbe4c0b6"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.3.0"

[[deps.Pkg]]
deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"

[[deps.PooledArrays]]
deps = ["DataAPI", "Future"]
git-tree-sha1 = "28ef6c7ce353f0b35d0df0d5930e0d072c1f5b9b"
uuid = "2dfb63ee-cc39-5dd5-95bd-886bf059d720"
version = "1.4.1"

[[deps.Preferences]]
deps = ["TOML"]
git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d"
uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.3.0"

[[deps.PrettyTables]]
deps = ["Crayons", "Formatting", "Markdown", "Reexport", "Tables"]
git-tree-sha1 = "dfb54c4e414caa595a1f2ed759b160f5a3ddcba5"
uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
version = "1.3.1"

[[deps.Printf]]
deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[[deps.PyCall]]
deps = ["Conda", "Dates", "Libdl", "LinearAlgebra", "MacroTools", "Serialization", "VersionParsing"]
git-tree-sha1 = "1fc929f47d7c151c839c5fc1375929766fb8edcc"
uuid = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
version = "1.93.1"

[[deps.QuadGK]]
deps = ["DataStructures", "LinearAlgebra"]
git-tree-sha1 = "78aadffb3efd2155af139781b8a8df1ef279ea39"
uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
version = "2.4.2"

[[deps.REPL]]
deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

[[deps.Random]]
deps = ["SHA", "Serialization"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[[deps.Reexport]]
git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b"
uuid = "189a3867-3050-52da-a836-e630ba90ab69"
version = "1.2.2"

[[deps.Rmath]]
deps = ["Random", "Rmath_jll"]
git-tree-sha1 = "bf3188feca147ce108c76ad82c2792c57abe7b1f"
uuid = "79098fc4-a85e-5d69-aa6a-4863f24498fa"
version = "0.7.0"

[[deps.Rmath_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "68db32dff12bb6127bac73c209881191bf0efbb7"
uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f"
version = "0.3.0+0"

[[deps.SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"

[[deps.ScikitLearn]]
deps = ["Compat", "Conda", "DataFrames", "Distributed", "IterTools", "LinearAlgebra", "MacroTools", "Parameters", "Printf", "PyCall", "Random", "ScikitLearnBase", "SparseArrays", "StatsBase", "VersionParsing"]
git-tree-sha1 = "ccb822ff4222fcf6ff43bbdbd7b80332690f168e"
uuid = "3646fa90-6ef7-5e7e-9f22-8aca16db6324"
version = "0.6.4"

[[deps.ScikitLearnBase]]
deps = ["LinearAlgebra", "Random", "Statistics"]
git-tree-sha1 = "7877e55c1523a4b336b433da39c8e8c08d2f221f"
uuid = "6e75b9c4-186b-50bd-896f-2d2496a4843e"
version = "0.5.0"

[[deps.SentinelArrays]]
deps = ["Dates", "Random"]
git-tree-sha1 = "6a2f7d70512d205ca8c7ee31bfa9f142fe74310c"
uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c"
version = "1.3.12"

[[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[[deps.SharedArrays]]
deps = ["Distributed", "Mmap", "Random", "Serialization"]
uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383"

[[deps.ShiftedArrays]]
git-tree-sha1 = "22395afdcf37d6709a5a0766cc4a5ca52cb85ea0"
uuid = "1277b4bf-5013-50f5-be3d-901d8477a67a"
version = "1.0.0"

[[deps.Sockets]]
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"

[[deps.SortingAlgorithms]]
deps = ["DataStructures"]
git-tree-sha1 = "b3363d7460f7d098ca0912c69b082f75625d7508"
uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c"
version = "1.0.1"

[[deps.SparseArrays]]
deps = ["LinearAlgebra", "Random"]
uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[[deps.SpecialFunctions]]
deps = ["ChainRulesCore", "IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
git-tree-sha1 = "5ba658aeecaaf96923dce0da9e703bd1fe7666f9"
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
version = "2.1.4"

[[deps.Statistics]]
deps = ["LinearAlgebra", "SparseArrays"]
uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[[deps.StatsAPI]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "c82aaa13b44ea00134f8c9c89819477bd3986ecd"
uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0"
version = "1.3.0"

[[deps.StatsBase]]
deps = ["DataAPI", "DataStructures", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"]
git-tree-sha1 = "8977b17906b0a1cc74ab2e3a05faa16cf08a8291"
uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
version = "0.33.16"

[[deps.StatsFuns]]
deps = ["ChainRulesCore", "InverseFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"]
git-tree-sha1 = "5950925ff997ed6fb3e985dcce8eb1ba42a0bbe7"
uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c"
version = "0.9.18"

[[deps.StatsModels]]
deps = ["DataAPI", "DataStructures", "LinearAlgebra", "Printf", "REPL", "ShiftedArrays", "SparseArrays", "StatsBase", "StatsFuns", "Tables"]
git-tree-sha1 = "03c99c7ef267c8526953cafe3c4239656693b8ab"
uuid = "3eaba693-59b7-5ba5-a881-562e759f1c8d"
version = "0.6.29"

[[deps.SuiteSparse]]
deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"]
uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9"

[[deps.TOML]]
deps = ["Dates"]
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"

[[deps.TableTraits]]
deps = ["IteratorInterfaceExtensions"]
git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39"
uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c"
version = "1.0.1"

[[deps.Tables]]
deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "LinearAlgebra", "OrderedCollections", "TableTraits", "Test"]
git-tree-sha1 = "5ce79ce186cc678bbb5c5681ca3379d1ddae11a1"
uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
version = "1.7.0"

[[deps.Tar]]
deps = ["ArgTools", "SHA"]
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"

[[deps.Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[[deps.TranscodingStreams]]
deps = ["Random", "Test"]
git-tree-sha1 = "216b95ea110b5972db65aa90f88d8d89dcb8851c"
uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
version = "0.9.6"

[[deps.UUIDs]]
deps = ["Random", "SHA"]
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[[deps.UnPack]]
git-tree-sha1 = "387c1f73762231e86e0c9c5443ce3b4a0a9a0c2b"
uuid = "3a884ed6-31ef-47d7-9d2a-63182c4928ed"
version = "1.0.2"

[[deps.Unicode]]
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"

[[deps.VersionParsing]]
git-tree-sha1 = "58d6e80b4ee071f5efd07fda82cb9fbe17200868"
uuid = "81def892-9a0e-5fdd-b105-ffc91e053289"
version = "1.3.0"

[[deps.WeakRefStrings]]
deps = ["DataAPI", "InlineStrings", "Parsers"]
git-tree-sha1 = "b1be2855ed9ed8eac54e5caff2afcdb442d52c23"
uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5"
version = "1.4.2"

[[deps.Zlib_jll]]
deps = ["Libdl"]
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"

[[deps.libblastrampoline_jll]]
deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"

[[deps.nghttp2_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"

[[deps.p7zip_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
"""

# ╔═╡ Cell order:
# ╠═303488a5-8102-4077-8cad-12655d8c5777
# ╟─6790c593-f5d3-4e45-be83-c974e581bbf2
# ╟─8b12df14-0a1f-4fc4-8677-02f26f16e1d6
# ╟─1023b0ba-fc8e-4d49-b795-ed3d75004b69
# ╟─66d8365c-cb3a-4a85-8da3-00eab0e1d1eb
# ╠═1a0f56fa-94a9-420f-b318-b6a0f197b140
# ╠═8b712ac8-37fe-4c0e-98c1-495f632ed305
# ╠═ef6a30bf-2f90-486d-879d-a1a089a89c84
# ╟─68ad9943-a5a8-492f-8c5b-93bc7b49d36f
# ╟─4006c742-dc11-403d-b921-459b64ddd3c2
# ╠═e74d5d8f-7d69-4e32-a036-c711194d216e
# ╠═54890bac-8050-4141-8e7e-2fb603bfe3b8
# ╠═b8ccebb7-b303-4315-94fb-8ff253a90271
# ╠═bde70fbd-d415-4d6b-b1d0-6eaba93012b9
# ╠═0148021c-b8a8-4c47-bcbe-3eae9ef6e75d
# ╟─00000000-0000-0000-0000-000000000001
# ╟─00000000-0000-0000-0000-000000000002


#========================================================================================#
#	Laboratory 10
#
# Working with arrays.
#
# Author: Niall Palfreyman, 22/03/2022
#========================================================================================#
[
	Activity(
		"""
		Arrays play a very fundamental role in Julia, so we need to learn how to deal with them
		efficiently. In this laboratory we learn how to index into arrays in Julia - that is,
		how to use indices to access and manipulate the entries in an array. We will also look
		at various ways of applying code to the elements in an array.

		Julia offers four kinds of indexing: SUBSCRIPTING, LINEAR indexing, MULTIPLE indexing
		and LOGICAL indexing. All four are extremely useful for scientific programming. To
		investigate these forms of indexing, first create a (4x4) matrix by reshaping a range:

			m = reshape(1:16,(4,4))

		First notice the order in which Julia has placed the numbers 1:16. Do they run down the
		columns or along the rows?
		""",
		"",
		x -> occursin("col",lowercase(x))
	),
	Activity(
		"""
		So: SUBSCRIPTING! To access a matrix element with subscripts, we use square brackets
		enclosing two subscripts i (row) and j (column) like this: m[i,j]. In Julia, the row
		index ALWAYS comes before the column index! Display the element m[3,4[] now, then tell
		me its value:
		""",
		"Just enter m[3,4] at the Julia prompt",
		x -> x==15
	),
	Activity(
		"""
		We have already used the colon operator to create ranges of numbers, for example 1:3 can
		be collected into a vector [1,2,3]. We can also use ranges to select regions of an array.
		Use m[1:3,2:4] to display the top-right (3×3) region of m, then tell me the result
		(Remember you can use reply(ans) to tell me the result of the previous calculation):
		""",
		"",
		x -> x == [5 9 13;6 10 14;7 11 15]
	),
	Activity(
		"""
		1:2:7 creates a range of numbers from 1 to 7 in steps of 2: [1,3,5,7]. Reshape 1:81 into a
		(9×9) matrix, then extract from it the (3×3) matrix obtained by taking the first, then
		every third, row and column of your (9×9) matrix. Then tell me the result:
		""",
		"",
		x -> x == [1 28 55;4 31 58;7 34 61]
	),
	Activity(
		"""
		We can select a whole row or column by writing ":". Display and tell me the third row of m:
		""",
		"",
		x -> x == [3 7 11 15]
	),
	Activity(
		"""
		To assign values to regions of a matrix, we select the desired matrix range and assign to it
		a value. However, we should first be aware of something important. Tell me the type of m:
		""",
		"",
		x -> x <: Base.ReshapedArray
	),
	Activity(
		"""
		Notice that m only LOOKS like an ordinary matrix, but is actually a reshaped range, so Julia
		will not let us change its value by writing to it. To see this, try out the following:

			m[2:3,3:4] .= 1

		Then tell me the name of the function that the error message recommends we use:
		""",
		"",
		x -> occursin("collect",lowercase(x))
	),
	Activity(
		"""
		Let's take the error message's advice:

			m = collect(m)
			m[2:3,3:4] .= 1

		Now tell me the new value of m:
		""",
		"",
		x -> x == [1 5 9 13;2 6 1 1;3 7 1 1;4 8 12 16]
	),
	Activity(
		"""
		And what is the type of m now?
		""",
		"",
		x -> x <: Matrix
	),
	Activity(
		"""
		What kind of error do you get if you try to add an element outside the size of the array m:

			m[5,1] = 1
		""",
		"",
		x -> occursin("bounds",lowercase(x))
	),
	Activity(
		"""
		Although we can't add elements outside m's bounds, we can extend the size of m by adding
		new rows or columns: Blank ' ' adds columns (hcat: horizontal concatenation), and
		semicolon ';' adds rows (vcat: vertical concatenation). Tell me the result of this line:

			p = ones(4)
			pp = [m p]
		""",
		"",
		x -> size(x) == (4,5)
	),
	Activity(
		"""
		And what is the result of this line of code?

			q = ones(4)'
			qq = [m;q]
		""",
		"",
		x -> size(x) == (5,4)
	),
	Activity(
		"""
		OK, now let's look at LINEAR indexing! Internally, Julia represents all arrays as vectors =
		it simply makes this vector look like a matrix to us. Create the following matrix:

			A = [1 2 3;4 5 6]

		If we just enter A at the Julia prompt, we see a (2×3) matrix, but if you enter A[:], you
		will see a list of all elements of A in a linear order. Does this order first run down the
		columns or along the rows?
		""",
		"",
		x -> occursin("col",lowercase(x))
	),
	Activity(
		"""
		We can use the linear ordering to index the elements of A. What is the value of A[5]?
		""",
		"",
		x -> x==3
	),
	Activity(
		"""
		We can also use linear indexing to change the elements of A. What is the contents of A
		after entering this line of code?

			A[5] = 99
		""",
		"",
		x -> [1 2 99;4 5 6]
	),
	Activity(
		"""
		Arrays can have MULTIPLE indices: Vectors have one index and Matrices have two, but we
		can have 3, 4, 5, ...! Of course we need to be a bit careful with the size of these arrays:
		A size (10) Vector contains 10 elements; a size (10,10) Matrix contains 100 elements. How
		many elements will there be in a size (10,10,10,10) array?
		""",
		"",
		x -> x == 1e4
	),
	Activity(
		"""
		We can create multiply indexed arrays using zeros(), ones(), rand(), randn() or reshape().
		A 3-dimensional array might represent 3-dimensional data, such as a chemical concentration
		at various locations in a cell, or it might represent the time-series of elements of a
		matrix A(t). In this case, A[3,5,4] might represent the element [3,5] at time t=4 of the
		time-series.

		Enter R = rand(2,3,4); at the Julia prompt, and study the following expressions:

			R[1,:,:]
			R[]:,1,:]
			R[]:,:,1]
			size(R)

		How many elements does R contain?
		""",
		"Use the length function",
		x -> x == 24
	),
	Activity(
		"""
		Here is a (4,4) magic square:

			A = [1 15 14 4;10 11 8 5;7 6 9 12;16 2 3 13]

		Use Julia's sum() function to find the sum of A's elements along any row, column or
		diagonal:
		""",
		"",
		x -> x==34
	),
	Activity(
		"""
		We can generate new magic squares by swapping columns of an existing magic square. Study
		the matrix B = A(:,[1,3,2,4]). Is this also a magic square (remember that rows, columns and
		diagonals of a magic square must all sum to the same number)? Which substructures of A were
		permuted by using the Vector [1,3,2,4] as a permutation index?
		""",
		"",
		x -> occursin("col",lowercase(x))
	),
	Activity(
		"""
		Use a permutation index to swap ROWS 2 and 3 of A, and tell me the result:
		""",
		"",
		x -> x == [1 15 14 4;7 6 9 12;10 11 8 5;16 2 3 13]
	),
	Activity(
		"""
		LOGICAL indexing uses an array of logical values as an index to another array. For example,
		suppose we want to reduce to zero all elements less than 3 in the Vector v = [1,2,3,4,5].
		One way would be to access each individual element in v, check whether it is less than 3,
		and set it to zero. But this would be very inefficient, because accessing each individual
		element costs time. Instead, we can use LOGICAL indexing to change the entire vector v in
		one sweep ...

		First, create the Vector v. Then tell me the result of entering this line:

			d = (v .< 3)
		""",
		"",
		x -> x == [1,1,0,0,0]
	),
	Activity(
		"""
		What is the type of the elements of d?
		""",
		"",
		x -> x == Bool
	),
	Activity(
		"""
		Notice that d is a Vector of Bools with the same length as v. We can use d to index
		elements of v:

			v[d]

		How many elements of v does d pick out?
		""",
		"",
		x -> x == 2
	),
	Activity(
		"""
		Now enter this line:

			v[d] .= 0

		You will see that this zeros out all elements of v that are less than 3. If you recreate
		v, you can even condense this entire process into one step:

			v = collect(1:5)
			v[v.<3] .= 0

		Now set to zero all numbers in the vector -5:2:5 which are greater than 2, and tell me
		the number of non-zero elements in your result:
		""",
		"",
		x -> x == 4
	),
	Activity(
		"""
		Before continuing, let's look at several different ways to apply some code to all elements
		in an array. If the code is just one function, this is easy: we just use the broadcast
		dot (.):

			v = 1:7
			isodd.(v)
		""",
		"",
		x -> x == isodd.(1:7)
	),
	Activity(
		"""
		If the code is a little more complicated, we might map() an anonymous function over
		the array:

			map( x->(sin(x) >= 0), v)
		""",
		"",
		x -> x == map( y->(sin(y) >= 0), 1:7)
	),
	Activity(
		"""
		And finally, if the code is particularly complicated, we can use a do statement that
		allows us to define a complicated mapping over all elements of the array:

			map(v) do x
				if x < 4
					isodd(x)
				else
					iseven(x)
				end
			end
		""",
		"",
		x -> x == (map(1:7) do y if y<4 isodd(y) else iseven(y) end end)
	),
	Activity(
		"""
		OK, now the last two activities in this laboratory give you practice in applying indexing,
		broadcasting and mapping to problems that often arise in signal-processing. Have fun! :)

		Use logical indexing to generate a list of all odd multiples of 3 in the range 1:50 :
		""",
		"Use isodd(), rem(), &, and remember to use broadcasts (.) and brackets",
		x -> x == [3,9,15,21,27,33,39,45]
	),
	Activity(
		"""
		This function decides whether or not its argument n is a prime number:

			function isprime(n::Int)
				if n < 2 return false end
				if n in 2:3 return true end
				for i in 2:floor(Int,sqrt(n))
					if rem(n,i) == 0 return false end
				end
				true
			end

		Use the isprime function to generate a list of twenty numbers from 1 to 20, in which
		all prime numbers AND all multiples of 3 are zeroed out:
		""",
		"Use rem(), isprime() and |",
		x -> x == [1,0,0,4,0,0,0,8,0,10,0,0,0,14,0,16,0,0,0,20]
	),
]


# This example came from the R package ggm.

using StructuralCausalModels

ProjDir = @__DIR__
cd(ProjDir)

df = DataFrame!(CSV.File(scm_path("..", "data", "marks.csv")));

#=
R_dag = "
           mechanics vectors algebra statistics analysis
mechanics          0       0       0          0        0
vectors            1       0       0          0        0
algebra            1       1       0          1        1
statistics         0       0       0          0        0
analysis           0       0       0          1        0
";
=#

d = from_ggm("DAG(
    mechanics ~ vectors+algebra,
    vectors ~ algebra,
    statistics ~ algebra+analysis,
    analysis ~ algebra)"
)
display(d)

# DAG accepts either an OrderedDict, an adjacency_matrix or a ggm/dagitty string.
# Below d_string holds a ggm DAG definition.

d_string = "DAG(
    mechanics ~ vectors+algebra,
    vectors ~ algebra,
    statistics ~ algebra+analysis,
    analysis ~ algebra)"

dag = DAG("marks", d_string, df=df);
show(dag)

fname = ProjDir * "/marks.dot"
to_graphviz(dag, fname)
Sys.isapple() && run(`open -a GraphViz.app $(fname)`)

display(dag.s); println()

bs = basis_set(dag)
display(bs); println()

t = shipley_test(dag)
display("shipley_test = $t"); println()

pt = pcor_test(dag, [:analysis, :statistics, :mechanics], 1, 88)
display("pcor_test = $pt"); println()

f = [:statistics]; s = [:mechanics]; sel = vcat(f, s)

e = d_separation(dag, f, s; c=:algebra)
println("d_separation($(dag.name), $f, $s; c=:algebra) = $e")

e = d_separation(dag, f, s)
println("d_separation($(dag.name), $f, $s) = $e")

print("d_separation($(dag.name), [:statistics], [:mechanics]; c=[:vectors]) = ")
println(d_separation(dag, [:statistics], [:mechanics]; c=[:vectors]))

print("d_separation($(dag.name), [:statistics], [:mechanics]; c=[:analysis, :vectors]) = ")
println(d_separation(dag, [:statistics], [:mechanics]; c=[:analysis, :vectors]))

print("d_separation($(dag.name), [:statistics, :analysis], [:mechanics]; c=[:algebra]) = ")
println(d_separation(dag, [:statistics, :analysis], [:mechanics]; c=[:algebra]))

print("d_separation($(dag.name), [:statistics], [:mechanics, :vectors]; c=[:algebra]) = ")
println(d_separation(dag, [:statistics], [:mechanics, :vectors]; c=[:algebra]))

print("d_separation($(dag.name), [:statistics], [:mechanics, :analysis]; c=[:algebra]) = ")
println(d_separation(dag, [:statistics], [:mechanics, :analysis]; c=[:algebra]))

print("d_separation($(dag.name), [:analysis], [:vectors]) = ")
println(d_separation(dag, [:analysis], [:vectors]))

print("d_separation($(dag.name), [:analysis], [:vectors]; c=[:algebra]) = ")
println(d_separation(dag, [:analysis], [:vectors]; c=[:algebra]))

print("d_separation($(dag.name), [:vectors], [:statistics]; c=[:algebra]) = ")
println(d_separation(dag, [:analysis], [:vectors]; c=[:algebra]))

print("d_separation($(dag.name), [:statistics], [:algebra]; c=[:analysis]) = ")
println(d_separation(dag, [:statistics], [:algebra]; c=[:analysis]))

print("d_separation($(dag.name), [:statistics, :analysis], [:mechanics, :vectors]) = ")
println(d_separation(dag, [:statistics, :analysis], [:mechanics, :vectors]))

print("d_separation($(dag.name), [:statistics, :analysis], [:mechanics, :vectors]; c=[:algebra]) = ")
println(d_separation(dag, [:statistics, :analysis], [:mechanics, :vectors]; c=[:algebra]))

adjustmentsets = adjustment_sets(dag, :statistics, :mechanics)
println("\nAdjustment sets:")
adjustmentsets |> display

adjustmentsets = adjustment_sets(dag, :mechanics, :statistics)
println("\nAdjustment sets:")
adjustmentsets |> display

#end


using LinearAlgebra
using Statistics

mutable struct LinearRegression
    X
    Y
    n_data
    input_dim
    w
    b
end

function LinearRegression(X, Y)
    n_data, input_dim = size(X)
    return LinearRegression(X, Y, n_data, input_dim, nothing, nothing)
end

# 2. 最小二乗法を用いてモデルパラメーターを最適化
function train!(model::LinearRegression)
    # 行列Xに「1」の要素を追加
    ndim = model.n_data
    Z = hcat(model.X, ones(ndim))

    # 分母の計算
    ZZ = 1 / ndim * Z' * Z

    # 分子の計算
    ZY = 1 / ndim * Z' * model.Y

    # パラメーターvの最適化
    # ZZ * v = ZY を解く
    v = ZZ \ ZY
    model.w = v[1:end-1]
    model.b = last(v)
    model
end

# 3. 予測
# X: 入力データ(データ数 x 次元数のArray)
predict(model::LinearRegression, x) = x * model.w .+ model.b

# 4. 平方平均二乗誤差(Root Mean Squared Error)
# X: 入力データ(データ数 x 次元数のArray)
# Y: 出力データ(データ数 x 1のArray)
rmse(model::LinearRegression, x, y) = sqrt.(mean((predict(model, x) - y).^2))

# 5. 決定係数の計算
# X: 入力データ(データ数 x 次元数のArray)
# Y: 出力データ(データ数 x 1のArray)
function r2(model::LinearRegression, x, y)
    sq = sum((predict(model, x) - y).^2)
    vr = sum((y .- mean(y; dims=1)).^2)
    return 1 - sq / vr
end

# L2ノルム正則化最小二乗法を用いてモデルパラメーターを最適化
# lamb: 正則化の重み係数(実数スカラー)
function train_regularized!(model::LinearRegression, lamb=0.1)
    # 行列Xに「1」の要素を追加
    ndim = model.n_data
    Z = hcat(model.X, ones(ndim))

    # 分母の計算
    ZZ = 1 / ndim * Z' * Z + lamb * I

    # 分子の計算
    ZY = 1 / ndim * Z' * model.Y

    # パラメーターvの最適化
    # ZZ * v = ZY を解く
    v = ZZ \ ZY
    model.w = v[1:end-1]
    model.b = last(v)
    model
end


using SemiDiscretizationMethod

function createMathieuProblem(δ,ε,b0,a1;T=2π)
    AMx =  ProportionalMX(t->@SMatrix [0. 1.; -δ-ε*cos(2π/T*t) -a1]);
    τ1=2π # if function is needed, the use τ1 = t->foo(t)
    BMx1 = DelayMX(τ1,t->@SMatrix [0. 0.; b0 0.]);
    cVec = Additive(t->@SVector [0.,sin(4π/T*t)])
    LDDEProblem(AMx,[BMx1],cVec)
end;

τmax=2π # the largest τ of the system
T=2π #Principle period of the system (sin(t)=sin(t+P))
mathieu_lddep=createMathieuProblem(3.,2.,-0.15,0.1,T=T); # LDDE problem for Hayes equation
method=SemiDiscretization(1,0.01) # 3rd order semi discretization with Δt=0.1
# if P = τmax, then n_steps is automatically calculated
mapping=DiscreteMapping_1step(mathieu_lddep,method,τmax,
    n_steps=Int((T+100eps(T))÷method.Δt),calculate_additive=true); #The discrete mapping of the system

@show spectralRadiusOfMapping(mapping); # spectral radius ρ of the mapping matrix (ρ>1 unstable, ρ<1 stable)
fp=fixPointOfMapping(mapping); # stationary solution of the hayes equation (equilibrium position)

plot(0.0:method.Δt:P,fp[1:2:end],
    xlabel=L"-s",title=L"t \in [nP,(n+1)P],\quad n \to \infty",guidefontsize=14,linewidth=3,
    label=L"x(t-s)",legendfontsize=11,tickfont = font(10))

plot!(0.0:method.Δt:P,fp[2:2:end],
    xlabel=L"-s",linewidth=3,
    label=L"\dot{x}(t-s)")

plot!(0.0:method.Δt:P,sin.(2*(0.0:method.Δt:P)),linewidth=3,label=L"\sin(2t)")

using MDBM

using Plots
gr();
using LaTeXStrings

a1=0.1;
ε=1;
τmax=2π;
T=1π;
method=SemiDiscretization(2,T/40);

foo(δ,b0) = log(spectralRadiusOfMapping(DiscreteMapping_1step(createMathieuProblem(δ,ε,b0,a1,T=T),method,τmax,
    n_steps=Int((T+100eps(T))÷method.Δt)))); # No additive term calculated

axis=[Axis(-1:0.2:5.,:δ),
    Axis(-2:0.2:1.5,:b0)]

iteration=3;
stab_border_points=getinterpolatedsolution(solve!(MDBM_Problem(foo,axis),iteration));

scatter(stab_border_points...,xlim=(-1.,5),ylim=(-2.,1.5),
    label="",title="Stability border of the delay Mathieu equation",xlabel=L"\delta",ylabel=L"b_0",
    guidefontsize=14,tickfont = font(10),markersize=2,markerstrokewidth=0)