const PATH_TO_MINIZINC = "C:/Program Files/MiniZinc/minizinc.exe"

function gendata(kx, ky, n, percentile_cutoff, rho=0)
    X = rand(1:kx, n)
    Y = rand(1:kx, n)
    Y = ifelse.(rand(Uniform(0,1), length(X)) .< rho, X, Y)
    A = Matrix(freqtable(X,Y))
    threshold = floor.(Int, percentile(vec(A),percentile_cutoff))
    independent_A = sum(A, dims=2) * sum(A, dims=1) / sum(A)
    observed_A = @. ifelse(A > threshold, A, missing)
    (obs=observed_A, gen=A, threshold=threshold, ind = independent_A)
end

function listoptions(data)
    locs = findall(ismissing, data.obs)
    open("listallsolutions.mzn", "w") do io
        for loc in locs
            println(io, "var 0..$(data.threshold): a$(loc[1])_$(loc[2]);")
        end
        for i in axes(data.obs, 1)
            print(io, "constraint ")
            l = []
            for j in axes(data.obs, 2)
                if ismissing(data.obs[i,j])
                    push!(l, "a$(i)_$j")
                else
                    push!(l, data.obs[i, j])
                end
            end
            println(io, join(l, "+"), "=", sum(data.gen[i, :]),";")
        end
        for j in axes(data.obs, 2)
            print(io, "constraint ")
            l = []
            for i in axes(data.obs, 1)
                if ismissing(data.obs[i,j])
                    push!(l, "a$(i)_$j")
                else
                    push!(l, data.obs[i, j])
                end
            end
            println(io, join(l, "+"), "=", sum(data.gen[:, j]),";")
        end
        println(io, "solve satisfy;")
        print(io, "output [")
        print(io, join(["show(a$(loc[1])_$(loc[2]))" for loc in locs], ",\" \","))
        println(io, "];")
    end
    df = DataFrame([Int[] for _ in 1:length(locs)],
         [Symbol("a", loc[1], "_", loc[2]) for loc in locs])
    res = read(`$PATH_TO_MINIZINC -a listallsolutions.mzn`, String)
    res_lines = split(res, "\n")
    for i in 1:2:length(res_lines)-2
        push!(df, parse.(Int, split(res_lines[i])))
    end
    return df
end

function compare(data, df)
    agg_df = by(stack(df, :), :variable, :value => mean)
    #closest_idx = argmin(sum(abs.(transpose(convert(Array, df)) .- agg_df.value_mean), dims = 2))[1]
    #agg_df.exact_case = vec(convert(Array,df[closest_idx,:]))
    #println("Closest to the mean is $closest_idx")
    agg_df.actual = [data.gen[parse.(Int, split(String(idx)[2:end], "_"))...] for idx in agg_df.variable]
    return agg_df
end

mse_compare(agg_df) = mean(@. (agg_df.value_mean - agg_df.actual) ^ 2)
mae_compare(agg_df) = mean(@. abs(agg_df.value_mean - agg_df.actual))

function method_exact(k, n, percentile_cutoff,rho; verbose=true)
    verbose && @info "generating data"
    data = gendata(k, k, n, percentile_cutoff,rho)
    verbose && @info "data has size $(size(data.gen)) with $(count(ismissing, data.obs)) missings"
    df = listoptions(data)
    verbose && @info "possible assignments: $(nrow(df))"
    agg_df = compare(data, df)
    agg_df.difference = agg_df.actual - agg_df.value_mean
    mae = mae_compare(agg_df)
    verbose && @info "mae: $mae"
    return (data=data, blanks=count(ismissing, data.obs), options=df, mae=mae, flatten_df = agg_df, vc = cramer_v(data.gen), fit = agg_df.value_mean)
end


@testset "Built-in predicates" begin

# Test handling of and/N and or/N
clauses = @julog [
    shocked(P) <<= and(surprised(P), upset(P)),
    upset(P) <<= or(unhappy(P), angry(P)),
]

# Avery is surprised. Are they shocked? (No.)
facts = @julog [surprised(avery) <<= true]
@test resolve(@julog(shocked(avery)), [facts; clauses])[1] == false
# Bailey is surprised and angry. Are they shocked? (Yes.)
facts = @julog [surprised(bailey) <<= true, angry(bailey) <<= true]
@test resolve(@julog(shocked(bailey)), [facts; clauses])[1] == true
# Casey is unhappy. Is anyone upset? (Yes, Casey.)
facts = @julog [unhappy(casey) <<= true]
@test @varsub({P => casey}) in resolve(@julog(upset(P)), [facts; clauses])[2]

# Test handling of unifies/2 and not/1
clauses = @julog [
    child(zeus, kronos) <<= true,
    child(hera, kronos) <<= true,
    sibling(A, B) <<= child(A, C) & child(B, C) & not(unifies(A, B)),
]

# Is Hera her own sibling? (No)
@test resolve(@julog(sibling(hera, hera)), clauses)[1] == false
# Is Hera the sibling of Zeus? (Yes)
@test resolve(@julog(sibling(hera, zeus)), clauses)[1] == true

# Test the is/2 predicate (see also math_ops.jl for more tests)
clauses = @julog [
    square(X, Y) <<= is(Y, X * X),
    cube(X, Y) <<= is(Y, X * X * X)
]

# Is the cube of -1 the negation of its square? (Yes)
@test resolve(@julog([square(-1, S), cube(-1, C), S == -C]), clauses)[1] == true
# Is the cube of 2 twice its square? (Yes)
@test resolve(@julog([square(2, S), cube(2, C), C == 2*S]), clauses)[1] == true

# Test negation and double negation
clauses = @julog [red(roses) <<= true, blue(violets) <<= true]
@test resolve(@julog(not(red(roses))), clauses)[1] == false
@test resolve(@julog(not(blue(violets))), clauses)[1] == false
@test resolve(@julog(not(not(red(roses)))), clauses)[1] == true
@test resolve(@julog(not(not(blue(violets)))), clauses)[1] == true

# Test exists/2, forall/2, implies/2
clauses = @julog [
    human(pythagoras) <<= true,
    human(pyrrho) <<= true,
    human(zeno) <<= true,
    human(epicurus) <<= true,
    human(aristotle) <<= true,
    human(plato) <<= true,
    god(hera) <<= true,
    god(zeus) <<= true,
    god(aphrodite) <<= true,
    god(ares) <<= true,
    person(X) <<= human(X),
    person(X) <<= god(X),
    mortal(X) <<= human(X),
    immortal(X) <<= god(X)
]

# Is there a human who is mortal? (Yes.)
@test resolve(@julog(exists(human(X), mortal(X))), clauses)[1] == true
# Is there a god who is immortal? (Yes.)
@test resolve(@julog(exists(god(X), immortal(X))), clauses)[1] == true
# Is there a person who is mortal? (Yes.)
@test resolve(@julog(exists(person(X), mortal(X))), clauses)[1] == true

# Are all persons mortal? (No.)
@test resolve(@julog(forall(person(X), mortal(X))), clauses)[1] == false
# Are all humans mortal? (Yes.)
@test resolve(@julog(forall(human(X), mortal(X))), clauses)[1] == true
# Are all gods immortal? (Yes.)
@test resolve(@julog(forall(god(X), immortal(X))), clauses)[1] == true

# Is it true that if Hera is mortal, she's human? (Yes, since she's not mortal.)
@test resolve(@julog(imply(mortal(hera), human(hera))), clauses)[1] == true
# Is it true that if Hera is immortal, she's human? (No.)
@test resolve(@julog(imply(immortal(hera), human(hera))), clauses)[1] == false
# Is it true that for all people, being a god implies immortality? (Yes.)
@test resolve(@julog(forall(person(X), god(X) => immortal(X))), clauses)[1] == true
# Is it true that for all people, being a person implies immortality? (No.)
@test resolve(@julog(forall(person(X), person(X) => mortal(X))), clauses)[1] == false

# Of those who are persons, which are immortal?
sat, subst = resolve(@julog(person(X) => immortal(X)), clauses)
ans = Set([@varsub({X => hera}), @varsub({X => zeus}),
           @varsub({X => aphrodite}), @varsub({X => ares})])
@test ans == Set(subst)

# Test findall/3 and countall/2
humans = @julog list[pythagoras, pyrrho, zeno, epicurus, aristotle, plato]
@test resolve(@julog(findall(X, human(X), :humans)), clauses)[1] == true
@test resolve(@julog(countall(human(X), 6)), clauses)[1] == true
gods = @julog list[hera, zeus, aphrodite, ares]
@test resolve(@julog(findall(X, god(X), :gods)), clauses)[1] == true
@test resolve(@julog(countall(god(X), 4)), clauses)[1] == true
persons = @julog list[pythagoras, pyrrho, zeno, epicurus, aristotle, plato,
                      hera, zeus, aphrodite, ares]
@test resolve(@julog(findall(X, person(X), :persons)), clauses)[1] == true
@test resolve(@julog(countall(person(X), 10)), clauses)[1] == true

# Test cut and fail by preventing infinite loops
clauses = @julog [
    fakeloop1(A) <<= fakeloop1(B),
    fakeloop1(A) <<= cut,
    fakeloop2(A) <<= fail & fakeloop2(B),
]

@test resolve(@julog(fakeloop1(0)), clauses)[1] == true
@test resolve(@julog(fakeloop2(0)), clauses)[1] == false

# Test the meta-call predicate call/N
clauses = @julog [
    test(x, y) <<= true,
    pred(test) <<= true,
    metatest1(A, B, C) <<= call(A, B, C),
    metatest2(A, B, C) <<= pred(A) & call(A, B, C)
]

@test_throws Exception resolve(@julog(call(P, A, B)), clauses)
@test @varsub({A => x, B => y}) in resolve(@julog(call(test, A, B)), clauses)[2]
@test @varsub({A => x, B => y}) in resolve(@julog(call(test(A), B)), clauses)[2]
@test @varsub({B => y}) in resolve(@julog(call(test(x), B)), clauses)[2]
@test @varsub({A => x}) in resolve(@julog(call(test(A), y)), clauses)[2]

@test_throws Exception resolve(@julog(metatest1(P, x, y)), clauses)
@test @varsub({B => y}) in resolve(@julog(metatest1(test, x, B)), clauses)[2]
@test @varsub({P => test}) in resolve(@julog(metatest2(P, x, y)), clauses)[2]
@test @varsub({B => y}) in resolve(@julog(metatest2(test, x, B)), clauses)[2]

end


using SparseQuadratureGrids
using Base.Test

# write your own tests here

Base.var(::Type{GenzKeister}) = 1/2
Base.var(::Type{KronrodPatterson}) = 1/3

p = 40, n = 1000
Am = randn(p, n);
Bsv = [StaticArrays.SVector{p}(randn(p)) for i ∈ 1:n];
Ab = SparseQuadratureGrids.MatrixVecSVec(Am, Val{p});
Bb = SparseQuadratureGrids.MatrixVecSVec(Bsv);

function testSplitWeights(g::SplitWeights{p,q}, ec::Int) where {p,q}
  @testset begin
    @test sum(g.weight_sum) ≈ 1.0
    @test g.total_count == ec
    @test minimum(g.nodes_positive) < minimum(g.nodes_negative)
    @test maximum(g.nodes_positive) > maximum(g.nodes_negative)
    @test all(isapprox.(g.nodes_positive * g.weights_positive, 0.0, atol = 1e-14))
    @test all(isapprox.(g.nodes_negative * g.weights_negative, 0.0, atol = 1e-14))
    @test all(isapprox.(g.nodes_positive .^ 2 * g.weights_positive .+ g.nodes_negative .^ 2 * g.weights_negative, var(q)))
  end
end

dsgk = [1, 3, 3, 9, 9, 9, 9, 19, 19, 19, 19, 19, 19, 19, 19, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 103]
dskp = [1, 3, 3, 7, 7, 7, 15, 15, 15, 15, 15, 15, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 63]
testSplitWeights(SplitWeights(10,6,GenzKeister), 115645)
testSplitWeights(SplitWeights(4,length(dsgk),GenzKeister,dsgk), 253649)

testSplitWeights(SplitWeights(8,6,KronrodPatterson), 31745)
testSplitWeights(SplitWeights(5,length(dskp),KronrodPatterson,dskp), 1297727)


fact(n) = prod(one(n):n)


using CausalityTools,StatsBase, DelayEmbeddings, LinearAlgebra, Statistics
import DelayEmbeddings.Neighborhood: Theiler, bulkisearch, NeighborNumber

export crossmap

"""
    crossmap(x, y, d, τ; r = 0, correspondence_measure = Statistics.cor) → Float64
    crossmap(x, y, d, τ, bootstrap_method::Symbol; r = 0, correspondence_measure = Statistics.cor,
        method = :segment, L = ceil(Int, (length(x)-d*τ)*0.2), nreps = 100) → Vector{Float64}

Compute the cross mapping [^Sugihara2012] between `x` and `y`, which is the correspondence (computed using
`correspondence measure`) between the values ``y(t)`` and the cross-map estimated values ``ỹ(t) | M_x``.
Returns the correspondence between original and cross mapped values (the default is
`ρ = correspondence_measure(y(t), ỹ(t) | M_x)`).

Here, ``y(t)`` are the raw values of the time series `y`, and ``ỹ(t)`` are the predicted values
computed from the out-of-sample embedding ``M_X`` constructed from the time series `x` with
embedding dimension `d` and embedding lag `τ`.

The Theiler window `r` indicates how many temporal neighbors of the predictee is to be excluded
during the nearest neighbors search (the default `r = 0` excludes only the predictee itself, while
`r = 2` excludes the point itself plus its two nearest neighbors in time).

If `bootstrap_method` is specified, then `nreps` different bootstrapped estimates of
`correspondence_measure(y(t), ỹ(t) | M_x)` are returned. The following bootstrap methods are available:

- `bootstrap_method = :random` selects training sets of length `L` consisting of randomly selected
    points from the embedding ``M_x``  (time ordering does not matter). This is method 3 from Luo
    et al. (2015)[^Luo2015], which critiqued the original Sugihara et al. methodology.
- `bootstrap_method = :segment` selects training sets consisting of time-contiguous segments
    (each of lenght `L`) of embedding vectors in ``M_x`` (time ordering matters). This is
    method 2 from Luo et al. (2015)[^Luo2015].

[^Sugihara2012]: Sugihara, George, et al. "Detecting causality in complex ecosystems." Science (2012): 1227079.[http://science.sciencemag.org/content/early/2012/09/19/science.1227079](http://science.sciencemag.org/content/early/2012/09/19/science.1227079)
[^Luo2015]: "Questionable causality: Cosmic rays to temperature." Proceedings of the National Academy of Sciences Aug 2015, 112 (34) E4638-E4639; DOI: 10.1073/pnas.1510571112 Ming Luo, Holger Kantz, Ngar-Cheung Lau, Wenwen Huang, Yu Zhou
"""
function crossmap(x, y, d, τ; correspondence_measure = Statistics.cor, r = 0)
    # Embed `x`
    Mₓ = crossmapembed(x, d, τ, CCMEmbedding())

    # Compute `ỹ | Mₓ` and return `correspondence_measure(ỹ | Mₓ, y).
    return crossmap_basic(Mₓ, y, d, τ, correspondence_measure = correspondence_measure, r = r)
end

function crossmap(x, y, d, τ, bootstrap_method::Symbol;
        L = ceil(Int, (length(x) - d * τ) * 0.2), nreps = 100,
        r = 0, correspondence_measure = Statistics.cor)

    # Embed `x`
    @assert length(x) == length(y)
    @assert length(x) - d * τ > 0
    Mₓ = crossmapembed(x, d, τ, CCMEmbedding());

    # Compute `ỹ | Mₓ` and return `nreps` values for `correspondence_measure(ỹ | Mₓ, y),
    # each value being computed on a random library (sequential or not) consisting of `L` points.
    return crossmap_bootstrap(Mₓ, y, d, τ, bootstrap_method; L = L, nreps = nreps, r = r,
        correspondence_measure = correspondence_measure)
end


using ModelingToolkit, OrdinaryDiffEq, Test

@parameters t σ ρ β
@variables x(t) y(t) z(t) F(t) u(t)
D = Differential(t)

eqs = [D(x) ~ σ*(y-x) + F,
       D(y) ~ x*(ρ-z)-y,
       D(z) ~ x*y - β*z]

aliases = [u ~ x + y - z]
lorenz1 = ODESystem(eqs,pins=[F],observed=aliases,name=:lorenz1)
lorenz2 = ODESystem(eqs,pins=[F],observed=aliases,name=:lorenz2)

connections = [lorenz1.F ~ lorenz2.u,
               lorenz2.F ~ lorenz1.u]
connected = ODESystem(Equation[],t,[],[],observed=connections,systems=[lorenz1,lorenz2])

sys = connected

@variables lorenz1₊F lorenz2₊F
@test pins(connected) == Variable[lorenz1₊F, lorenz2₊F]
@test isequal(observed(connected),
              [connections...,
               lorenz1.u ~ lorenz1.x + lorenz1.y - lorenz1.z,
               lorenz2.u ~ lorenz2.x + lorenz2.y - lorenz2.z])

collapsed_eqs = [D(lorenz1.x) ~ (lorenz1.σ * (lorenz1.y - lorenz1.x) +
                                 (lorenz2.x + lorenz2.y - lorenz2.z)),
                 D(lorenz1.y) ~ lorenz1.x * (lorenz1.ρ - lorenz1.z) - lorenz1.y,
                 D(lorenz1.z) ~ lorenz1.x * lorenz1.y - (lorenz1.β * lorenz1.z),
                 D(lorenz2.x) ~ (lorenz2.σ * (lorenz2.y - lorenz2.x) +
                                 (lorenz1.x + lorenz1.y - lorenz1.z)),
                 D(lorenz2.y) ~ lorenz2.x * (lorenz2.ρ - lorenz2.z) - lorenz2.y,
                 D(lorenz2.z) ~ lorenz2.x * lorenz2.y - (lorenz2.β * lorenz2.z)]

simplifyeqs(eqs) = Equation.((x->x.lhs).(eqs), simplify.((x->x.rhs).(eqs)))

@test isequal(simplifyeqs(equations(connected)), simplifyeqs(collapsed_eqs))


struct RegularRectilinearGrid{TX, TY, TZ, R} <: AbstractGrid{TX, TY, TZ}
    # corrdinates at cell centers, unit: meter
    xC::R
    yC::R
    zC::R
    # corrdinates at cell faces, unit: meter
    xF::R
    yF::R
    zF::R
    # grid spacing, unit: meter
    Δx::Float64
    Δy::Float64
    Δz::Float64
    # number of grid points
    Nx::Int
    Ny::Int
    Nz::Int
    # number of halo points
    Hx::Int
    Hy::Int
    Hz::Int
end

"""
    RegularRectilinearGrid(;size, spacing,
                            topology = (Periodic, Periodic, Bounded),
                            halo = (2, 2, 2))
Creats a `RegularRectilinearGrid` struct with `size = (Nx, Ny, Nz)` grid points.

Keyword Arguments (Required)
============================
- `size` : A tuple prescribing the number of grid points.
                `size` is a 3-tuple no matter for 3D, 2D, or 1D model.
- `spacing` : A tuple prescribing the length of each grid point in x, y, and z directions.
                `spacing` is a 3-tuple no matter for 3D, 2D, or 1D model.

Keyword Arguments (Optional)
============================
- `topology` : A 3-tuple specifying the topology of the domain.
                The topology can be either Periodic or Bounded in each direction.
- `halo` : A tuple of integers that specifies the size of the halo region of cells
                surrounding the physical interior for each direction.
                `halo` is a 3-tuple no matter for 3D, 2D, or 1D model.
                At least 2 halo points are needed for DST3FL advection scheme.
"""
function RegularRectilinearGrid(;size, spacing,
                                topology = (Periodic, Periodic, Bounded),
                                halo = (2, 2, 2))
    Nx, Ny, Nz = size
    Hx, Hy, Hz = halo
    Δx, Δy, Δz = spacing
    TX, TY, TZ = validate_topology(topology)

    xF = range(-Hx * Δx, (Nx + Hx - 1) * Δx, length = Nx + 2 * Hx)
    yF = range(-Hy * Δy, (Ny + Hy - 1) * Δy, length = Ny + 2 * Hy)
    zF = -range(-Hz * Δz, (Nz + Hz - 1) * Δz, length = Nz + 2 * Hz)

    xC = range((0.5 - Hx) * Δx, (Nx + Hx - 0.5) * Δx, length = Nx + 2 * Hx)
    yC = range((0.5 - Hy) * Δy, (Ny + Hy - 0.5) * Δy, length = Ny + 2 * Hy)
    zC = -range((0.5 - Hz) * Δz, (Nz + Hz - 0.5) * Δz, length = Nz + 2 * Hz)

    return RegularRectilinearGrid{TX, TY, TZ, typeof(xF)}(
        xC, yC, zC, xF, yF, zF, Δx, Δy, Δz, Nx, Ny, Nz, Hx, Hy, Hz)
end

function show(io::IO, g::RegularRectilinearGrid{TX, TY, TZ}) where {TX, TY, TZ}
    xL, xR = g.xF[g.Hx+1], g.xF[g.Hx+1+g.Nx]
    yL, yR = g.yF[g.Hy+1], g.yF[g.Hy+1+g.Ny]
    zL, zR = g.zF[g.Hz+1], g.zF[g.Hz+1+g.Nz]
    print(io, "RegularRectilinearGrid{$TX, $TY, $TZ}\n",
              "domain: x ∈ [$xL, $xR], y ∈ [$yL, $yR], z ∈ [$zL, $zR]\n",
              "topology (Tx, Ty, Tz):     ", (TX, TY, TZ), '\n',
              "resolution (Nx, Ny, Nz):   ", (g.Nx, g.Ny, g.Nz), '\n',
              "halo size (Hx, Hy, Hz):    ", (g.Hx, g.Hy, g.Hz), '\n',
              "grid spacing (Δx, Δy, Δz): ", (g.Δx, g.Δy, g.Δz))
end

function short_show(grid::RegularRectilinearGrid{TX, TY, TZ}) where {TX, TY, TZ}
    return "RegularRectilinearGrid{$TX, $TY, $TZ}(Nx=$(grid.Nx), Ny=$(grid.Ny), Nz=$(grid.Nz))"
end

function validate_topology(topology)
    for T in topology
        if !isa(T(), AbstractTopology)
            e = "$T is not a valid topology!" *
                "Valid topologies are: Periodic and Bounded."
            throw(ArgumentError(e))
        end
    end

        return topology
end


export
    bom_to_hamiltonian

I(::Type{T}) where T<:Real = ones(T, 2)
Z(::Type{T}) where T<:Real = [one(T), -one(T)]

function _string_to_vec(T, s::String)
    result = [one(T)]
    for b = s
        if b == 'I'
            result = kron(result, I(T))
        elseif b == 'Z'
            result = kron(result, Z(T))
        else
            error("Bad string: chars have to be \'I\' or \'Z\'")
        end
    end
    result
end

function bom_to_hamiltonian(ising::Ising{T}) where T<:Real
    variables = used_variables(ising)
    degoffreedom = length(variables)

    result = zeros(T, 2^degoffreedom)
    for (key, val) = values(ising)
        pauli = ones(T, 1)
        for i=1:degoffreedom
            pauli = kron(pauli, variables[i] ∈ key ? Z(T) : I(T))
        end
        result += val * pauli
    end
    result
end

bom_to_hamiltonian(hobo::HOBO) = bom_to_hamiltonian(Ising(hobo))


using MonteCarloMeasurements, ForwardDiff, Test
const FD = ForwardDiff

@testset "forwarddiff" begin
    c = 1 ± 0.1 # These are the uncertain parameters
    d = 1 ± 0.1 # These are the uncertain parameters
    # In the cost function below, we ensure that $cx+dy > 10 \; ∀ \; c,d ∈ P$ by looking at the worst case
    function cost(params)
        x,y = params
        -(3x+2y) + 10000sum(params .< 0) + 10000*(maximum(c*x+d*y) > 10)
    end

    params = [1., 2] # Initial guess
    paramsp = [1., 2] .± 0.001 # Initial guess
    @test cost(params) == -7    # Try the cost function

    @test FD.gradient(cost, params) == @unsafe FD.gradient(cost, paramsp)
    @test FD.gradient(sum, params) == FD.gradient(sum, paramsp)
    @test all(FD.gradient(prod, params) .≈ FD.gradient(prod, paramsp))

    unsafe_comparisons(false)
    @test FD.gradient(x -> params'x, params) == params
    @test FD.gradient(x -> paramsp'x, params) == paramsp
    @test FD.gradient(x -> params'x, paramsp) == params
    r = FD.gradient(x -> paramsp'x, paramsp)
    @test mean(mean(r[1])) ≈ params[1] atol=1e-2
    @test mean(mean(r[2])) ≈ params[2] atol=1e-2

    @test FD.jacobian(x -> params+x, params) == I
    @test FD.jacobian(x -> paramsp+x, params) == I
    @test FD.jacobian(x -> params+x, paramsp) == I

    @test FD.jacobian(x -> params-x, params) == -I
    @test FD.jacobian(x -> paramsp-x, params) == -I
    @test FD.jacobian(x -> params-x, paramsp) == -I

    @test FD.jacobian(x -> x-params, params) == I
    @test FD.jacobian(x -> x-paramsp, params) == I
    @test FD.jacobian(x -> x-params, paramsp) == I

    function strange(x,y)
        (x.^2)'*(y.^2)
    end
    ref = FD.gradient(x->strange(x,params), params)
    FD.gradient(x->strange(x,params), paramsp) != ref
    FD.gradient(x->strange(x,params), paramsp) ≈ ref
    FD.gradient(x->strange(x,paramsp), params) != ref
    FD.gradient(x->strange(x,paramsp), params) ≈ ref
    r = FD.gradient(x->strange(x,paramsp), paramsp) # maybe this is a bit overkill
    @test mean(mean(r[1])) ≈ ref[1] atol=1e-2
    @test mean(mean(r[2])) ≈ ref[2] atol=1e-2
    @test mean(mean(r[1])) != ref[1]
    @test mean(mean(r[2])) != ref[2]

    unsafe_comparisons(false)
end


using CompScienceMeshes, BEAST

function blkdiagm(blks...)
    m = sum(size(b,1) for b in blks)
    n = sum(size(b,2) for b in blks)
    A = zeros(eltype(first(blks)), m, n)
    o1 = 1
    o2 = 1
    for b in blks
        m1 = size(b,1)
        n1 = size(b,2)
        A[o1:o1+m1-1,o2:o2+n1-1] .= b
        o1 += m1
        o2 += n1
    end
    A
end

width, height, h = 1.0, 0.5, 0.05
G1 = meshrectangle(width, height, h)
G2 = CompScienceMeshes.rotate(G1, 0.5π * x̂)
G3 = CompScienceMeshes.rotate(G1, 1.0π * x̂)

G12 = weld(G1,-G2)
G23 = weld(G2,-G3)
G31 = weld(G3,-G1)

X12 = lagrangec0d1(G12)
X23 = lagrangec0d1(G23)
X31 = lagrangec0d1(G31)

Y12 = duallagrangecxd0(G12)
Y23 = duallagrangecxd0(G23)
Y31 = duallagrangecxd0(G31)

κ = 1.0
SL = Helmholtz3D.singlelayer(wavenumber=κ)
HS = Helmholtz3D.hypersingular(gamma=κ*im)
N = Identity()

X = X12 × X23 × X31
Y = Y12 × Y23 × Y31

E = Helmholtz3D.planewave(direction=ẑ, wavenumber=κ)
e = strace(E, G12)
ex = assemble(e, X)

Sxx = assemble(SL, X, X)
N1 = assemble(Identity(), X12, Y12)
N2 = assemble(Identity(), X23, Y23)
N3 = assemble(Identity(), X31, Y31)
Nxy = blkdiagm(N1,N2,N3)
Syy = assemble(HS, Y, Y)

Dyx = inv(Nxy)
Q = transpose(Dyx) * Syy * Dyx * Sxx
R = transpose(Dyx) * Syy * Dyx * ex

u1, ch1 = solve(BEAST.GMRESSolver(Sxx),ex)
u2, ch2 = solve(BEAST.GMRESSolver(Q),R)

@show ch1.iters
@show ch2.iters

using LinearAlgebra
cond(Sxx)
cond(Q)

w1 = eigvals(Sxx)
w2 = eigvals(Q)

using Plots
plot()
scatter!(w1)
scatter!(w2)
# u = gmres(mtefie)
#
# offset = 1; u12 = u[offset:offset+numfunctions(X12)-1]
# offset += numfunctions(X12); u23 = u[offset:offset+numfunctions(X23)-1]
# offset += numfunctions(X23); u31 = u[offset:offset+numfunctions(X31)-1]
#
# fcr12, _ = facecurrents(u12, X12)
# fcr23, _ = facecurrents(u23, X23)
# fcr31, _ = facecurrents(u31, X31)

# import PlotlyJS
using LinearAlgebra
# p1 = PlotlyJS.Plot(patch(G12, norm.(fcr12)))
# p2 = PlotlyJS.Plot(patch(G23, norm.(fcr23)))
# p3 = PlotlyJS.Plot(patch(G31, norm.(fcr31)))

# PlotlyJS.plot([p1, p2, p3])

Sxx = BEAST.sysmatrix(mtefie)

Syy = assemble(SL, Y, Y)
iNxy = inv(Nxy)

# gmres()


# # Heat equation tutorial

# In this tutorial, we'll be solving the [heat
# equation](https://en.wikipedia.org/wiki/Heat_equation):

# ``
# \frac{∂ ρcT}{∂ t} + ∇ ⋅ (-α ∇ρcT) = 0
# ``

# where
#  - `t` is time
#  - `α` is the thermal diffusivity
#  - `T` is the temperature
#  - `ρ` is the density
#  - `c` is the heat capacity
#  - `ρcT` is the thermal energy

# To put this in the form of ClimateMachine's [`BalanceLaw`](@ref
# ClimateMachine.BalanceLaws.BalanceLaw), we'll re-write the equation as:

# ``
# \frac{∂ ρcT}{∂ t} + ∇ ⋅ (F(α, ρcT, t)) = 0
# ``

# where
#  - ``F(α, ρcT, t) = -α ∇ρcT`` is the second-order flux

# with boundary conditions
#  - Fixed temperature ``T_{surface}`` at ``z_{min}`` (non-zero Dirichlet)
#  - No thermal flux at ``z_{min}`` (zero Neumann)

# Solving these equations is broken down into the following steps:
# 1) Preliminary configuration
# 2) PDEs
# 3) Space discretization
# 4) Time discretization / solver
# 5) Solver hooks / callbacks
# 6) Solve
# 7) Post-processing

# # Preliminary configuration

# ## [Loading code](@id Loading-code-heat)

# First, we'll load our pre-requisites:
#  - load external packages:
using MPI
using OrderedCollections
using Plots
using StaticArrays
using OrdinaryDiffEq
using DiffEqBase

#  - load CLIMAParameters and set up to use it:

using CLIMAParameters
struct EarthParameterSet <: AbstractEarthParameterSet end
const param_set = EarthParameterSet()

#  - load necessary ClimateMachine modules:
using ClimateMachine
using ClimateMachine.Mesh.Topologies
using ClimateMachine.Mesh.Grids
using ClimateMachine.DGMethods
using ClimateMachine.DGMethods.NumericalFluxes
using ClimateMachine.BalanceLaws:
    BalanceLaw, Prognostic, Auxiliary, Gradient, GradientFlux

using ClimateMachine.Mesh.Geometry: LocalGeometry
using ClimateMachine.MPIStateArrays
using ClimateMachine.GenericCallbacks
using ClimateMachine.ODESolvers
using ClimateMachine.VariableTemplates
using ClimateMachine.SingleStackUtils

#  - import necessary ClimateMachine modules: (`import`ing enables us to
#  provide implementations of these structs/methods)
import ClimateMachine.BalanceLaws:
    vars_state,
    source!,
    flux_second_order!,
    flux_first_order!,
    compute_gradient_argument!,
    compute_gradient_flux!,
    nodal_update_auxiliary_state!,
    nodal_init_state_auxiliary!,
    init_state_prognostic!,
    boundary_state!

import ClimateMachine.DGMethods: calculate_dt

# ## Initialization

# Define the float type (`Float64` or `Float32`)
FT = Float64;
# Initialize ClimateMachine for CPU.
ClimateMachine.init(; disable_gpu = true);

const clima_dir = dirname(dirname(pathof(ClimateMachine)));

# Load some helper functions for plotting
include(joinpath(clima_dir, "docs", "plothelpers.jl"));

# # Define the set of Partial Differential Equations (PDEs)

# ## Define the model

# Model parameters can be stored in the particular [`BalanceLaw`](@ref
# ClimateMachine.BalanceLaws.BalanceLaw), in this case, a `HeatModel`:

Base.@kwdef struct HeatModel{FT, APS} <: BalanceLaw
    "Parameters"
    param_set::APS
    "Heat capacity"
    ρc::FT = 1
    "Thermal diffusivity"
    α::FT = 0.01
    "Initial conditions for temperature"
    initialT::FT = 295.15
    "Bottom boundary value for temperature (Dirichlet boundary conditions)"
    T_bottom::FT = 300.0
    "Top flux (α∇ρcT) at top boundary (Neumann boundary conditions)"
    flux_top::FT = 0.0
end

# Create an instance of the `HeatModel`:
m = HeatModel{FT, typeof(param_set)}(; param_set = param_set);

# This model dictates the flow control, using [Dynamic Multiple
# Dispatch](https://en.wikipedia.org/wiki/Multiple_dispatch), for which
# kernels are executed.

# ## Define the variables

# All of the methods defined in this section were `import`ed in
# [Loading code](@ref Loading-code-heat) to let us provide
# implementations for our `HeatModel` as they will be used by
# the solver.

# Specify auxiliary variables for `HeatModel`
vars_state(::HeatModel, ::Auxiliary, FT) = @vars(z::FT, T::FT);

# Specify prognostic variables, the variables solved for in the PDEs, for
# `HeatModel`
vars_state(::HeatModel, ::Prognostic, FT) = @vars(ρcT::FT);

# Specify state variables whose gradients are needed for `HeatModel`
vars_state(::HeatModel, ::Gradient, FT) = @vars(ρcT::FT);

# Specify gradient variables for `HeatModel`
vars_state(::HeatModel, ::GradientFlux, FT) = @vars(α∇ρcT::SVector{3, FT});

# ## Define the compute kernels

# Specify the initial values in `aux::Vars`, which are available in
# `init_state_prognostic!`. Note that
# - this method is only called at `t=0`
# - `aux.z` and `aux.T` are available here because we've specified `z` and `T`
# in `vars_state` given `Auxiliary`
# in `vars_state`
function nodal_init_state_auxiliary!(
    m::HeatModel,
    aux::Vars,
    tmp::Vars,
    geom::LocalGeometry,
)
    aux.z = geom.coord[3]
    aux.T = m.initialT
end;

# Specify the initial values in `state::Vars`. Note that
# - this method is only called at `t=0`
# - `state.ρcT` is available here because we've specified `ρcT` in
# `vars_state` given `Prognostic`
function init_state_prognostic!(
    m::HeatModel,
    state::Vars,
    aux::Vars,
    coords,
    t::Real,
)
    state.ρcT = m.ρc * aux.T
end;

# The remaining methods, defined in this section, are called at every
# time-step in the solver by the [`BalanceLaw`](@ref
# ClimateMachine.BalanceLaws.BalanceLaw) framework.

# Compute/update all auxiliary variables at each node. Note that
# - `aux.T` is available here because we've specified `T` in
# `vars_state` given `Auxiliary`
function nodal_update_auxiliary_state!(
    m::HeatModel,
    state::Vars,
    aux::Vars,
    t::Real,
)
    aux.T = state.ρcT / m.ρc
end;

# Since we have second-order fluxes, we must tell `ClimateMachine` to compute
# the gradient of `ρcT`. Here, we specify how `ρcT` is computed. Note that
#  - `transform.ρcT` is available here because we've specified `ρcT` in
#  `vars_state` given `Gradient`
function compute_gradient_argument!(
    m::HeatModel,
    transform::Vars,
    state::Vars,
    aux::Vars,
    t::Real,
)
    transform.ρcT = state.ρcT
end;

# Specify where in `diffusive::Vars` to store the computed gradient from
# `compute_gradient_argument!`. Note that:
#  - `diffusive.α∇ρcT` is available here because we've specified `α∇ρcT` in
#  `vars_state` given `Gradient`
#  - `∇transform.ρcT` is available here because we've specified `ρcT`  in
#  `vars_state` given `Gradient`
function compute_gradient_flux!(
    m::HeatModel,
    diffusive::Vars,
    ∇transform::Grad,
    state::Vars,
    aux::Vars,
    t::Real,
)
    diffusive.α∇ρcT = -m.α * ∇transform.ρcT
end;

# We have no sources, nor non-diffusive fluxes.
function source!(m::HeatModel, _...) end;
function flux_first_order!(m::HeatModel, _...) end;

# Compute diffusive flux (``F(α, ρcT, t) = -α ∇ρcT`` in the original PDE).
# Note that:
# - `diffusive.α∇ρcT` is available here because we've specified `α∇ρcT` in
# `vars_state` given `GradientFlux`
function flux_second_order!(
    m::HeatModel,
    flux::Grad,
    state::Vars,
    diffusive::Vars,
    hyperdiffusive::Vars,
    aux::Vars,
    t::Real,
)
    flux.ρcT += diffusive.α∇ρcT
end;

# ### Boundary conditions

# Second-order terms in our equations, ``∇⋅(F)`` where ``F = -α∇ρcT``, are
# internally reformulated to first-order unknowns.
# Boundary conditions must be specified for all unknowns, both first-order and
# second-order unknowns which have been reformulated.

# The boundary conditions for `ρcT` (first order unknown)
function boundary_state!(
    nf,
    m::HeatModel,
    state⁺::Vars,
    aux⁺::Vars,
    n⁻,
    state⁻::Vars,
    aux⁻::Vars,
    bctype,
    t,
    _...,
)
    ## Apply Dirichlet BCs
    if bctype == 1 # At bottom
        state⁺.ρcT = m.ρc * m.T_bottom
    elseif bctype == 2 # At top
        nothing
    end
end;

# The boundary conditions for `ρcT` are specified here for second-order
# unknowns
function boundary_state!(
    nf,
    m::HeatModel,
    state⁺::Vars,
    diff⁺::Vars,
    aux⁺::Vars,
    n⁻,
    state⁻::Vars,
    diff⁻::Vars,
    aux⁻::Vars,
    bctype,
    t,
    _...,
)
    ## Apply Neumann BCs
    if bctype == 1 # At bottom
        nothing
    elseif bctype == 2 # At top
        diff⁺.α∇ρcT = n⁻ * m.flux_top
    end
end;

# # Spatial discretization

# Prescribe polynomial order of basis functions in finite elements
N_poly = 5;

# Specify the number of vertical elements
nelem_vert = 10;

# Specify the domain height
zmax = FT(1);

# Establish a `ClimateMachine` single stack configuration
driver_config = ClimateMachine.SingleStackConfiguration(
    "HeatEquation",
    N_poly,
    nelem_vert,
    zmax,
    param_set,
    m,
    numerical_flux_first_order = CentralNumericalFluxFirstOrder(),
);

# # Time discretization / solver

# Specify simulation time (SI units)
t0 = FT(0)
timeend = FT(40)

# In this section, we initialize the state vector and allocate memory for
# the solution in space (`dg` has the model `m`, which describes the PDEs
# as well as the function used for initialization). `SolverConfiguration`
# initializes the ODE solver, by default an explicit Low-Storage
# [Runge-Kutta](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods)
# method. In this tutorial, we prescribe an option for an implicit
# `Kvaerno3` method.

# First, let's define how the time-step is computed, based on the
# [Fourier number](https://en.wikipedia.org/wiki/Fourier_number)
# (i.e., diffusive Courant number) is defined. Because
# the `HeatModel` is a custom model, we must define how both are computed.
# First, we must define our own implementation of `DGMethods.calculate_dt`,
# (which we imported):
function calculate_dt(dg, model::HeatModel, Q, Courant_number, t, direction)
    Δt = one(eltype(Q))
    CFL = DGMethods.courant(diffusive_courant, dg, model, Q, Δt, t, direction)
    return Courant_number / CFL
end

# Next, we'll define our implementation of `diffusive_courant`:
function diffusive_courant(
    m::HeatModel,
    state::Vars,
    aux::Vars,
    diffusive::Vars,
    Δx,
    Δt,
    t,
    direction,
)
    return Δt * m.α / (Δx * Δx)
end

# Finally, we initialize the state vector and solver
# configuration based on the given Fourier number.
# Note that, we can use a much larger Fourier number
# for implicit solvers as compared to explicit solvers.
use_implicit_solver = false
if use_implicit_solver
    given_Fourier = FT(30)

    solver_config = ClimateMachine.SolverConfiguration(
        t0,
        timeend,
        driver_config;
        ode_solver_type = ImplicitSolverType(OrdinaryDiffEq.Kvaerno3(
            autodiff = false,
            linsolve = LinSolveGMRES(),
        )),
        Courant_number = given_Fourier,
        CFL_direction = VerticalDirection(),
    )
else
    given_Fourier = FT(0.7)

    solver_config = ClimateMachine.SolverConfiguration(
        t0,
        timeend,
        driver_config;
        Courant_number = given_Fourier,
        CFL_direction = VerticalDirection(),
    )
end;

grid = solver_config.dg.grid;
Q = solver_config.Q;
aux = solver_config.dg.state_auxiliary;

# ## Inspect the initial conditions

# Let's export a plot of the initial state
output_dir = @__DIR__;

mkpath(output_dir);

z_scale = 100; # convert from meters to cm
z_key = "z";
z_label = "z [cm]";
z = get_z(grid, z_scale);

# Create an array to store the solution:
all_data = Dict[dict_of_nodal_states(solver_config, [z_key])]  # store initial condition at ``t=0``
time_data = FT[0]                                      # store time data

export_plot(
    z,
    all_data,
    ("ρcT",),
    joinpath(output_dir, "initial_condition.png");
    xlabel = "ρcT",
    ylabel = z_label,
    time_data = time_data,
);
# ![](initial_condition.png)

# It matches what we have in `init_state_prognostic!(m::HeatModel, ...)`, so
# let's continue.

# # Solver hooks / callbacks

# Define the number of outputs from `t0` to `timeend`
const n_outputs = 5;

# This equates to exports every ceil(Int, timeend/n_outputs) time-step:
const every_x_simulation_time = ceil(Int, timeend / n_outputs);

# The `ClimateMachine`'s time-steppers provide hooks, or callbacks, which
# allow users to inject code to be executed at specified intervals. In this
# callback, a dictionary of prognostic and auxiliary states are appended to
# `all_data` for time the callback is executed. In addition, time is collected
# and appended to `time_data`.
callback = GenericCallbacks.EveryXSimulationTime(every_x_simulation_time) do
    push!(all_data, dict_of_nodal_states(solver_config, [z_key]))
    push!(time_data, gettime(solver_config.solver))
    nothing
end;

# # Solve

# This is the main `ClimateMachine` solver invocation. While users do not have
# access to the time-stepping loop, code may be injected via `user_callbacks`,
# which is a `Tuple` of callbacks in [`GenericCallbacks`](@ref ClimateMachine.GenericCallbacks).
ClimateMachine.invoke!(solver_config; user_callbacks = (callback,));

# Append result at the end of the last time step:
push!(all_data, dict_of_nodal_states(solver_config, [z_key]));
push!(time_data, gettime(solver_config.solver));

# # Post-processing

# Our solution is stored in the array of dictionaries `all_data` whose keys are
# the output interval. The next level keys are the variable names, and the
# values are the values along the grid:

# To get `T` at ``t=0``, we can use `T_at_t_0 = all_data[1]["T"][:]`
@show keys(all_data[1])

# Let's plot the solution:

export_plot(
    z,
    all_data,
    ("ρcT",),
    joinpath(output_dir, "solution_vs_time.png");
    xlabel = "ρcT",
    ylabel = z_label,
    time_data = time_data,
);
# ![](solution_vs_time.png)

# The results look as we would expect: a fixed temperature at the bottom is
# resulting in heat flux that propagates up the domain. To run this file, and
# inspect the solution in `all_data`, include this tutorial in the Julia REPL
# with:

# ```julia
# include(joinpath("tutorials", "Land", "Heat", "heat_equation.jl"))
# ```


lines = readlines("input.txt")

function syntax_check(lines)
    B = Dict('(' => ')', '[' => ']', '{' => '}', '<' => '>') # brackets
    ES = Dict(')' => 3, ']' => 57, '}' => 1197, '>' => 25137) # error scores
    CS = Dict('(' => 1, '[' => 2, '{' => 3, '<' => 4) # completion scores

    lefts = Char[]
    error_score = 0
    completion_score = 0
    completion_scores = Int[]

    for line in lines
        for c in line
            if c in keys(B)
                push!(lefts, c)
            else
                if c == B[pop!(lefts)]
                    continue
                else
                    error_score += ES[c]
                    empty!(lefts)
                    break
                end
            end
        end
        if !isempty(lefts)
            while !isempty(lefts)
                completion_score = 5*completion_score + CS[pop!(lefts)]
            end
            push!(completion_scores, completion_score)
            completion_score -= completion_score
        end
    end
    return error_score, completion_scores
end

error_score, completion_scores = syntax_check(lines)
@info "Syntax error score = $error_score"
@info "Median completion score = $(sort!(completion_scores)[end÷2+1])"


# scoring.jl
# 2019-12-28
# David Merrell
#
# Methods for scoring performance in network inference tasks.
# In particular, for the simulation study (where we score
# edge prediction as a probabilistic classification task)

module Scoring

using JSON, CSV, ArgParse

export aucroc, aucpr, average_precision, julia_main

"""
Compute AUCROC from the given predictions,
with respect to the given ground-truth.
Integral is computed via trapezoidal rule.

Has n*log(n) complexity
(superlinear expense incurred by sorting;
 the AUCROC loop has linear expense).
"""
function aucroc(preds::Vector, truths::Vector{Bool}; return_curve::Bool=false)

    if return_curve
        curve = Vector{Vector{Float64}}()
        push!(curve, [0.0;0.0])
    end

    # Sort the predictions (and corresponding ground truth)
    # by confidence level
    srt_inds = sortperm(preds, order=Base.Order.Reverse)
    srt_preds = preds[srt_inds]
    srt_truths = truths[srt_inds]

    N = length(srt_preds)
    npos = sum(truths)
    nneg = N - npos

    area = 0.0
    tp = 0
    fp = 0
    prev_fp = 0
    prev_tp = 0

    # Loop through the predictions
    # (in order of decreasing confidence level)
    for i=1:N
        if (i > 1) && (srt_preds[i] != srt_preds[i-1])
            area += 0.5*(prev_tp + tp)*(fp - prev_fp)/(npos*nneg)
            if return_curve
                push!(curve, [fp/nneg; tp/npos])
            end
            prev_tp = tp
            prev_fp = fp
        end

        if srt_truths[i]
            tp += 1
        else
            fp += 1
        end
    end
    area += 0.5*(prev_tp + tp)*(fp - prev_fp)/(npos*nneg)

    if return_curve
        push!(curve, [1.0; 1.0])
        return area, transpose(hcat(curve...))
    else
        return area
    end

end

"""
Compute AUCPR from the given predictions,
with respect to the given ground-truth.
Integral is computed via trapezoidal rule.

Has n*log(n) complexity
(superlinear expense incurred by sorting;
 the AUCPR loop has linear expense).
"""
function aucpr(preds::Vector, truths::Vector{Bool}; return_curve::Bool=false)

    if return_curve
        curve = Vector{Vector{Float64}}()
        push!(curve, [0.0;1.0])
    end

    # Sort the predictions (and corresponding ground truth)
    # by confidence level
    srt_inds = sortperm(preds, order=Base.Order.Reverse)
    srt_preds = preds[srt_inds]
    srt_truths = truths[srt_inds]

    area = 0.0
    tp = 0
    fp = 0
    prev_p = 1.0
    prev_r = 0.0
    N = length(srt_truths)
    npos = sum(srt_truths)
    nneg = N - npos

    for i=1:N
        if (i>1) && (srt_preds[i] < srt_preds[i-1])

            recall = tp / npos
            precision = tp / (tp + fp)
            if return_curve
                push!(curve, [recall; precision])
            end
            area += 0.5*(precision + prev_p)*(recall - prev_r)

            prev_r = recall
            prev_p = precision
        end

        if srt_truths[i]
            tp += 1
        else
            fp += 1
        end
    end

    recall = tp / npos
    precision = tp / (tp + fp)
    area += 0.5*(precision + prev_p)*(recall - prev_r)

    if return_curve
        push!(curve, [recall; precision])
        return area, transpose(hcat(curve...))
    end

    return area
end

"""
Compute average precision from the given predictions,
with respect to the given ground-truth.

Has n*log(n) complexity
(superlinear expense incurred by sorting)

return_curve: return the PR curve for these predictions
"""
function average_precision(preds::Vector, truths::Vector{Bool}; return_curve::Bool=false)

    if return_curve
        curve = Vector{Vector{Float64}}()
        push!(curve, [0.0;1.0])
    end

    # Sort the predictions (and corresponding ground truth)
    # by confidence level
    srt_inds = sortperm(preds, order=Base.Order.Reverse)
    srt_preds = preds[srt_inds]
    srt_truths = truths[srt_inds]

    s = 0.0
    tp = 0
    fp = 0
    prev_r = 0.0
    N = length(srt_truths)
    npos = sum(srt_truths)
    nneg = N - npos

    for i=1:N
        if (i>1) && (srt_preds[i] < srt_preds[i-1])

            recall = tp / npos
            precision = tp / (tp + fp)
            if return_curve
                push!(curve, [recall; precision])
            end
            s += precision*(recall - prev_r)

            prev_r = recall
        end

        if srt_truths[i]
            tp += 1
        else
            fp += 1
        end
    end

    recall = tp / npos
    precision = tp / (tp + fp)
    s += precision*(recall - prev_r)

    if return_curve
        push!(curve, [recall; precision])
        return s, transpose(hcat(curve...))
    end

    return s
end

"""
Take a "vector of vectors" representation of the
network edges and return a single vector in canonical order.
"""
function ps_to_vec(parent_sets::Vector)
    return [e for ps in parent_sets for e in ps]
end

"""
Read a JSON file containing edge predictions.
We assume there's a "edge_conf_key" field specifying
where to access the predicted parent sets.

Return a single vector of confidence levels for all edges.
"""
function load_ps_predictions(pred_fname)

    f = open(pred_fname, "r")
    js_d = JSON.parse(read(f, String))
    close(f)

    key = js_d["edge_conf_key"]

    parent_sets = js_d[key]

    return ps_to_vec(parent_sets)
end

"""
Read a CSV file containing a true adjacency matrix.
We assume the file is comma-delimited with no header
or index column; the (i,j)-th entry denotes the jth
parent of the ith vertex (that is: the columns are
parent sets). Each entry must be 0 or 1.
"""
function load_ps_truths(truths_fname)

    df = CSV.read(truths_fname; header=false)
    adj_mat = convert(Matrix{Bool}, df)

    parent_sets = [adj_mat[:,i] for i=1:size(adj_mat,2)]

    return ps_to_vec(parent_sets)
end

"""
Given
* a vector of edge prediction filenames
* a vector of edge truth filenames
* a scoring function;
return a vector of the scores
"""
function edge_score(pred_fname::String,
                    truth_fname::String,
                    score_fn::Function)

    preds = load_ps_predictions(pred_fname)
    truths = load_ps_truths(truth_fname)
    return score_fn(preds, truths)

end

function get_args(args::Vector{String})

    s = ArgParseSettings()
    @add_arg_table! s begin
        "--pred-file"
            help="The path to a JSON file containing edge predictions"
            required=true
            arg_type=String
        "--truth-file"
            help="The path to a CSV file containing true adjacency matrice(s)."
            required=true
            arg_type=String
        "--output-file"
            help="The path of an output JSON file"
            required=true
            arg_type=String
        "--skip-roc"
            help="Do NOT compute ROC or AUCROC for the predictions."
            action=:store_true
    end

    args = parse_args(args, s)
    return args
end

# Main function -- for purposes of static compilation
function julia_main()

    args = get_args(ARGS)

    pred_fname = args["pred-file"]
    truth_fname = args["truth-file"]
    output_fname = args["output-file"]
    skip_roc = args["skip-roc"]

    result = Dict{String,Any}()

    # Compute mean precision and PR curve
    pr_score_fn = (pred, truth) -> average_precision(pred, truth; return_curve=true)
    pr_stuff = edge_score(pred_fname, truth_fname, pr_score_fn)
    result["aucpr"] = pr_stuff[1]
    result["pr_curve"] = pr_stuff[2]

    if !skip_roc
        # Compute AUCROC and ROC curve for each set of predictions
        roc_score_fn = (pred, truth) -> aucroc(pred, truth; return_curve=true)
        roc_stuff = edge_score(pred_fname, truth_fname, roc_score_fn)
        result["aucroc"] = roc_stuff[1]
        result["roc_curve"] = roc_stuff[2]
    end

    f = open(output_fname, "w")
    JSON.print(f, result)
    close(f)

    return 0
end

# END MODULE
end

using .Scoring

julia_main()


using CompScienceMeshes
using BEAST
using Test

############################################################################################
#
# Test the sign of the double layer potential
#
############################################################################################

γ = 0.0
K = MWDoubleLayer3D(γ)

τ = Mesh(
    [
        point(-4,0,0),
        point(-6,0,0),
        point(-5,-1,0)
    ],
    [
        index(1,2,3)])

σ = Mesh(
    [
        point(4,0,0),
        point(6,0,0),
        point(5,0,-1)
    ],
    [
        index(1,2,3)
    ]
)

@test numcells(τ) == 1
@test numcells(σ) == 1

T = raviartthomas(τ, boundary(τ))
S = raviartthomas(σ, boundary(σ))

@test numfunctions(T) == 3
@test numfunctions(S) == 3

t = chart(τ, first(cells(τ)))
s = chart(σ, first(cells(σ)))

p = neighborhood(t, [1,1]/2)
q = neighborhood(s, [1,1]/2)

R = norm(cartesian(p)-cartesian(q))

rt = refspace(T)
f = (refspace(T))(p)[3][1]
g = (refspace(S))(q)[3][1]

@test dot(f, point(0,1,0)) > 0
@test dot(g, point(0,0,1)) > 0

@show 1/R^2

z = assemble(K, T, S)

qd = quaddata(K, rt, rt, elements(τ), elements(σ))
zlocal = zeros(promote_type(scalartype(K), scalartype(S), scalartype(T)), 3, 3)
strat = BEAST.quadrule(K, rt, rt, 1, t, 1, s, qd)
strat =  BEAST.DoubleQuadStrategy(qd.tpoints[1,1],  qd.bpoints[1,1])
BEAST.momintegrals!(K, rt, rt, t, s, zlocal, strat)
z = zlocal[3,3]


#   Copyright (c) 2020-2022 Velexi Corporation
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

"""
AbstractBlade.jl defines the AbstractBlade type, interface, and core methods
"""

# --- Exports

# Types
export AbstractBlade

# Properties
import Base.sign
export basis, grade, volume

# Functions
import Base.inv

# --- Types

"""
    AbstractBlade{<:AbstractFloat}

Supertype for all blade types.

Interface
=========

Properties
----------

    grade(B::AbstractBlade)::Int

    basis(B::AbstractBlade)::Matrix

    volume(B::AbstractBlade)::AbstractFloat

    sign(B::AbstractBlade)::Int8

Operations
----------

    dual(B::AbstractBlade, C::AbstractBlade)::AbstractBlade

    inv(B::AbstractBlade)::AbstractBlade

Implementation
==============

* For the AbstractBlade type, the `volume` of a blade should encode both norm
  and orientation information.

* The return value of all methods should preserve the precision of its
  AbstractBlade arguments (when possible).

"""
abstract type AbstractBlade{T<:AbstractFloat} <: AbstractMultivector{T} end

# --- Public functions/methods

"""
    grade(B::AbstractBlade)::Int

Return grade of `B`.
"""
function grade end

"""
    basis(B::AbstractBlade)::Matrix

Return an orthonormal basis for the subspace represented by `B`.

When `B` is an AbstractScalar, 1 (at the precision of `B`) is returned. When
`B` is a Pseudoscalar, LinearAlgebra.I is returned.
"""
function basis end

"""
    volume(B::AbstractBlade)::AbstractFloat

Return the signed volume of `B`.

When `B` is a Blade, `volume(B)` is the signed norm of the blade _relative_ to
its unit basis.
"""
function volume end

"""
    sign(B::AbstractBlade)::Int8

Return the sign of `B` relative to its unit basis.
"""
sign(B::AbstractBlade)::Int8 = sign(volume(B))

"""
    dual(B::AbstractBlade, C::AbstractBlade)::AbstractBlade

Compute the dual `B` relative to the subspace represented by `C`.

Notes
=====

* `dual(B, C)` is only defined if (1) `B` and `C` are extended from real
  vector spaces of the same dimension and (2) the subspace represented by `B`
  is contained in subspace represented by `C`.

* The volume of `C` is ignored.
"""
dual(B::AbstractBlade, C::AbstractBlade)::AbstractBlade =
    nothing  # no-op method to attach docstring to because an empty generic function for
             # `dual` already exists in AbstractMultivector.jl

"""
    inv(B::AbstractBlade)::AbstractBlade

Compute the multiplicative inverse of blade `B`.
"""
function inv end

# --- Method definitions for AbstractMultivector interface functions

grades(B::AbstractBlade) = [grade(B)]

blades(B::AbstractBlade) = Vector{AbstractBlade}([B])

norm(B::AbstractBlade) = abs(volume(B))

Base.getindex(B::AbstractBlade, k::Int) =
    k == grade(B) ? Vector{AbstractBlade}([B]) : Vector{AbstractBlade}()


# Python version created in 2012.
# Ported to julia in January, 2018.
#
# Author: Rich Plevin
#
# Copyright (c) 2012-2018. The Regents of the University of California (Regents)
# and Richard Plevin. See the file COPYRIGHT.txt for details.
#
# Implements the Latin Hypercube Sampling technique as described by Iman and Conover, 1982,
# including correlation control both for no correlation or for a specified rank correlation
# matrix for the sampled parameters. Original python version was heavily modified from
# http://nullege.com/codes/show/src@m@o@model-builder-HEAD@Bayes@lhs.py
#
import StatsBase
using Statistics
using LinearAlgebra

"""
    rank_corr_coef(m::Matrix{Float64})

Take a 2-D array of values and produce a array of rank correlation
coefficients representing the rank correlations pairs of columns.
"""
function rank_corr_coef(m::Matrix{Float64})
    cols = size(m, 2)
    corrCoef = Matrix(1.0I, cols, cols)    # identity matrix

    for i in 1:cols
        for j in (i + 1):cols
            corr = corspearman(m[:, i], m[:, j])
            corrCoef[i, j] = corrCoef[j, i] = corr
        end
    end

    return corrCoef
end

"""
    _gen_rank_values(params::Int, trials::Int, corrmatrix::Matrix{Float64})

Generate a data set of 'trials' ranks for 'params'
parameters that obey the given correlation matrix.

params: number of parameters.
trials: number of trials.
corrmatrix: rank correlation matrix for parameters.
corrmatrix[i,j] denotes the rank correlation between parameters
i and j.

Output is a Matrix with 'trials' rows and 'params' columns.
The i'th column represents the ranks for the i'th parameter.
"""
function _gen_rank_values(params::Int, trials::Int, corrmatrix::Matrix{Float64})
    # Create van der Waarden scores
    strata = collect(1.0:trials) / (trials + 1)
    vdwScores = quantile.(Normal(), strata)

    S = zeros(trials, params)
    for i in 1:params
        shuffle!(vdwScores)
        S[:, i] = vdwScores
    end

    P = Matrix(cholesky(corrmatrix, Val(false)).L)
    E = rank_corr_coef(S)
    Q = Matrix(cholesky(E, Val(false)).L)
    final = (S * inv(Q)') * P'

    ranks = zeros(Int, trials, params)
    for i in 1:params
        ranks[:, i] = ordinalrank(final[:, i])
    end

    return ranks
end

"""
    _get_percentiles(trials::Int)

Generate a list of 'trials' values, one from each of 'trials' equal-size
segments from a uniform distribution. These are used with an RV's ppf
(percent point function = inverse cumulative function) to retrieve the
values for that RV at the corresponding percentiles.
"""
function _get_percentiles(trials::Int)
    segmentSize = 1.0 / trials
    points = rand(Uniform(), trials) * segmentSize + collect(0:trials-1) * segmentSize
    return points
end

"""
    lhs(rvlist::Vector{RandomVariable}, trials::Int; corrmatrix::Union{Matrix{Float64},Nothing}=nothing, asDataFrame::Bool=true)

Produce an array or DataFrame of 'trials' rows of values for the given parameter
list, respecting the correlation matrix 'corrmatrix' if one is specified, using Latin
Hypercube (stratified) sampling.

The values in the i'th column are drawn from the ppf function of the i'th parameter
from rvlist, and each columns i and j are rank correlated according to corrmatrix[i,j].

rvlist: (list of rv-like objects representing parameters) Only requirement
       on parameter objects is that they must implement the ppf function.

trials: (int) number of trials to generate for each parameter.

corrmatrix: a numpy matrix representing the correlation between the parameters.
       corrmatrix[i,j] should give the correlation between the i'th and j'th
       entries of rvlist.

columns: (None or list(str)) Column names to use to return a DataFrame.

skip: (list of params)) Parameters to process later because they are
       dependent on other parameter values (e.g., they're "linked"). These
       cannot be correlated.

Returns DataFrame with `trials` rows of values for the `rvlist`.
"""
function lhs(rvlist::Vector{RandomVariable}, trials::Int;
             corrmatrix::Union{Matrix{Float64},Nothing}=nothing,
             asDataFrame::Bool=true)

    num_rvs = length(rvlist)
    ranks = corrmatrix === nothing ? nothing : _gen_rank_values(num_rvs, trials, corrmatrix)

    samples = zeros(trials, num_rvs)

    for (i, rv) in enumerate(rvlist)
        values = quantile.(Ref(rv.dist), _get_percentiles(trials))  # extract values from the RV for these percentiles

        if corrmatrix === nothing
            shuffle!(values)           # randomize the stratified samples
        else
            indices = ranks[:, i]
            values = values[indices]   # reorder to respect correlations
        end

        samples[:, i] = values
    end

    return asDataFrame ? DataFrame(samples, map(rv->rv.name, rvlist)) : samples
end

function lhs!(mcs::MonteCarloSimulation; corrmatrix::Union{Matrix{Float64},Nothing}=nothing)
    # TBD: verify that any correlated values are actual distributions, not stored vectors?

    trials = mcs.trials
    rvdict = mcs.rvdict
    num_rvs = length(rvdict)
    rvlist = mcs.dist_rvs

    samples = lhs(rvlist, mcs.trials, corrmatrix=corrmatrix, asDataFrame=false)

    for (i, rv) in enumerate(rvlist)
        dist = rv.dist
        name = rv.name
        values = samples[:, i]
        rvdict[name] = RandomVariable(name, SampleStore(values))
    end
    return nothing
end

# TBD: handle this outside lhs since it applies to any linked variables (or drop support for this...)
"""
    lhs_amend!(df::DataFrame, rvlist::Vector{RandomVariable}, trials::Int)

Amend the DataFrame with LHS data by adding columns for the given parameters.
This allows "linked" parameters to refer to the values of other parameters.

df: Generated by prior call to LHS or something similar.
rvlist: The random variables to add to the df
trials: the number of trials to generate for each parameter
"""
function lhs_amend!(df::DataFrame, rvlist::Vector{RandomVariable}, trials::Int, sampling::SamplingOptions=LHS)
    for rv in rvlist
        if sampling == LHS
            values = quantile.(rv.dist, _get_percentiles(trials))  # extract values from the RV for these percentiles
            shuffle!(values)                                       # randomize the stratified samples

        else # sampling == Random
            values = rv.dist.rand(trials)
        end

        df[rv.name] = values
    end
    return nothing
end

"""
    correlation_matrix(mcs::MonteCarloSimulation)

Return a Matrix holding the correlations between random variables
as indicated in the MonteCarloSimulation, or nothing if no correlations
have been defined.

TBD: if needed, compute correlation matrix only for correlated
     RVs, leaving all uncorrelated RVs alone.
"""
function correlation_matrix(mcs::MonteCarloSimulation)
    if length(mcs.corrlist) == 0
        return nothing
    end

    rvdict = mcs.rvdict

    # create a mapping of names to RV position in list
    names = Dict([(rv.name, i) for (i, rv) in enumerate(values(rvdict))])

    count = length(rvdict)
    corrmatrix = Matrix(1.0I, count, count)

    for corr in mcs.corrlist
        n1 = corr.name1
        n2 = corr.name2

        # We don't support correlation between stored samples
        # if rvdict[n1].dist isa SampleStore || rvdict[n2].dist isa SampleStore
        #     error("Correlations with SampleStores is not supported ($n1, $n2)")
        # end

        i = names[n1]
        j = names[n2]

        corrmatrix[i, j] = corrmatrix[j, i] = corr.value
    end

    return corrmatrix
end


#=
Finds the phase over time for a given angle vs time array
using the Hilbert Transform

Kleinfeld and Deschenes 2011
=#
function get_phase(aa;bp_l=8.0,bp_h=30.0,sampling_rate=500.0)
    responsetype = Bandpass(bp_l,bp_h; fs=sampling_rate)
    designmethod=Butterworth(4)
    df1=digitalfilter(responsetype,designmethod)

    #Zero lag filtering to make sure there is no phase shift.
    filter_aa=filtfilt(df1,aa)

    hh=hilbert(filter_aa)

    angle.(hh)
end

function distance_along(x::Array{T,1},y::Array{T,1},i::Int64) where T
    sqrt((y[i]-y[i-1])^2+(x[i]-x[i-1])^2)
end

function total_length(x::Array{T,1},y::Array{T,1}) where T
    s=0.0
    for i=2:length(x)
       s += distance_along(x,y,i)
    end
    s
end

function get_ind_at_dist(x,y,thres)
    ind = length(x)
    s=0.0
    for i=2:length(x)
        s=WhiskerTracking.distance_along(x,y,i) + s
        if s > thres
            ind = i
            break
        end
    end
    ind
end

#=
Angle Calculation
=#
function get_angle(x::Array{T,1},y::Array{T,1},angle_dist=30.0) where T

    s=0.0
    myangle = 0.0
    for i=2:length(x)
        s = distance_along(x,y,i) + s

        if s>angle_dist
            myangle = atan(y[i]-y[1],x[i]-x[1])
            break
        end
    end
    myangle
end

#=
Curvature
=#

function get_curvature(x::Array{T,1},y::Array{T,1},points = 2.0:2.0:(total_length(x,y)-1)) where T

    s_i = zeros(Float64,length(x))
    for i=2:length(x)
       s_i[i]=distance_along(x,y,i) + s_i[i-1]
    end

    itp_x = interpolate((s_i,), x, Gridded(Linear()));
    itp_y = interpolate((s_i,), y, Gridded(Linear()));

    curv = zeros(Float64,length(points))

    for i=1:length(curv)

        x_0 = itp_x(points[i]-1)
        y_0 = itp_y(points[i]-1)

        x_1 = itp_x(points[i])
        y_1 = itp_y(points[i])

        x_2 = itp_x(points[i]+1)
        y_2 = itp_y(points[i]+1)

        num = abs(c_diff(x_2,x_0) * c_diff_2(y_0,y_1,y_2) - (c_diff(y_2,y_0)*c_diff_2(x_0,x_1,x_2)))

        denom = ((c_diff(x_2,x_0))^2 + (c_diff(y_2,y_0))^2) ^(3/2)

        curv[i]=num / denom
    end
    curv
end

function make_whisker_segment(x::Array{T,1},y::Array{T,1},ip1::Real,ip2::Real) where T

    (new_x, new_y) = WhiskerTracking.interpolate_whisker(x,y)

    ip_1 = WhiskerTracking.get_ind_at_dist(new_x,new_y,ip1)
    ip_2 = WhiskerTracking.get_ind_at_dist(new_x,new_y,ip2)

    x_part = new_x[ip_1:ip_2]
    y_part = new_y[ip_1:ip_2]

    (x_part,y_part)
end

function least_squares_quad(x::AbstractArray{T,1},y::AbstractArray{T,1}) where T

    x_mat = zeros(Float64,size(x,1),3)
    for i=1:size(x,1)
        x_mat[i,1] = x[i] * x[i]
        x_mat[i,2] = x[i]
        x_mat[i,3] = 1.0
    end
    coeffs = x_mat \ y
    coeffs[1]
end

function least_squares_quad_rot(x::AbstractArray{T,1},y::AbstractArray{T,1}) where T
   (my_in, rot_mat) = rotate_cov_eigen(x,y)
   sign_out = sign(cross([rot_mat[:,1]; 0.0],[rot_mat[:,2]; 0.0])[3])
   least_squares_quad(my_in[:,1],my_in[:,2])
end

function rotate_cov_eigen(x::AbstractArray{T,1},y::AbstractArray{T,1}) where T

    my_in = [x y]
    cov_mat = cov(my_in)
    rot_mat = eigvecs(cov_mat)
    eig_val = eigvals(cov_mat)

    max_eig = findmax(eig_val)[2]
    min_eig = findmin(eig_val)[2]

    (my_in * rot_mat[:,[max_eig,min_eig]], rot_mat[:,[max_eig,min_eig]])
end

function c_diff(x::Array{T,1},i::Int64) where T
   c_diff(x[i+1],x[i-1])
end

function c_diff(x_2::Float64,x_0::Float64)
   (x_2 - x_0)/2
end

function c_diff_2(x::Array{T, 1},i::Int64) where T
   c_diff_2(x[i-1],x[i],x[i+1])
end

function c_diff_2(x_0::Float64,x_1::Float64,x_2::Float64)
   x_0 - 2*x_1 + x_2
end

function remove_nan_whiskers(xx::Array,yy::Array)

    for i=1:length(xx)
        nan_inds = findall(isnan.(xx[i]).|isnan.(yy[i]))

        deleteat!(xx[i],nan_inds)
        deleteat!(yy[i],nan_inds)
    end
    nothing
end


"""
    parity_sign(P)

Given an array or tuple of integers representing
a permutation or a subset of a permutation,
compute the parity sign defined as -1 for a
permutation consisting of an odd number of swaps
and +1 for an even number of swaps. This
implementation uses an O(n^2) algorithm and is
intended for small permutations only.
"""
function parity_sign(P)::Int
  L = length(P)
  s = +1
  for i=1:L, j=i+1:L
    s *= sign(P[j]-P[i])
  end
  return s
end


# Tests for NEP types functionality

using NonlinearEigenproblemsTest
using NonlinearEigenproblems
using Test,Random

@bench @testset "NEPTypes" begin

    n=3;
    Random.seed!(0);
A1 = rand(n,n); A2 = rand(n,n); A3 = rand(n,n); A4 = rand(n,n);
f1 = S -> one(S); f2 = S -> -S; f3 = S -> exp(-S); f4 = S -> sqrt(S)
nep=SPMF_NEP([A1,A2,A3,A4],[f1,f2,f3,f4]);

f1 = S -> 1
@test_logs (:warn, r".*you have not provided valid matrix-functions.*") begin
    nep = SPMF_NEP([A1,A2,A3,A4], [f1,f2,f3,f4], check_consistency=true)
end

end


_string(::EickerHuberWhite) = "Eicker White Huber variance"

Base.@kwdef struct NaiveLocalLinearRD{K,B,V<:VarianceEstimator} <: SharpRD
    kernel::K
    bandwidth::B
    variance::V = EickerHuberWhite()
end

Base.@kwdef struct FittedLocalLinearRD{R,F,K,B,S,T,C}
    rdd_setting::R
    fitted_lm::F
    fitted_kernel::K
    fitted_bandwidth::B
    data_subset::S
    tau_est::T
    se_est::T
    coeftable::C
end

linearweights(fitted::FittedLocalLinearRD) = linearweights(fitted.fitted_lm)

function linearweights(fitted_lm::RegressionModel; idx = 2)
    wts = fitted_lm.model.rr.wts
    γs = (fitted_lm.model.pp.chol\fitted_lm.model.pp.X'*Diagonal(wts))[idx, :]
    γs
end

function var(::EickerHuberWhite, fitted_lm::RegressionModel)
    γs = linearweights(fitted_lm)
    dot(abs2.(γs), abs2.(residuals(fitted_lm)))
end

fit(method::SharpRD, ZsR::RunningVariable, Y) = fit(method, RDData(Y, ZsR))

function fit(method::NaiveLocalLinearRD, rddata::RDData; level=0.95)
    c = rddata.cutoff
    kernel = method.kernel
    variance = method.variance

    h = bandwidth(method.bandwidth, kernel, rddata)
    fitted_kernel = setbandwidth(kernel, h)

    lb = support(fitted_kernel).lb
    ub = support(fitted_kernel).ub

    new_support = Interval(lb + c, ub + c)

    rddata_filt = rddata[new_support]
    wts = weights(fitted_kernel, rddata_filt.ZsR)

    fitted_lm = fit(LinearModel, @formula(Ys ~ Ws * ZsC), rddata_filt, wts = wts)

    tau_est = coef(fitted_lm)[2]
    se_est = sqrt(var(variance, fitted_lm))
    γs = linearweights(fitted_lm)

    z = tau_est/se_est
    pval = 1-cdf(Normal(), abs(z))
    z_quantile = quantile(Normal(), 1-level/2)
    ci = Interval(tau_est - se_est*z_quantile, tau_est + se_est*z_quantile)
    # as in GLM.jl
    levstr = isinteger(level*100) ? string(Integer(level*100)) : string(level*100)

    res = [h tau_est se_est "unaccounted" z pval leftendpoint(ci) rightendpoint(ci)]
    colnms = ["h"; "τ̂"; "se"; "bias"; "z"; "p-val"; "Lower $levstr%"; "Upper $levstr%"]
    rownms = ["Sharp RD estimand"]
    coeftbl = CoefTable(res, colnms, rownms, 6, 5)

    FittedLocalLinearRD(
        rdd_setting = method,
        fitted_lm = fitted_lm,
        fitted_kernel = fitted_kernel,
        fitted_bandwidth = h,
        data_subset = rddata_filt,
        tau_est = tau_est,
        se_est = se_est,
        coeftable = coeftbl,
    )
end

function Base.show(io::IO, rdd_fit::RegressionDiscontinuity.FittedLocalLinearRD)
    println(io, "Local linear regression for regression discontinuity design")
    println(io, "       ⋅⋅⋅⋅ " * "Naive inference (not accounting for bias)")
    println(io, "       ⋅⋅⋅⋅ " * _string(rdd_fit.rdd_setting.kernel))
    println(io, "       ⋅⋅⋅⋅ " * _string(rdd_fit.rdd_setting.bandwidth))
    println(io, "       ⋅⋅⋅⋅ " * _string(rdd_fit.rdd_setting.variance))
    Base.show(io, rdd_fit.coeftable)
end

@recipe function f(rdd_fit::FittedLocalLinearRD; show_local_support = false)
    sorted_subset = rdd_fit.data_subset |> DataFrame |> df -> sort(df, [:Zs])
    sorted_preds = predict(rdd_fit.fitted_lm, sorted_subset)
    if show_local_support
        lb = support(rdd_fit.fitted_kernel).lb
        ub = support(rdd_fit.fitted_kernel).ub
        @series begin
            label := nothing
            seriestype := :vline
            linecolor := :lightgrey
            inestyle := :dot
            linewidth := 2
            [lb; ub]
        end
    end
    seriestype = :path
    linecolor --> :lightblue
    linewidth --> 2
    label --> "Local linear fit"
    sorted_subset.Zs, sorted_preds
end


using FastTransforms, Compat
using Compat.Test
if VERSION ≥ v"0.7-"
    using FFTW
end

@testset "BigFloat FFT and DCT" begin

    c = collect(range(-big(1.0),stop=1,length=16))
    @test norm(fft(c) - fft(Float64.(c))) < 3Float64(norm(c))*eps()
    @test norm(ifft(c) - ifft(Float64.(c))) < 3Float64(norm(c))*eps()

    c = collect(range(-big(1.0),stop=1.0,length=201))
    @test norm(ifft(fft(c))-c) < 200norm(c)eps(BigFloat)

    p = plan_dct(c)
    @test norm(FastTransforms.generic_dct(c) - p*c) == 0

    pi = plan_idct!(c)
    @test norm(pi*dct(c) - c) < 1000norm(c)*eps(BigFloat)

    @test norm(dct(c)-dct(map(Float64,c)),Inf) < 10eps()

    cc = cis.(c)
    @test norm(dct(cc)-dct(map(Complex{Float64},cc)),Inf) < 10eps()

    c = big.(rand(100)) + im*big.(rand(100))
    @test norm(dct(c)-dct(map(ComplexF64,c)),Inf) < 10eps()
    @test norm(idct(c)-idct(map(ComplexF64,c)),Inf) < 10eps()
    @test norm(idct(dct(c))-c,Inf) < 1000eps(BigFloat)
    @test norm(dct(idct(c))-c,Inf) < 1000eps(BigFloat)

    # Make sure we don't accidentally hijack any FFTW plans
    for T in (Float32, Float64)
        @test plan_fft(rand(BigFloat,10)) isa FastTransforms.DummyPlan
        @test plan_fft(rand(BigFloat,10), 1:1) isa FastTransforms.DummyPlan
        @test plan_fft(rand(Complex{BigFloat},10)) isa FastTransforms.DummyPlan
        @test plan_fft(rand(Complex{BigFloat},10), 1:1) isa FastTransforms.DummyPlan
        @test plan_fft!(rand(Complex{BigFloat},10)) isa FastTransforms.DummyPlan
        @test plan_fft!(rand(Complex{BigFloat},10), 1:1) isa FastTransforms.DummyPlan
        @test !( plan_fft(rand(T,10)) isa FastTransforms.DummyPlan )
        @test !( plan_fft(rand(T,10), 1:1) isa FastTransforms.DummyPlan )
        @test !( plan_fft(rand(Complex{T},10)) isa FastTransforms.DummyPlan )
        @test !( plan_fft(rand(Complex{T},10), 1:1) isa FastTransforms.DummyPlan )
        @test !( plan_fft!(rand(Complex{T},10)) isa FastTransforms.DummyPlan )
        @test !( plan_fft!(rand(Complex{T},10), 1:1) isa FastTransforms.DummyPlan )

        @test plan_ifft(rand(T,10)) isa FFTW.ScaledPlan
        @test plan_ifft(rand(T,10), 1:1) isa FFTW.ScaledPlan
        @test plan_ifft(rand(Complex{T},10)) isa FFTW.ScaledPlan
        @test plan_ifft(rand(Complex{T},10), 1:1) isa FFTW.ScaledPlan
        @test plan_ifft!(rand(Complex{T},10)) isa FFTW.ScaledPlan
        @test plan_ifft!(rand(Complex{T},10), 1:1) isa FFTW.ScaledPlan

        @test plan_bfft(rand(BigFloat,10)) isa FastTransforms.DummyPlan
        @test plan_bfft(rand(BigFloat,10), 1:1) isa FastTransforms.DummyPlan
        @test plan_bfft(rand(Complex{BigFloat},10)) isa FastTransforms.DummyPlan
        @test plan_bfft(rand(Complex{BigFloat},10), 1:1) isa FastTransforms.DummyPlan
        @test plan_bfft!(rand(Complex{BigFloat},10)) isa FastTransforms.DummyPlan
        @test plan_bfft!(rand(Complex{BigFloat},10), 1:1) isa FastTransforms.DummyPlan
        @test !( plan_bfft(rand(T,10)) isa FastTransforms.DummyPlan )
        @test !( plan_bfft(rand(T,10), 1:1) isa FastTransforms.DummyPlan )
        @test !( plan_bfft(rand(Complex{T},10)) isa FastTransforms.DummyPlan )
        @test !( plan_bfft(rand(Complex{T},10), 1:1) isa FastTransforms.DummyPlan )
        @test !( plan_bfft!(rand(Complex{T},10)) isa FastTransforms.DummyPlan )
        @test !( plan_bfft!(rand(Complex{T},10), 1:1) isa FastTransforms.DummyPlan )

        @test plan_dct(rand(BigFloat,10)) isa FastTransforms.DummyPlan
        @test plan_dct(rand(BigFloat,10), 1:1) isa FastTransforms.DummyPlan
        @test plan_dct(rand(Complex{BigFloat},10)) isa FastTransforms.DummyPlan
        @test plan_dct(rand(Complex{BigFloat},10), 1:1) isa FastTransforms.DummyPlan
        @test plan_dct!(rand(Complex{BigFloat},10)) isa FastTransforms.DummyPlan
        @test plan_dct!(rand(Complex{BigFloat},10), 1:1) isa FastTransforms.DummyPlan
        @test !( plan_dct(rand(T,10)) isa FastTransforms.DummyPlan )
        @test !( plan_dct(rand(T,10), 1:1) isa FastTransforms.DummyPlan )
        @test !( plan_dct(rand(Complex{T},10)) isa FastTransforms.DummyPlan )
        @test !( plan_dct(rand(Complex{T},10), 1:1) isa FastTransforms.DummyPlan )
        @test !( plan_dct!(rand(Complex{T},10)) isa FastTransforms.DummyPlan )
        @test !( plan_dct!(rand(Complex{T},10), 1:1) isa FastTransforms.DummyPlan )

        @test plan_idct(rand(BigFloat,10)) isa FastTransforms.DummyPlan
        @test plan_idct(rand(BigFloat,10), 1:1) isa FastTransforms.DummyPlan
        @test plan_idct(rand(Complex{BigFloat},10)) isa FastTransforms.DummyPlan
        @test plan_idct(rand(Complex{BigFloat},10), 1:1) isa FastTransforms.DummyPlan
        @test plan_idct!(rand(Complex{BigFloat},10)) isa FastTransforms.DummyPlan
        @test plan_idct!(rand(Complex{BigFloat},10), 1:1) isa FastTransforms.DummyPlan
        @test !( plan_idct(rand(T,10)) isa FastTransforms.DummyPlan )
        @test !( plan_idct(rand(T,10), 1:1) isa FastTransforms.DummyPlan )
        @test !( plan_idct(rand(Complex{T},10)) isa FastTransforms.DummyPlan )
        @test !( plan_idct(rand(Complex{T},10), 1:1) isa FastTransforms.DummyPlan )
        @test !( plan_idct!(rand(Complex{T},10)) isa FastTransforms.DummyPlan )
        @test !( plan_idct!(rand(Complex{T},10), 1:1) isa FastTransforms.DummyPlan )

        @test plan_rfft(rand(BigFloat,10)) isa FastTransforms.DummyPlan
        @test plan_rfft(rand(BigFloat,10), 1:1) isa FastTransforms.DummyPlan
        @test plan_brfft(rand(Complex{BigFloat},10), 19) isa FastTransforms.DummyPlan
        @test plan_brfft(rand(Complex{BigFloat},10), 19, 1:1) isa FastTransforms.DummyPlan
        @test !( plan_rfft(rand(T,10)) isa FastTransforms.DummyPlan )
        @test !( plan_rfft(rand(T,10), 1:1) isa FastTransforms.DummyPlan )
        @test !( plan_brfft(rand(Complex{T},10), 19) isa FastTransforms.DummyPlan )
        @test !( plan_brfft(rand(Complex{T},10), 19, 1:1) isa FastTransforms.DummyPlan )

    end

end


using EvolvingGraphs.Centrality

g = EvolvingGraph{Node{String}, Int}()
add_edge!(g, "a", "b", 1)
add_edge!(g, "b", "c", 1)
add_edge!(g, "c", "d", 2)
add_edge!(g, "a", "b", 2)
katz(g)
katz(g, 0.3, 0.2, mode = :receive)
katz(g, 0.2, 0.4, mode = :broadcast)
try
    katz(g, 0.3, 0.4, mode = :unknown)
catch ArgumentError
    println("unknown mode")
end


module NeuralNetwork
export NeuralNet, fit!, predict, score, weave!, addLayer!,
       addSigmoidLayer!, FullyConnected, backpropagation,
       feedforward, minibatch, evaluate, addSoftmaxLayer!

using ..Classification: Optimizer, GDContext, SGDContext, NAGDContext,
                        GradientDescent, StochasticGradientDescent,
                        NesterovGradientDescent, partition, postprocess!
import ..Classification: fit!, predict, score, logcrossentropy
using Random: randn
using Statistics: mean
using NPZ: npzwrite, npzread

abstract type Layer end

struct FullyConnected <: Layer
    inputsize::Int
    numneurons::Int
    W::Matrix{Float32}
    b::Vector{Float32}
    f::Function
    f′::Function
    function FullyConnected(inputsize, numneurons, transform::Function, derivative::Function)
        # Normal distributed random numbers with N(0, 0.1)
        weights = 1randn(numneurons, inputsize)
        bias = 1randn(numneurons)
        new(inputsize, numneurons, weights, bias, transform, derivative)
    end
end

function feed(layer::FullyConnected, a)
    layer.W * a .+ layer.b
end

function newsize(layer::FullyConnected, newsize)::FullyConnected
    FullyConnected(newsize, layer.numneurons, layer.f, layer.f′)
end

struct NeuralNet
    layers::Vector{Layer}
    ∇cost::Function
    NeuralNet(layers::Vector{Layer}, ∇f::Function=MSE′) = new(layers, ∇f)
end
NeuralNet(layer::Layer) = NeuralNet([layer], MSE′)
NeuralNet() = NeuralNet(Layer[])

function NeuralNet(neurons::Vector{Int})
    net = NeuralNet()
    if length(neurons) > 0
        # First layer keeps the input 0 until an input is given
        push!(net.layers, FullyConnected(0, neurons[1], σ, ∇σ))
        for numneurons in neurons[2:end]
            addSigmoidLayer!(net, numneurons)
        end
    end
    net
end

function NeuralNet(architecture::AbstractArray{Tuple{Int, Symbol}})
    layers = Layer[]
    numneurons, activation = architecture[1]
    a, ∇a = getactivation(activation)
    push!(layers, FullyConnected(0, numneurons, a, ∇a))
    for i in 2:length(architecture)
        input, _ = architecture[i-1]
        numneurons, activation = architecture[i]
        a, ∇a = getactivation(activation)
        push!(layers, FullyConnected(input, numneurons, a, ∇a))
    end
    NeuralNet(layers)
end

function getactivation(f::Union{Symbol, Function})::Tuple{Function, Function}
    if f == σ || f == :sigmoid
        σ, ∇σ
    elseif f == relu || f == :relu
        relu, ∇relu
    elseif f == leakyrelu || f == :leakyrelu
        leakyrelu, ∇leakyrelu
    elseif f == :id
        x -> x, x -> one(x)
    else
        throw(KeyError("Activation $f not supported"))
    end
end

function addLayer!(net::NeuralNet, numneurons, transform::Function, derivative::Function)
    inputsize = try
        net.layers[end].numneurons
    catch
        0
    end
    layer = FullyConnected(inputsize, numneurons, transform, derivative)
    push!(net.layers, layer)
end

function addSigmoidLayer!(net::NeuralNet, numneurons)
    addLayer!(net, numneurons, σ, ∇σ)
end

function addReluLayer!(net::NeuralNet, numneurons)
    addLayer!(net, numneurons, relu, ∇relu)
end

function addLeakyReluLayer!(net::NeuralNet, numneurons)
    addLayer!(net, numneurons, leakyrelu, ∇leakyrelu)
end

function weave!(net, X, y)
    # X has dimension #predictors×#samples
    if length(net.layers) == 0
        push!(net.layers, FullyConnected(size(X, 2), 1, σ, ∇σ))
    else
        net.layers[1] = newsize(net.layers[1], size(X, 1))
    end
end

function backpropagation(net::NeuralNet, X, y)
    ∇b = [zeros(Float32, size(layer.b)) for layer in net.layers]
    ∇W = [zeros(Float32, size(layer.W)) for layer in net.layers]

    # Forward propagation
    a = X
    as = [a]
    zs = []
    for layer in net.layers
        z = layer.W * a .+ layer.b
        a = layer.f.(z)
        push!(zs, z)
        push!(as, a)
    end

    # Backpropagation
    # Handle final layer L
    ∇aᴸC = net.∇cost(as[end] |> softmax, y)
    δ = ∇aᴸC .* net.layers[end].f′.(zs[end])
    ∇b[end] .= δ
    ∇W[end] .= δ*as[end-1]'

    # Handle the remaining layers
    for l in 1:(length(net.layers)-1)
        ∂a∂z = net.layers[end-l].f′.(zs[end-l])
        δ = net.layers[end-l+1].W' * δ .* ∂a∂z
        ∇b[end-l] .= δ
        ∇W[end-l] .= δ * as[end-l-1]'
    end
    ∇b, ∇W
end

function feedforward(net::NeuralNet, X)
    for layer in net.layers
        X = layer.f.(layer.W*X .+ layer.b)
    end
    X .= softmax(X)
    X
end

function evaluate(net::NeuralNet, X, labels)::Float32
    ŷ = predict(net, X)
    @assert size(ŷ) == size(labels)
    mean(labels .== ŷ)
end

function evaluate!(net::NeuralNet, o::Optimizer, X, labels)::Nothing
    ŷ = predict(net, X)
    @assert size(ŷ) == size(labels)
    o.loss[o.iterations] = mean(labels .== ŷ)

    !o.hasvalidationset && return

    ŷ = predict(net, o.validationset[1])
    o.validationloss[o.iterations] = mean(o.validationset[2]' .== ŷ)
    return
end

function logcrossentropy(net::NeuralNet, X, y)
    ŷ = feedforward(net, X)
    loss = logcrossentropy(y, ŷ)
end

function predict(net::NeuralNet, X)::Matrix{Int}
    probabilities = feedforward(net, X)
    return [imax[1] - 1 for imax in argmax(probabilities, dims=1)]
end

function MSE(net::NeuralNet, X, y)
    ŷ = feedforward(net, X)
    mean((y .- ŷ).^2)
end

function MSE′(ŷ, y)
    ŷ .- y
end

"""
    σ(x)

Sigmoid activation function
"""
σ(x::Real) = one(x)/(one(x) + exp(-x))
∇σ(x::Real) = σ(x)*(one(x) - σ(x))

"""
    relu(x)

Rectified Linear Unit activation function
"""
relu(x::Real) = max(zero(x), x)
∇relu(x::Real) = if x ≥ zero(x) one(x) else zero(x) end

"""
    leakyrelu(x)

Leaky Rectified Linear Unit activation function
"""
leakyrelu(x::Real, a = oftype(x, 0.01)) = max(a*x, x)
∇leakyrelu(x::Real, a = oftype(x, 0.01)) = if x ≥ zero(x) one(x) else a end

function softmax(xs::AbstractArray; dims=1)
    max_ = maximum(xs, dims=dims)
    exp_ = exp.(xs .- max_)
    exp_ ./ sum(exp_, dims=dims)
end

end


import Random123
using KernelAbstractions.Extras
using FastClosures

"""
Return a tuple of 4 random UInt32
Uses the philox random CBRNG 4x32_10
"""
function randbits_4x32(key::UInt64, ctr1::UInt64, ctr2::UInt64)::NTuple{4, UInt32}
    r= Random123.Philox4x{UInt32, 10}(0,0,0,0,
                                    key%UInt32, UInt32(key>>>32),
                                    ctr1%UInt32, UInt32(ctr1>>>32),
                                    ctr2%UInt32, UInt32(ctr2>>>32), 0)
    Random123.random123_r(r)
end

"""
Return a tuple of 2 random UInt64
Uses the philox random CBRNG 4x32_10
"""
function randbits_2x64(key::UInt64, ctr1::UInt64, ctr2::UInt64)::NTuple{2, UInt64}
    x= randbits_4x32(key, ctr1, ctr2)
    (UInt64(x[1])|UInt64(x[2])<<32, UInt64(x[3])|UInt64(x[4])<<32)
end

"""
Return a tuple of 2 normal random Float64
Uses the philox random CBRNG 4x32_10 and box-muller transform
"""
function randn_2x64(key::UInt64, ctr1::UInt64, ctr2::UInt64)::NTuple{2, Float64}
    x= randbits_2x64(key, ctr1, ctr2)
    a= signed(x[1]) * 2.0^-63 # uniform [-1,1]
    b= x[2] * 2.0^-64 + 2.0^-65 # uniform (0,1]
    r= sqrt(-2*log(b))
    r .* sincos(a*pi)
end

"""
Return a tuple of 4 normal random Float32
Uses the philox random CBRNG 4x32_10 and box-muller transform
"""
function randn_4x32(key::UInt64, ctr1::UInt64, ctr2::UInt64)::NTuple{4, Float32}
    x= randbits_4x32(key, ctr1, ctr2)
    a= signed(x[1]) * 2.0f0^-31 # uniform [-1,1]
    b= x[2] * 2.0f0^-32 + 2.0f0^-33 # uniform (0,1]
    r= sqrt(-2*log(b))
    n1= r*sin(a*pi)
    n2= r*cos(a*pi)
    a= signed(x[3]) * 2.0f0^-31 # uniform [-1,1]
    b= x[4] * 2.0f0^-32 + 2.0f0^-33 # uniform (0,1]
    r= sqrt(-2*log(b))
    n3= r*sin(a*pi)
    n4= r*cos(a*pi)
    (n1,n2,n3,n4)
end

"""
Return a tuple of n normal random Float32, incrementing ctr2 for each 4.
Uses the philox random CBRNG 4x32_10 and box-muller transform
"""
function randn_32(key::UInt64, ctr1::UInt64, ctr2::UInt64, n::Val{N}) where N
    ntuple(@closure(i -> @inbounds(randn_4x32(key, ctr1, ctr2 + (i-1)÷4)[(i-1)%4+1])),n)
end


#We  are using this to make the Cd v. Velocity Plots
#import necessary libraries

using Plots

function air_Density_Calculation(h)
    temp = 59 - .00356 * h;         #h = altitude(ft), temp = Fereinheidt.

    rho = 2116 * [(temp + 459.7)/ 518.6]^5.256;
    #Density as a function of tempurature. Only of temp below 36152 ft.

    return rho;

end
function root_Mean_Chord(c_r, c_t)
    c_mac = (2/3) * (c_r + c_t + ((c_r*c_t)/(c_r+c_t)));
    return c_mac
end

function form_Factor_Calc(t_c, c_mac, Delta)
    #requires tip chord, root mean Chord, and sweep angle (Delta)
    #returns value of k

    Z = 2*cos(Delta)
    k = 1 + Z*(t_c/c_mac) + 100*(t_c/c_mac)^4

end
#Function Needs: Speed, Altitude, Surface Area, Tip Chord, Root Mean Chord, Sweep Angle, and mu.
function parasitic_Drag_Calculation(V_inf, q_inf, S_wet, tip2chord, c_mac, Delta, mu)
    #S_Wet = 2*(1+ 0.2*(tip2chord)) * S_wing;  #S_Wing passed in vector.
    #q_inf = (1/2) * p_inf * (V_inf^2);
    k = form_Factor_Calc(t, c, Delta);
    #Pass root chord and tip chord lengths, Only needed for Cessna, so don't do it here.
    RE = (p_inf * V_inf * c)/(mu);
    C_f = 1.328/sqrt(RE);           #Laminar flow, we won't worry about turbulent flow.

    #Use atmospheric caluculator online to find specific values for mu, T, etc.

    #mu is passed on vector, c is the mean aerodynamic chord of the aircraft.
    #mu is the dynamic viscocity of the fluid.

    #fineness ratio, fr, is the ratio of an airplanes length to its width.c

    #D_p = k * C_f * q_inf * S_Wet
    #q_inf = (1/2) * p_inf * (V_inf)^2
    #RE = (p*V*c) / (mu)
    #   p = fluid density
    #   mu = fluid viscocity

    D_p = k * C_f * q_inf * S_Wet;  #So far only need k and C_f

    return D_p;
end
function induced_Drag_Calculation(L, v_inf, h, )
    p_inf

    D_i = L^2 / (q_inf * pi * b^2 * e_inv)
end
function main()
    #We need 2 values, induced drag and parasitic drag.
    #If plane cruises above Mach 0.3 (>230 mph), include Compressiblity Drag

    #Choose 2 planes whose values you mimic. One should be glider esq. (high aspect ratio) and the other more like a fighter jet (low aspect ratio)

    #Cessna 172R
        # Aspect Ratio: 7.32 (according to wikipedia)
        # Length: 27 ft, 2 in
        # Wing Span: 36 ft, 1 in.
        # Surface Area: 174 ft^2, S_wing
        # Aspect Ratio: 4.24153... (according to my calculations)
        # Cruising Speed: 140 mph (top 188 mph)
        # Stalling speed: 54 mph
        # Loaded Wight: 2450 lb
        # Cruising Alt: 6000 ft., mu = 3.6162e-7   lb-s / ft^2
        # Overall Length: 26 ft, 11 in.

    #Vought V-173, Flying Pancake (oldmachinepress.com)
        # Span: 23 ft, 4 in.
        # Surface Area: 427 ft^2, S_wing
        # Aspect Ratio = 1.275....
        # Max Speed: 138 mph
        # Cruising Speed: 75 mph
        # Stalling Speed:
        # Cruising Altitude: 28000 ft., mu = 3.1510e-7  lb-s / ft^2
        # Root mean chord: SurfaceArea / Span

        # Loaded Weight = 3050 lb

        #Calculate the root mean chord, c, before calling drag functions.

        rho = air_Density_Calculation(h);
        S_Wet = 2*(1+ 0.2*(tip2chord)) * S_wing;  #S_Wing passed in vector.
        q_inf = (1/2) * p_inf * (V_inf^2);

        mu = 3.1510e-7; #28000 ft.
        # mu = 3.6162e-7; #6000 ft.

    #Parasitic Drag:
        D_p = parasitic_Drag_Calculation(velocity, rho, S_Wet, tip2chord, c_mac, delta, mu);
        #D_p = k * C_f * q_inf * S_wet
        #k = Pressure drag constant
        #C_f = Skin Friction drag constant
        #q_inf = Wind speed over wing
        #S_wet = 2*(1+ 0.2*(t/c)) * S_wing
        # c_mean is the root mean chord.
        # t_c is the thickness to chord ratio, as a decimal-percent

        #Function Needs: Speed, Altitude, Surface Area, Tip Chord, Root Mean Chord, Sweep Angle, and mu.

    #Induced Drag
        df = 1;                         #Diameter of the fuselage.
        e_inv = 0.98 *(1-(2*(d_f/b)^2))
        D_i = induced_Drag_Calcuation();
        #D_i = L^2 / (q_inf * pi * b^2 * e_inv)
        #C_di = C_l^2 / (pi * AR * e_inv)

        #e_inv(Invicide span efficiency) = 0.98 * [1-2*(d_f/b)^2]
        #d_f = diameter of the fuselage
        #L is lift, which is just equal to the weightof the aircraft.

        #D = D_p + D_i

        #Velocity range (0, cruising speed), Do this to start
        #Find the point at which the two drags equalize, this is the minimum
        # value we want
end


struct AAProperties <: AbstractVector{Float64}
    charge::Float64
    hydropathy::Float64   # from https://www.sciencedirect.com/science/article/pii/0022283682905150?via%3Dihub & https://en.wikipedia.org/wiki/Amino_acid
    volume::Float64       # from http://proteinsandproteomics.org/content/free/tables_1/table08.pdf (van der Waals volume in Å³)
end
Base.size(v::AAProperties) = (3,)
Base.getindex(v::AAProperties, i::Int) = i == 1 ? v.charge :
                                         i == 2 ? v.hydropathy :
                                         i == 3 ? v.volume : Base.throw_boundserror(v, i)
const aa_properties = Dict(
    Residue('A') => AAProperties(0, 1.8, 67),
    Residue('R') => AAProperties(1, -4.5, 148),
    Residue('N') => AAProperties(0, -3.5, 96),
    Residue('D') => AAProperties(-1, -3.5, 91),
    Residue('C') => AAProperties(0, 2.5, 86),
    Residue('Q') => AAProperties(0, -3.5, 114),
    Residue('E') => AAProperties(-1, -3.5, 109),
    Residue('G') => AAProperties(0, -0.4, 48),
    Residue('H') => AAProperties(0.1, -3.2, 118),   # at pH 7.4, His has a 10% chance of being charged
    Residue('I') => AAProperties(0, 4.5, 124),
    Residue('L') => AAProperties(0, 3.8, 124),
    Residue('K') => AAProperties(1, -3.9, 135),
    Residue('M') => AAProperties(0, 1.9, 124),
    Residue('F') => AAProperties(0, 2.8, 135),
    Residue('P') => AAProperties(0, -1.6, 90),
    Residue('S') => AAProperties(0, -0.8, 73),
    Residue('T') => AAProperties(0, -0.7, 93),
    Residue('W') => AAProperties(0, -0.9, 163),
    Residue('Y') => AAProperties(0, -1.3, 141),
    Residue('V') => AAProperties(0, 4.2, 105),
)

# z-score the amino acid features using the full covariance matrix
featμ = sum(p for (r, p) in aa_properties)/length(aa_properties)
featC = sum((Δp = p - featμ; Δp*Δp') for (r, p) in aa_properties)/(length(aa_properties)-1)
featChol = cholesky(featC)

const aa_properties_zscored = Dict(r => SVector{3}(featChol.L \ (p - featμ)) for (r, p) in aa_properties)


using Distributions
using Turing
using Turing: gradient_forward, invlink, link, realpart
using ForwardDiff
using ForwardDiff: Dual
using Test

# Define model
@model ad_test_3() = begin
  v ~ Wishart(7, [1 0.5; 0.5 1])
  v
end
# Turing.TURING[:modelex]
# Call Turing's AD
# The result out is the gradient information on R
ad_test_3_f = ad_test_3()
vi = ad_test_3_f()
vvn = collect(Iterators.filter(vn -> vn.sym == :v, keys(vi)))[1]
_v = map(d -> realpart(d), vi[vvn])
_, grad_Turing = gradient_forward(vi[nothing], vi, ad_test_3_f)

dist_v = Wishart(7, [1 0.5; 0.5 1])

# Hand-written logp
function logp3(x::Vector)
  v = [x[1] x[3]; x[2] x[4]]
  lp = Turing.logpdf_with_trans(dist_v, v, false)
  lp
end

# Call ForwardDiff's AD
g = x -> ForwardDiff.gradient(logp3, vec(x));
# _s = link(dist_v, _s)
grad_FWAD = -g(_v)

# Compare result
@test grad_Turing ≈ grad_FWAD atol=1e-9


"""
Physical constants

The definition and default values are:

- `K₀ = -273.15`: absolute zero (°C)
- `R = 8.314`: universal gas constant (``J\\ mol^{-1}\\ K^{-1}``).
- `Rd = 287.0586`: gas constant of dry air (``J\\ kg^{-1}\\ K^{-1}``).
- `Dₕ₀ = 21.5e-6`: molecular diffusivity for heat at base temperature, applied in the integrated form of the
Fick’s Law of diffusion (``m^2\\ s^{-1}``). See eq. 3.10 from Monteith and Unsworth (2013).
- `Cₚ = 1013.0`: Specific heat of air at constant pressure (``J\\ K^{-1}\\ kg^{-1}``), also
known as efficiency of impaction of particles. See Allen et al. (1998), or Monteith and
Unsworth (2013). NB: bigleaf R package uses 1004.834 intead.
- `ε = 0.622`: ratio of molecular weights of water vapor and air. See Monteith and
Unsworth (2013).
- `λ₀ = 2.501`: latent heat of vaporization for water at 0 degree (``J\\ kg^{-1}``).
- `σ = 5.670373e-08` [Stefan-Boltzmann constant](https://en.wikipedia.org/wiki/Stefan%E2%80%93Boltzmann_law)
in (``W\\ m^{-2}\\ K^{-4}``).
- `Gbₕ_to_Gbₕ₂ₒ = 1.075`: conversion coefficient from conductance to heat to conductance to water
vapor.
- `Gsc_to_Gsw = 1.57`: conversion coefficient from stomatal conductance to CO₂ to conductance to water
vapor.
- `Gbc_to_Gbₕ = 1.32`: conversion coefficient from boundary layer conductance to CO₂ to heat.
- `Mₕ₂ₒ = 18.0e-3` (kg mol-1): Molar mass for water.

# References

Allen, Richard G., Luis S. Pereira, Dirk Raes, et Martin %J Fao Smith. 1998.
« Crop evapotranspiration-Guidelines for computing crop water requirements-FAO Irrigation
and drainage paper 56 » 300 (9): D05109.

Monteith, John, et Mike Unsworth. 2013. Principles of environmental physics: plants,
animals, and the atmosphere. Academic Press.
"""
Base.@kwdef struct Constants{T}
    K₀::T = -273.15
    R::T = 8.314
    Rd::T = 287.0586
    Dₕ₀::T = 21.2e-6
    Cₚ::T = 1013.0
    ε::T = 0.622
    λ₀::T = 2.501e6
    σ::T = 5.670373e-08
    Gbₕ_to_Gbₕ₂ₒ::T = 1.075
    Gsc_to_Gsw::T = 1.57
    Gbc_to_Gbₕ::T = 1.32
    Mₕ₂ₒ::T = 18.0e-3
    J_to_umol::T = 4.57
end


export Scheffer

struct Scheffer <: LakeModel end

@with_kw_noshow mutable struct SchefferParameters <: LakeParameters
    @deftype Float64
    nutrients = 0.7 # current nutrient level
    ib = 2e-5 # g⋅m⁻²⋅day⁻¹ immigration rate of bream (9e-3 g⋅m⁻²⋅year⁻¹)
    ip = 2e-5 # g⋅m⁻²⋅day⁻¹ immigration rate of pike (9e-3 g⋅m⁻²⋅year⁻¹)
    r = 7.5e-3 # day⁻¹ maximum growth rate of bream (2.74 year⁻¹)
    H₁ = 0.5 # half saturation constant
    H₂ = 10 # % half saturation constant
    H₃ = 20 # g⋅m⁻² half saturation constant
    H₄ = 15 # g⋅m⁻² half saturation constant
    cb = 7.5e-5 # m⁻²⋅g⁻¹⋅day⁻¹ intraspecific competition constant for bream (0.0274 m⁻²⋅g⁻¹⋅year⁻¹)
    cp = 2.75e-4 # m⁻²⋅g⁻¹⋅day⁻¹ intraspecific competition constant for pike (0.1 m⁻²⋅g⁻¹⋅year⁻¹)
    prmax = 5e-2 # day⁻¹ maximum predation rate of pike (18.25 year⁻¹)
    ce = 0.1 # pike food conversion efficiency to growth
    mp = 2.25e-3 # day⁻¹ mortality rate of pike (0.82 year⁻¹)
    K = 100 # % maximum vegetation coverage
end

function lake_dynamics!(du, u, p::SchefferParameters, t)
    #TODO: New intervention dynamics not considered for Scheffer model
    B, P = u # g⋅m⁻² bream/pike density

    V = p.K * (p.H₃^2 / (p.H₃^2 + B^2)) # % of lake covered vegetation
    FR = B^2 / (B^2 + p.H₄^2) # fuctional response of pike

    du[1] =
        dB =
            p.ib + p.r * (p.nutrients / (p.nutrients + p.H₁)) * B - p.cb * B^2 -
            p.prmax * FR * P
    du[2] = dP = p.ip + p.ce * p.prmax * FR * P * (V / (V + p.H₂)) - p.mp * P - p.cp * P^2
end

function lake_initial_state(
    nutrients::Float64,
    bream::Float64,
    pike::Float64,
    ::Type{Scheffer};
    kwargs...,
)
    ([bream, pike], SchefferParameters(; nutrients = nutrients, kwargs...))
end

LakeParameters(::Type{Scheffer}, nutr; kwargs...) = SchefferParameters(; nutrients = nutr, kwargs...)

preset_conditions(::Type{Clear}, ::Type{Scheffer}) = (0.7, [20.0, 1.8])
preset_conditions(::Type{Turbid}, ::Type{Scheffer}) = (2.5, [84.0, 0.04])

function Base.show(io::IO, ::MIME"text/plain", p::SchefferParameters)
    println(io, "Parameters for lake dynamics (Scheffer) model):")
    println(io, "Nutrient level: $(p.nutrients)")
    println(io, "Immigration rate (g⋅m⁻²⋅day⁻¹) for bream: $(p.ib), pike: $(p.ip)")
    println(
        io,
        "Growth rate (day⁻¹) for bream: $(p.r), Predation rate (day⁻¹) of pike: $(p.prmax)",
    )
    println(
        io,
        "Half sturation constants: H₁ $(p.H₁), H₂ $(p.H₂)%, H₃ $(p.H₃) g⋅m⁻², H₄ $(p.H₄) g⋅m⁻²",
    )
    println(
        io,
        "Intraspecific competition constant (g⋅m⁻²⋅day⁻¹) for bream: $(p.cb), pike: $(p.cp)",
    )
    println(
        io,
        "Pike food conversion efficiency to growth: $(p.ce), Mortalitiy rate (day⁻¹): $(p.mp)",
    )
    println(io, "Maximum vegetation coverage: $(p.K)%")
end