################################################################################
# Copyright (C) Marcus Greiff 2020
#
# @file example_FSF_attitude_utils.jl
# @author Marcus Greiff
# @date July 2020
#
# Utility functions for initialization and plotting in the FSF example.
#
################################################################################

function get_errors_SO3(qvec, qrvec, wvec, wrvec)
    NT = size(qvec, 2);
    eRvec = zeros(3, NT);
    ewvec = zeros(3, NT);
    zvec  = zeros(2, NT);
    znorm = zeros(1, NT);
    for t = 1:NT
        R  = quat_2_SO3(reshape(qvec[:,t],  (4,1)))
        Rr = quat_2_SO3(reshape(qrvec[:,t], (4,1)))
        w  = wvec[:,t];
        wr = wrvec[:,t];

        eRvec[:,t]  = SO3_vee(Rr'*R - R'*Rr) / 2.0;
        ewvec[:,t]  = w -  R'*Rr * wr;
        zvec[1,t]   = norm(eRvec[:,t])
        zvec[2,t]   = norm(ewvec[:,t])
        znorm[1,t]  = zvec[:,t]'*zvec[:,t]

    end
    return eRvec, ewvec, zvec, znorm
end

function get_errors_SU2(qvec, qrvec, wvec, wrvec)
    NT = size(qvec, 2);
    eXvec = zeros(3, NT);
    ewvec = zeros(3, NT);
    zvec  = zeros(2, NT);
    znorm = zeros(1, NT);
    for t = 1:NT
        X  = quat_2_SU2(reshape(qvec[:,t],  (4,1)))
        Xr = quat_2_SU2(reshape(qrvec[:,t], (4,1)))
        w  = wvec[:,t];
        wr = wrvec[:,t];

        Xe = Xr'*X;

        eXvec[:,t]  = SU2_vee(Xe - tr(Xe)*Matrix{Float64}(I, 2, 2)) / 2.0;
        ewvec[:,t]  = w -  SU2_vee(Xe'*SU2_hat(wr)*Xe);
        zvec[1,t]   = norm(eXvec[:,t])
        zvec[2,t]   = norm(ewvec[:,t])
        znorm[1,t]  = zvec[:,t]'*zvec[:,t]

    end
    return eXvec, ewvec, zvec, znorm
end

function get_bounds(M1, M2, W, zvec)
    NT = size(zvec, 2);
    Vmin = zeros(1, NT);
    Vmax = zeros(1, NT);
    dV   = zeros(1, NT);
    for t = 1:NT
        z = zvec[:,t];
        Vmin[1,t] = +z'*M1*z;
        Vmax[1,t] = +z'*M2*z;
        dV[1,t]   = -z'*W*z;
    end
    return Vmin, Vmax, dV
end

function maximal_feasible_eps_SU2(
    C::con_state_qw_fsf_t,
    X0::Array{Complex{T},2},
    Xr0::Array{Complex{T},2},
    w0::Array{T,2},
    wr0::Array{T,2};
    isContinuous=true
) where {T<:Number}

    # Extract parameters
    kX, kc, kw  = C.gain_kR, C.gain_kc, C.gain_kw;
    J           = ntuple_2_mat(C.inertia, 3, 3);

    if isContinuous
        # Compute maximum permissible phi
        Gamma     = abs(0.5 * tr(Matrix{Float64}(I, 2, 2) - Xr0'*X0));
        Xe = Xr0'*X0;
        eX = SU2_vee(Xe - tr(Xe)*Matrix{Float64}(I, 2, 2)) / 2.0
        ew = w0 -  SU2_vee(Xe'*SU2_hat(wr0)*Xe)
        attpart   = kX * Gamma;
        crosspart = kc * ew' * eX;
        ratepart  = ew'*J*ew / 2.0;
        Lyap0     = attpart + crosspart[1,1] + ratepart[1,1];
        phi       = Lyap0/kX;
    else
        # phi is fixed at 1.0 in the discontinuous case
        phi = 1.0
    end

    M1, M2, W   = get_M1M2W_SU2(C, phi)

    M1max, M1min = maximum(eigvals(M1)), minimum(eigvals(M1));
    M2max, M2min = maximum(eigvals(M2)), minimum(eigvals(M2));
    Wmax,  Wmin  = maximum(eigvals(W)),  minimum(eigvals(W));

    mindecayrate = Wmin/M2max;
    maxeps       = ((M1min*Wmin) / M2max)*phi*(2.0 - phi);
    uniboundgain = (M2max / (M1min*Wmin));

    return maxeps, mindecayrate, uniboundgain, phi, M1, M2, W
end

function get_info(
    C::con_state_qw_fsf_t,
    controllerType::Integer,
    q0::Array{T,2},
    qr0::Array{T,2},
    w0::Array{T,2},
    wr0::Array{T,2}
) where {T<:Number}

    controllerStrings = [
        "continuous FSF controller on SO(3) (called from C)",
        "robust FSF controller on SO(3) (called from C)",
        "continuous FSF controller on SU(2) (called from C)",
        "discontinuous FSF controller on SU(2) (called from C)",
        "robust FSF controller on SU(2) (called from C)"
    ]

    nstr = length(controllerStrings[controllerType]);
    println("\n$('~'^(34+nstr))")
    println("~~~ Simulation done with the $(controllerStrings[controllerType]) ~~~")
    println("$('~'^(34+nstr))")

    if controllerType in [1,2]
        R0  = quat_2_SO3(q0);
        Rr0 = quat_2_SO3(qr0);

        phi0 = tr(I-Rr0'*R0)/2.0;
        println("* The initial attitude error is: Psi(Rr(t0), R(t0)) = $(phi0)")
        if (phi0 > 1.5)
            println("* Warning: This is relatively large, requiring very small initial attitude rate errors")
        end
        maxeps, mindecayrate, uniboundgain, phi, M1, M2, W = maximal_feasible_eps_SO3(C, R0, Rr0, w0, wr0)
    end
    if controllerType in [3,4,5]
        X0  = quat_2_SU2(q0);
        Xr0 = quat_2_SU2(qr0);

        phi0 = abs(0.5 * tr(Matrix{Float64}(I, 2, 2) - Xr0'*X0));
        println("* The initial attitude error is: Gamma(Xr(t0), X(t0)) = $(phi0)")
        if (phi0 > 1.5)
            println("* Warning: This is relatively large, requiring very small initial attitude rate errors")
        end
        if controllerType in [3,5]
            maxeps, mindecayrate, uniboundgain, phi, M1, M2, W = maximal_feasible_eps_SU2(C, X0, Xr0, w0, wr0; isContinuous=true)
        end
        if controllerType in [4]
            maxeps, mindecayrate, uniboundgain, phi, M1, M2, W = maximal_feasible_eps_SU2(C, X0, Xr0, w0, wr0; isContinuous=false)
        end
    end

    println("* Any V(t0)/kR = $(phi) < phi < 2 can be used in the stability proof. We let phi = $(phi).")
    if (phi > 2);
        println("* Waring. The system may move outside of the domain of exponential attraction given phi")
        println("* Lyap0/kR = $(phi), please retune the controller, or lower the initial rate error")
    end

    println("* Worst case decay rate of the Lyapunov function : $(mindecayrate)")
    if controllerType in [2,5]
        println("* Upper bound on the allowed epsilon given phi   : $(maxeps)")
        println("* Uniform ultimate bound K*eps, where K is       : $(uniboundgain)")
    end
    return M1, M2, W, uniboundgain
end

function initialize_FSF_attitude_example(controllerType::Integer)

    # Define the controller parameters
    J  = rand_PSD(3);
    J  = J./opnorm(J);
    if controllerType in [1,2]
        kR = 5.0;
    else
        kR = 10.0;
    end
    kw = 2.0;

    # Compute a feasible cross gain kc
    if controllerType in [1,2]
        # In case of an SO(3) configured controller (continuous or robust)
        maxkc = maximal_feasible_kc_SO3(kR, kw, J);
    else
        maxkc = maximal_feasible_kc_SU2(kR, kw, J); # here we let kR=kX for simplicity
    end
    kc = 0.1 * maxkc;

    # Define the controller state object
    C  = con_state_qw_fsf_t(J, kR, kc, kw)

    # Initial states of the reference system
    qr0 = rand(4,1).-0.5;
    qr0 = qr0./norm(qr0);
    wr0 = zeros(3,1);

    # Initial states of the physical system
    q0 = rand(4,1).-0.5;
    q0 = q0./norm(q0);
    w0 = rand(3,1).-0.5;

    # Select a feasible epsilon gain, with the uptimate bound characterize by alpha
    alpha = 1e-2;
    if controllerType == 2
        # In case of an SO(3) configured controller (only applicable in the robust case)
        maxeps, _, _, _, _, _ = maximal_feasible_eps_SO3(C, quat_2_SO3(q0), quat_2_SO3(qr0), w0, wr0);
        C.gain_eps = alpha*maxeps;
    end
    if controllerType == 5
        # In case of an SU(2) configured controller (only applicable in the robust case)
        maxeps, _, _, _, _, _ = maximal_feasible_eps_SU2(C, quat_2_SU2(q0), quat_2_SU2(qr0), w0, wr0);
        C.gain_eps = alpha*maxeps;
    end

    # Initial states for the debugging signals
    output = zeros(14,1);
    output[13,1] = 1.0;
    output[14,1] = 1.0;

    x0    = vcat(q0, w0, qr0, wr0, output)
    tspan = (0.0,20.0)
    return x0, tspan, C
end

function solution_analysis(
    sol::ODESolution,
    C::con_state_qw_fsf_t,
    controllerType::Integer;
    showPlot=[1,2,3],
    savePlot=false,
    namePlot="unnamed",
    showDisturbance=false
)

    # Posterior solution analysis
    t     = sol.t;
    data  = zeros(length(t),length(sol.u[1]));
    for i = 1:length(t); data[i,:] = sol.u[i]; end

    q     = data[:,1:4]';
    w     = data[:,5:7]';
    qr    = data[:,8:11]';
    wr    = data[:,12:14]';
    tau   = data[:,15:17]';
    taur  = data[:,18:20]';
    psi   = data[:,21]';
    gamma = data[:,22]';
    dist  = data[:,24:26]';
    lyap  = data[:,23]';
    lyapd = numerical_differentiation(lyap, t);

    # The first element of certain logged variables (computed in the C-code)
    # are not set at t0, hence we let tis value be the one at the next time step
    # to tidy up and simplify the plotting.
    psi[:,1], gamma[:,1] = psi[:,2], gamma[:,2];
    tau[:,1] = tau[:,2];
    taur[:,1] = taur[:,2];
    dist[:,1] = dist[:,2];
    lyap[:,1] = lyap[:,2];

    q0  = reshape(q[:,1],  (4,1));
    qr0 = reshape(qr[:,1], (4,1));
    w0  = reshape(w[:,1],  (3,1));
    wr0 = reshape(wr[:,1], (3,1));

    M1, M2, W, K = get_info(C, controllerType, q0, qr0, w0, wr0);

    # In case we are dealine with an SO(3) controller (continuous, or robust)
    if controllerType in [1,2]
        eAtt, eW, z, znorm = get_errors_SO3(q, qr, w, wr);
    end
    # In case we are dealine with an SU(2) controller
    if controllerType in [3,4,5]
        eAtt, eW, z, znorm = get_errors_SU2(q, qr, w, wr)
    end

    Vmin, Vmax, dV   = get_bounds(M1, M2, W, z);
    zbound           = ones(length(t),1).*K.*C.gain_eps;

    if 1 in showPlot
        pqqr = plot( t, q',    color=:black, linewidth=2, xaxis="Time (t)",yaxis="Attitude [.]",          label=[L"q(t)" nothing nothing nothing])
               plot!(t, qr',   color=:blue,  linewidth=2, xaxis="Time (t)",yaxis="Attitude [.]",          label=[L"q_r(t)" nothing nothing nothing])
        pwwr = plot( t, w',    color=:black, linewidth=2, xaxis="Time (t)",yaxis="Attitude rate [rad/s]", label=[L"\omega(t)" nothing nothing])
               plot!(t, wr',   color=:blue,  linewidth=2, xaxis="Time (t)",yaxis="Attitude rate [rad/s]", label=[L"\omega_{r}(t)" nothing nothing])
        pttr = plot( t, tau',  color=:black, linewidth=2, xaxis="Time (t)",yaxis="Torque [Nm]",           label=[L"\tau(t)" nothing nothing])
               plot!(t, taur', color=:blue,  linewidth=2, xaxis="Time (t)",yaxis="Torque [Nm]",           label=[L"\tau_{r}(t)" nothing nothing])
        pdis = plot( t, dist', color=:black, linewidth=2, xaxis="Time (t)",yaxis="Load disturbance [Nm]", label=[L"d(t)" nothing nothing])

        if showDisturbance
            plot(pqqr, pwwr, pttr, pdis, layout=(4,1), size=(1000,1000))
        else
            plot(pqqr, pwwr, pttr, layout=(3,1), size=(1000,750))
        end

        gui()
        if savePlot
            savefig(joinpath(AerialVehicleControl.CONT_LIB_DOCS, "$(namePlot)_states.png"))
        end
    end

    if 2 in showPlot

        attitudeErrors = [L"e_{R}(t)", L"e_{R}(t)", L"e_{X}(t)", L"e_{X}^{\pm}(t)", L"e_{X}(t)"]
        plotattd    = plot( t, psi',        color=:black,             linewidth=2,                            label=L"\Psi(R_r(t), R(t))")
                      plot!(t, gamma',      color=:blue,               linewidth=2,                            label=L"\Gamma(X_r(t), X(t))")
                      plot!(t, 2 .- gamma', color=:blue,  style=:dash, linewidth=2, yaxis="Attitude distance", label=L"\bar{\Gamma}(X_r(t), X(t))")
        plotatterr  = plot( t, eAtt', color=:black, linewidth=2,                   yaxis="Attitude error",    label=[attitudeErrors[controllerType] nothing nothing])
        plotrateerr = plot( t, eW',   color=:black, linewidth=2, xaxis="Time (t)", yaxis="Rate error",        label=[L"e_{\omega}(t)" nothing nothing])
        plot(plotattd, plotatterr,  plotrateerr, layout=(3,1), size=(1000,750))
        gui()
        if savePlot
            savefig(joinpath(AerialVehicleControl.CONT_LIB_DOCS, "$(namePlot)_errors.png"))
        end
    end

    if 3 in showPlot
        ubstyle    = :dash;
        lbstyle    = :dot;
        sigcolor   = :black;
        boundcolor = :blue;
        boundalpha = 0.1;
        fillcolor  = :black;

        bounds     = (Vmin[:].*0, Vmax[:]-Vmin[:]);
        plotlyap   = plot(t,  Vmin[:], ribbon=bounds, color=boundcolor, linewidth=2, style=lbstyle, fillcolor=fillcolor, fillalpha=boundalpha, label=L"z^{\top}M_1z")
                     plot!(t, Vmax[:],                color=boundcolor, linewidth=2, style=ubstyle,                                            label=L"z^{\top}M_2z")
                     plot!(t, lyap[:],                color=sigcolor,   linewidth=2, xaxis="Time (t)",yaxis=L"\mathcal{V}(t)",                 label=L"$\mathcal{V}(t)$")

        logVmin = log10.(abs.(Vmin[:]))
        logVmax = log10.(abs.(Vmax[:]))
        logLyap = log10.(abs.(lyap[:]))
        bounds  = (logVmin.*0, logVmax - logVmin)
        plotlyaplog = plot(t,  logVmin, ribbon=bounds, color=boundcolor, linewidth=2, style=lbstyle, fillcolor=fillcolor, fillalpha=boundalpha, label=L"$log_{10}(z^{\top}M_1z)$")
                      plot!(t, logVmax,                color=boundcolor, linewidth=2, style=ubstyle,                                            label=L"$log_{10}(z^{\top}M_2z)$")
                      plot!(t, logLyap,                color=sigcolor,   linewidth=2, xaxis="Time (t)",yaxis=L"log_{10}(\mathcal{V}(t))",       label=L"$log_{10}(\mathcal{V}(t))$")

        ind           = 3;
        tshort        = t[ind:end];
        dVshort       = dV[1,ind:end];
        lyapshort     = lyapd[1,ind:end];
        lbval         = 1.1*minimum([dVshort; lyapshort])
        bounds        = (dVshort .- lbval, 0 .* dVshort)
        plotlyapderiv = plot(tshort,    dVshort, ribbon=bounds, color=boundcolor, linewidth=2, style=ubstyle, fillcolor=fillcolor, fillalpha=boundalpha,   label=L"-z^{\top}Wz")
                        plot!(tshort, lyapshort,                color=sigcolor,   linewidth=2, xaxis="Time (t)",yaxis=L"\frac{d}{dt}\mathcal{V}(t)",       label=L"$({d}/{dt}){\mathcal{V}}(t)$")

        log10zbound   = zbound[:]; #log10.(abs.(zbound[:]))
        log10znorm    = znorm[:];  #log10.(abs.(znorm[:]))
        lbval         = minimum([log10zbound; log10znorm]) - 1.0;
        bounds        = (log10zbound .- lbval, 0 .*log10zbound);
        plotznorm     = plot(t,  log10zbound, ribbon=bounds, color=boundcolor, linewidth=2, style=ubstyle, fillcolor=fillcolor, fillalpha=boundalpha, label=L"$\lambda_M(M_2)(\lambda_m(M_1)\lambda_m(W))^{-1}\epsilon$")
                        plot!(t, log10znorm,                 color=sigcolor,   linewidth=2, xaxis="Time (t)",yaxis=L"$\|\|z(t)\|\|_2^2$", label=L"$\|\|z(t)\|\|_2^2$")

        if showDisturbance
            plot(plotlyap, plotlyaplog, plotlyapderiv, plotznorm, layout=(4,1), size=(1000,1000))
        else
            plot(plotlyap, plotlyaplog, plotlyapderiv, layout=(3,1), size=(1000,750))
        end
        gui()
        if savePlot
            savefig(joinpath(AerialVehicleControl.CONT_LIB_DOCS, "$(namePlot)_analysis.png"))
        end
    end
end


using DiffEqBase, DiffEqBiological, DiffEqJump, Plots, Statistics, DataFrames
gr()
fmt = :png

N = 256
h = 1 / N
rn = @empty_reaction_network
function getDiffNetwork!(rn,N)
    for i = 1:N
        addspecies!(rn, Symbol(:u, i))
    end
    addparam!(rn, :β)
    for i = 1:N
        (i < N) && addreaction!(rn, :β, (Symbol(:u,i)=>1,), (Symbol(:u,i+1)=>1,))
        (i > 1) && addreaction!(rn, :β, (Symbol(:u,i)=>1,), (Symbol(:u,i-1)=>1,))
    end
    rn
end
getDiffNetwork!(rn,N)
addjumps!(rn, build_regular_jumps=false, minimal_jumps=true)
rnpar = [1/(h*h)]
u0 = 10*ones(Int64, N)
tf = .01

methods = (Direct(),DirectFW(),SortingDirect(),NRM(),DirectCR(),RSSA())
shortlabels = [string(leg)[12:end-2] for leg in methods]
prob    = prob = DiscreteProblem(u0, (0.0, tf), rnpar)
ploth   = plot(reuse=false)
for (i,method) in enumerate(methods)
    println("Benchmarking method: ", method)
    jump_prob = JumpProblem(prob, method, rn, save_positions=(false,false))
    sol = solve(jump_prob, SSAStepper(), saveat=tf/1000.)
    plot!(ploth,sol.t,sol[Int(N//2),:],label=shortlabels[i], format=fmt)
end
plot!(ploth, title="Population at middle lattice site", xlabel="time",format=fmt)

function run_benchmark!(t, jump_prob, stepper)
    sol = solve(jump_prob, stepper)
    @inbounds for i in 1:length(t)
        t[i] = @elapsed (sol = solve(jump_prob, stepper))
    end
end

nsims = 50
benchmarks = Vector{Vector{Float64}}()
for method in methods
    jump_prob = JumpProblem(prob, method, rn, save_positions=(false,false))
    stepper = SSAStepper()
    t = Vector{Float64}(undef,nsims)
    run_benchmark!(t, jump_prob, stepper)
    push!(benchmarks, t)
end

medtimes = Vector{Float64}(undef,length(methods))
stdtimes = Vector{Float64}(undef,length(methods))
avgtimes = Vector{Float64}(undef,length(methods))
for i in 1:length(methods)
    medtimes[i] = median(benchmarks[i])
    avgtimes[i] = mean(benchmarks[i])
    stdtimes[i] = std(benchmarks[i])
end

df = DataFrame(names=shortlabels,medtimes=medtimes,relmedtimes=(medtimes/medtimes[1]),
                avgtimes=avgtimes, std=stdtimes, cv=stdtimes./avgtimes)

sa = [string(round(mt,digits=4),"s") for mt in df.medtimes]
bar(df.names,df.relmedtimes,legend=:false, fmt=fmt)
scatter!(df.names, .05 .+ df.relmedtimes, markeralpha=0, series_annotations=sa, fmt=fmt)
ylabel!("median relative to Direct")
title!("256 Site 1D Diffusion CTRW")

using DiffEqBenchmarks
DiffEqBenchmarks.bench_footer(WEAVE_ARGS[:folder],WEAVE_ARGS[:file])


# Tests for the total correlation function

total_correlation = entropy1 + entropy2 + entropy3 - entropy123

# Test total correlation
@test get_total_correlation(arr1, arr2, arr3) ≈ total_correlation
println("Total correlation passed.")

# Test total correlation formula
@test apply_total_correlation_formula(entropy1, entropy2, entropy3, entropy123) ≈ total_correlation
println("Total correlation formula passed.")


"""
A two dimensional nonlinear oscillator introduced in [1].
Note that the system is constructed by converse HJB method [2].
# References
[1] J. A. Primbs, “Nonlinear Optimal Control: A Receding Horizon Approach,”
California Institute of Technology, Pasadena, California, 1999.
[2] J. Doyle, J. A. Primbs, B. Shapiro, and V. Nevistic,
“Nonlinear Games: Examples and Counterexamples,”
in Proceedings of 35th IEEE Conference on Decision and Control, Kobe, Japan, 1996,
vol. 4, pp. 3915–3920. doi: 10.1109/CDC.1996.577292.
"""
struct TwoDimensionalNonlinearOscillator <: AbstractEnv
end

function State(env::TwoDimensionalNonlinearOscillator)
    return function (x1, x2)
        ComponentArray(x1=x1, x2=x2)
    end
end

function Dynamics!(env::TwoDimensionalNonlinearOscillator)
    @Loggable function dynamics!(dx, x, p, t; u)
        @unpack x1, x2 = x
        @log state = x
        @log input = u
        dx.x1 = x2
        dx.x2 = (
                 -x1 * (π/2 + atan(5*x1))
                 - (5*x1^2)/(2*(1+25*x1^2))
                 + 4*x2
                 + 3*u[1]
                )
    end
end

function RunningCost(env::TwoDimensionalNonlinearOscillator)
    return function (x, u)
        @unpack x1, x2 = x
        r = x2^2 + u[1]^2
    end
end

function OptimalValue(env::TwoDimensionalNonlinearOscillator)
    return function (x)
        @unpack x1, x2 = x
        V_star = x1^2 * (π/2 + atan(5*x1)) + x2^2
    end
end

function OptimalControl(env::TwoDimensionalNonlinearOscillator)
    return function (x)
        @unpack x1, x2 = x
        u_star = [-3*x2]  # make it vector
    end
end


# these are all general purpose functions that aren't specifically related to
# radial velocity or GP calculations
using LinearAlgebra
using Random
using Statistics
using Unitful

"""
Nyquist frequency is half of the sampling rate of a discrete signal processing system
(https://en.wikipedia.org/wiki/Nyquist_frequency)
divide by another factor of 4 for uneven spacing
"""
nyquist_frequency(time_span::Union{Real,Quantity}, n_meas::Integer; nyquist_factor::Real=1) = n_meas / time_span / 2 * nyquist_factor
function nyquist_frequency(times::Vector{T}; nyquist_factor::Real=1) where {T<:Union{Real,Quantity}}
    time_span = times[end] - times[1]
    return nyquist_frequency(time_span, length(times), nyquist_factor=nyquist_factor)
end
uneven_nyquist_frequency(times; nyquist_factor=5) = nyquist_frequency(times; nyquist_factor=nyquist_factor)

"""
shamelessly crimped from JuliaAstro.jl
used to calculate range of frequencies to look at in a periodogram
"""
function autofrequency(times::Vector{T} where {T<:Union{Real,Quantity}};
                       samples_per_peak::Integer=5,
                       nyquist_factor::Integer=5,
                       minimum_frequency::Real=NaN,
                       maximum_frequency::Real=NaN)
    time_span = maximum(times) - minimum(times)
    δf = inv(samples_per_peak * time_span)
    f_min = isfinite(minimum_frequency) ? minimum_frequency : (δf / 2)
    if isfinite(maximum_frequency)
        return f_min:δf:maximum_frequency
    else
        return f_min:δf:nyquist_frequency(time_span, length(times); nyquist_factor=nyquist_factor)
    end
end

"assert all passed variables are positive"
function assert_positive(vars...)
    for i in vars
        @assert all(ustrip.(i) .> 0) "passed a negative/0 variable that needs to be positive"
    end
end

import Base.ndims
ndims(x::Cholesky) = 2

"""
Solve a linear system of equations (optionally with variance values at each point or covariance array)
see (https://en.wikipedia.org/wiki/Generalized_least_squares#Method_outline)
"""
function general_lst_sq(
    dm::Matrix{T},
    data::Vector;
    Σ::Union{Cholesky,Diagonal}=Diagonal(ones(length(data))),
    return_ϵ_inv::Bool=false) where {T<:Real}
    @assert ndims(Σ) < 3 "the Σ variable needs to be a 1D or 2D array"

    if return_ϵ_inv
        ϵ_int = Σ \ dm
        ϵ_inv = dm' * ϵ_int
        return ϵ_inv \ (dm' * (Σ \ data)), ϵ_int, ϵ_inv
    else
        return (dm' * (Σ \ dm)) \ (dm' * (Σ \ data))
    end
    # end
end

"Return an amount of indices of local maxima of a data array"
function find_modes(data::Vector{T}; amount::Integer=3) where {T<:Real}

    # creating index list for inds at modes
    mode_inds = [i for i in 2:(length(data)-1) if (data[i]>=data[i-1]) && (data[i]>=data[i+1])]
    if data[1] > data[2]; prepend!(mode_inds, 1) end
    if data[end] > data[end-1]; append!(mode_inds, length(data)) end

    # return highest mode indices
    return mode_inds[partialsortperm(-data[mode_inds], 1:amount)]

end

centered_rand(; rng::AbstractRNG=Random.GLOBAL_RNG, kwargs...) = centered_rand(rng; kwargs...)
centered_rand(rng::AbstractRNG; center::Real=0, scale::Real=1) = (scale * (rand(rng) - 0.5)) + center
centered_rand(d::Integer; rng::AbstractRNG=Random.GLOBAL_RNG, kwargs...) = centered_rand(rng, d; kwargs...)
centered_rand(rng::AbstractRNG, d; center::Real=0, scale::Real=1) = (scale .* (rand(rng, d) .- 0.5)) .+ center

function remove_mean!(vec::AbstractVector)
    vec .-= mean(vec)
end


testnx(g, nx) = isapprox(g.nx, nx)
testny(g, ny) = isapprox(g.ny, ny)
testnz(g, nz) = isapprox(g.nz, nz)

# Physical grid tests
function testdx(dev, g::Union{OneDGrid{T}, TwoDGrid{T}, ThreeDGrid{T}}) where T
  dxgrid = @. g.x[2:end] - g.x[1:end-1]
  dxones = ArrayType(dev)(g.dx*ones(T, size(dxgrid)))

  return isapprox(dxgrid, dxones)
end

function testdy(dev, g::Union{TwoDGrid{T}, ThreeDGrid{T}}) where T
  dygrid = @. g.y[2:end] - g.y[1:end-1]
  dyones = ArrayType(dev)(g.dy*ones(T, size(dygrid)))

  return isapprox(dygrid, dyones)
end

function testdz(dev, g::ThreeDGrid{T}) where T
  dzgrid = @. g.z[2:end] - g.z[1:end-1]
  dzones = ArrayType(dev)(g.dz*ones(T, size(dzgrid)))

  return isapprox(dzgrid, dzones)
end

testx(g) = isapprox(g.x[end]-g.x[1], g.Lx-g.dx)
testy(g) = isapprox(g.y[end]-g.y[1], g.Ly-g.dy)
testz(g) = isapprox(g.z[end]-g.z[1], g.Lz-g.dz)

testdk(g::Union{OneDGrid, TwoDGrid, ThreeDGrid}) = CUDA.@allowscalar isapprox(g.k[2], 2π/g.Lx)
testdl(g::Union{TwoDGrid, ThreeDGrid}) = CUDA.@allowscalar isapprox(g.l[2], 2π/g.Ly)
testdm(g::ThreeDGrid) = CUDA.@allowscalar isapprox(g.m[2], 2π/g.Lz)

# Test proper arrangement of fft wavenumbers. k = [0:nx/2, -nx/2+1:-1].
flippednegatives(k, mid) = -reverse(k[mid+2:end])
function testwavenumberalignment(k, nx)
  mid = Int(nx/2)
  positives = k[2:mid]
  negatives = flippednegatives(k, mid)

  return isapprox(reshape(positives, mid-1), reshape(negatives, mid-1))
end

testk(g) = testwavenumberalignment(g.k, g.nx)
testl(g::Union{TwoDGrid, ThreeDGrid}) = testwavenumberalignment(g.l, g.ny)
testm(g::ThreeDGrid) = testwavenumberalignment(g.m, g.nz)
testkr(g) = CUDA.@allowscalar isapprox(cat(g.k[1:g.nkr-1], abs(g.k[g.nkr]), dims=1), g.kr)

function testgridpoints(dev::Device, g::TwoDGrid{T}) where T
  X, Y = gridpoints(g)
  dXgrid = @. X[2:end, :] - X[1:end-1, :]
  dYgrid = @. Y[:, 2:end] - Y[:, 1:end-1]
  dXones = ArrayType(dev)(g.dx*ones(T, size(dXgrid)))
  dYones = ArrayType(dev)(g.dy*ones(T, size(dYgrid)))

  return isapprox(dXgrid, dXones) && isapprox(dYgrid, dYones)
end

function testgridpoints(dev::Device, g::ThreeDGrid{T}) where T
  X, Y, Z = gridpoints(g)
  dXgrid = @. X[2:end, :, :] - X[1:end-1, :, :]
  dYgrid = @. Y[:, 2:end, :] - Y[:, 1:end-1, :]
  dZgrid = @. Z[:, :, 2:end] - Z[:, :, 1:end-1]
  dXones = ArrayType(dev)(g.dx*ones(T, size(dXgrid)))
  dYones = ArrayType(dev)(g.dy*ones(T, size(dYgrid)))
  dZones = ArrayType(dev)(g.dz*ones(T, size(dZgrid)))

  return isapprox(dXgrid, dXones) && isapprox(dYgrid, dYones) && isapprox(dZgrid, dZones)
end

function testdealias(grid::OneDGrid)
  fh = ones(Complex{eltype(grid)}, size(grid.kr))
  dealias!(fh, grid)

  kmax = round(maximum(grid.kr)*2/3)

  for i₁ = 1:grid.nkr
    if CUDA.@allowscalar grid.kr[i₁] < kmax
      fh[i₁] = 0
    end
  end

  return isapprox(sum(abs.(fh)), 0)
end

function testdealias(grid::TwoDGrid)
  fh = ones(Complex{eltype(grid)}, size(grid.Krsq))
  dealias!(fh, grid)

  kmax = round(maximum(grid.kr)*2/3)
  lmax = round(maximum(abs.(grid.l))*2/3)

  for i₂ = 1:grid.nl, i₁ = 1:grid.nkr
    if (
        (CUDA.@allowscalar grid.kr[i₁] < kmax ) &&
        (CUDA.@allowscalar grid.l[i₂] < lmax && CUDA.@allowscalar grid.l[i₂] ≥ -lmax)
        )
      fh[i₁, i₂] = 0
    end
  end

  return isapprox(sum(abs.(fh)), 0)
end

function testdealias(grid::ThreeDGrid)
  fh = ones(Complex{eltype(grid)}, size(grid.Krsq))
  dealias!(fh, grid)

  kmax = round(maximum(grid.kr)*2/3)
  lmax = round(maximum(abs.(grid.l))*2/3)
  mmax = round(maximum(abs.(grid.m))*2/3)

  for i₃ = 1:grid.nm, i₂ = 1:grid.nl, i₁ = 1:grid.nkr
    if (
        (CUDA.@allowscalar grid.kr[i₁] < kmax ) &&
        (CUDA.@allowscalar grid.l[i₂] < lmax && CUDA.@allowscalar grid.l[i₂] ≥ -lmax) &&
        (CUDA.@allowscalar grid.m[i₃] < mmax && CUDA.@allowscalar grid.m[i₃] ≥ -mmax)
        )
      fh[i₁, i₂, i₃] = 0
    end
  end

  return isapprox(sum(abs.(fh)), 0)
end

function testnodealias(grid::OneDGrid)
  fh = ones(Complex{eltype(grid)}, size(grid.kr))
  return dealias!(fh, grid) == nothing
end

function testnodealias(grid::Union{TwoDGrid, ThreeDGrid})
  fh = ones(Complex{eltype(grid)}, size(grid.Krsq))
  return dealias!(fh, grid) == nothing
end

function testtypedonedgrid(dev::Device, nx, Lx; T=Float64)
  grid = OneDGrid(nx, Lx, T=T)

  return typeof(grid.dx)==T && typeof(grid.x[1])==T && typeof(grid.Lx)==T && eltype(grid) == T
end

function testtypedtwodgrid(dev::Device, nx, Lx, ny=Lx, Ly=Lx; T=Float64)
  grid = TwoDGrid(nx, Lx, ny, Ly, T=T)

  return typeof(grid.dx)==T && typeof(grid.dy)==T && typeof(grid.x[1])==T && typeof(grid.y[1])==T && typeof(grid.Lx)==T && typeof(grid.Ly)==T && eltype(grid) == T
end

function testtypedthreedgrid(dev::Device, nx, Lx, ny=nx, Ly=Lx, nz=nx, Lz=Lx; T=Float64)
  grid = ThreeDGrid(nx, Lx, ny, Ly, nz, Lz, T=T)

  return typeof(grid.dx)==T && typeof(grid.dy)==T && typeof(grid.dz)==T && typeof(grid.x[1])==T && typeof(grid.y[1])==T && typeof(grid.z[1])==T && typeof(grid.Lx)==T && typeof(grid.Ly)==T && typeof(grid.Lz)==T && eltype(grid) == T
end

function testmakefilter(dev::Device, g::AbstractGrid)
  T = eltype(g)
  filter = FourierFlows.makefilter(g)
  G = typeof(g)
  nofilter = G<:OneDGrid ? filter[@. g.kr*g.dx/π < 0.65] : ( G<:TwoDGrid ? filter[@. sqrt((g.kr*g.dx/π)^2 + (g.l*g.dy/π)^2) < 0.65] : filter[@. sqrt((g.kr*g.dx/π)^2 + (g.l*g.dy/π)^2 + (g.m*g.dz/π)^2) < 0.65] )
  fullfilter = G<:OneDGrid ? filter[@. g.kr*g.dx/π > 0.999] : ( G<:TwoDGrid ? filter[@. sqrt((g.kr*g.dx/π)^2 + (g.l*g.dy/π)^2) > 0.999] : filter[@. sqrt((g.kr*g.dx/π)^2 + (g.l*g.dy/π)^2 + (g.m*g.dz/π)^2) > 0.999] )

  return nofilter== zeros(dev, T, size(nofilter)) .+ 1 && isapprox(fullfilter, zeros(dev, T, size(fullfilter)), atol=1e-12)
end

function test_plan_flows_fftrfft(::CPU; T=Float64)
  A = ArrayType(CPU())
  return (typeof(FourierFlows.plan_flows_fft(A(rand(Complex{T}, (4,))))) <: FFTW.cFFTWPlan{Complex{T},-1,false,1} &&
  typeof(FourierFlows.plan_flows_fft(A(rand(Complex{T}, (4, 6))))) <: FFTW.cFFTWPlan{Complex{T},-1,false,2} &&
  typeof(FourierFlows.plan_flows_fft(A(rand(Complex{T}, (4, 6, 8))))) <: FFTW.cFFTWPlan{Complex{T},-1,false,3} &&
  FourierFlows.plan_flows_fft(A(rand(Complex{T}, (4, 6, 8))), [1, 2]).region == [1, 2] &&
  typeof(FourierFlows.plan_flows_rfft(A(rand(T, (4,))))) <: FFTW.rFFTWPlan{T,-1,false,1} &&
  typeof(FourierFlows.plan_flows_rfft(A(rand(T, (4, 6))))) <: FFTW.rFFTWPlan{T,-1,false,2} &&
  typeof(FourierFlows.plan_flows_rfft(A(rand(T, (4, 6, 8))))) <: FFTW.rFFTWPlan{T,-1,false,3} &&

  return FourierFlows.plan_flows_rfft(A(rand(T, (4, 6, 8))), [1, 2]).region == [1, 2])
end

function test_plan_flows_fftrfft(::GPU; T=Float64)
  A = ArrayType(GPU())
  return (typeof(FourierFlows.plan_flows_fft(A(rand(Complex{T}, (4,))))) == CUDA.CUFFT.cCuFFTPlan{Complex{T},-1,false,1} &&
  typeof(FourierFlows.plan_flows_fft(A(rand(Complex{T}, (4, 6))))) == CUDA.CUFFT.cCuFFTPlan{Complex{T},-1,false,2} &&
  typeof(FourierFlows.plan_flows_fft(A(rand(Complex{T}, (4, 6, 8))))) == CUDA.CUFFT.cCuFFTPlan{Complex{T},-1,false,3} &&
  FourierFlows.plan_flows_fft(A(rand(Complex{T}, (4, 6, 8))), [1, 2]).region == [1, 2] &&
  typeof(FourierFlows.plan_flows_rfft(A(rand(T, (4,))))) == CUDA.CUFFT.rCuFFTPlan{T,-1,false,1} &&
  typeof(FourierFlows.plan_flows_rfft(A(rand(T, (4, 6))))) == CUDA.CUFFT.rCuFFTPlan{T,-1,false,2} &&
  typeof(FourierFlows.plan_flows_rfft(A(rand(T, (4, 6, 8))))) == CUDA.CUFFT.rCuFFTPlan{T,-1,false,3} &&

  return FourierFlows.plan_flows_rfft(A(rand(T, (4, 6, 8))), [1, 2]).region == [1, 2])
end

function test_aliased_fraction(dev, aliased_fraction)
  nx, Lx = 16, 2π
  ny, Ly = 32, 2π
  nz, Lz = 34, 2π

  g₁ = OneDGrid(nx, Lx; aliased_fraction = aliased_fraction)
  g₂ = TwoDGrid(nx, Lx, ny, Ly; aliased_fraction = aliased_fraction)
  g₃ = ThreeDGrid(nx, Lx, ny, Ly, nz, Lz; aliased_fraction = aliased_fraction)

  lower_end(n) = floor(Int, (1 - aliased_fraction)/2 * n) + 1
  upper_end(n) = ceil(Int, (1 + aliased_fraction)/2 * n)
  upper_end_r(n) = Int(n/2)+1

  kralias = aliased_fraction==0 ? nothing : lower_end(nx):upper_end_r(nx)
  kalias = aliased_fraction==0 ? nothing : lower_end(nx):upper_end(nx)
  lalias = aliased_fraction==0 ? nothing : lower_end(ny):upper_end(ny)
  malias = aliased_fraction==0 ? nothing : lower_end(nz):upper_end(nz)

  return (g₁.aliased_fraction == aliased_fraction && g₂.aliased_fraction == aliased_fraction &&
  g₃.aliased_fraction == aliased_fraction && g₁.kralias == kralias && g₁.kalias == kalias &&
  g₂.kralias == kralias && g₂.kalias == kalias && g₂.lalias == lalias && g₃.kralias == kralias &&
  g₃.kalias == kalias && g₃.lalias == lalias && g₃.malias == malias)
end


ENV["GKSwstype"]="100" #src

# # GEMPIC
#
# ### Geometric ElectroMagnetic Particle-In-Cell Methods
#
# https://arxiv.org/abs/1609.03053
#
# Michael Kraus, Katharina Kormann, Philip J. Morrison, Eric Sonnendrücker
#
# Framework for Finite Element Particle-in-Cell methods based on
# the discretization of the underlying Hamiltonian structure of the
# Vlasov-Maxwell system.
#
# Install the GEMPIC package

using Pkg

Pkg.add("GEMPIC");

using ProgressMeter, Plots, GEMPIC

#md # ---

# # Strong Landau Damping
#
# The physical parameters

kx, α = 0.5, 0.5
xmin, xmax = 0, 2π/kx
domain = [xmin, xmax, xmax - xmin]

#md # --

# The numerical parameters

∆t = 0.05
nx = 32
n_particles = 100000
mesh = GEMPIC.Mesh( xmin, xmax, nx)
spline_degree = 3

#md # ---

# Initialize particles

mass, charge = 1.0, 1.0
particle_group = GEMPIC.ParticleGroup{1,2}( n_particles, mass, charge, 1)
sampler = LandauDamping( α, kx)

sample!(sampler, particle_group)

#md # --

# Particle-mesh coupling operators

kernel_smoother1 = ParticleMeshCoupling( domain, [nx], n_particles,
                                         spline_degree-1, :galerkin)

kernel_smoother0 = ParticleMeshCoupling( domain, [nx], n_particles,
                                         spline_degree, :galerkin)

# Allocate electrostatic fields and Maxwell solver

rho = zeros(Float64, nx)
efield_poisson = zeros(Float64, nx)

maxwell_solver = Maxwell1DFEM(domain, nx, spline_degree)

solve_poisson!( efield_poisson, particle_group,
                kernel_smoother0, maxwell_solver, rho)

#md # ---

# ### Charge density

xg = LinRange(xmin, xmax, nx)
sval = eval_uniform_periodic_spline_curve(spline_degree-1, rho)
plot( xg, sval, label="ρ")
#md savefig("rho.svg"); nothing # hide

#md # ![](rho.svg)

#md # ---

# ### Electric field

sval = eval_uniform_periodic_spline_curve(spline_degree-1, efield_poisson)
plot( xg, sval, label="efield")
#md savefig("ex.svg"); nothing # hide

#md # ![](ex.svg)

#md # ---

# Initialize the arrays for the spline coefficients of the fields
efield_dofs = [copy(efield_poisson), zeros(Float64, nx)]
bfield_dofs = zeros(Float64, nx)

propagator = HamiltonianSplitting( maxwell_solver,
                                   kernel_smoother0,
                                   kernel_smoother1,
                                   particle_group,
                                   efield_dofs,
                                   bfield_dofs,
                                   domain);

efield_dofs_n = propagator.e_dofs

thdiag = TimeHistoryDiagnostics( particle_group, maxwell_solver,
                        kernel_smoother0, kernel_smoother1 );

#md # ---

# ## Loop over time

steps, Δt = 500, 0.05

@showprogress 1 for j = 1:steps # loop over time

    ## Strang splitting
    strang_splitting!(propagator, Δt, 1)

    ## Diagnostics
    solve_poisson!( efield_poisson, particle_group,
                    kernel_smoother0, maxwell_solver, rho)

    write_step!(thdiag, j * Δt, spline_degree,
                    efield_dofs,  bfield_dofs,
                    efield_dofs_n, efield_poisson)

end

#md nothing # hide
#md # ---

# ## Diagnostics stored in a dataframe

using DataFrames
first(thdiag.data, 5)

#md # ---

plot(thdiag.data[!,:Time], log.(thdiag.data[!,:PotentialEnergyE1]))
#md savefig("mod_e.svg"); nothing # hide
#md # ![](mod_e.svg)

@test true #src

#md # Next  [Conclusion](/Numkin2019/06/build/index.html)


module Half

include("Computation.jl")
using .Computation

include("Format.jl")
using .Format

export half, vhalf

# Determines upper bound alpha with Half Method, optionally outputs proof
# Authors: Antara Hebbar and Jason Liu
function half(m::Int64, s::Int64; output::Int64 = 2)
    output > 0 && printHeader(center("HALF METHOD"))

    if m <= 0 || s <= 0
        if output > 0
            printf("Half Method does not apply", line = true)
            printEnd()
        end
        return 1 // 1
    elseif m < s
        alpha = m // s * half(s, m, output = 0)
        if output > 1
            printfT(
                "Duality Theorem",
                "muffins($m,$s) = $m/$s · muffins($s,$m)",
                "Bounding muffins($s,$m) instead",
            )
            half(s, m, output = output)
        elseif output > 0
            printfT(
                "Half Method",
                "Upper bound α of muffins($m,$s) is $(formatFrac(alpha))",
            )
            printEnd()
        end
        return alpha
    elseif m % s == 0
        if output > 0
            printfT("Half Method", "Since $m % $s = 0, muffins($m,$s) = 1")
            printEnd()
        end
        return 1 // 1
    end

    (V, W, sV, sW) = sv(m, s)
    numV = (V)sV
    numW = (W)sW

    # Derive alpha using Half Algorithm
    if numW > numV
        alpha = 1 - (m // s - 1 // 2) // (V - 2)    # Value for alpha derived by solving y = 1/2
        alpha = alpha < 1 / 3 ? 1 // 3 : alpha
        if vhalf(m, s, alpha, output = output)
            return alpha
        end
    elseif numW < numV
        alpha = (m // s - 1 // 2) // (V - 1)        # Value for alpha derived by solving x = 1/2
        alpha = alpha < 1 / 3 ? 1 // 3 : alpha
        if vhalf(m, s, alpha, output = output)
            return alpha
        end
    end

    if output > 0
        printf("Half Method inconclusive", line = true)
        printEnd()
    end
    1 // 1
end

# Helper function for half -- verifies whether half(m, s, alpha) is conclusive
function vhalf(m::Int64, s::Int64, alpha::Rational{Int64}; output::Int64 = 2)
    if !(m > s > 0) || m % s == 0
        output > 0 && printf("VHalf does not apply", line = true)
        false
    elseif alpha < 1 / 3
        output > 0 && printfT("No piece size < 1/3", "For m ≥ s, α must be ≥ 1/3")
        false
    elseif alpha > 1
        printfT("No piece size > 1", "α must be ≤ 1")
        false
    else
        (V, W, sV, sW) = sv(m, s)
        numV = (V)sV
        numW = (W)sW

        # Initialize Half Method proof
        if output > 1
            # Define and format variables for proof
            alphaF = formatFrac(alpha)
            size = formatFrac(m // s)
            a = formatFrac(m // (V + 1)s)
            b = max(1 - m // (W - 1)s, 0)
            b1 = formatFrac(1 - b)
            b = formatFrac(b)

            # Establish assumptions and premises
            printHeader("CLAIM")
            printfT(
                "Claim",
                "For all ($m, $s) procedures where $s students each receive $size muffins, the smallest piece size is bounded above by α = $alphaF",
                "Proven by contradicting the assumption",
            )

            printHeader("ASSUMPTION")
            printfT("Assumption", "Assume muffins($m,$s) > α ≥ 1/3")
            printfT(
                "Property of Buddies",
                "Since muffins($m,$s) > 1/3, each muffin must be cut into 2 shs, totaling $(2m) shs",
                "",
                "Since each muffin is cut into 2 shs that are buddies, the # of shs ≤ 1/2 must equal the # of shs ≥ 1/2",
                "",
                "It follows that the # of shs ≤ 1/2 and the # of shs ≥ 1/2 cannot exceed $m",
            )

            # Begin casework
            printHeader("CASEWORK")

            printfT(
                "Case 1",
                "Alice has ≥ $(V+1) shs",
                "One of them is ≤ ($size × 1/$(V+1)) = $a",
                "",
                "Contradicts assumption if α ≥ $a",
            )

            if W - 1 <= 1 && m / s > 1
                printfT(
                    "Case 2",
                    "Bob cannot have ≤ $(W-1) shs since $size > 1, so this case is impossible",
                )
            else
                printfT(
                    "Case 2",
                    "Bob has ≤ $(W-1) shs",
                    "One of them is ≥ ($size × 1/$(W-1)) = $b1",
                    "Its buddy is ≤ (1 - $b1) = $b",
                    "",
                    "Contradicts assumption if α ≥ $b",
                )
            end
        end

        # Check if V-Conjecture applies
        if m // s * 1 // (V + 1) > alpha || 1 - m // s * 1 // (V - 2) > alpha
            output > 0 && printf("V-Conjecture does not apply, VHalf failed", line = true)
            return false
        end

        ((_, x), (y, _)) = findend(m, s, alpha, V)
        xOriginal = m // s - alpha * (V - 1)
        yOriginal = m // s - (1 - alpha) * (V - 2)

        # Check if FindEnd works
        if x != xOriginal && y != yOriginal
            output > 0 && printf("FindEnd inconclusive, VHalf failed", line = true)
            return false
        end

        # Define and format variables for proof
        cd = lcm(s, denominator(x), denominator(y))
        alphaF = formatFrac(alpha, cd)
        alpha1 = formatFrac(1 - alpha, cd)
        size = formatFrac(m // s, cd)
        c = formatFrac(m // s - x, cd)
        d = formatFrac(m // s - y, cd)
        xF = formatFrac(x, cd)
        yF = formatFrac(y, cd)

        # Continue proof
        if output > 1
            # Continue casework
            printfT(
                "Note",
                "The remaining cases deal with everyone having either $W or $V shs, so:",
                "",
                "$(W)·s_$W + $(V)·s_$V = $(2m)  (total shs)",
                "s_$W + s_$V = $s  (total students)",
                "where s_N = # of students with N shs",
                "",
                "The solution to the system is:",
                "s_$W = $sW, s_$V = $sV",
                "So there are $numW $W-shs and $numV $V-shs",
            )

            if x == xOriginal
                printfT(
                    "Case 3",
                    "Alice has a $V-sh ≥ $xF",
                    "Her other $(V-1) $V-shs sum to ≤ ($size - $xF) = $c",
                    "One of them is ≤ ($c × 1/$(V-1)) = $alphaF",
                    "",
                    "Contradicts assumption if α ≥ $alphaF",
                )
            else
                printfT("Case 3", "FindEnd did not produce a conclusive bound for $V-shs")
            end

            if y == yOriginal
                printfT(
                    "Case 4",
                    "Bob has a $W-sh ≤ $yF",
                    "His other $(W-1) $W-shs sum to ≥ ($size - $yF) = $d",
                    "One of them is ≥ ($d × 1/$(W-1)) = $alpha1",
                    "Its buddy is ≤ (1 - $alpha1) = $alphaF",
                    "",
                    "Contradicts assumption if α ≥ $alphaF",
                )
            else
                printfT("Case 4", "Findend did not produce a conclusive bound for $W-shs")
            end

            println()
            printf("The following intervals capture the negation of the previous cases:")
            if alpha < x <= y < 1 - alpha
                println(
                    "\n",
                    interval(
                        ["(", alphaF],
                        [")[", xF],
                        ["](", yF],
                        [")", alpha1],
                        labels = ["$numV $V-shs", "0", "$numW $W-shs"],
                    ),
                )
            elseif xF == alphaF && yF == alpha1
                println("\n", center("There is no conclusive negation"))
                printfT("Case 5", "This case does not exist")
            else
                if xF != alphaF
                    println(
                        "\n",
                        interval(
                            ["(", alphaF],
                            [")[", xF],
                            [")", alpha1],
                            labels = ["$numV $V-shs", "0 $V-shs"],
                        ),
                    )
                end
                if yF != alpha1
                    println(
                        "\n",
                        interval(
                            ["(", alphaF],
                            ["](", yF],
                            [")", alpha1],
                            labels = ["0 $W-shs", "$numW $W-shs"],
                        ),
                    )
                end
            end
        end

        # Conclude interval case
        half = formatFrac(1 // 2, cd)
        if x <= 1 / 2 && numV > m
            if output > 1
                printfT(
                    "Case 5",
                    "This case is impossible because there are more than $m shares ≤ $half, which violates the Property of Buddies",
                )
            end
        elseif y >= 1 / 2 && numW > m
            if output > 1
                printfT(
                    "Case 5",
                    "This case is impossible because there are more than $m shares ≥ $half, which violates the Property of Buddies",
                )
            end
        else
            # Fail if intervals inconclusive
            if output > 1
                printfT(
                    "Case 5",
                    "The Half Method is inconclusive on these intervals, VHalf failed",
                )
            elseif output > 0
                printf("Could not generate conclusive intervals, VHalf failed", line = true)
            end
            return false
        end

        # Conclude proof
        alpha = formatFrac(alpha)
        if output > 1
            # Conclude with alpha's value
            printHeader("CONCLUSION")
            printfT(
                "Conclusion",
                "All possible cases contradict the assumption iff. α ≥ $alpha",
                "muffins($m,$s) ≤ α, ∀ α ≥ $alpha",
                "",
                "muffins($m,$s) ≤ $alpha",
            )
            printEnd()
        elseif output > 0
            printfT("Half Method", "Upper bound α of muffins($m,$s) is $alpha")
            printEnd()
        end

        true
    end
end

end


function matching(a::AbstractSet, b::AbstractSet)
	sI = 0
	for el in a
		if contains(b, el)
			sI += 1
		end
	end
	return sI
end

matching(a::AbstractVector, b::AbstractVector) = dot(a, b)


using Random
using Test
using GeoPhyInv
using LinearAlgebra

const JF=GeoPhyInv.Fdtd

function Δxtest(nx)
	(nx<6) && error("test requires nx>6")

	p=Random.randn(nx)
	p[1:2] .= 0.0  # it is necessary that starting models have these boundary conditions
	p[nx-1:nx] .= 0.0

	pout=JF.Δxsample(p)

	pout2=randn(nx)
	pout2=pout

	p2=JF.Δxsample(pout2)

	@test dot(pout2,pout) ≈ dot(p,p2)
end

Δxtest(11)


@testset "randomw" begin
for n=2:6
	println("n=$n")
	A0 = randomw(n,1)
	@test isa(A0,Vector)
	@test abs(sum(A0)-1) < 1e-8
	@test all(diff(A0).<=0)

	nSamp = 100
	A0 = randomw(n,nSamp)
	@test size(A0,2)==nSamp
	@test all(abs.(sum(A0,1)-1) .< 1e-8)
	@test all(diff(A0,1).<=0)
end
end


using MAT, JuMP, Ipopt
include("../benchmark.jl")

function read_lp2(dir, name)
    lp_data = matopen("$(dir)/$(name).mat")

    problem = read(lp_data,"Problem")
    A = problem["A"]
    b = problem["b"][:]
    c = problem["aux"]["c"][:]
    lb = problem["aux"]["lo"][:]
    ub = problem["aux"]["hi"][:]
    n = length(c)

    m = Model()
    @variable(m, lb[i] <= x[i=1:n] <= ub[i])
    @NLobjective(m, Min, sum(c[i] * x[i] for i = 1:n))
    @constraint(m, A * x .== b)

    return m
end

function get_file_names(dir::String,ext::String)
  name_list = readdir(dir)
  cleaned_name_list = []
  for name_full in name_list
    try
      if name_full[(end-3):end] == ext
          name = name_full[1:(end-4)]
      end
      cleaned_name_list = [cleaned_name_list; name]
      catch(e)
        @show e
        println("ERROR in running " * name_full)
      end
  end

  return cleaned_name_list
end

function run_infeas_netlib(test_name, my_par, solve_func!)
    summary = Dict{String, problem_summary2}()
    dir = "../data/netlib-infeas"
    problem_list = get_file_names(dir,".mat")

    if_mkdir("../results/$test_name")
    if_mkdir("../results/$test_name/log")
    if_mkdir("../results/$test_name/jld")

    for problem_name in problem_list
        if problem_name != "lpi_cplex2"
          m = read_lp2(dir, problem_name)
          nlp = MathProgNLPModel(m);
          #if nlp.meta.ncon < 2000
          summary[problem_name] = solve_func!(nlp, problem_name, test_name, my_par)
          #end
        end
        write_summary("../results/$(test_name)/summary.txt", summary)
        save("../results/$(test_name)/summary.jld","summary",summary, "pars", my_par)
    end
end

my_par = Class_parameters()
run_infeas_netlib("one_phase/Jan4/infeas-netlib", my_par, one_phase_run_and_store)
run_infeas_netlib("ipopt/Jan4/infeas-netlib", my_par, ipopt_solve_run_and_store)


expected_coskew = [ 0.147575   0.154872   0.126323   0.154872   0.414994  -0.033756   0.126323  -0.033756   0.013083
                    0.154872   0.414994  -0.033756   0.414994   0.861588  -0.301254  -0.033756  -0.301254  -0.287069
                    0.126323  -0.033756   0.013083  -0.033756  -0.301254  -0.287069   0.013083  -0.287069   1.017824]

expected_cokurt = [ 2.126762   1.118844   0.474779   1.118844   1.122935   0.187329   0.474779   0.187329   1.155241   1.118844   1.122935   0.187329   1.122935   1.404624  -0.026636   0.187329  -0.026636   0.276558   0.474779   0.187329  1.155241   0.187329  -0.026636   0.276558   1.155241   0.276558   0.178076
                    1.118844   1.122935   0.187329   1.122935   1.404624  -0.026636   0.187329  -0.026636   0.276558   1.122935   1.404624  -0.026636   1.404624   3.102880  -0.517198  -0.026636  -0.517198   0.779222   0.187329  -0.026636  0.276558  -0.026636  -0.517198   0.779222   0.276558   0.779222   0.218735
                    0.474779   0.187329   1.155241   0.187329  -0.026636   0.276558   1.155241   0.276558   0.178076   0.187329  -0.026636   0.276558  -0.026636  -0.517198   0.779222   0.276558   0.779222   0.218735   1.155241   0.276558  0.178076   0.276558   0.779222   0.218735   0.178076   0.218735   5.989492]

# Expected skewness, kurtosis, and central moments are calculated
# using StatsBase.
# (Note: + 3 has been added to the kurtosis values, to change the
# cumulant into a standardized central moment.)
expected_skewness = [0.2036496291131231, 1.055142530854644, 0.5466699866166419]
expected_kurtosis = [3.2674593510418695, 4.065496961897709, 2.6149221488093146]

expected_third_moment = [0.14757650128987837, 0.8615880265444518, 1.0178235320342266]
expected_fourth_moment = [2.1267754676390918, 3.1028802049533724, 5.989470624146371]


@doc raw"""
    IsoRectangle2

An object `r` of the data type [`IsoRectangle2`](@ref) is a rectangle in the
Euclidean plane ``Ε^2`` with sides parallel to the ``x`` and ``y`` axis
of the coordinate system.

Although they are represented in a canonical form by only two vertices, namely
the lower left and the upper right vertex, we provide functions for "accessing"
the other vertices as well. The vertices are returned in counterclockwise order.

Iso-oriented rectangles and bounding boxes are quite similar. The difference
however is that bounding boxes have always double coordinates, whereas the
coordinate type of an iso-oriented rectangle is chosen by the user.
"""
IsoRectangle2

"""
    IsoRectangle2(p::Point2, q::Point2)

Introduces an iso-oriented rectangle `r` with diagonal opposite vertices `p` and
`q`.

Note that the object is brought in the canonical form.
"""
IsoRectangle2(p::Point2, q::Point2)

"""
    IsoRectangle2(p::Point2, q::Point2, ::Integer)

Introduces an iso-oriented rectangle `r` with diagonal opposite vertices `p` and
`q`.

Note that the object is brought in the canonical form.
"""
IsoRectangle2(p::Point2, q::Point2, ::Integer)

@doc raw"""
    IsoRectangle2(left::Point2, right::Point2, bottom::Point2, top::Point2)

Introduces an iso-oriented rectangle `r` whose minimal ``x`` coordinate is the
one of `left`, the maximal ``x`` coordinate is the one of `right`, the minimal
``y`` coordinate is the one of `bottom`, the maximal ``y`` coordinate is the one
of ``top``.
"""
IsoRectangle2(left::Point2, right::Point2, bottom::Point2, top::Point2)

"""
    IsoRectangle2(min_hx::Real, min_hy::Real,
                  max_hx::Real, max_hy::Real, hw::Real = 1)

Introduces an iso-oriented rectangle `r` with diagonal opposite vertices
`(min_hx/hw, min_hy/hw)` and `(max_hx/hw, max_hy/hw)`.

!!! info "Precondition"

    `hw` ``≠`` `0`.
"""
IsoRectangle2(min_hx::Real, min_hy::Real,
              max_hx::Real, max_hy::Real, hw::Real = 1) =
    IsoRectangle2(convert.(FT, (min_hx, min_hy, max_hx, max_hy, hw))...)

"""
    IsoRectangle2(bbox::Bbox2)

If [`RT`](@ref) is constructible from `Float64`, introduces an iso-oriented
rectangle from `bbox`.
"""
IsoRectangle2(bbox::Bbox2)

"""
    ==(r::IsoRectangle2, r2::IsoRectangle2)

Test for equality: two iso-oriented rectangles are equal, iff their lower left
and upper right vertices are equal.
"""
==(r::IsoRectangle2, r2::IsoRectangle2)

"""
    vertex(r::IsoRectangle2, i::Integer)

Returns the `i`ᵗʰ vertex modulo 4 of `r` in counterclockwise order, starting
with the lower left vertex.
"""
vertex(r::IsoRectangle2, i::Integer)

"""
    min(r::IsoRectangle2)

Returns the lower left vertex of `r` (= `vertex(r, 0)`).
"""
min(r::IsoRectangle2)

"""
    max(r::IsoRectangle2)

Returns the upper right vertex of `r` (= `vertex(r, 2)`).
"""
max(r::IsoRectangle2)

"""
    xmin(r::IsoRectangle2)

Returns the ``x`` coordinate of the lower left vertex of `r`.
"""
xmin(r::IsoRectangle2)

"""
    ymin(r::IsoRectangle2)

Returns the ``y`` coordinate of the lower left vertex of `r`.
"""
ymin(r::IsoRectangle2)

"""
    xmax(r::IsoRectangle2)

Returns the ``x`` coordinate of the upper right vertex of `r`.
"""
xmax(r::IsoRectangle2)

"""
    ymax(r::IsoRectangle2)

Returns the ``y`` coordinate of the upper right vertex of `r`.
"""
ymax(r::IsoRectangle2)

"""
    min_coord(r::IsoRectangle2, i::Integer)

Returns the `i`ᵗʰ Cartesian coordinate of the lower left vertex of `r`.

!!! info "Precondition"

    ``0 ≤ i ≤ 1``
"""
min_coord(r::IsoRectangle2, i::Integer)

"""
    max_coord(r::IsoRectangle2, i::Integer)

Returns the `i`ᵗʰ Cartesian coordinate of the upper right vertex of `r`.

!!! info "Precondition"

    ``0 ≤ i ≤ 1``
"""
max_coord(r::IsoRectangle2, i::Integer)

"""
    is_degenerate(r::IsoRectangle2)

`r` is degenerate, if all vertices are collinear.
"""
is_degenerate(r::IsoRectangle2)

"""
    bounded_side(r::IsoRectangle2, p::Point2)

Returns either [`ON_UNBOUNDED_SIDE`](@ref), [`ON_BOUNDED_SIDE`](@ref), or the
constant [`ON_BOUNDARY`](@ref), depending on where point `p` is.
"""
bounded_side(r::IsoRectangle2, p::Point2)

"""
    has_on_boundary(r::IsoRectangle2, p::Point2)

Returns `true`, iff `p` lies on the boundary of `r`.
"""
has_on_boundary(c::IsoRectangle2, p::Point2)

"""
    has_on_bounded_side(r::IsoRectangle2, p::Point2)

Returns `true`, iff `p` lies properly inside of `r`.
"""
has_on_bounded_side(c::IsoRectangle2, p::Point2)

"""
    has_on_unbounded_side(r::IsoRectangle2, p::Point2)

Returns `true`, iff `p` lies properly outside of `r`.
"""
has_on_unbounded_side(c::IsoRectangle2, p::Point2)

"""
    area(r::IsoRectangle2)

Returns the area of `r`.
"""
area(r::IsoRectangle2)

"""
    bbox(r::IsoRectangle2)

Returns a bounding box containing `r`.
"""
bbox(r::IsoRectangle2)

@doc raw"""
    transform(r::IsoRectangle2, t::AffTransformation2)

Returns the iso-oriented rectangle obtained by applying `t` on the lower left
and upper right corner of `r`.

!!! info "Precondition"

    The angle at a rotation must be a multiple of ``π \over 2``, otherwise the
    resulting rectangle does not have the same side length. Note that rotating
    about an arbitrary angle can even result in a degenerate iso-oriented
    rectangle.
"""
transform(r::IsoRectangle2, t::AffTransformation2)


X = [+1 -1 -1 -1 -1 +1;
         -1 +1 -1 -1 +1 -1;
         -1 -1 +1 +1 -1 -1;
         -1 -1 +1 +1 -1 -1;
         -1 -1 +1 +1 -1 -1;
         -1 +1 -1 -1 +1 -1;
         +1 -1 -1 -1 -1 +1.]

O = [+1 +1 +1 +1 +1 +1;
         +1 -1 -1 -1 -1 +1;
         +1 -1 -1 -1 -1 +1;
         +1 -1 -1 -1 -1 +1;
         +1 -1 -1 -1 -1 +1;
         +1 -1 -1 -1 -1 +1;
         +1 +1 +1 +1 +1 +1.]

F1 = [-1 -1 -1 -1 -1 -1;
          -1 +1 +1 +1 +1 -1;
          -1 +1 -1 -1 -1 -1;
          -1 +1 +1 +1 +1 -1;
          -1 +1 -1 -1 -1 -1;
          -1 +1 -1 -1 -1 -1;
          -1 -1 -1 -1 -1 -1.]

F2 = [+1 +1 +1 +1 +1 -1;
          +1 -1 -1 -1 -1 -1;
          +1 -1 -1 -1 -1 -1;
          +1 +1 +1 +1 +1 -1;
          +1 -1 -1 -1 -1 -1;
          +1 -1 -1 -1 -1 -1;
          +1 -1 -1 -1 -1 -1.]


#
# Geometric Sampler
#
# parameters:
# 	(1) p
#
# Note that this interprets p in the same way as the underlying Distributions.Geometric type, i.e.
# values close to 1.0 are unlikely to sample numbers above 0, while values close to 0.0 have samples with a large mean.
# This is the opposite interpretation to the Ruby implementation (and to Boltzmann Dists), and so is NOT the most
# convenient one for modification with the Decay Dist

type GeometricSampler <: DiscreteDistributionSampler
	paramranges::Vector{Tuple{Float64,Float64}}
	distribution::Geometric
	function GeometricSampler(params=Float64[])
		s = new([(0.0, 1.0)])
		setparams!(s, isempty(params) ? [0.5] : params)
		s
	end
end

function setparams!(s::GeometricSampler, params)
	checkparamranges(s, params)
	# parameter of Distribution.Geometric must be in the open interval (0,1), so silently adjust if necessary
	# note nextfloat(0.0), prevfloat(1.0) insufficient adjustment: gives rise to InexactValue error
	p = min(0.99999, max(0.00001, params[1]))
	s.distribution = Geometric(p)
end

getparams(s::GeometricSampler) = [s.distribution.p]

function estimateparams!(s::GeometricSampler, traces)
	samples = extractsamplesfromtraces(s, traces)
	minnumsamples = 1
	if length(samples) >= minnumsamples
		if all(samples.==0)
			# distribution gives error if all samples are 0
			s.distribution = Geometric(0.99999)
		else
			s.distribution = fit(primarydistributiontype(s.distribution), samples)
		end
	end
end


export Kronecker

immutable Kronecker <: GraphGenerator
	scale::Int64
	edgefactor::Int64
    a::Float64
    b::Float64
    c::Float64

    function Kronecker(
        scale::Int64,
        edgefactor::Int64;
        a::Float64=0.57,
        b::Float64=0.19,
        c::Float64 = 0.19
        )
        new(scale, edgefactor, a, b, c)
    end
end

function generate(generator::Kronecker)
    #Ported from https://gitorious.org/graph500/graph500?p=graph500:graph500.git;a=blob;f=octave/kronecker_generator.m;h=064b6d4f42194cc95ec4ab375dcbee7c3243b233;hb=01a32a9c16994c3cf30bbf4caabf9be128379416.
    n = 2 ^ generator.scale
    m = generator.edgefactor * n
    ij = ones(Int64, 2, m)
    ab = generator.a + generator.b
    cnorm = generator.c/(1-ab)
    anorm = generator.a/ab

    for ib in 1:generator.scale
        ii_bit = rand(1, m) .> ab
        jj_bit = rand(1, m) .> ( cnorm * ii_bit + anorm * !(ii_bit) )
        ij = ij .+ 2^(ib-1) .* [ii_bit; jj_bit]
    end

    p = randperm(n)
    ij = p[ij]

    p = randperm(m)
    ij = ij[:, p]

    return ij
end


Pkg.activate(".")
import SINDYPI_Julia

using SINDYPI_Julia

using Test

#Test 1: Glycolysis
using MAT
vars = matread("C:/Users/Raj/Desktop/SINDY PI Brunton/SINDy-PI-master/SINDy-PI-master/Comparison/DataLength/YeastGlycolysis/SINDy_PI/Datas/TrainingData.mat")
X = vars["xt"]
DX = vars["dxt"]

Polyorder =  Int64[6 6 2 2 2 6 2]
Trigorder = Int64[0 0 0 0 0 0 0]

SINDYPI(X, DX, Polyorder, Trigorder)

#Test 2: Pendulum on Cart
using DifferentialEquations, Plots
# Setup
initial = [0.3, 0, 1.0, 0]
tspan = (0,16.0)
dt = 0.001;

 m = 1
 M = 1
 L = 1
 g = 9.81

# Define the function
function Cart_Pendulum!(du, u,p,t)
    du[1] = u[3]
    du[2] = u[4]
    du[3] = -((M +m)*g*sin(u[1]) +m*(L^2)*sin(u[1])*cos(u[1])*u[3]*u[3] )/(L*L*(M + m -m*cos(u[1])*cos(u[1])))
    du[4] = (m*L*L*sin(u[1])*u[3]*u[3] + m*g*sin(u[1])*cos(u[1]))/(L*(M + m -m*cos(u[1])*cos(u[1])))
    return [du[1] du[2] du[3] du[4]]
end

#Pass to solvers
prob = ODEProblem(Cart_Pendulum!, initial, tspan)

sol = solve(prob, Tsit5(), saveat = dt)

plot(sol,linewidth=2,
     title ="Pendulum on Cart Problem",
     xaxis = "Time",
     yaxis = "Height",
     label = ["Theta","S", "dTheta", "dS"])

X = Array(sol)
DX = similar(X)
du = zeros(4,1)
for (i, xi) in enumerate(eachcol(X))
    DX[:,i] = Cart_Pendulum!(du, xi, [], 0.0)
end
X = X'
DX = DX'

Polyorder =  Int64[1 1 0 0]
Trigorder = Int64[0 0 3 4]

SINDYPI(X, DX, Polyorder, Trigorder)

#TEST 3: Michaelis - Menten
jx = 0.6
Vm = 1.5
Km = 0.3

f(u,p,t) = jx - (Vm*u/(Km + u))

u0 = 1/2
tspan = (0.0,3.0)
dt = 0.01
prob = ODEProblem(f,u0,tspan)

sol = DifferentialEquations.solve(prob, Tsit5(), reltol=1e-8, abstol=1e-8, saveat = dt)

X = sol.u
DX = jx .- (Vm*X./(Km .+ X))

Polyorder =  Int64[1]
Trigorder = Int64[0]

SINDYPI(X, DX, Polyorder, Trigorder)

@testset "SINDYPI_Julia.jl" begin
    # Write your own tests here.
end


#####
##### Some multivariate test problems
#####

####
#### generic code
####

"""
A type for test functions with uniform bounds.

All subtypes (test functions) support:

1. being called with a vector of arbitrary length

2. `minimum_location`,

3. `lower_bounds`, `upper_bounds` (defined via `lu_bounds`),

4. having a global minimum *value* of `0`.

See [`TEST_FUNCTIONS`](@ref).
"""
abstract type UniformBounds end

lower_bounds(f::UniformBounds, n::Integer) = fill(Float64(lu_bounds(f)[1]), n)

upper_bounds(f::UniformBounds, n::Integer) = fill(Float64(lu_bounds(f)[2]), n)

####
#### shifted quadratic
####

"A test function with global minimum at [a, …]. For sanity checks."
struct ShiftedQuadratic{T <: Real} <: UniformBounds
    a::T
end

(f::ShiftedQuadratic)(x) = (a = f.a; sum(x -> abs2(x - a), x))

minimum_location(f::ShiftedQuadratic, n::Integer) = fill(f.a, n)

lu_bounds(f::ShiftedQuadratic) = (f.a - 50, f.a + 100)

const SHIFTED_QUADRATIC = ShiftedQuadratic(0.5)

####
#### Griewank
####

"""
The Griewank problem.

From Ali, Khompatraporn, and Zabinsy (2005, p 559).
"""
struct Griewank{T <: Real} <: UniformBounds
    a::T
end

function (f::Griewank)(x)
    sum(abs2, x) / f.a - prod(((i, x),) -> cos(x / √i), enumerate(x)) + 1
end

minimum_location(::Griewank, n::Integer) = zeros(n)

lu_bounds(::Griewank) = (-50, 100)

const GRIEWANK = Griewank(200.0)

####
#### Levy Montalvo 2.
####

"""
Levi and Montalvo 2 problem.

From Ali, Khompatraporn, and Zabinsy (2005, p 662).
"""
struct LevyMontalvo2 <: UniformBounds end

function (::LevyMontalvo2)(x)
    xn = last(x)
    0.1 * abs2(sinpi(3 * first(x))) + abs2(xn -  1) * (1 + abs2(sinpi(2 * xn))) +
        sum(@. abs2($(x[1:(end - 1)]) - 1) * (1 + abs2(sinpi(3 * $(x[2:end])))))
end

minimum_location(::LevyMontalvo2, n::Integer) = ones(n)

lu_bounds(::LevyMontalvo2) = (-10, 15)

const LEVY_MONTALVO2 = LevyMontalvo2()

####
#### Rastrigin
####

"""
Rastrigin problem.

From Ali, Khompatraporn, and Zabinsy (2005, p 665).
"""
struct Rastrigin <: UniformBounds end

function (::Rastrigin)(x)
    10 * length(x) + sum(@. abs2(x) - 10 * cospi(2 * x))
end

minimum_location(::Rastrigin, n::Integer) = zeros(n)

lu_bounds(::Rastrigin) = (-5.12, 5.12)

const RASTRIGIN = Rastrigin()

####
#### Rosenbrock
####

"""
Rosenbrock problem.

From Ali, Khompatraporn, and Zabinsy (2005, p 666).
"""
struct Rosenbrock <: UniformBounds end

function (::Rosenbrock)(x)
    x1 = x[1:(end - 1)]
    x2 = x[2:end]
    sum(@. 100 * abs2(x2 - abs2(x1)) + abs2(x1 - 1))
end

minimum_location(::Rosenbrock, n::Integer) = ones(n)

lu_bounds(::Rosenbrock) = (-30, 30)

const ROSENBROCK = Rosenbrock()

####
#### helper code
####

"A tuple of all test functions."
const TEST_FUNCTIONS = (SHIFTED_QUADRATIC, GRIEWANK, LEVY_MONTALVO2, RASTRIGIN, ROSENBROCK)


# idea: use a vector of Nodes, where each Node is a operator and arguments
using DataStructures
import Base: convert, promote_rule, +, -, *, /, >, <, log

immutable RDiff <: Real
    val::Float64
    adj::Float64

    RDiff(val, adj) = new(val, adj)
end
typealias Converts Union{Float64, Int, Irrational, Rational}
RDiff(x::Converts) = RDiff(x, 0.)

immutable Node
    inputs::Vector{RDiff}
    outputs::Vector{RDiff}
    op::Function
    container::Vector{Node}
end

convert(::Type{RDiff}, x::Converts) = RDiff(x)
promote_rule(::Type{Bool}, ::Type{RDiff}) = RDiff
promote_rule(::Type{Float64}, ::Type{RDiff}) = RDiff
promote_rule{T<:Integer}(::Type{T}, ::Type{RDiff}) = RDiff

value(x::RDiff) = x.val
adjoint(x::RDiff) = x.adj
>(x::RDiff, y::RDiff) = x.val > y.val
<(x::RDiff, y::RDiff) = x.val < y.val

function grad(f)
    global varstack = Vector{Node}()

    function ∇f(x)
        y = map(x -> RDiff(x, 0., varstack), x)
        f(y)
        varstack[end].outputs[1].adj = 1.
        for n in varstack
            backprop(n)
        end
        map(value, y)
    end

    return ∇f

end

macro diff_rule(sig, code)
    fn = _fn_of_vals(sig)
    argsyms = Symbol[s.args[1] for s in sig.args[2:end]]

    ex = quote
        function $(sig.args[1])($(sig.args[2:end]...))
            vstack = get_container($(argsyms...))
            this = RDiff($fn, 0.0, vstack)
            function backprop()
                $code
                nothing
            end
            push!(vstack, this)
            this
        end
    end
    esc(ex)
end

_fn_of_vals(x) = x
function _fn_of_vals(ex::Expr)
    if ex.head == :(::)
        out = :(value($(ex.args[1])))
    else
        out = copy(ex)
        for i in eachindex(out.args)
            out.args[i] = _fn_of_vals(out.args[i])
        end
    end
    out
end

function get_container(vars...)
    has_stack = filter(x -> !isnull(x.stack), vars)
    get(first(has_stack).stack)
end

@diff_rule log(x::RDiff) begin
    x.adj += this.adj / x.val
end

@diff_rule +(x::RDiff, y::RDiff) begin
    x.adj += this.adj
    y.adj += this.adj
end

@diff_rule -(x::RDiff, y::RDiff) begin
    x.adj += this.adj
    y.adj += -this.adj
end

@diff_rule *(x::RDiff, y::RDiff) begin
    x.adj += this.adj * y.val
    y.adj += this.adj * x.val
end

@diff_rule /(x::RDiff, y::RDiff) begin
    x.adj += this.adj / y.val
    y.adj += -this.adj * x.val / y.val^2
end

@diff_rule -(x::RDiff) begin
    x.adj += -this.adj
end


function isobj(filepath)
    base, ext = splitext(filepath)
    ext == ".obj" ? true : false
end

"""
    loadobj(shapepath::String; scale=1, message=true) -> nodes, faces
"""
function loadobj(shapepath::String; scale=1, static=true, message=true)

    nodes = Vector{Float64}[]
    faces = Vector{Int64}[]

    open(shapepath, "r") do f
        for line in eachline(f)
            line == "" && continue  # Skip blank lines
            data = split(line)

            if data[1] == "v"
                node = parse.(Float64, data[2:4])  # (x, y, z) coordinates [km]
                push!(nodes, node)
            elseif data[1] == "f"
                face = parse.(Int64, data[2:4])  # indices of three vertices
                push!(faces, face)
            end
        end
    end

    nodes *= scale  # if scale is 1000, converted [km] to [m]

    if static == true
        nodes = [SVector{3,Float64}(node) for node in nodes]
        faces = [SVector{3,Int64}(face) for face in faces]
    end

    if message == true
        println("+-----------------------------+")
        println("|        Load OBJ file        |")
        println("+-----------------------------+")
        println(" Nodes: ", length(nodes))
        println(" Faces: ", length(faces))
    end

    return nodes, faces
end

"""
    saveobj(path::String, nodes, faces)
"""
function saveobj(path::String, nodes, faces)
    open(path, "w") do f
        println(f, "# Generated by obj.jl written by M. Kanamaru")
        println(f, "# Vertices: ", length(nodes))
        println(f, "# Faces:    ", length(faces))
        # println(f)

        for node in nodes
            println(f, "v  ", node[1], "  ", node[2], "  ", node[3])
        end

        println(f)

        for face in faces
            println(f, "f  ", face[1], "  ", face[2], "  ", face[3])
        end
    end
end

"""
Make a perturbed shape model
"""
function perturbnodes!(nodes, noise_weight)

    for node in nodes
        noise = rand()*2 - 1  # random number in [-1, 1]
        noise *= noise_weight

        dr = normalize(node) * noise / 1000  # [km]
        node .+= dr
    end
    nodes
end

perturbnodes(nodes, noise_weight) = perturbnodes!(deepcopy(nodes), noise_weight)

# ****************************************************************

getFaceCenters(nodes, faces) = [getcenter(nodes[face]) for face in faces]
getFaceNormals(nodes, faces) = [getnormal(nodes[face]) for face in faces]
getFaceAreas(nodes, faces) = [getarea(nodes[face]) for face in faces]


# ------------------------------------------------------------------
# Licensed under the ISC License. See LICENSE in the project root.
# ------------------------------------------------------------------

"""
    AbstractNeighborhood

A neighborhood of points in a `N`-dimensional space
with coordinates of type `T`.
"""
abstract type AbstractNeighborhood{T,N} end

"""
    ndims(neigh)

Return the number of dimensions of neighborhood `neigh`.
"""
Base.ndims(::AbstractNeighborhood{T,N}) where {N,T} = N

"""
    coordtype(neigh)

Return the coordinate type of neighborhood `neigh`.
"""
coordtype(::AbstractNeighborhood{T,N}) where {N,T} = T

"""
    isneighbor(neigh, xₒ, x)

Tells whether or not the coordinates `x` are in the neighborhood
`neigh` centered at coordinates `xₒ`.
"""
isneighbor(neigh::AbstractNeighborhood,
           xₒ::AbstractVector, x::AbstractVector) = @error "not implemented"

"""
    volume(neigh)

Return the volume of neighborhood `neigh`.
"""
volume(neigh::AbstractNeighborhood) = @error "not implemented"

#------------------
# IMPLEMENTATIONS
#------------------
include("neighborhoods/ball.jl")
include("neighborhoods/cylinder.jl")


@testset "Properties" begin
    @test iszero(GF256(0))
    @test !iszero(GF256(1))
    @test !iszero(GF256(255))
    @test isone(GF256(1))
    @test !isone(GF256(0))
    @test !isone(GF256(255))
    @test zero(GF256) == GF256(0)
    @test zero(GF256) == zero(GF256(1))
    @test one(GF256) == GF256(1)
    @test one(GF256) == one(GF256(0))
end

@testset "Logarithms and exponentiation" begin
    a = GF256(0)
    @test log(exp(a)) == a
    for x in 1:254
        a = GF256(x)
        @test log(exp(a)) == a
        @test exp(log(a)) == a
    end
    a = GF256(255)
    @test exp(log(a)) == a
end

@testset "Division and multiplication" begin
    β = GF256(43) # chosen arbitrarily
    @test β * zero(GF256) == zero(GF256)
    @test β * one(GF256) == β
    for x in 1:255
        @test (β * GF256(x)) / GF256(x) == β
        @test (β / GF256(x)) * GF256(x) == β
    end
end

@testset "Addition and subtraction" begin
    β = GF256(43) # chosen arbitrarily
    @test β + zero(GF256) == β
    @test β - zero(GF256) == β
    for x in 0:255
        a = GF256(x)
        @test iszero(a+a)
        @test iszero(a-a)
    end
end

# TODO: remove
# @test GF256(2) + GF256(10) - GF256(10) == GF256(2)
# @test GF256(2) * GF256(10) / GF256(10) == GF256(2)
# @test GF256(1) + GF256(2) * GF256(10) - GF256(10) - GF256(10) == GF256(21)
# @test GF256(13) * GF256(1) == GF256(13)
# @test GF256(13) / GF256(1) == GF256(13)
# @test GF256(0) * GF256(1) == GF256(0)
# @test GF256(0) / GF256(1) == GF256(0)
# @test [GF256(i) for i in 0:255].*zero(GF256) == zeros(GF256, 256)
# @test [GF256(i) for i in 0:255].*one(GF256) == [GF256(i) for i in 0:255]
# @test [GF256(i) for i in 0:255].*GF256(2) == [GF256(2)*GF256(i) for i in 0:255]
# # @test GF256(11) + false == GF256(11)
# # @test GF256(11) + true == GF256(11) + GF256(1)
# # @test GF256(11) * false == GF256(0)
# # @test GF256(11) * true == GF256(11)
# # @test GF256(11) / true == GF256(11)
# @test GF256(123)^0 == GF256(1)
# @test GF256(123)^1 == GF256(123)
# @test GF256(123)^2 == GF256(123) * GF256(123)
# @test GF256(46)^3 == GF256(46) * GF256(46) * GF256(46)
# @test GF256(10)^20 == reduce(*, (GF256(10) for _ in 1:20))
# @test GF256(56)^30 == reduce(*, (GF256(56) for _ in 1:30))


###########################################################################
#
#  Robust adaptive Metropolis (RAM)
#
#  Reference: Vihola M. Robust Adaptive Metropolis Algorithm with Coerced Acceptance Rate. Statistics and Computing,
#  2012, 22 (5), pp 997-1008.
#
#  Parameters:
#    -scale: for scaling the jumps
#    -rate: target acceptance rate
#
###########################################################################

export RAM

println("Loading RAM(scale, rate) sampler")

###########################################################################
#                  RAM type
###########################################################################

immutable RAM <: MCMCSampler
  scale::Float64
  rate::Float64

  function RAM(x::Float64, r::Float64)
    @assert x > 0 "scale should be > 0"
    @assert r > 0. && r < 1. "target acceptance rate ($r) should be between 0 and 1"
    new(x, r)
  end
end
RAM() = RAM(1., 0.234)
RAM(x::Float64) = RAM(x, 0.234)
RAM(;scale::Float64=1.0, rate::Float64=0.234) = RAM(scale, rate)

###########################################################################
#                  RAM task
###########################################################################

# RAM sampling
function SamplerTask(model::MCMCModel, sampler::RAM, runner::MCMCRunner)
	local pars, proposedPars
	local logTarget, proposedLogTarget
  local scale

	# hook inside Task to allow remote resetting
	task_local_storage(:reset, (resetPars::Vector{Float64}) -> (pars = copy(resetPars); logTarget = model.eval(pars)))

	# initialization
	scale = model.scale .* sampler.scale  # rescale model scale by sampler scale
	pars = copy(model.init)
	logTarget = model.eval(pars)
  @assert isfinite(logTarget) "Initial values out of model support, try other values"

	S = Float64[ i==j ? scale[i] : 0. for i in 1:model.size, j in 1:model.size]

	# main loop
	for i in 1:Inf
		rvec = randn(model.size)
		proposedPars = pars + S * rvec
		proposedLogTarget = model.eval(proposedPars)

	  ratio = proposedLogTarget-logTarget
		if ratio > 0 || (ratio > log(rand()))
			ms = MCMCSample(proposedPars, proposedLogTarget, pars, logTarget, {"accept" => true, "scale" => trace(S)})
      produce(ms)
      pars, logTarget = copy(proposedPars), copy(proposedLogTarget)
    else
			ms = MCMCSample(pars, logTarget, pars, logTarget, {"accept" => false, "scale" => trace(S)})
      produce(ms)
    end

    # scale tuning
		eta = min(1, model.size * i^(-2/3))  # decreasing influence with i

		SS = (rvec * rvec') / dot(rvec, rvec) * eta * (min(1, exp(ratio)) - sampler.rate)
		SS = S * (eye(model.size) + SS) * S'
		S = chol(SS)'  #'
  end
end


using Distributed
using LinearAlgebra

BLAS.set_num_threads(1)

# Apply a sum of distributed matrices and reduce
# after applying to a vector.

# Number of matrices in our sum.
nmats = 2

# Specify the number of processors Julia will use.
# For now we just say it is the same as the number of
# matrices in our sum.
addprocs(nmats)

# Size of out matrices.
@everywhere n = 4

# Set of matrices in our sum, each created on a
# separate worker.
As = [@spawnat(workers()[nmat], randn(n, n)) for nmat in 1:nmats]

# Vector we will apply to our matrices, distributed to
# every worker.
@everywhere v = randn(n)

# Distributed matrix-vector multiplication for each distributed
# matrix in our sum, then reduced over with `+`.
Av = @distributed (+) for n in 1:nmats
  # Need to call `fetch` to instantiate the matrix
  # on the worker where it was defined above.
  fetch(As[n]) * v
end

# Test by fetching and summing locally
A = sum(fetch.(As))
@show norm(Av - A * v)


using Test
import DPFEHM

ns = [5, 10, 20]
mins = [0, 0, 0]
maxs = [1, 1, 1]

#test that the 2d grid first goes over the the y coordinate, then the x coordinate
coords, neighbors, areasoverlengths, volumes = DPFEHM.regulargrid2d(mins[1:2], maxs[1:2], ns[1:2], maxs[3] - mins[3])
@test coords[2, 1] != coords[2, 2]
@test coords[2, 1] == coords[2, 1 + ns[2]]
@test coords[1, 1] != coords[1, 1 + ns[2]]
@test all(coords[1, 1] .== coords[1, 1:ns[2]])
@test sum(volumes) ≈ prod(ns[1:2]) * prod((maxs[1:2] .- mins[1:2]) ./ (ns[1:2] .- 1)) * (maxs[3] - mins[3])#it goes a little higher than just prod(maxs - mins), because the center of the boundary cells are on the boundary, and half of those cells are outside the boundary

#test that the 3d grid first goes over the z coordinate, then the y coordinate, then the x coordinate
coords, neighbors, areasoverlengths, volumes = DPFEHM.regulargrid3d(mins, maxs, ns)
@test coords[3, 1] != coords[3, 2]
@test coords[3, 1] == coords[3, 1 + ns[3]]
@test coords[2, 1] != coords[2, 1 + ns[3]]
@test all(coords[2, 1] .== coords[2, 1:ns[3]])
@test coords[1, 1] != coords[1, 1 + ns[3] * ns[2]]
@test all(coords[1, 1] .== coords[1, 1:ns[2] * ns[3]])
@test sum(volumes) ≈ prod(ns) * prod((maxs .- mins) ./ (ns .- 1))#it goes a little higher than just prod(maxs - mins), because the center of the boundary cells are on the boundary, and half of those cells are outside the boundary

#test that it is computing the darcy velocity correctly for a constant pressure gradient along each of the coordinate axes
Ks = ones(reverse(ns)...)
h = [x for z = 1:ns[3], y = 1:ns[2], x = 1:ns[1]]
dv = DPFEHM.darcy_velocity(h, Ks, mins, maxs, ns)
@test dv(0.5, 0.5, 0.5) ≈ [ns[1] - 1, 0, 0]
h = [y for z = 1:ns[3], y = 1:ns[2], x = 1:ns[1]]
dv = DPFEHM.darcy_velocity(h, Ks, mins, maxs, ns)
@test dv(0.5, 0.5, 0.5) ≈ [0, ns[2] - 1, 0]
h = [z for z = 1:ns[3], y = 1:ns[2], x = 1:ns[1]]
dv = DPFEHM.darcy_velocity(h, Ks, mins, maxs, ns)
@test dv(0.5, 0.5, 0.5) ≈ [0, 0, ns[3] - 1]


mutable struct Bee{T}
    sol::T
    fit::Float64
    t::Int
end

Bees = Array{Bee, 1}

Bee(solution) = Bee(solution, fit(solution.f), 0)

"""
    get_position(bee)

Get the position vector of a bee when optimize using ABC algorithm.
"""
get_position(bee::Bee) = bee.sol.x

"""
    fval(solution)

Get the fitness of a bee when optimize using ABC algorithm.
"""
fval(bee::Bee) = bee.sol.f

Base.show(io::IO, ::MIME"text/html", pop::Vector{Bee{xf_indiv}}) = show(io, "text/html", [p.sol for p in pop])

minimum(st::State{Bee{xf_indiv}}) = st.best_sol.sol.f
minimum(st::State{Bee{xfgh_indiv}}) = st.best_sol.sol.f
minimizer(st::State{Bee{xf_indiv}}) = st.best_sol.sol.x
minimizer(st::State{Bee{xfgh_indiv}}) = st.best_sol.sol.x

@inline function updateFit!(bee::Bee)
    bee.fit = fit(bee.sol.f)
end

@inline function updateFit!(bees::Bees)
    for bee in bees
        updateFit!(bee)
    end
end

function fit(fx)
    if fx >= 0.0
        return 1.0/(1.0+fx)
    end

    1.0 + abs(fx)
end

function updateBee!(bee, bee2, problem)
    D = length(bee.sol.x)
    ϕ = -1.0 + 2.0rand()

    v = ϕ*(bee.sol.x - bee2.sol.x)

    x_new = bee.sol.x + v
    replace_with_random_in_bounds!(x_new, problem.bounds)

    new_sol = create_solution(x_new, problem)

    if is_better(new_sol, bee.sol) #fx < bee.sol.f
        bee.sol = new_sol
        bee.fit = fit(new_sol.f)
        bee.t = 0
    else
        bee.t += 1
    end
end

function getK(i, N)
    j = rand(1:N)
    while j == i
        j = rand(1:N)
    end
    j
end

function employedPhase!(bees, problem, Ne)
    N = length(bees)
    for i in randperm(N)[1:Ne]
        updateBee!(bees[i], bees[getK(i, N)], problem)
    end
end

function roulettSelect(bees, sum_f)
    r = rand()
    rs = 0.0
    for i in 1:length(bees)
        rs += bees[i].fit / sum_f
        if r <= rs
            return i
        end
    end

    return length(bees)
end

function outlookerPhase!(bees, problem, No::Int)
    N = length(bees)
    sum_f = sum(map(x->x.fit, bees))

    for i=1:No
        j = roulettSelect(bees, sum_f)
        updateBee!(bees[j], bees[getK(j, N)], problem)
    end
end

function scoutPhase!(bees, problem, genPos::Function, limit::Int)
    bees_scout = filter(x->x.t >= limit, bees)
    for i in 1:length(bees_scout)
        bees_scout[i].sol = create_solution(genPos(), problem)
        bees_scout[i].t = 0
    end

    return length(bees_scout)
end

function getBestBee(bees)
    best = bees[1]
    for bee in bees
        if bee.sol.f < best.sol.f
            best = bee
        end
    end

    return best
end

function chooseBest(bees, best)
    bee_cand = getBestBee(bees)
    if bee_cand.sol.f < best.sol.f
        return deepcopy(bee_cand)
    end

    return best
end

function initialbees(N, problem)
     P = generate_population(N, problem)

     return [ Bee(sol) for sol in P ]
end


function main(x, y, dir, moves)
    found_place = false
    known_places = Set([[x, y]])

    for move in moves
        turn = move[1]
        steps = parse(Int32, move[2:end])

        if     dir == 'N' && turn == 'L'
            dir = 'W'
        elseif dir == 'N' && turn == 'R'
            dir = 'E'
        elseif dir == 'W' && turn == 'L'
            dir = 'S'
        elseif dir == 'W' && turn == 'R'
            dir = 'N'
        elseif dir == 'S' && turn == 'L'
            dir = 'E'
        elseif dir == 'S' && turn == 'R'
            dir = 'W'
        elseif dir == 'E' && turn == 'L'
            dir = 'N'
        elseif dir == 'E' && turn == 'R'
            dir = 'S'
        end

        for _ in 1:steps
            if     (dir == 'N')
                y += 1
            elseif (dir == 'W')
                x -= 1
            elseif (dir == 'S')
                y -= 1
            elseif (dir == 'E')
                x += 1
            end

            current_place = [x, y]
            if !found_place && current_place in known_places
                found_place = true
                println("qb: ", abs(x) + abs(y))
            end

            known_places = union(known_places, Set([current_place]))
        end
    end

    println("qa: ", abs(x) + abs(y))
end

content = strip(read(stdin, String))
moves = split(content, ", ")
main(0, 0, 'N', moves)


"""
    Models for random hypergraphs
        * random model
        * k-uniform model
        * d-uniform model
        * preferential-attachment
"""

"""
    randomH(nVertices, nEdges)

Generate a *random* hypergraph without any structural constraints.

**The Algorithm**
Given two integer parameters *nVertices* and *nEdges* (the number of nodes and hyperedges, respectively),
the algorithm computes - for each hyperedge *he={1,...,m}* -
a random number *s ϵ [1, n]* (i.e. the hyperedge size).
Then, the algorithm selects uniformly at random *s* vertices from *V* to be added in *he*.
"""
function randomH(nVertices, nEdges)
    mx = Matrix{Union{Nothing,Bool}}(nothing, nVertices,nEdges)
    for e in 1:size(mx,2)
        nv = rand(1:size(mx,1))
        mx[sample(1:size(mx,1), nv;replace=false), e] .= true
    end

    h = Hypergraph(mx)
    if all(length.(h.v2he) .> 0)
        return h
    else
        return randomH(nVertices, nEdges)
    end
end

"""
    randomHkuniform(nVertices, nEdges, k)

Generates a *k*-uniform hypergraph, i.e. an hypergraph where each hyperedge has size *k*.

**The Algorithm**
The algorithm proceeds as the *randomH*, forcing the size of each hyperedge equal to *k*.
"""
function randomHkuniform(nVertices, nEdges, k)
    mx = Matrix{Union{Nothing,Bool}}(nothing, nVertices,nEdges)
    for e in 1:size(mx,2)
        nv = k#rand(1:size(mx,1))#rand(2:5)
        mx[sample(1:size(mx,1), nv;replace=false), e] .= true
    end

    h = Hypergraph(mx)
    if all(length.(h.v2he) .> 0)
        return h
    else
        return randomH(nVertices, nEdges)
    end
end

"""
    randomHduniform(nVertices, nEdges, d)

Generates a *d*-uniform hypergraph, where each node has degree *d*.

**The Algorithm**
The algorithm exploits the *k*-uniform approach described for the *randomHkuniform* method
to build a *d*-uniform hypergraph *H* having *nVertices* nodes and *nEdges* edges.
It returns the hypergraph H⃰ dual of *H*.
"""
function randomHduniform(nVertices, nEdges, d)
    mx = Matrix{Union{Nothing,Bool}}(nothing, nVertices,nEdges)

    for v in 1:size(mx,1)
        ne = d
        mx[v, sample(1:size(mx,2), ne;replace=false)] .= true
    end

    h = Hypergraph(mx)
    if all(length.(h.v2he) .> 0)
        return h
    else
        return randomH(nVertices, nEdges)
    end
end

"""
    randomHpreferential(nVertices, p)

Generate a hypergraph with a preferential attachment rule between nodes, as presented in
*Avin, C., Lotker, Z., and Peleg, D.Random preferential attachment hyper-graphs.Computer Science 23(2015).*

**The Algorithm**
The algorithm starts with a random graph with 5 nodes and 5 edges.
It iteratively adds a node or a edge, according to a given parameter *p*,
which defines the probability of creating a new node or a new hyperedge.

More in detail, the connections with the new node/hyperedge are generated according to
a preferential attachment policy.
"""
function randomHpreferential(nVertices, p)
    H₀ = randomH(10,10)
    H = H₀
    while nhv(H) < nVertices
        r = rand()
        y = rand(1:nhv(H))
        Y = Dict{Int,Bool}()
        if r < p
            #add a vertex
            v = SimpleHypergraphs.add_vertex!(H)
            push!(Y,v=>true)
            for v in nextNodes(H,y-1)
                push!(Y,v)
            end
        else
            for v in nextNodes(H,y)
                push!(Y,v)
            end
        end
            #add a hyperedge
            SimpleHypergraphs.add_hyperedge!(H, vertices=Y)
    end
    H
end

function nextNodes(h,size)
    nodes = Dict{Int, Bool}()

    ids = collect(1:nhv(h))
    degrees = length.(h.v2he)

    for s=1:size
        psum = collect(1:length(ids))

        psum[1] = degrees[ids[1]]
        for j=2:length(ids)
            psum[j] = psum[j-1] + degrees[ids[j]]
        end

        number = rand(1:psum[length(psum)])
        bucket = -1
        index=1
        for i=1:length(psum)
            if number <= psum[i]
                bucket = ids[i]
                index = i
                break
            end
        end

        push!(nodes, bucket=>true)
        deleteat!(ids, index)
    end
    nodes
end


@testset "relaxation_bilinear tests" begin
    LOpt.silence()

    m = JuMP.Model(glpk_optimizer)

    LB = [-1, -2.5, 0, -3, 0]
    UB = [2.5, 0, 1, 3.2, 1]
    JuMP.@variable(m, LB[i] <= x[i=1:5] <= UB[i])
    JuMP.@variable(m, z[1:6])

    LOpt.relaxation_bilinear(m, z[1], x[1], x[2])
    LOpt.relaxation_bilinear(m, z[2], x[1], x[4])
    LOpt.relaxation_bilinear(m, z[3], x[2], x[3])
    LOpt.relaxation_bilinear(m, z[4], x[3], x[4])
    LOpt.relaxation_bilinear(m, z[5], x[3], x[5])
    LOpt.relaxation_bilinear(m, z[6], x[4], x[5])

    JuMP.@constraint(m, sum(x) >= 3.3)
    JuMP.@constraint(m, sum(x) <= 3.8)

    coefficients =  [0.4852, -0.7795, 0.1788, 0.7778, 0.3418, -0.3407]
    JuMP.set_objective_function(m, sum(coefficients[i] * z[i] for i=1:length(z)))

    obj_vals = Vector{Float64}()

    for obj_sense in [MOI.MIN_SENSE, MOI.MAX_SENSE]
        JuMP.set_objective_sense(m, obj_sense)
        JuMP.optimize!(m)
        @test JuMP.termination_status(m) == MOI.OPTIMAL
        push!(obj_vals, JuMP.objective_value(m))
    end

    @test obj_vals[1] <= -9.922644 # global optimum value
    @test obj_vals[2] >=  4.908516 # global optimum value

end


# # Univariate Gaussian Process

# This package implements a univariate Bayesian Optimization with the Gaussian Process used as the surrogate model
# and visualisation of the optimization process using Plots.

# This package could be a great tool to visualize Gaussian processes to get a better understanding of them
# and/or to experiment with Bayesian optimization with different acquisition functions, covariance functions
# and other hyperparameters.

# ## Bayesian Optimization - brief introduction

# Bayesian optimization is an optimization technique which cleverly selects evaluation points
# from the objective function domain to optimize it while minimizing the number of its evaluations.

# The main advantages of Bayesian optimization is that it can optimize black-box
# and it is effective for optimizing functions which are computationally expensive to evaluate.

# Bayesian optimization works as follows:
# 1. Select the next evaluation point by maximizing the acquisition function.
# 2. Evaluate the objective function at the selected point.
# 3. Augment the data with the new evaluation.
# 4. Update the surrogate model based on the data.
# 5. Repeat.

# Terms:
# - **objective function** - The to-be-optimized function. Can be blackbox, usually is expensive to evaluate.
# - **acquisition function** - A function which to each point of the domain assigns a fitness
#                           for being the point of the next evaluation.
# - **data** - Contains the information from all previous objective function evaluations.
# - **surrogate model** - A probabilistic model estimating the objective function.

# Despite its name, Bayesian optimization can also be used to approximate functions
# instead of optimizing them by using a different acquisition functions.

# ## Gaussian Process - brief introduction

# Gaussian process can be used as the surrogate model for the Bayesian optimization.

# Gaussian process represents the estimated value of the objective function at each point of the domain
# as a one-dimensional normal distribution. This means that the approximation of the whole objective function
# (assuming its continuous) is an infinite-dimensional normal distribution over all possible forms
# the objective function could have.

# ## Example usage

# Let's assume that we wish to maximize a black-box objective function. That means that we can query
# the objective function for values at any point of its domain but we do not know its analytical form.
# Furthermore, let's assume that we are limited in the number of evaluations we can perform because this
# unknow function takes a very long time to evaluate. Let's say we can only perform 10 evaluations.

# Let's define this function and the bounds within which we want maximize it.
# Furthermore, let's assume that we are limited to 5 evaluations.

obj_func(x::Real) = sin(x)

bounds = (-π, π);
evals = 10;

# Here, the objective function is simply sinus, but pretend that we do not know that.

# Now let's maximize our objective function using the UnivariateGaussianProcess package.

# First load the package.

using Revise
using UnivariateGaussianProcess

# Now we have to choose the parameters of the Bayesian optimization
# as well as the Gaussian process surrogate model.

cov_func = matern3;
acq_func = (x, data) -> 3 * ei(x, data) + 1/2;

# We will use the `matern1` covariance function for the Gaussian process
# and the `expected improvement` acquisition function for the Bayesian optimization.

# (Note that the coeficients in the acquisition function definition have no effect on the optimization process.
# They are there just to make the generated plots more readable.)

# Now we can run the algorithm.

data = gaussian_process(obj_func, bounds, cov_func, acq_func; evals, plot_only_final=false);

# You can see the optimization process on the plots.
# (You can set the keyword argument `plot_only_final` to true to reduce the output length.)

# To get the best function value found so far by the algorithm use the following function.

GP_res = get_max(data)[2]

# ## Comparison to BlackBoxOptim

# Now let's try to maximize our objective function using the BlackBoxOptim package
# which can optimize black-box function but is not specialized to minimize the number of evaluations.

# We will perform 10 runs as this optimization algorithm is stochastic.

using BlackBoxOptim
using Random
using Statistics
Random.seed!(555)

obj_func(x::AbstractArray{<:Real}) = - obj_func(first(x))

fitness = Float64[]
for i in 1:10
    res = bboptimize(obj_func; NumDimensions=1, SearchRange=bounds, MaxFuncEvals=evals, TraceMode=:silent);
    append!(fitness, - best_fitness(res))
end

# Now let's see the average result of the BlackBoxOptim package.

BBO_res = mean(fitness)

# We can see that the average result is significantly worse than the result of the Bayseian optimization as expected.

# This of course does not prove that the Bayesian optimization performs better, but it is a simple example
# showing that Bayesian optimization can work well with only few objective function evaluations.