@testset "pwc_density" begin
    # test a few specific examples
    @testset "examples" begin
        @test pwc_density([0.]) == [0.0, 0.0]
        @test pwc_density([0., 1.]) == [0.0, 1.0, 0.0]
        @test pwc_density([0, 1, 3]) == [0.0, 0.5, 0.25, 0.0]
    end

    # test 1-S pwc_density for sum == 1
    @testset "1-S sum == 1" begin
        @testset for len in [2, 5, 10, 50, 100, 500, 1000]
            x = sort(randn(len))
            d = pwc_density(x)
            @test sum(d[2:end-1] .* diff(x)) ≈ 1.  atol=100eps()
        end
    end

    # test 2-S pwc_density for sum == 1
    @testset "2-S sum == 1" begin
        @testset for len in [2, 5, 10, 50, 100, 500, 1000]
            x = ArrayPartition((sort(randn(len)) for _ in 1:2)...)
            d = pwc_density(x.x...)
            for s in 1:2
                @test sum(d[s][s, 1, 2:end] .* diff(x.x[s])) ≈ 1.  atol=100eps()
            end
        end
    end

    # test left/right consistency of `pwc_density` with 2 species
    @testset "left/right" begin
        @testset for len in [2, 3, 10, 100]
            x = ArrayPartition((sort(randn(len)) for _ in 1:2)...)
            d = pwc_density(x.x...)
            for s in 1:2
                @test d[s][s, 1, 2:end] == d[s][s, 2, 1:end-1]
            end
        end
    end

    # test consistency of `pwc_density` with 1 and 2 species
    @testset "1-S/2-S" begin
        @testset for len in [2, 3, 10, 100]
            x = ArrayPartition((sort(randn(len)) for _ in 1:2)...)
            d = pwc_density(x.x...)
            for s in 1:2
                @test d[s][s, 1, 2:end] == pwc_density(x.x[s])[2:end-1]
            end
        end
    end
end

@testset "pwc_densities" begin
    # test left/right consistency of `pwc_densities` with N species
    @testset "left/right" begin
        @testset for N in 1:5
            @testset for len in [2, 3, 10, 100]
                x = ArrayPartition((sort(randn(len)) for _ in 1:N)...)
                d = pwc_densities(x.x...)
                for s in 1:N
                    @test d[s][s, 1, 2:end] == d[s][s, 2, 1:end-1]
                end
            end
        end
    end

    # test N-S pwc_densities for sum == 1
    @testset "sum == 1" begin
        @testset for N in 1:5
            @testset for len in [2, 5, 10, 50, 100, 500, 1000]
                x = ArrayPartition((sort(randn(len)) for _ in 1:N)...)
                d = pwc_densities(x.x...)
                for s in 1:N
                    @test sum(d[s][s, 1, 2:end] .* diff(x.x[s])) ≈ 1.  atol=100eps()
                end
            end
        end
    end

    # test consistency of `pwc_densities` with 1 and N species
    @testset "1-S/N-S" begin
        @testset for N in 1:5
            @testset for len in [2, 3, 10, 100]
                x = ArrayPartition((sort(randn(len)) for _ in 1:N)...)
                d = pwc_densities(x.x...)
                for s in 1:N
                    @test d[s][s:s, :, :] == pwc_densities(x.x[s])[1]
                end
            end
        end
    end
end

# test 1-S/2-S pwc_density == pwc_densities
@testset "pwc_density == pwc_densities" begin
    @testset "1-S" begin
        @testset for len in [2, 3, 10, 100]
            x = sort(randn(len))
            @test pwc_density(x)[2:end-1] == pwc_densities(x)[1][1, 1, 2:end]
        end
    end

    @testset "2-S" begin
        @testset for len in [2, 3, 10, 100]
            x = ArrayPartition((sort(randn(len)) for _ in 1:2)...)
            @test pwc_density(x.x...) == pwc_densities(x.x...)
        end
    end

    @testset "2-S (common points)" begin
        common = collect(-1:0.1:1)
        @testset for lengths in [(2,3), (5,10), (50,100)]
            x = ArrayPartition((sort(vcat(randn(l), common)) for l in lengths)...)
            @test pwc_density(x.x...) == pwc_densities(x.x...)
        end
    end
end


using IterTools, ProgressBars
using JLD
using LaTeXStrings
using PyPlot

include("synthetic.jl")

SEED    = 42
Random.seed!(SEED)

RUNS    = 1
EPOCHS  = 1000
T       = n * EPOCHS

# synthetic d-dimensional least squares problem
wgen = rand(d);

# Quasi-random distributions
qrngs = Dict([("IID Uniform"     , UniformSample()),
              ("Sobol"           , SobolSample())])
qrng_names = collect(keys(qrngs))

function gen_syn_data_all()
    gaussian_samples = Dict()
    # for each distribution, pre-generate the stream of examples
    # to be used in T iterations of online SGD
    # for a specified number of runs
    iter_qrngs = ProgressBar(qrngs)
    for (qrng_name, ~) in iter_qrngs
        println(iter_qrngs, "generating data for $qrng_name...")

        data = []
        iter_runs = ProgressBar(1:RUNS)
        for run in iter_runs
            println(iter_runs, "working on run $run")
            data_run = gen_syn_data(T, qrng_name, d+1, SEED+run)
            push!(data, data_run)
        end
        gaussian_samples[qrng_name] = data
    end
    return gaussian_samples
end

function grad_errors(T::Int64, wtau::Vector{Float64}, fullgrad::Vector{Float64},
             samples::Vector{Any}, ordering::Vector{Int64})
    errs = zeros(T)
    acc = zeros(d)
    for m in 1:T
        (xi, yi) = samples[ordering[m]]
        acc += (xi'wtau - yi) * xi
        errs[m] = norm(acc/m - fullgrad)^2
    end
    return errs
end

println("generating data...")
samples_online = gen_syn_data_all()

println("begin computing gradient errors...")
result = Dict()

w0 = zeros(d)
for qrng_name in qrng_names
    err_list = zeros(RUNS, T)

    for run in 1:RUNS
        samples = samples_online[qrng_name][run]
        full_grad = w0 - wgen
        err_list[run, :] = grad_errors(T, w0, full_grad, samples, collect(1:T))
    end
    result[qrng_name] = err_list
end

println("completed")
println("plotting...")

figure(figsize = (6,6))
for qrng_name in ["IID Uniform", "Sobol"]
    err_list = result[qrng_name]
    err_mean = mean(err_list, dims=1)'[1000:1000:T]

    loglog(collect(1000:1000:T), err_mean, label=qrng_name)
end

legend(loc="lower left", fontsize=18)

xlabel(L"m", fontsize=20)
ylabel(L"\Vert \frac{1}{m}\Sigma_{t=0}^{m-1}\nabla R(w_0; x_t) - \nabla R(w_0)\Vert^2", fontsize=20)
title("Online", fontsize=24)
xticks(fontsize=20)
yticks(fontsize=20)

ax = gca()
locmin = matplotlib.ticker.LogLocator(base=10.0, subs=(0.25,0.5,0.75), numticks=50)
ax.yaxis.set_minor_locator(locmin)
ax.yaxis.set_minor_formatter(matplotlib.ticker.NullFormatter())

tight_layout()
plot_fn = joinpath(pwd(),"figs", "qmc-graderr-online.pdf")
savefig(plot_fn)
println("plot saved at $plot_fn")


@testset "Scaler" begin

    S = randn(5, 100)
    scaler = StandardScaler(S)
    Ss = scaler * S
    @test isapprox(mean(Ss, dims = 2), zeros(5), atol = 10 * eps())
    @test isapprox(std(Ss, dims = 2), ones(5))
    Si = scaler \ Ss
    @test isapprox(S, Si)
    S = [-5.0 5.0]
    scaler = MinMaxScaler(S)
    Ss = scaler * S
    @test isapprox([0.0 1.0], Ss)
    @test isapprox(S, scaler \ Ss)
    scaler = MinMaxScaler([-10.0], [10.0])
    Ss = scaler * S
    @test isapprox([0.25 0.75], Ss)
    @test isapprox(S, scaler \ Ss)
end


@testset "Embeddings" begin
  Qx, x = QQ["x"]
  K, a = number_field(x^3 - 2, "a")
  @test (1, 1) == @inferred signature(K)
  emb = @inferred complex_embeddings(K)
  @test (@inferred number_field(emb[1])) === K
  @test length(emb) == 3
  @test length(real_embeddings(K)) == 1
  @test real_embeddings(K) == real_embeddings(K)
  @test complex_embeddings(K) === complex_embeddings(K) # caching

  @test @inferred isreal(emb[1])
  @test @inferred !isreal(emb[2])
  @test @inferred is_imaginary(emb[2])
  @test conj(emb[1]) === emb[1]
  @test conj(emb[2]) === emb[3]
  @test conj(emb[3]) === emb[2]

  for e in emb
    @test sprint(show, MIME"text/plain"(), e) isa String
    @test sprint(show, e) isa String
  end

  for e in emb
    for p in [32, 64, 128, 256]
      @test Hecke.radiuslttwopower(e(a, p), -p)
      @test Hecke.radiuslttwopower(evaluate(a, e, p), -p)
    end
  end

  @test overlaps(conj(emb[2](a)), (conj(emb[2]))(a))

  k, b = number_field(x - 1)
  f = hom(k, K, one(K))
  for e in emb
    length(unique([restrict(e, f) for e in emb])) == 1
  end

  ee = complex_embeddings(k)[1]
  @test length(@inferred extend(ee, f)) == 3

  ee = restrict(emb[1], f)
  c = ee(b)
  @test overlaps(c, one(parent(c)))

  z = rand(K, -10:10)^100
  for p in [32, 128, 256, 1024]
    for e in emb
      fn = evaluation_function(e, p)
      @test Hecke.radiuslttwopower(fn(z), -p)
      c = e(z, p)
      @test Hecke.radiuslttwopower(c, -p)
    end
  end

end


using Statistics
using Formatting
using Crayons.Box
using NumericalDataManipulation.Common
using NumericalDataManipulation.CommonMath

struct DataGrid
    knots::Vector{Float64}
    Δmin::Float64
    Δmax::Float64
    Δmean::Float64
    Δlog10::Float64
    Δ𝐆::Float64
    Δknots::Vector{Float64}
    function DataGrid(knots::Vector{Float64})
        @assert(!isempty(knots), "Data grid cannot be empty.")
        Δknots, Δmin, Δmax, Δmean, Δlog10, Δ𝐆 = give_Δknots(knots)
        grid = new(knots, Δmin, Δmax, Δmean, Δlog10, Δ𝐆, Δknots)
    end
    function DataGrid(range::StepRange)
        @assert(issorted(range), "The range must be sorted in an ascending order.")
        knots = collect(range)
        Δknots, Δmin, Δmax, Δmean, Δlog10, Δ𝐆 = give_Δknots(knots)
        grid = new(knots, Δmin, Δmax, Δmean, Δlog10, Δ𝐆, Δknots)
    end
end
function Base.show(io::IO, grid::DataGrid)
    compact = get(io, :compact, false)

    knots = grid.knots; a = knots[1]; b = knots[end]; N = length(knots)
    Δmin = grid.Δmin
    Δmax = grid.Δmax
    Δmean = grid.Δmean
    Δlog10 = grid.Δlog10
    Δ𝐆 = grid.Δ𝐆
    Δknots = grid.Δknots

    if compact
        printfmt(io, "$(LIGHT_GRAY_FG("DataGrid([{1:.3e}, {2:.3e}]($N), Δlog10 = {3:.6e}, Δmin = {4:.6e}, Δmax = {5:.3e}, Δmean = {6:.5e}, Δ𝐆 = {7:.6e})"))",
            a, b, Δlog10, Δmin, Δmax, Δmean, Δ𝐆)
    else
        printfmt(io, "$(LIGHT_GRAY_FG("DataGrid([{1:.3e}, {2:.3e}]($N), Δlog10 = {3:.6e}, Δmin = {4:.6e}, Δmax = {5:.3e}, Δmean = {6:.5e}, Δ𝐆 = {7:.6e})"))",
            a, b, Δlog10, Δmin, Δmax, Δmean, Δ𝐆)
    end

end
function Base.show(io::IO, ::MIME"text/plain", grid::DataGrid)
    show(io, grid)
end

function give_Δknots(knots::Vector{Float64})
    @assert(length(knots) ≥ 3, "Grid must contain at least 3 knots.")
    @assert(issorted(knots), "The knots must be sorted in an ascending order.")
    Δknots = knots[2:end] - knots[1:end-1]
    Δmin = minimum(Δknots)
    Δmax = maximum(Δknots)
    Δmean = mean(Δknots)
    Δ𝐆 = mean_log10_golden_lower(Δmin, Δmax)
    Δlog10 = mean_log10(Δmin, Δmax)
    return Δknots, Δmin, Δmax, Δmean, Δlog10, Δ𝐆
end


using PyPlot, transD_GP, Random, LinearAlgebra

## model
ρbg        = 1000
zfixed     = [-1e5,   0,   (20:20:200)...]
rho        = [1e12,   ρbg, ρbg*ones(10)...]
ianom      = 5
rho[ianom] = 10.
δ          = 1e-4  # fd step size in log10

## Gauss Newton settings
use_fd          = false # in the one step for Gauss Newton
startfromcloser = false  # whether to start midway between anomaly and bg or plain bg in G-N step
λ²              = 5e-17 # model update damping

##  geometry and frequencies for VMD in frequency domain
nFreqsPerDecade     = 5
freqLowLimit        = 1e-3
freqHighLimit       = 1e5
freqs = 10 .^(log10(freqLowLimit):1/nFreqsPerDecade:log10(freqHighLimit))
rRx = 100.
zRx = -37.
zTx = -35.
nkᵣeval = 50
modelprimary = true

Fvmd = transD_GP.AEM_VMD_HMD.HFieldDHT(
                      calcjacobian = true,
                      nmax = length(zfixed),
                      zTx    = zTx,
                      rRx    = rRx,
                      freqs  = freqs,
                      zRx    = zRx,
                      nkᵣeval = nkᵣeval,
                      modelprimary = modelprimary)
transD_GP.AEM_VMD_HMD.getfieldFD!(Fvmd, zfixed, rho)

## plot
f, ax = plt.subplots(1,3, sharey=true, figsize=(8,6))
delz, delf = diff(zfixed[2:end]), diff(log10.(freqs))
flabel = [(log10.(freqs) .- delf[1]/2)..., log10.(freqs)[end]+delf[end]/2]
zlab = [zfixed[2:end]..., zfixed[end]+delz[end]]
ax[1].step(log10.(rho[2:end]), zfixed[2:end])
im2 = ax[2].pcolormesh(flabel, zlab, abs.(Fvmd.HFD_z_J[2:end,:]), shading="auto")
cb2 = colorbar(im2, ax=ax[2])
im3 = ax[3].pcolormesh(flabel, zlab, log10.(abs.(Fvmd.HFD_z_J[2:end,:])))
cb2 = colorbar(im3, ax=ax[3])
ax[1].set_xlabel("log10 Ω-m")
ax[1].invert_xaxis()
ax[2].invert_yaxis()
ax[2].set_xlabel("log10 Hz")
ax[3].set_xlabel("log10 Hz")
ax[1].set_ylabel("Depth m")
ax[2].set_title("∂f/∂(log10σ)")
ax[3].set_title("∂f/∂(log10σ)")

## approximate partials with central differences and compare
J = copy(Fvmd.HFD_z_J)

function getJ!(Fin, zin, ρ, Δ)
    # sensitivities computed in log10σ
    # same as in AEM_VMD_HMD code
    Jout = similar(Fin.HFD_z_J)
    modelfd = copy(ρ)
    jflag = Fin.calcjacobian
    Fin.calcjacobian = false
    for i = 2:length(zin)
        modelfd[:] = ρ
        modelfd[i] = 10 .^-(-log10(ρ[i]) .+ Δ/2)
        transD_GP.AEM_VMD_HMD.getfieldFD!(Fin, zin, modelfd)
        HFD_z1 = copy(Fin.HFD_z)
        modelfd[i] = 10 .^-(-log10(ρ[i]) .- Δ/2)
        transD_GP.AEM_VMD_HMD.getfieldFD!(Fin, zin, modelfd)
        Jout[i,:] = (HFD_z1 - Fin.HFD_z) / Δ
    end
    Fin.calcjacobian = jflag
    Jout
end
J_fd = getJ!(Fvmd, zfixed, rho, δ)

## comparison plots
f, ax = plt.subplots(1,3, sharey=true, figsize=(10,6))
ax[1].step(log10.(rho[2:end]), zfixed[2:end])
im2 = ax[2].pcolormesh(flabel, zlab, abs.(J[2:end,:]))
cb2 = colorbar(im2, ax=ax[2])
im3 = ax[3].pcolormesh(flabel, zlab, abs.(J_fd[2:end,:]))
cb2 = colorbar(im3, ax=ax[3])
ax[1].set_xlabel("log10 Ω-m")
ax[1].invert_xaxis()
ax[2].invert_yaxis()
ax[2].set_xlabel("log10 Hz")
ax[3].set_xlabel("log10 Hz")
ax[1].set_ylabel("Depth m")
ax[2].set_title("∂f/∂(log10σ) analytic")
ax[3].set_title("∂f/∂(log10σ) FD")

## let's try this in an optimisation framework to see if the gradient makes sense
log₁₀σ₀ = -3
mtrue = copy(rho)
m = copy(mtrue)
m[ianom] = startfromcloser ? ρbg+(rho[ianom]-ρbg)/2 : ρbg
transD_GP.AEM_VMD_HMD.getfieldFD!(Fvmd, zfixed, mtrue)
d = copy(Fvmd.HFD_z)
transD_GP.AEM_VMD_HMD.getfieldFD!(Fvmd, zfixed, m)
f = copy(Fvmd.HFD_z)
r = (f-d)
J = use_fd ? getJ!(Fvmd, zfixed, m, δ)[2:end,:] : Fvmd.HFD_z_J[2:end,:]
Δm = -real((J*J' + λ²*I)\(J*r + λ²*I*(-log10.(m[2:end]) .- log₁₀σ₀)))
m2 = vcat(mtrue[1], 10 .^-(-log10.(m[2:end]) + Δm))

## plot everything
figure()
step(log10.(m[2:end]), zfixed[2:end], linewidth=2, label="start model")
step(log10.(m2[2:end]), zfixed[2:end], label="1 step")
step(log10.(mtrue[2:end]), zfixed[2:end], linestyle="--", color="k", label="true model")
gca().invert_yaxis()
gca().invert_xaxis()
legend()
xlabel("log10 ρ")
ylabel("Depth m")


## F updates

"""
    updateF!(M::HePPCATModel,Y,method)

Update the factor matrix in `M` with respect to data `Y` using `method`.
"""
function updateF! end

# Update method: Expectation Maximization
"""
    updateF!(M::HePPCATModel,Y,::ExpectationMaximization)

Expectation maximization update of `F`.
"""
function updateF!(M::HePPCATModel,Y,::ExpectationMaximization)
    n, L = size.(Y,2), length(Y)
    Λ = Diagonal(M.λ)
    Γ = [inv(Λ + M.v[l]*I) for l in 1:L]
    Z = [Γ[l]*sqrt(Λ)*M.U'*Y[l] for l in 1:L]
    num = sum(Y[l]*Z[l]'/M.v[l] for l in 1:L)
    den = sum(Z[l]*Z[l]'/M.v[l] + n[l]*Γ[l] for l in 1:L)

    F = svd((num / den) * M.Vt)
    M.U .= F.U
    M.λ .= F.S.^2
    M.Vt .= F.Vt
    return M
end


pint(s) = parse(Int, s)

max = 0
for a = 1:100, b = 1:100
  global max
  n = BigInt(a) ^ BigInt(b)
  digits = split(string(n), "")
  s = sum(pint, digits)
  if s > max
    max = s
  end
end
println(max)


# Model m4.2s.jl

using Pkg, DrWatson

begin
    using StanQuap
    using StatisticalRethinking
end

begin
    df = CSV.read(sr_datadir("Howell1.csv"), DataFrame; delim=';')
    df = filter(row -> row[:age] >= 18, df);
end;

stan4_2 = "
data {
  int N;
  real<lower=0> height[N];
}
parameters {
  real<lower=0> sigma;
  real<lower=0,upper=250> mu;
}
model {
  // Priors for mu and sigma
  mu ~ normal(178, 0.1);
  sigma ~ uniform(0 , 50);

  // Observed heights
  height ~ normal(mu, sigma);
}
";

data = (N = length(df.height), height = df.height)
init = (mu = 180.0, sigma = 10.0)
q4_2s, m4_2s, o4_2s = stan_quap("m4.2s", stan4_2; data, init);

if q4_2s.converged
  quap4_2s_df = sample(q4_2s)          # DataFrame with samples
  precis(quap4_2s_df)
  post4_2s = read_samples(m4_2s)
  post4_2s |> display
end

# End of m4.2s.jl


@testset "Computing common ring for the PB-reduction" begin
    ode = @ODEmodel(
        x1'(t) = x2(t),
        x2'(t) = a * x1(t),
        y(t) = x1(t)
    )
    ioeqs = find_ioequations(ode)
    pbr = PBRepresentation(ode, ioeqs)
    R, (y_2, y_5, c) = Nemo.PolynomialRing(Nemo.QQ, ["y_2", "y_5", "c"])
    p = y_2^2 + c * y_5
    (r, der) = common_ring(p, pbr)
    @test Set(map(var_to_str, gens(r))) == Set(["y_0", "y_1", "y_2", "y_3", "y_4", "y_5", "c", "a"])

    ode = @ODEmodel(
        x1'(t) = x3(t),
        x2'(t) = a * x2(t),
        x3'(t) = x1(t),
        y1(t) = x1(t),
        y2(t) = x2(t) + u(t)
    )
    ioeqs = find_ioequations(ode)
    pbr = PBRepresentation(ode, ioeqs)
    R, (y1_0, y2_3, u_3) = Nemo.PolynomialRing(Nemo.QQ, ["y1_0", "y2_3", "u_3"])
    p = y1_0 + y2_3 + u_3
    (r, der) = common_ring(p, pbr)
    @test Set([var_to_str(v) for v in gens(r)]) == Set([
        "y1_0", "y1_1", "y1_2", "y1_3", "y1_4",
        "y2_0", "y2_1", "y2_2", "y2_3",
        "u_0", "u_1", "u_2", "u_3", "a"
    ])
end


module Stats

# Backend module for statistics functionality

using Statistics, Distributions, DataFrames

export

    runaboutmedian, eaboutmedian, runupdown, eupdown,

    cluster, mixture, trend, oscillation,

    cpk, ppk

# ------------------------------------------------------------------------------------------------
# Run chart analysis
function runaboutmedian(points::Vector{Real})
    n = 1
    center = median(points)
    for (i,v) in enumerate(points[2:end])
       (v - center) * (points[i] - center) < 0 && (n += 1)
    end
    n
 end

function eaboutmedian(points::Vector{Real})
    N = length(points)
    m = length(findall(x->x > median(points), points))
    n = N - m
    1 + 2 * m * n / N
end

function cluster(points::Vector{Real})
    N = length(points)
    R = runaboutmedian(points)
    E = eaboutmedian(points)
    Z = (R - E) / sqrt((E - 1) * (E - 2) / (N - 1))
    cdf(Normal(0, 1), Z)
end

mixture(data::Vector{Real}) = 1 - cluster(data)

function runupdown(points::Vector{Real})
    n = 1
    df = diff(points)
    prod = df[1] > 0
    for i in df[2:end]
        i  != 0 && (prod * i < 0) && (prod *= -1 ; n += 1)
    end
    n
end

eupdown(points::Vector{Real}) = 2 / 3 * (2 * length(points) - 1)

function trend(points::Vector{Real})
    N = length(points)
    v = runupdown(points)
    Z = (v - (2 * N - 1) / 3) / sqrt((16 * N  - 29) / 90)
    cdf(Normal(0, 1), Z)
end

oscillation(points::Vector{Real}) = 1 - trend(points)

# ---------------------------------------------------------------------------------------------------------
# Process capability/performance index
# cp/pp/z.bench/z.usl/z.lsl/ppm

function cpk(x::Vector{Real}; s::Real = 3, USL::Real = 110, LSL::Real = 90, correct::Bool = true)
    d = length(x) - 1
    μ = mean(x)
    correct ? (sd = std(x) / (sqrt(2 / d) * gamma(d / 2 + 0.5) / gamma(d / 2))) : (sd = std(x))
    min((USL - μ) / s / sd, (μ - LSL) / s / sd)
 end

function cpk(x::Matrix{Real}; s::Real = 3 USL::Real = 110, LSL::Real = 90, correct::Bool = true)
    sd = map(1:size(x, 2)) do i
       std(x[:,i])
    end
    d = size(x, 1) * size(x, 2) - size(x, 2)
    μ = mean(x)
    sd = sum(sd .* (size(x, 1) - 1)) / d
    correct && (sd /= (sqrt(2 / d) * gamma(d / 2 + 0.5) / gamma(d / 2)))
    min((USL - μ) / s / sd, (μ - LSL) / s  / sd)
end

cpk(x::DataFrame; s::Real = 3 USL::Real = 110, LSL::Real = 90, correct::Bool = true) =
    cpk(Matrix{Real}(x); s = s, USL = USL, LSL = LSL, correct = correct)

function ppk(x::Matrix{Real}; s::Real = 3, USL::Real = 110, LSL::Real = 90, correct::Bool = false)
    d = size(x, 1) * size(x, 2) - 1
    μ = mean(x)
    correct ? (sd = std(x) / (sqrt(2 / d) * gamma(d / 2 + 0.5) / gamma(d / 2))) : (sd = std(x))
    min((USL - μ) / s / sd, (μ - LSL) / s / sd)
end

ppk(x::DataFrame; s::Real, USL::Real = 110, LSL::Real = 90, correct::Bool = false) =
    ppk(Matrix{Real}(x), s = s, USL = USL, LSL = LSL, correct = correct)

end


using PowerDynamics
using ForwardDiff
using Distributions:Uniform
import PowerDynamics:rhs
using DifferentialEquations
using LinearAlgebra
using Optim
#using DiffEqCallbacks
include("../PDpatches.jl")
include("system.jl")
include("PowerFlow.jl")

"""
# neuer text?
A method to find random pertubations in the case of systems with algebraic
constraints. There is then a set of valid initial conditions which lies on a so
called constrained manifold.
The system must still lie on this manifold after the random pertubation.
This could be applied to Basin Stability Methods in the case of complexer
Generator Methods which contain algebraic constraints.
"""
function RandPertWithConstrains(pg::PowerGrid, operationpoint, node, interval_V, interval_θ,Q; ProjectionMethod = "Optim")
    #Pi = pg.nodes[node].P # Power of the specific node
    g(θ,i,u) = Q - 3 * i * exp(-1im * θ) * u * exp(-1im * θ) # Constraint, Power Flow Analysis
    z = [operationpoint[node,:θ], operationpoint[node,:iabs],operationpoint[node,:v]]

    #Jacg_imag = ForwardDiff.jacobian(x -> imag(g(x[1],x[2],x[3])), z)
    #Jacg_real = ForwardDiff.jacobian(x -> real(g(x[1],x[2],x[3])), z)

    #Jacg = Jacg_real + 1im .* Jacg_imag  # I could not find a package which is able to handle complex valued function
    Jacg = ForwardDiff.gradient(x -> real(g(x[1],x[2],x[3])), z) .+ 1im .* ForwardDiff.gradient(x -> imag(g(x[1],x[2],x[3])), z)

    kerJacg = nullspace(Matrix(Jacg')) # kernel of the Jacobian, returns orthogonal vectors

    num_basis_vec = size(kerJacg,2)

    Frand = [rand(Uniform(interval_θ[1],interval_θ[2])), rand(Uniform(interval_V[1],interval_V[2]))]

    dz = kerJacg * Frand

    if ProjectionMethod == "Optim"
        p_dash = ProjOptim(dz, g) # Calculates the projection onto M using Optim

    elseif ProjectionMethod == "ODE"
        p_dash = ProjODE(g, node, operationpoint,dz, Q)

    else
        ArgumentError("Please use either Optim or ODE as the ProjectionMethod.")
    end

    PerturbedState = copy(operationpoint) # otherwise operationpoint is overwritten
    PerturbedState[2,:θ] = abs(p_dash[1])
    PerturbedState[2,:u] = p_dash[3]
    return PerturbedState, p_dash
end

"""
Calculates and returns the orthogonal Projection of a vector w on the vector v.
"""
function Projvw(v,w)
    dot(w,v) * v * (1 / norm(v)^2)
end

"""
Calculates the Projection from the Tangetial Room TzM to the the Manifold M by
minimizing the function:
    f(x) = a * |dz-p''| + b * |g(p')|.
Where g(p') is the Constraint at point p'.
Inputs:
        dz: The calculated pertubation inside of TzM
        g: The constraint function
        Q: Reactive Power at the perturbed node
Outputs:
        p_dash: Solution of the ODE problem, a Pertubation on M
"""
function ProjOptim(dz, g::Function; a = 1.0, b = 100.0)
    Proj(x) = a * norm(dz - [x[1] + 1im * x[2], x[3] + 1im * x[4], x[5] + 1im * x[6]]) +
              b * abs(g(x[1] + 1im * x[2], x[3] + 1im * x[4], x[5] + 1im * x[6]))

    x0 = [real(dz[1]), imag(dz[1]),
          real(dz[2]), imag(dz[2]),
          real(dz[3]), imag(dz[3])]

    proj = optimize(Proj,x0)

    minimum = proj.minimizer
    p_dash = [minimum[1] + 1im * minimum[2],
              minimum[3] + 1im * minimum[4],
              minimum[5] + 1im * minimum[6]]

    return p_dash
end

"""
Calculates the Projection from the Tangetial Room TzM to the the Manifold M by
solving a ODE of the form:
    f(x) = y' = - g(dz, y).
Where dz is the result of the pertubation and a point in TzM.
Which could result in a fixed point g(x_v, y*) = 0.
Inputs:
        dz: The calculated pertubation inside of TzM
        g: The constraint function
        Q: Reactive Power at the perturbed node
        endtime: Integration time of the problem
Outputs:
        p_dash: The result of optimization, a Pertubation on M
"""
function ProjODE(g::Function,node,op,dz,Q;endtime = 10.0)
    tspan = (0.0,endtime)

    u0 = [dz[1], dz[2], dz[3], g(dz[1], dz[2], dz[3]), op[node,:ω]]
    vars = powergrid.nodes[node]
    #function constraint(du,u,p,t)
    #    du[4] = - (Q - 3 * u[3] * u[2] * exp(-2im * u[1]))
    #end
    function constraint(du,u,p,t)
        Ω_H = vars.Ω / (2 * vars.H)
        i_c = 1im * u[2] * exp(-1im * u[1])
        e_c = 1im * u[3] * exp(-1im * u[1])
        p = real(u[3] * conj(u[2]))
        e_d = 0                         # simplification from K.Schmietendorf paper
        e_q = imag(e_c)
        i_d = real(i_c)
        i_q = imag(i_c)

        du[1] = u[5]
        du[5] = (vars.P - vars.D * u[5] - p - (vars.X_q_dash - vars.X_d_dash) * i_d * i_q) * Ω_H
        de_q = (1 / vars.T_d_dash) * (vars.E_f - e_q + i_d * (vars.X_d - vars.X_d_dash))
        # -> u = e_q * exp(1im * θ)
        du[3] = de_q * exp(1im * u[1]) + u[3] * 1im * u[5]
        du[4] = u[4] - (Q - 3 * u[3] * u[2] * exp(-2im * u[1]))
    end

    prob = ODEProblem(constraint,u0,tspan)

    sol = solve(prob, Rosenbrock23(autodiff=false), abstol=1e-8,reltol=1e-6,tspan=Inf, force_dtmin = true)
    p_dash = sol[1:3,end]
end

function rhs(pg::PowerGrid)
    network_dynamics(map(construct_vertex, pg.nodes), map(construct_edge, pg.lines), pg.graph)
end

"""
A method to assures that the conditions given from g stay on the contrained
manifold. A DifferentialEquations Callback is used.
This DOES NOT work because of a type missmatch regarding complex numbers
Inputs:
    pg: Power grid, a graph containg nodes and lines
    endtime: Endtime of the simulation
    u0: current state of the powergrid pg
Outputs:
    solution: a PowerGrid solution containg the timeseries and the Powergrid pg
"""
function SimManifoldCd(pg::PowerGrid, Q, u0::State, endtime)
    ode = ODEProblem(rhs(pg), u0.vec, (0.0, endtime), nothing)

    function g(resid, x, e_s, e_d, p, t)
            i = total_current(e_s, e_d) + Y_n * (x[1] + x[2] * im)
            u = x[1] + x[2] * im
            θ = x[3]
            ω = x[4]
            resid[1] = abs(Q) - 3 * abs(i * u * exp((-2im) * θ))
    end

    function g1(resid, x, e_s, e_d, p, t)
        resid[1] = x[2]^2 + x[1]^2 - 2
        resid[2] = 0
    end

    cb = ManifoldProjection(g1)

    dqsol = solve(ode, Rodas5(autodiff = false), save_everystep=false, callback=cb)
    solution = PowerGridSolution(dqsol, pg)
    return solution
end


export Semiring, Probability_semiring, Tropical_semiring, Log_semiring

# A semiring type that you can create. Currently the onus is on the user to
# make sure the semiring makes sense.
immutable Semiring
	addition::Function
	multiplication::Function
	ring_zero::Float64
	ring_one::Float64
end

Probability_semiring = Semiring(+, *, 0, 1)
Tropical_semiring = Semiring(min, +, Inf, 0)
Log_semiring = Semiring((x,y) -> -log(e^-x + e^-y), +, Inf, 0)


### A Pluto.jl notebook ###
# v0.14.8

using Markdown
using InteractiveUtils

# ╔═╡ d09dec61-ab12-4424-bfaf-30f155c90ef9
using Revise

# ╔═╡ 89fcb3a0-db48-11eb-30ac-b501d7fcd55f
using PolaronMobility

# ╔═╡ 4a7ed3cb-88e4-4e86-90c1-285849994ab9
using Plots

# ╔═╡ 8c54eedc-7c8f-4c5d-879e-62743e302801
using DataFrames

# ╔═╡ 3edfd42b-4b8a-4080-b617-740ef3fbf76b
using CSV

# ╔═╡ 27af9453-8044-4b1f-85fd-9e2c5dd104eb
using QuadGK

# ╔═╡ 97aa8c27-f2fe-418c-a662-906dbc126fb6
begin
	ħ = 1.05457162825e-34
	eV = 1.602176487e-19
	me = 9.10938188e-31
	kB =  1.3806504e-23
	ϵ_0 = 8.854E-12
	amu = 1.660_539_066_60e-27
	Ha = 4.35974820e-18
	Bohr = 5.29177249e-11
end

# ╔═╡ d08e66f9-636b-4377-b653-389ee7b78a24
T_range = 100:100:400

# ╔═╡ 50c1a877-807e-498e-87f9-0e909080a452
MAPI= [
# 96.20813558773261 0.4996300522819191
# 93.13630357703363 1.7139631746083817
# 92.87834578121567 0.60108592692181
# 92.4847918585963 0.0058228799414729
# 92.26701437594754 0.100590086574602
# 89.43972834606603 0.006278895133832249
# 46.89209141511332 0.2460894564364346
# 46.420949316788 0.14174282581124137
# 44.0380222871706 0.1987196948553428
# 42.89702947649343 0.011159939465770681
# 42.67180170168193 0.02557751102757614
# 41.46971205834201 0.012555230726601503
# 37.08982543385215 0.00107488277468418
# 36.53555265689563 0.02126940080871224
# 30.20608114002676 0.009019481779712388
# 27.374810898415028 0.03994453721421388
# 26.363055017011728 0.05011922682554448
# 9.522966890022039 0.00075631870522737
4.016471586720514 0.08168931020200264
3.887605410774121 0.006311654262282101
3.5313112232401513 0.05353548710183397
2.755392921480459 0.021303020776321225
2.4380741812443247 0.23162784335484837
2.2490917637719408 0.2622203718355982
2.079632190634424 0.23382298607799906
2.0336707697261187 0.0623239656843172
1.5673011873879714 0.0367465760261409
1.0188379384951798 0.0126328938653956
1.0022960504442775 0.006817361620021601
0.9970130778462072 0.0103757951973341
0.9201781906386209 0.01095811116040592
0.800604081794174 0.0016830270365341532
0.5738689505255512 0.00646428491253749
# 0.022939578929507105 8.355742795827834e-06   # Acoustic modes!
# 0.04882611767873102 8.309858592685e-06
# 0.07575149723846182 2.78248540373041e-05
]

# ╔═╡ e349bf0a-554f-46ae-8e88-8a7b80d0c4cd
# Multiple Phonon Theory

# ╔═╡ 4cd925a7-048a-4696-b2b9-6a6b394e0528
B_scheme = PolaronMobility.HellwarthBScheme(MAPI)

# ╔═╡ b04ccf74-853d-4ab2-886f-5b6c21cc9a51
phonon_mode_freqs = MAPI[:, 1]

# ╔═╡ 4e349b51-0590-4f98-b777-f13db8050b6d
ir_activities = MAPI[:, 2]

# ╔═╡ 1c6fff5f-839a-43e1-acfe-040df2ed4822
ϵ_ionic = [PolaronMobility.ϵ_ionic_mode(f, r, (6.29e-10)^3) for (f, r) in zip(phonon_mode_freqs, ir_activities)]

# ╔═╡ 2f524fa1-c08b-4c11-b05a-e2d016ce6ec0
ϵ_total = sum(ϵ_ionic)

# ╔═╡ 8510da63-37d7-45d0-9012-8dd17745a9e8
α_j = [PolaronMobility.frohlich_α_j(4.5, ϵ_i, ϵ_total, f, 0.12) for (ϵ_i, f) in zip(ϵ_ionic, phonon_mode_freqs)]

# ╔═╡ d9fca06f-ee9c-4790-a059-98667dcd8428
begin
	multi_data = DataFrames.DataFrame(
			alpha = α_j,
			phonon_freqs = phonon_mode_freqs,
			ir_activities = ir_activities,
			ionic = ϵ_ionic
	    )
	CSV.write("multi_data.csv", multi_data)
end

# ╔═╡ 04e16581-ca4f-4456-bea2-2e8bf44a08f8
α_eff = sum(α_j)

# ╔═╡ ba626ebc-d7da-4eca-8149-907d724a2ef5
var_params = [PolaronMobility.multi_variation(i, 4.5, 0.12, (6.29e-10)^3, MAPI; N = 1) for i in T_range]

# ╔═╡ af26a6c8-9c97-4130-bd54-c9fe40e58879
[var_params[i][1] for i in 1:length(T_range)]

# ╔═╡ dd41fbf9-b4b9-46b9-abbf-96eddf59b0ad
 v_j = [var_params[i][1][j] for j in 1:length(MAPI[:, 1]), i in 1:length(T_range)]

# ╔═╡ 2975a6b8-bcec-4e4e-be94-5a3897c650db
begin
	multi_v = DataFrames.DataFrame([[0.0, [i for i in T_range]...]'; [phonon_mode_freqs v_j]], :auto)
	CSV.write("multi_v.csv", multi_v)
end

# ╔═╡ 748d6916-6125-4f9a-adbf-cee3b25dc3ba
w_j = [var_params[i][2][j] for j in 1:length(MAPI[:, 1]), i in 1:length(T_range)]

# ╔═╡ a304f008-b3fa-4a92-a8c1-9ce2f7b4b3df
begin
	multi_w = DataFrames.DataFrame([[0.0, [i for i in T_range]...]'; [phonon_mode_freqs w_j]], :auto)
	CSV.write("multi_w.csv", multi_w)
end

# ╔═╡ 5652b186-0e8d-469c-bfea-bf665f75c06a
F_j = [PolaronMobility.multi_free_energy(var_params[i][1][j, :], var_params[i][2][j, :], T_range[i], 4.5, 0.12, (6.29e-10)^3, MAPI, j) for j in 1:length(MAPI[:, 1]), i in 1:length(T_range)]

# ╔═╡ 2e0eb8f9-3b6b-4464-b715-258c96e22422
begin
	multi_energy = DataFrames.DataFrame([[0.0, [i for i in T_range]...]'; [phonon_mode_freqs F_j]], :auto)
	CSV.write("multi_energy.csv", multi_energy)
end

# ╔═╡ 143b1c45-ce4b-4f3c-9674-ff181c64fe90
F_total = [sum(F_j[:, i]) for i in 1:length(T_range)]

# ╔═╡ 3900999b-fdfa-48ae-afd4-bca5d03008e3
Ω_range = 0.01:0.1:20.01

# ╔═╡ 30d37afa-7b70-479f-8dcc-860af723a96e
β_red = [ħ / kB / T * f * 2π * 1e12 for f in MAPI[:, 1], T in T_range]

# ╔═╡ 9c140cb6-286b-4e25-a3dc-829b1eea9dfb
begin
	multi_beta = DataFrames.DataFrame([[0.0, [i for i in T_range]...]'; [phonon_mode_freqs β_red]], :auto)
	CSV.write("multi_beta.csv", multi_beta)
end

# ╔═╡ 65a76ecd-1dcd-4d5c-88be-1b57d31369da
function h_i(i, v, w) # some vector relating to harmonic eigenmodes
    h = v[i]^2 - w[i]^2
    if length(v) > 1
        for j in 1:length(v)
            if j != i
                h *= (w[j]^2 - v[i]^2) / (v[j]^2 - v[i]^2)
            end
        end
    end
    return h
end

# ╔═╡ 6a793986-844a-4a82-84b0-b71a48aa8242
function D_j(τ, β, v, w) # log of dynamic structure factor for polaron
    D = τ * (1 - τ / β)
    for i in 1:length(v)
        if v[i] != w[i]
        D += (h_i(i, v, w) / v[i]^2) * (2 * sinh(v[i] * τ / 2) * sinh(v[i] * (β - τ) / 2) / (v[i] * sinh(v[i] * β / 2)) - τ * (1 - τ / β))
        end
    end
    return D
end

# ╔═╡ 146bcc43-15fc-45de-bb9b-f16a68d84e41
function multi_memory_function(Ω, β, α, v, w, f, m_eff)

    m_e = 9.10938188e-31; # Electron Mass (kg)
    eV = 1.602176487e-19; # Electron Volt (kg m^2 s^{-2})

    # FHIP1962, page 1009, eqn (36).
    S(t, β_j, v_j, w_j) = cos(t - 1im * β_j / 2) / sinh(β_j / 2) / D_j(-1im * t, β_j, v_j, w_j)^(3 / 2)

    # FHIP1962, page 1009, eqn (35a). Scale Frequency Ω by phonon branch frequency f_j.
    integrand(t, β_j, v_j, w_j, Ω) = (1 - exp(-1im * Ω * t)) * imag(S(t, β_j, v_j, w_j))

    impedence = 0.0
    for j in 1:length(f) # sum over phonon branches
        impedence += 1im * (Ω - 2 * α[j] * (f[j])^2 * quadgk(t -> integrand(t, β[j], v[j], w[j], Ω * 2π / f[j]), 0.0, Inf)[1] / (3 * √π * Ω))
    end
    return impedence / eV * m_eff * m_e * 1e12 / 100^2
end

# ╔═╡ 3c7564b0-b9fe-49e8-9ad7-eaa80e9841a7
begin
	Z = Array{ComplexF64}(undef, length(Ω_range), length(T_range))
	for j in 1:length(T_range), i in 1:length(Ω_range)
		println("Ω: $(Ω_range[i]), T: $(T_range[j])")
		Z[i, j] = multi_memory_function(Ω_range[i], β_red[:, j], α_j, var_params[j][1], var_params[j][2], [i for i in phonon_mode_freqs], 0.12)
		CSV.write("multi_conductivity_data.csv", DataFrames.DataFrame([[0.0, [i for i in T_range]...]'; [Ω_range 1 ./ Z]], :auto))
	end
end

# ╔═╡ 0c424ddb-aecd-4fc6-9f26-54bb1fc9f099
Z

# ╔═╡ fff9cb8d-00aa-426c-bb66-4b7b21871c36
Plots.plot(Ω_range, real.(1 ./ Z), minorgrid = true, label = hcat(["T = $i K" for i in T_range]...), yaxis = :log)

# ╔═╡ 8ace5afe-29da-4bd1-9599-b51c03b67b6e
Plots.plot(Ω_range, imag.(1 ./ Z), minorgrid = true, label = hcat(["T = $i K" for i in T_range]...))

# ╔═╡ fab3ad29-61f6-45dd-b63f-4ea40c960afb
Plots.plot(Ω_range, abs.(1 ./ Z), minorgrid = true, label = hcat(["T = $i K" for i in T_range]...))

# ╔═╡ 3fb0f337-c6dd-4f56-a694-00d1c2f403bd
# Hellwarth Theory

# ╔═╡ 53117290-63b5-469c-93a9-bab8a40b2658
# B Scheme

# ╔═╡ a6b7c896-04d8-4704-8173-e8895ff84cbd
α_hellwarth_B = PolaronMobility.frohlichalpha(4.5, 24.1, B_scheme * 1e12, 0.12)

# ╔═╡ 6d322516-d247-4bd0-bcd5-9b8371b47750
β_red_hellwarth_B = [ħ * 2π / kB / T * B_scheme * 1e12 for T in T_range]

# ╔═╡ ae756cc9-f4ca-4073-abff-c56ecd1fdade
var_B = PolaronMobility.feynmanvw.(α_hellwarth_B, β_red_hellwarth_B)

# ╔═╡ 4c90b122-1bb9-460e-ac68-608ca8074a50
v_B = [i[1] for i in var_B]

# ╔═╡ 83068836-36d9-42aa-9867-7555d89e0d42
w_B = [i[2] for i in var_B]

# ╔═╡ 8082688f-ae0f-4aad-9bb9-71852be0fe00
Hellwarth_energy_B = F.(v_B, w_B, β_red_hellwarth_B, α_hellwarth_B) .* 1e3 .* ħ .* 2π .* B_scheme .* 1e12 ./ eV

# ╔═╡ 466354b5-7d93-4e2b-ab6d-f7a01112fbfb
begin
	B_data = DataFrames.DataFrame(
			alpha = [α_hellwarth_B for i in 1:length(T_range)],
			efffreq = [B_scheme for i in 1:length(T_range)],
			temp = T_range,
			beta = β_red_hellwarth_B,
			v = v_B,
			w = w_B,
			F = Hellwarth_energy_B
		)
	CSV.write("B_data.csv", B_data)
end

# ╔═╡ b1cf4fe2-8dda-4882-b39f-f23adb2738d1
begin
	ZB = Array{ComplexF64}(undef, length(Ω_range), length(T_range))
	for j in 1:length(T_range), i in 1:length(Ω_range)
		println("Ω: $(Ω_range[i]), T: $(T_range[j])")
		ZB[i, j] = multi_memory_function(Ω_range[i], β_red_hellwarth_B[j], [α_hellwarth_B], v_B[j], w_B[j], [B_scheme / 2π], 0.12) * 2π
		CSV.write("B_conductivity_data.csv", DataFrames.DataFrame([[0.0, [i for i in T_range]...]'; [Ω_range 1 ./ ZB]], :auto))
	end
end

# ╔═╡ a15e4d30-9ed2-492a-bfa4-0608180738c1
ZB

# ╔═╡ 2015d16c-e9f8-4072-834b-a317223b854f
Plots.plot(Ω_range, real.(1 ./ ZB), minorgrid = true, label = hcat(["T = $i K" for i in T_range]...), yaxis = :log)

# ╔═╡ 313a5d22-9a70-4c20-acb2-402c1683fbf8
Plots.plot(Ω_range, imag.(1 ./ ZB), minorgrid = true, label = hcat(["T = $i K" for i in T_range]...))

# ╔═╡ b8061ceb-1610-481b-986b-c5b82b9678db
Plots.plot(Ω_range, abs.(1 ./ ZB), minorgrid = true, label = hcat(["T = $i K" for i in T_range]...))

# ╔═╡ c7cdee43-9ac6-490b-9466-367a1f136766
# A Scheme

# ╔═╡ bb33a5c2-555b-4078-b278-90e5ae864fed
A_scheme = reverse([PolaronMobility.HellwarthAScheme(MAPI, T = i) for i in T_range])

# ╔═╡ 44604f2b-e5fd-438d-87df-0f10f874d00d
α_hellwarth_A = PolaronMobility.frohlichalpha.(4.5, 24.1, A_scheme .* 1e12, 0.12)

# ╔═╡ e858791b-e869-490f-9af3-2826f480d722
β_red_hellwarth_A = [ħ * 2π / kB / T_range[i] * A_scheme[i] * 1e12 for i in 1:length(T_range)]

# ╔═╡ 1f47a9e8-c2de-4963-ad58-aca71dbbb2ac
var_A = PolaronMobility.feynmanvw.(α_hellwarth_A, β_red_hellwarth_A)

# ╔═╡ 105665d5-ba53-4ddd-86df-dc40d5fefa8f
v_A = [i[1] for i in var_A]

# ╔═╡ 4468bf47-7668-4309-8f67-59fee2a1cf43
w_A = [i[2] for i in var_A]

# ╔═╡ 4bae3659-666b-4ae7-924a-b20d98a43148
Hellwarth_energy_A = F.(v_A, w_A, β_red_hellwarth_A, α_hellwarth_A) .* 1e3 .* ħ .* 2π .* A_scheme .* 1e12 ./ eV

# ╔═╡ 6e03cbc9-c6cd-440b-8652-fa1be4d7934a
begin
	A_data = DataFrames.DataFrame(
		alpha = α_hellwarth_A,
		efffreq = A_scheme,
		temp = T_range,
		beta = β_red_hellwarth_A,
		v = v_A,
		w = w_A,
		F = Hellwarth_energy_A
    )
	CSV.write("A_data.csv", A_data)
end

# ╔═╡ 53ac6ee6-f5d1-4923-9352-9d1cd96817fc
begin
	ZA = Array{ComplexF64}(undef, length(Ω_range), length(T_range))
	for j in 1:length(T_range), i in 1:length(Ω_range)
		println("Ω: $(Ω_range[i]), T: $(T_range[j])")
		ZA[i, j] = multi_memory_function(Ω_range[i], β_red_hellwarth_A[j], α_hellwarth_A[j], v_A[j], w_A[j], A_scheme[j] / 2π, 0.12) * 2π
		CSV.write("A_conductivity_data.csv", DataFrames.DataFrame([[0.0, [i for i in T_range]...]'; [Ω_range 1 ./ ZA]], :auto))
	end
end

# ╔═╡ 9ec88954-f8a6-4ae0-a800-3723de537d10
ZA

# ╔═╡ e129c967-858e-46cb-b6ce-29c238f45277
Plots.plot(Ω_range, real.(1 ./ ZA), label = hcat(["T = $i K" for i in T_range]...), minorgrid = true)

# ╔═╡ c8cdaa0e-05cb-4b9a-99dc-6932312ec56e
Plots.plot(Ω_range, imag.(1 ./ ZA), label = hcat(["T = $i K" for i in T_range]...), minorgrid = true)

# ╔═╡ 0f1066e0-d13a-4e1d-b9c0-c51509f25eb6
Plots.plot(Ω_range, abs.(1 ./ ZA), label = hcat(["T = $i K" for i in T_range]...), minorgrid = true)

# ╔═╡ Cell order:
# ╠═d09dec61-ab12-4424-bfaf-30f155c90ef9
# ╠═89fcb3a0-db48-11eb-30ac-b501d7fcd55f
# ╠═4a7ed3cb-88e4-4e86-90c1-285849994ab9
# ╠═8c54eedc-7c8f-4c5d-879e-62743e302801
# ╠═3edfd42b-4b8a-4080-b617-740ef3fbf76b
# ╠═97aa8c27-f2fe-418c-a662-906dbc126fb6
# ╠═d08e66f9-636b-4377-b653-389ee7b78a24
# ╠═50c1a877-807e-498e-87f9-0e909080a452
# ╠═e349bf0a-554f-46ae-8e88-8a7b80d0c4cd
# ╠═4cd925a7-048a-4696-b2b9-6a6b394e0528
# ╠═b04ccf74-853d-4ab2-886f-5b6c21cc9a51
# ╠═4e349b51-0590-4f98-b777-f13db8050b6d
# ╠═1c6fff5f-839a-43e1-acfe-040df2ed4822
# ╠═2f524fa1-c08b-4c11-b05a-e2d016ce6ec0
# ╠═8510da63-37d7-45d0-9012-8dd17745a9e8
# ╠═d9fca06f-ee9c-4790-a059-98667dcd8428
# ╠═04e16581-ca4f-4456-bea2-2e8bf44a08f8
# ╠═ba626ebc-d7da-4eca-8149-907d724a2ef5
# ╠═af26a6c8-9c97-4130-bd54-c9fe40e58879
# ╠═dd41fbf9-b4b9-46b9-abbf-96eddf59b0ad
# ╠═2975a6b8-bcec-4e4e-be94-5a3897c650db
# ╠═748d6916-6125-4f9a-adbf-cee3b25dc3ba
# ╠═a304f008-b3fa-4a92-a8c1-9ce2f7b4b3df
# ╠═5652b186-0e8d-469c-bfea-bf665f75c06a
# ╠═2e0eb8f9-3b6b-4464-b715-258c96e22422
# ╠═143b1c45-ce4b-4f3c-9674-ff181c64fe90
# ╠═3900999b-fdfa-48ae-afd4-bca5d03008e3
# ╠═30d37afa-7b70-479f-8dcc-860af723a96e
# ╠═9c140cb6-286b-4e25-a3dc-829b1eea9dfb
# ╠═27af9453-8044-4b1f-85fd-9e2c5dd104eb
# ╠═65a76ecd-1dcd-4d5c-88be-1b57d31369da
# ╠═6a793986-844a-4a82-84b0-b71a48aa8242
# ╠═146bcc43-15fc-45de-bb9b-f16a68d84e41
# ╠═3c7564b0-b9fe-49e8-9ad7-eaa80e9841a7
# ╠═0c424ddb-aecd-4fc6-9f26-54bb1fc9f099
# ╠═fff9cb8d-00aa-426c-bb66-4b7b21871c36
# ╠═8ace5afe-29da-4bd1-9599-b51c03b67b6e
# ╠═fab3ad29-61f6-45dd-b63f-4ea40c960afb
# ╠═3fb0f337-c6dd-4f56-a694-00d1c2f403bd
# ╠═53117290-63b5-469c-93a9-bab8a40b2658
# ╠═a6b7c896-04d8-4704-8173-e8895ff84cbd
# ╠═6d322516-d247-4bd0-bcd5-9b8371b47750
# ╠═ae756cc9-f4ca-4073-abff-c56ecd1fdade
# ╠═4c90b122-1bb9-460e-ac68-608ca8074a50
# ╠═83068836-36d9-42aa-9867-7555d89e0d42
# ╠═8082688f-ae0f-4aad-9bb9-71852be0fe00
# ╠═466354b5-7d93-4e2b-ab6d-f7a01112fbfb
# ╠═b1cf4fe2-8dda-4882-b39f-f23adb2738d1
# ╠═a15e4d30-9ed2-492a-bfa4-0608180738c1
# ╠═2015d16c-e9f8-4072-834b-a317223b854f
# ╠═313a5d22-9a70-4c20-acb2-402c1683fbf8
# ╠═b8061ceb-1610-481b-986b-c5b82b9678db
# ╠═c7cdee43-9ac6-490b-9466-367a1f136766
# ╠═bb33a5c2-555b-4078-b278-90e5ae864fed
# ╠═44604f2b-e5fd-438d-87df-0f10f874d00d
# ╠═e858791b-e869-490f-9af3-2826f480d722
# ╠═1f47a9e8-c2de-4963-ad58-aca71dbbb2ac
# ╠═105665d5-ba53-4ddd-86df-dc40d5fefa8f
# ╠═4468bf47-7668-4309-8f67-59fee2a1cf43
# ╠═4bae3659-666b-4ae7-924a-b20d98a43148
# ╠═6e03cbc9-c6cd-440b-8652-fa1be4d7934a
# ╠═53ac6ee6-f5d1-4923-9352-9d1cd96817fc
# ╠═9ec88954-f8a6-4ae0-a800-3723de537d10
# ╠═e129c967-858e-46cb-b6ce-29c238f45277
# ╠═c8cdaa0e-05cb-4b9a-99dc-6932312ec56e
# ╠═0f1066e0-d13a-4e1d-b9c0-c51509f25eb6


#normalized root mean square error
function nrms(res, w)
    @assert length(res) == length(w)
    return sqrt( chi2(res, w)/length(res) )
end

# chi square
function chi2(res, w)
    @assert length(res) == length(w)
    return sum(w .* (res.^2))
end

# B and C matrices from Milani and Gronchi (2010)
function BHC(res, w, npar)
    nobs = length(res)
    B_mat = Matrix{TaylorN{Float64}}(undef, nobs, npar)
    H_mat = Array{TaylorN{Float64}}(undef, nobs, npar, npar)
    C_mat = Array{TaylorN{Float64}}(undef, npar, npar)
    for i in 1:nobs
        B_mat[i,:] .= TaylorSeries.gradient(res[i])
    end
    for i in 1:nobs
        for j in 1:npar
                H_mat[i,j,:] .= TaylorSeries.gradient(B_mat[i,j])
        end
    end
    sqrtw_B = sqrt.(w) .* B_mat
    C_mat .= (sqrtw_B') * sqrtw_B
    return B_mat, H_mat, C_mat
end

# transpose(ξ)*H matrix, with weights
function ξTH(w, res, H_mat, npar)
    ξTHv = Array{Float64}(undef, npar, npar)
    for j in 1:npar
        for i in 1:npar
           ξTHv[i,j] = (w .* res)' * (H_mat[:,i,j])
        end
    end
    return ξTHv
end

# differential corrections subroutine for least-squares fitting
function diffcorr(res, w, x0, niters=5)
    @assert length(res) == length(w)
    nobs = length(res)
    npar = length(x0)
    B_mat, H_mat, C_mat = BHC(res, w, npar)
    D_mat = B_mat' * (w .* res)
    # ξTH_mat = ξTH(w, res, H_mat, npar)
    x_new = x0
    for i in 1:niters
        D = D_mat(x_new)
        C = C_mat(x_new) #.+ ξTH_mat(x_new)
        Δx = - inv(C)*D
        x_new = x_new .+ Δx
        @show sqrt(((Δx')*(C*Δx))/npar)
    end
    C = C_mat(x_new)
    Γ = inv(C)
    return x_new, Γ
end

# Newton method subroutine for least-squares fitting
function newtonls(res, w, x0, niters=5)
    @assert length(res) == length(w)
    nobs = length(res)
    npar = length(x0)
    Q = chi2(res, w)/nobs
    x_new = x0
    for i in 1:niters
        dQ = TaylorSeries.gradient(Q)(x_new)
        d2Q = TaylorSeries.hessian(Q, x_new)
        Δx = - inv(d2Q)*dQ
        x_new = x_new + Δx
        C = d2Q/(2/nobs) # C = d2Q/(2/m)
        @show sqrt(((Δx')*(C*Δx))/npar)
    end
    C = TaylorSeries.hessian(Q, x_new)/(2/nobs) # C = d2Q/(2/m)
    Γ = inv(C)
    return x_new, Γ
end

function newtonls_Q(Q, nobs, x0, niters=5)
    npar = length(x0)
    x_new = x0
    for i in 1:niters
        dQ = TaylorSeries.gradient(Q)(x_new)
        d2Q = TaylorSeries.hessian(Q, x_new)
        Δx = - inv(d2Q)*dQ
        x_new = x_new + Δx
        C = d2Q/(2/nobs) # C = d2Q/(2/m)
        @show sqrt(((Δx')*(C*Δx))/npar)
    end
    C = TaylorSeries.hessian(Q, x_new)/(2/nobs) # C = d2Q/(2/m)
    Γ = inv(C)
    return x_new, Γ
end

# specialized version of newtonls on 6 variables for parametrized orbit determination wrt A2 nongrav coefficient
function newtonls_6v(res, w, x0, niters=5)
    @assert length(res) == length(w)
    nobs = length(res)
    npar = 6 # length(x0)
    Q = chi2(res, w)/nobs
    x_new = x0
    for i in 1:niters
        dQ = TaylorSeries.gradient(Q)(x_new)[1:6]
        d2Q = TaylorSeries.hessian(Q, x_new)[1:6,1:6]
        Δx = - inv(d2Q)*dQ
        x_new[1:6] = x_new[1:6] + Δx
        C = d2Q/(2/nobs) # C = d2Q/(2/m)
        @show sqrt(((Δx')*(C*Δx))/npar)
    end
    C = TaylorSeries.hessian(Q, x_new)/(2/nobs) # C = d2Q/(2/m)
    Γ = inv(C[1:6,1:6])
    return x_new, Γ
end

function newtonls_A2(res, w, x0, niters=5)
    @assert length(res) == length(w)
    nobs = length(res)
    npar = length(x0)
    Q = chi2(res, w)/nobs
    x_new = x0
    for i in 1:niters
        dQ = TaylorSeries.gradient(Q)(x_new)
        d2Q = TaylorSeries.hessian(Q, x_new)
        Δx = - inv(d2Q)*dQ
        x_new[7] = x_new[7] + Δx[7]
        C = d2Q/(2/nobs) # C = d2Q/(2/m)
        @show sqrt(((Δx')*(C*Δx))/npar)
    end
    C = TaylorSeries.hessian(Q, x_new)/(2/nobs) # C = d2Q/(2/m)
    Γ = inv(C)
    return x_new, Γ, C
end


# convenience function to calculate the mean-squared error
function mse(arr1::AbstractArray, arr2::AbstractArray)
    @assert length(arr1) == length(arr2)
    N = length(arr1)
    err = 0.0
    for i in 1:N
        err += (arr2[i] - arr1[i])^2
    end
    err /= N
end

mse(X::AbstractArray, thresh=1e-8) = Y::AbstractArray -> begin
    if size(X) != size(Y)
        return false
    end

    return mse(X, Y) < thresh
end

issubtype(T::Type) = x -> typeof(x) <: T
lessthan(rhs) = lhs -> lhs < rhs
greaterthan(rhs) = lhs -> lhs > rhs


# ---
# title: 1235. Maximum Profit in Job Scheduling
# id: problem1235
# author: Tian Jun
# date: 2020-10-31
# difficulty: Hard
# categories: Binary Search, Dynamic Programming, Sort
# link: <https://leetcode.com/problems/maximum-profit-in-job-scheduling/description/>
# hidden: true
# ---
#
# We have `n` jobs, where every job is scheduled to be done from `startTime[i]`
# to `endTime[i]`, obtaining a profit of `profit[i]`.
#
# You're given the `startTime` , `endTime` and `profit` arrays, you need to
# output the maximum profit you can take such that there are no 2 jobs in the
# subset with overlapping time range.
#
# If you choose a job that ends at time `X` you will be able to start another
# job that starts at time `X`.
#
#
#
# **Example 1:**
#
# **![](https://assets.leetcode.com/uploads/2019/10/10/sample1_1584.png)**
#
#
#
#     Input: startTime = [1,2,3,3], endTime = [3,4,5,6], profit = [50,10,40,70]
#     Output: 120
#     Explanation: The subset chosen is the first and fourth job.
#     Time range [1-3]+[3-6] , we get profit of 120 = 50 + 70.
#
#
# **Example 2:**
#
# **![](https://assets.leetcode.com/uploads/2019/10/10/sample22_1584.png)**
#
#
#
#     Input: startTime = [1,2,3,4,6], endTime = [3,5,10,6,9], profit = [20,20,100,70,60]
#     Output: 150
#     Explanation: The subset chosen is the first, fourth and fifth job.
#     Profit obtained 150 = 20 + 70 + 60.
#
#
# **Example 3:**
#
# **![](https://assets.leetcode.com/uploads/2019/10/10/sample3_1584.png)**
#
#
#
#     Input: startTime = [1,1,1], endTime = [2,3,4], profit = [5,6,4]
#     Output: 6
#
#
#
#
# **Constraints:**
#
#   * `1 <= startTime.length == endTime.length == profit.length <= 5 * 10^4`
#   * `1 <= startTime[i] < endTime[i] <= 10^9`
#   * `1 <= profit[i] <= 10^4`
#
#
## @lc code=start
using LeetCode

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


"""
    struct KmeansResult{T<:AbstractFloat}
        K::Int
        centroids::Vector{Vector{T}}
        cluster::Vector{Int}
        withinss::T
        iter::Int
    end

Object resulting from kmeans algorithm that contains the number of clusters, centroids, clusters prediction,
total-variance-within-cluster and number of iterations until convergence.
"""
struct KmeansResult{T<:AbstractFloat}
    K::Int
    centroids::Vector{Vector{T}}
    cluster::Vector{Int}
    withinss::T
    iter::Int
end

function Base.print(io::IO, model::KmeansResult{T}) where {T<:AbstractFloat}
    p = ["     $(v)\n" for v in model.centroids]

    print(IOContext(io, :limit => true), "KmeansResult{$T}:
 K = $(model.K)
 centroids = [\n", p..., " ]
 cluster = ", model.cluster, "
 within-cluster sum of squares = $(model.withinss)
 iterations = $(model.iter)")
end

Base.show(io::IO, model::KmeansResult) = print(io, model)

"""
    ClusterAnalysis.euclidean(a::AbstractVector, b::AbstractVector)

Calculate euclidean distance from two vectors. √∑(aᵢ - bᵢ)².

# Arguments (positional)
- `a`: First vector.
- `b`: Second vector.

# Example
```julia
julia> using ClusterAnalysis

julia> a = rand(100); b = rand(100);

julia> ClusterAnalysis.euclidean(a, b)
3.8625780213774954
```
"""
function euclidean(a::AbstractVector{T},
                   b::AbstractVector{T}) where {T<:AbstractFloat}
    @assert length(a) == length(b)

    # euclidean(a, b) = √∑(aᵢ- bᵢ)²
    s = zero(T)
    @simd for i in eachindex(a)
        @inbounds s += (a[i] - b[i])^2
    end
    return √s
end

"""
    ClusterAnalysis.squared_error(data::AbstractMatrix)
    ClusterAnalysis.squared_error(col::AbstractVector)

Function that evaluate the kmeans, using the Sum of Squared Error (SSE).

# Arguments (positional)
- `data` or `col`: Matrix of data observations or a Vector which represents one column of data.

# Example
```julia
julia> using ClusterAnalysis

julia> a = rand(100, 4);

julia> ClusterAnalysis.squared_error(a)
34.71086095943974

julia> ClusterAnalysis.squared_error(a[:, 1])
10.06029322934825
```
"""
function squared_error(data::AbstractMatrix{T}) where {T<:AbstractFloat}
    error = zero(T)
    @simd for i in axes(data, 2)
        error += squared_error(view(data, :, i))
    end
    return error
end

function squared_error(col::AbstractVector{T}) where {T<:AbstractFloat}
    μ = mean(col)
    error = zero(T)
    @simd for i in eachindex(col)
        @inbounds error += (col[i] - μ)^2
    end
    return error
end

"""
    ClusterAnalysis.totalwithinss(data::AbstractMatrix, K::Int, cluster::Vector)

Calculate the total-variance-within-cluster using the `squared_error()` function.

# Arguments (positional)
- `data`: Matrix of data observations.
- `K`: number of clusters.
- `cluster`: Vector of cluster for each data observation.

# Example
```julia
julia> using ClusterAnalysis
julia> using CSV, DataFrames

julia> iris = CSV.read(joinpath(pwd(), "path/to/iris.csv"), DataFrame);
julia> df = iris[:, 1:end-1];
julia> model = kmeans(df, 3);

julia> ClusterAnalysis.totalwithinss(Matrix(df), model.K, model.cluster)
78.85144142614601
```
"""
function totalwithinss(data::AbstractMatrix{T}, K::Int, cluster::AbstractVector{Int}) where {T<:AbstractFloat}
    # evaluate total-variance-within-clusters
    error = zero(T)
    @simd for k in 1:K
        error += squared_error(data[cluster .== k, :])
    end
    return error
end

"""
    kmeans(table, K::Int; nstart::Int = 1, maxiter::Int = 10, init::Symbol = :kmpp)
    kmeans(data::AbstractMatrix, K::Int; nstart::Int = 1, maxiter::Int = 10, init::Symbol = :kmpp)

Classify all data observations in k clusters by minimizing the total-variance-within each cluster.

# Arguments (positional)
- `table` or `data`: table or Matrix of data observations.
- `K`: number of clusters.

## Keyword
- `nstart`: number of starts.
- `maxiter`: number of maximum iterations.
- `init`: centroids inicialization algorithm - `:kmpp` (default) or `:random`.

# Example
```julia
julia> using ClusterAnalysis
julia> using CSV, DataFrames

julia> iris = CSV.read(joinpath(pwd(), "path/to/iris.csv"), DataFrame);
julia> df = iris[:, 1:end-1];

julia> model = kmeans(df, 3)
KmeansResult{Float64}:
 K = 3
 centroids = [
     [5.932307692307693, 2.755384615384615, 4.42923076923077, 1.4384615384615382]
     [5.006, 3.4279999999999995, 1.462, 0.24599999999999997]
     [6.874285714285714, 3.088571428571429, 5.791428571428571, 2.117142857142857]
 ]
 cluster = [2, 2, 2, 2, 2, 2, 2, 2, 2, 2  …  3, 3, 1, 3, 3, 3, 1, 3, 3, 1]
 within-cluster sum of squares = 78.85144142614601
 iterations = 7
```

# Pseudo-code of the algorithm:
* Repeat `nstart` times:
    1. Initialize `K` clusters centroids using KMeans++ algorithm or random init.
    2. Estimate clusters.
    3. Repeat `maxiter` times:
        * Update centroids using the mean().
        * Reestimates the clusters.
        * Calculate the total-variance-within-cluster.
        * Evaluate the stop rule.
* Keep the best result (minimum total-variance-within-cluster) of all `nstart` executions.

For more detailed explanation of the algorithm, check the
[`Algorithm's Overview of KMeans`](https://github.com/AugustoCL/ClusterAnalysis.jl/blob/main/algo_overview/kmeans_overview.md).
"""
function kmeans(table, K::Int; kwargs...)
    Tables.istable(table) ? (data = Tables.matrix(table)) : throw(ArgumentError("The table argument passed does not implement the Tables.jl interface."))
    return kmeans(data, K; kwargs...)
end

kmeans(data::AbstractMatrix{T}, K::Int; kwargs...) where {T} = kmeans(convert(Matrix{Float64}, data), K; kwargs...)

function kmeans(data::AbstractMatrix{T}, K::Int;
                nstart::Int = 1,
                maxiter::Int = 10,
                init::Symbol = :kmpp) where {T<:AbstractFloat}

    nl = size(data, 1)

    centroids = Vector{Vector{T}}(undef, K)
    cluster = Vector{Int}(undef, nl)
    withinss = Inf
    iter = 0

    # run multiple kmeans to get the best result
    for _ in 1:nstart

        new_centroids, new_cluster, new_withinss, new_iter = _kmeans(data, K, maxiter, init)

        if new_withinss < withinss
            centroids .= new_centroids
            cluster .= new_cluster
            withinss = new_withinss
            iter = new_iter
        end
    end

    return KmeansResult(K, centroids, cluster, withinss, iter)
end

function _kmeans(data::AbstractMatrix{T}, K::Int, maxiter::Int, init::Symbol) where {T<:AbstractFloat}

    nl = size(data, 1)

    # generate random centroids
    centroids = _initialize_centroids(data, K, init)

    # first clusters estimate
    cluster = Vector{Int}(undef, nl)
    for (i, obs) in enumerate(eachrow(data))
        dist = [euclidean(obs, c) for c in centroids]
        @inbounds cluster[i] = argmin(dist)
    end

    # first evaluation of total-variance-within-cluster
    withinss = totalwithinss(data, K, cluster)

    # variables to update during the iterations
    new_centroids = copy(centroids)
    new_cluster = copy(cluster)
    iter = 1
    norms = norm.(centroids)

    # start kmeans iterations until maxiter or convergence
    for _ in 2:maxiter

        # update new_centroids using the mean
        @simd for k in 1:K             # mean.(eachcol(data[new_cluster .== k, :]))
            @inbounds new_centroids[k] = vec(mean(view(data, new_cluster .== k, :), dims = 1))
        end

        # estimate cluster to all observations
        for (i, obs) in enumerate(eachrow(data))
            dist = [euclidean(obs, c) for c in new_centroids]
            @inbounds new_cluster[i] = argmin(dist)
        end

        # update iter, withinss-variance and calculate centroid norms
        new_withinss = totalwithinss(data, K, new_cluster)
        new_norms = norm.(new_centroids)
        iter += 1

        # convergence rule
        norm(norms - new_norms) ≈ 0 && break

        # update centroid norms
        norms .= new_norms

        # update centroids, cluster and whithinss
        if new_withinss < withinss
            centroids .= new_centroids
            cluster .= new_cluster
            withinss = new_withinss
        end

    end

    return centroids, cluster, withinss, iter
end

function _initialize_centroids(data::AbstractMatrix{T}, K::Int, init::Symbol) where {T<:AbstractFloat}
    nl = size(data, 1)

    if init == :random
        indexes = rand(1:nl, K)
        centroids = Vector{T}[data[i, :] for i in indexes]
    elseif init == :kmpp
        centroids = Vector{Vector{T}}(undef, K)
        centroids[1] = data[rand(1:nl), :]

        # distance vector for each observation
        dists = Vector{T}(undef, nl)

        # get each new centroid by the furthest observation (maximum distance)
        for k in 2:K

            # for each observation get the nearest centroid by the minimum distance
            for (i, row) in enumerate(eachrow(data))
                dist_c = [euclidean(row, c) for c in @view centroids[1:(k-1)]]
                @inbounds dists[i] = minimum(dist_c)
            end

            # new centroid by the furthest observation
            @inbounds centroids[k] = data[argmax(dists), :]
        end
    else
        throw(ArgumentError("The symbol :$init is not a valid argument. Use :random or :kmpp."))
    end

    return centroids
end


# jarque_bera.jl
# Jarque-Bera goodness-of-fit test
#
# Copyright (C) 2017   Benjamin Born
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

export JarqueBeraTest

struct JarqueBeraTest <: HypothesisTest
    n::Int         # number of observations
    JB::Float64    # test statistic
    skew::Float64  # skewness
    kurt::Float64  # excess kurtosis
end

"""
    JarqueBeraTest(y::AbstractVector)

Compute the Jarque-Bera statistic to test the null hypothesis that a real-valued vector `y`
is normally distributed.

Note that the approximation by the Chi-squared distribution does not work well and the
speed of convergence is slow. In small samples, the test tends to be over-sized for
nominal levels up to about 3% and under-sized for larger nominal levels (Mantalos, 2010).

# References

  * Panagiotis Mantalos, 2011, "The three different measures of the sample skewness and
    kurtosis and the effects to the Jarque-Bera test for normality", International Journal
    of Computational Economics and Econometrics, Vol. 2, No. 1,
    [link](http://dx.doi.org/10.1504/IJCEE.2011.040576).

# External links

  * [Jarque-Bera test on Wikipedia](https://en.wikipedia.org/wiki/Jarque–Bera_test)
"""
function JarqueBeraTest(y::AbstractVector{T}) where T<:Real
    n = length(y)
    M = Base.promote_op(/, T, typeof(n))
    m1r = m2r = m3r = m4r = zero(M)
    @inbounds for yi in y # compute raw moments
        m1r += yi / n
        m2r += yi^2 / n
        m3r += yi^3 / n
        m4r += yi^4 / n
    end
    # compute central moments (http://mathworld.wolfram.com/CentralMoment.html)
    m2 = -m1r^2 + m2r
    m3 = 2 * m1r^3 - 3 * m1r * m2r + m3r
    m4 = -3 * m1r^4 + 6 * m1r^2 * m2r - 4 * m1r * m3r + m4r

    skew = m3 / m2^(3/2)
    kurt = m4 / m2^2

    stat = n * skew^2 / 6 + n * (kurt - 3)^2 / 24
    JarqueBeraTest(n, stat, skew, kurt)
end

testname(::JarqueBeraTest) = "Jarque-Bera normality test"
population_param_of_interest(x::JarqueBeraTest) =
    ("skewness and kurtosis", "0 and 3", "$(x.skew) and $(x.kurt)")
default_tail(test::JarqueBeraTest) = :right

function show_params(io::IO, x::JarqueBeraTest, ident)
    println(io, ident, "number of observations:         ", x.n)
    println(io, ident, "JB statistic:                   ", x.JB)
end

pvalue(x::JarqueBeraTest) = pvalue(Chisq(2), x.JB; tail=:right)


x=3                      # 3
typeof(x)                # Int64
x=3.3                    # 3.3
typeof(x)                # Float64
x=3.3+3.3im              # 3.3 + 3.3im
typeof(x)                # ComplexF64 (alias for Complex{Float64})


using SuperFastGraphs
using Base.Test
using LightGraphs

gDWeakly = DiGraph(6)

add_edge!(gDWeakly, 1, 2)
add_edge!(gDWeakly, 1, 3)
add_edge!(gDWeakly, 1, 4)
add_edge!(gDWeakly, 5, 1)
add_edge!(gDWeakly, 2, 3)
add_edge!(gDWeakly, 3, 6)
add_edge!(gDWeakly, 4, 5)
println(is_directed(gDWeakly) && !is_strongly_connected(gDWeakly) && is_weakly_connected(gDWeakly))
println(preProcess(gDWeakly))

println("-----------------")
gDStrongly = DiGraph(3)
add_edge!(gDStrongly, 1, 2)
add_edge!(gDStrongly, 2, 3)
add_edge!(gDStrongly, 3, 1)
println(is_directed(gDStrongly) && is_strongly_connected(gDStrongly))
println(preProcess(gDStrongly))

println("---------------")

gNotD_NotC = Graph(6)
add_edge!(gNotD_NotC, 1, 2)
add_edge!(gNotD_NotC, 1, 3)
add_edge!(gNotD_NotC, 2, 4)
add_edge!(gNotD_NotC, 2, 5)
add_edge!(gNotD_NotC, 5, 4)
println("connesso: ",is_connected(gNotD_NotC))
println("componenti connesse: ", connected_components(gNotD_NotC))
println(!is_directed(gNotD_NotC) && !is_connected(gNotD_NotC))
println(preProcess(gNotD_NotC))

println("-------------------")
# g = loadgraph("testdata/karate.lg", "graph")
g = loadgraph("testdata/internet.lg", "graph")
println(g)
tic()
topKcc(g, 33)
toc()
# println(fastClosenessCentrality(g, 5)[1])
println("-----------------------")
tic()
closeness_centrality(g)
toc()
# println(sort!(closeness_centrality(g))[end-5])
#println(connected_components(g))


using Zygote

@doc raw"
    einsum_grad(::EinCode{ixs, iy}, xs, size_dict, cdy, i)

return the gradient of the result of evaluating the `EinCode` w.r.t
the `i`th tensor in `xs`. `cdy` is the result of applying the `EinCode`
to the `xs`.

# example
```jldoctest; setup = :(using OMEinsum)
julia> using OMEinsum: einsum_grad, get_size_dict

julia> a, b = rand(2,2), rand(2,2);

julia> c = einsum(EinCode((('i','j'),('j','k')), ('i','k')), (a,b));

julia> sd = get_size_dict((('i','j'),('j','k')), (a,b));

julia> einsum_grad(EinCode((('i','j'),('j','k')), ('i','k')), (a,b), sd, c, 1) ≈ c * transpose(b)
true
```
"
function einsum_grad(::EinCode{ixs, iy}, xs, size_dict, cdy, i) where {ixs, iy}
    nixs = TupleTools.insertat(ixs, i, (iy,))
    nxs  = TupleTools.insertat( xs, i, (cdy,))
    niy = ixs[i]
    y = conj!(einsum(EinCode(nixs, niy), nxs, size_dict))
    typeof(y) == typeof(xs[i]) && return y
    xs[i] isa Array{<:Real} && return convert(typeof(xs[i]), real(y))
    convert(typeof(xs[i]), y)
end

@Zygote.adjoint function einsum(code::EinCode{ixs, iy}, xs::NTuple{N,T where T}, size_dict::IndexSize) where {N, ixs, iy}
    y = einsum(code, xs, size_dict)
    return y, dy -> let cdy = map(conj,dy)
                (
                    nothing,
                    ntuple(i -> einsum_grad(code, xs, size_dict, cdy, i), N),
                    nothing
                )
            end
end

@doc raw"
    bpcheck(f, args...; η = 1e-5, verbose=false)
returns a `Bool` indicating whether Zygote calculates the gradient of `f(args...) -> scalar`
correctly using the relation `f(x - ηg) ≈ f(x) - η|g|²` with a relative tolerance
of 1e-2 and an absolute tolerance of 1e-8.
If `verbose=true`, print `f(x) - f(x - ηg)`and `η|g|²`.

# example

```jldoctest; setup = :(using OMEinsum)
julia> using OMEinsum: bpcheck

julia> a, b = rand(2,2), rand(2,2);

julia> bpcheck(sum ∘ ein\"ij,jk -> ik\", a, b)
true
```
"
function bpcheck(f, args...; η = 1e-5, verbose = false)
    g = gradient(f, args...)
    all(==(nothing), g) && error()
    dy_ref = 0
    for x in g
        x === nothing && continue
        x isa Tuple && (dy_ref += η * mapreduce(y -> y == nothing ? 0 : sum(abs2,y), +, x))
        x isa AbstractArray && (dy_ref += η * sum(abs2,x))
    end
    dy = f(args...) - f([gi == nothing ? arg : arg .- η .* gi for (arg, gi) in zip(args,g)]...)

    verbose && @show dy
    verbose && @show dy_ref

    isapprox(dy, dy_ref, rtol=1e-2, atol=1e-8)
end

@Zygote.nograd get_size_dict


# ---
# title: 1453. Maximum Number of Darts Inside of a Circular Dartboard
# id: problem1453
# author: Tian Jun
# date: 2020-10-31
# difficulty: Hard
# categories: Geometry
# link: <https://leetcode.com/problems/maximum-number-of-darts-inside-of-a-circular-dartboard/description/>
# hidden: true
# ---
#
# You have a very large square wall and a circular dartboard placed on the wall.
# You have been challenged to throw darts into the board blindfolded. Darts
# thrown at the wall are represented as an array of `points` on a 2D plane.
#
# Return the maximum number of points that are within or lie on  **any**
# circular dartboard of radius `r`.
#
#
#
# **Example 1:**
#
# ![](https://assets.leetcode.com/uploads/2020/04/29/sample_1_1806.png)
#
#
#
#     Input: points = [[-2,0],[2,0],[0,2],[0,-2]], r = 2
#     Output: 4
#     Explanation: Circle dartboard with center in (0,0) and radius = 2 contain all points.
#
#
# **Example 2:**
#
# **![](https://assets.leetcode.com/uploads/2020/04/29/sample_2_1806.png)**
#
#
#
#     Input: points = [[-3,0],[3,0],[2,6],[5,4],[0,9],[7,8]], r = 5
#     Output: 5
#     Explanation: Circle dartboard with center in (0,4) and radius = 5 contain all points except the point (7,8).
#
#
# **Example 3:**
#
#
#
#     Input: points = [[-2,0],[2,0],[0,2],[0,-2]], r = 1
#     Output: 1
#
#
# **Example 4:**
#
#
#
#     Input: points = [[1,2],[3,5],[1,-1],[2,3],[4,1],[1,3]], r = 2
#     Output: 4
#
#
#
#
# **Constraints:**
#
#   * `1 <= points.length <= 100`
#   * `points[i].length == 2`
#   * `-10^4 <= points[i][0], points[i][1] <= 10^4`
#   * `1 <= r <= 5000`
#
#
## @lc code=start
using LeetCode

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


# ============================================================================ #
# Point
# ============================================================================ #
abstract type AbstractPoint end

struct Point{T} <: AbstractPoint
    x::T
    y::T
    function Point{T}(x::T, y::T) where {T}
        new(x, y)
    end
end

Point(x::T, y::T) where {T} = Point{T}(x, y)
Point(x, y) = Point( promote(x, y)... )
Point{T}(x, y) where {T} = Point( promote( convert(T, x), convert(T, y))... )
Point(a::A) where {A<:AbstractArray} = Point(a[1], a[2])
Point() = Point(0.0, 0.0)

mutable struct MutablePoint{T} <: AbstractPoint
    x::T
    y::T
    function MutablePoint{T}(x::T, y::T) where {T}
        new(x, y)
    end
end

MutablePoint(x::T, y::T) where {T} = MutablePoint{T}(x, y)
MutablePoint(x, y) = MutablePoint( promote(x, y)... )
MutablePoint{T}(x, y) where {T} = MutablePoint( promote( convert(T, x), convert(T, y))... )
MutablePoint(a::A) where {A<:AbstractArray} = MutablePoint(a[1], a[2])
MutablePoint() = MutablePoint(0.0, 0.0)

# ---------------------------------------------------------------------------- #
# Methods
# ---------------------------------------------------------------------------- #

function Base.getindex(p::T, index::Int) where {T <: AbstractPoint}
    if index == 1
        return p.x
    elseif index == 2
        return p.y
    else
        return NaN
    end
end

function Base.size(p::T) where {T <: AbstractPoint}
    return (2,)
end

function Base.size(p::T, dim = 1) where {T <: AbstractPoint}
    if dim == 1
        return 2
    elseif dim == 2
        return 1
    else
        return 0
    end
end

Base.:(==)(p1::AbstractPoint, p2::AbstractPoint) = p1.x == p2.x && p1.y == p2.y

Base.:+(p1::AbstractPoint, p2::AbstractPoint) = [p1.x + p2.x, p1.y + p2.y]
Base.:-(p1::AbstractPoint, p2::AbstractPoint) = [p1.x - p2.x, p1.y - p2.y]

Base.:+(p1::AbstractPoint, p2::AbstractArray) = [p1.x + p2[1], p1.y + p2[2]]
Base.:-(p1::AbstractPoint, p2::AbstractArray) = [p1.x - p2[1], p1.y - p2[2]]

Base.:+(p1::AbstractArray, p2::AbstractPoint) = [p1[1] + p2.x, p1[2] + p2.y]
Base.:-(p1::AbstractArray, p2::AbstractPoint) = [p1[1] - p2.x, p1[2] - p2.y]

Base.:+(p1::AbstractArray, p2::T) where {T <: Number} = [p1[1] + p2, p1[2] + p2]
Base.:-(p1::AbstractArray, p2::T) where {T <: Number} = [p1[1] - p2, p1[2] - p2]

Base.:+(p1::AbstractPoint, p2::T) where {T <: Number} = [p1[1] + p2, p1[2] + p2]
Base.:-(p1::AbstractPoint, p2::T) where {T <: Number} = [p1[1] - p2, p1[2] - p2]

Base.:*(f::N, point::P) where {N <: Number, P <: AbstractPoint} = P(f * point.x, f * point.y)
Base.:*(point::P, f::N) where {N <: Number, P <: AbstractPoint} = f * point;

Base.:/(point::P, f::N) where {N <: Number, P <: AbstractPoint} =  P( point.x / f, point.y / f);

function shift(p::T, v) where {T <: AbstractPoint}
    return shift(p::T, v[1], v[2])
end

function shift(p::T, x::S, y::S) where {S, T <: AbstractPoint}
    return T(p.x + x, p.y + y)
end
function scale(p::T, factor) where {T <: AbstractPoint}
    return factor * p
end
function rotate(p::T, rot_mat) where {T <: AbstractPoint}
    x = rot_mat[1,1] * p.x + rot_mat[1,2] * p.y
    y = rot_mat[2,1] * p.x + rot_mat[2,2] * p.y
    return T(x, y)
end

function random_point(n::Int, t::Type{PointType})  where {PointType <: AbstractPoint}
    # point x, y in the range of [0,1) x [0,1)

    point_array = Vector{t}(undef, n)
    x = rand(n)
    y = rand(n)
    for kk = 1:n
        point_array[kk] = t(x[kk], y[kk])
    end
    return point_array
end

random_point(n::Int) = random_point(n, Point)

function discretize_for_plot(point::AbstractPoint)
    return (point.x, point.y)
end

function discretize_for_plot(array::Vector{<:AbstractPoint}, closed_curve::Bool = false)
    L = length(array)
    if L <= 0
        return ([],[])
    end
    Lc = closed_curve ? 1 : 0

    ElementType = typeof(array[1].x)
    x_ = Vector{ElementType}(undef, L + Lc)
    y_ = Vector{ElementType}(undef, L + Lc)
    for kk = 1:L
        x_[kk] = array[kk].x
        y_[kk] = array[kk].y
    end
    if closed_curve
        x_[end] = array[1].x
        y_[end] = array[1].y
    end
    return (x_, y_)
end

function x(array::Vector{<:AbstractPoint})
    return map(p->p.x, array)
end

function y(array::Vector{<:AbstractPoint})
    return map(p->p.y, array)
end

function point_array_to_matrix(point_array)

    L = length(point_array)
    mat = zeros(L,2)

    for kk = 1:L
        mat[kk, 1] = point_array[kk].x
        mat[kk, 2] = point_array[kk].y
    end

    return mat
end

function matrix_to_point_array(mat)
    L = size(mat, 1)
    pa = Vector{MutablePoint}(undef, L)
    for kk = 1:L
        pa[kk] = MutablePoint(mat[kk,1], mat[kk,2])
    end
    return pa
end


function calc_grad( pw::PWGrid, Potentials, Focc, W::Array{ComplexF64,2} )
    Ngwx    = size(W)[1]
    Nstates = size(W)[2]
    Ω = pw.Ω
    Ns = pw.Ns
    #
    grad = zeros( ComplexF64, Ngwx, Nstates )

    H_psi = op_H( pw, Potentials, W )
    for i = 1:Nstates
        grad[:,i] = H_psi[:,i]
        for j = 1:Nstates
            grad[:,i] = grad[:,i] - dot( W[:,j], H_psi[:,i] ) * W[:,j]
        end
        grad[:,i] = Focc[i]*grad[:,i]
    end

    #println("Pass here ...")

    F = Matrix(Diagonal(Focc))
    HW = op_H( pw, Potentials, W )
    ℍ = W' * HW
    HFH = ℍ*F - F*ℍ
    #denom = ones(Nstates,Nstates)*2.0
    #ℚ = HFH ./ denom
    ℚ = 0.5*HFH
    grad[:,:] = grad[:,:] + W*ℚ

    return grad
end

#function calc_grad( pw::PWGrid, Potentials, Focc, W::Array{ComplexF64,2} )
#  Ngwx    = size(W)[1]
#  Nstates = size(W)[2]
#  Ω = pw.Ω
#  Ns = pw.Ns
#  #
#  grad = zeros( ComplexF64, Ngwx, Nstates )
#
#  F = diagm(Focc)
#  HW = op_H( pw, Potentials, W )
#  ℍ = W' * HW
#  HFH = ℍ*F - F*ℍ
#  denom = ones(Nstates,Nstates)*2.0
#  ℚ = HFH ./ denom
#  grad = (HW - W * ℍ)*F + W*ℚ
#
#  return grad
#end
#


@doc raw"""
    LogisticLikelihood() -> BernoulliLikelihood

Bernoulli likelihood with a logistic link for the Bernoulli likelihood
```math
    p(y|f) = \sigma(yf) = \frac{1}{1 + \exp(-yf)},
```
(for more info see : [wiki page](https://en.wikipedia.org/wiki/Logistic_function))

---

For the analytic version the likelihood, it is augmented via:
```math
    p(y|f,ω) = \exp\left(\frac{1}{2}(yf - (yf)^2 \omega)\right)
```
where ``ω \sim \mathcal{PG}(\omega | 1, 0)``, and ``\mathcal{PG}`` is the Polya-Gamma distribution.
See paper : [Efficient Gaussian Process Classification Using Polya-Gamma Data Augmentation](https://arxiv.org/abs/1802.06383).
"""
LogisticLikelihood() = BernoulliLikelihood(LogisticLink())

function implemented(
    ::BernoulliLikelihood{<:LogisticLink},
    ::Union{<:AnalyticVI,<:QuadratureVI,<:GibbsSampling},
)
    return true
end

function Distributions.loglikelihood(
    ::BernoulliLikelihood{<:LogisticLink}, y::Real, f::Real
)
    return -log(one(f) + exp(-y * f))
end

function compute_proba(l::BernoulliLikelihood, f::Real)
    return pdf(l(f), 1)
end

### Local Updates Section ###
function local_updates!(
    local_vars,
    ::BernoulliLikelihood{<:LogisticLink},
    ::AbstractVector,
    μ::AbstractVector,
    diagΣ::AbstractVector,
)
    map!(sqrt_expec_square, local_vars.c, μ, diagΣ) # √E[f^2]
    map!(local_vars.θ, local_vars.c) do c
        tanh(c / 2) / (2c)
    end # E[ω]
    return local_vars
end

function sample_local!(
    local_vars, ::BernoulliLikelihood{<:LogisticLink}, ::AbstractVector, f::AbstractVector
)
    map!(local_vars.θ, f) do f
        rand(PolyaGamma(1, abs(f)))
    end
    return local_vars
end

### Natural Gradient Section ###

function ∇E_μ(::BernoulliLikelihood{<:LogisticLink}, ::AOptimizer, y::AbstractVector, state)
    return (y / 2,)
end
function ∇E_Σ(::BernoulliLikelihood{<:LogisticLink}, ::AOptimizer, ::AbstractVector, state)
    return (state.θ / 2,)
end

### ELBO Section ###

function expec_loglikelihood(
    ::BernoulliLikelihood{<:LogisticLink},
    ::AnalyticVI,
    y::AbstractVector,
    μ::AbstractVector,
    diag_cov::AbstractVector,
    state,
)
    tot = -length(y) * logtwo / 2
    tot += (dot(μ, y) - dot(state.θ, diag_cov) - dot(state.θ, μ)) / 2
    return tot
end

AugmentedKL(l::BernoulliLikelihood{<:LogisticLink}, state, ::Any) = PolyaGammaKL(l, state)

function PolyaGammaKL(::BernoulliLikelihood{<:LogisticLink}, state)
    return sum(
        broadcast(PolyaGammaKL, ones(eltype(state.c), length(state.c)), state.c, state.θ)
    )
end

### Gradient Section ###

∇loglikehood(::BernoulliLikelihood{<:LogisticLink}, y::Real, f::Real) = y * logistic(-y * f)

function hessloglikehood(::BernoulliLikelihood{<:LogisticLink}, y::Real, f::Real)
    return -exp(y * f) / logistic(-y * f)^2
end


@testset "basic_dh" begin

    zero_3 = zeros(SVector{3,Float64})
    rot_I = one(RotMatrix{3,Float64})
    sm44_I = one(SMatrix{4,4,Float64,16})
    sm33_I = one(SMatrix{3,3,Float64,9})
    sm33_rand = rand(SMatrix{3,3,Float64,9})
    rot_rand = rand(RotMatrix{3,Float64})
    t_rand = rand(SVector{3,Float64})

    # Rotation Only
    @test sm44_I == basic_dh(rot_I).mat
    R, t = dh_R_t(basic_dh(rot_rand))
    @test R == rot_rand
    @test t == zero_3

    # Translation Only
    @test sm44_I == basic_dh(zero_3).mat
    R, t = dh_R_t(basic_dh(t_rand))
    @test R == rot_I
    @test t == t_rand

    # Rotation and Translation
    @test sm44_I == basic_dh(rot_I, zero_3).mat
    R, t = dh_R_t(basic_dh(rot_rand, t_rand))
    @test R == rot_rand
    @test t == t_rand

    # Matrix
    @test sm44_I == basic_dh(sm44_I).mat

    @test R isa SMatrix{3,3,Float64,9}
    @test t isa SVector{3,Float64}

    # SMatrix and Translation
    @test sm44_I == basic_dh(sm33_I, zero_3).mat
    R, t = dh_R_t(basic_dh(sm33_rand, t_rand))
    @test R == sm33_rand
    @test t == t_rand

    # Scale 3
    dh = basic_dh(Diagonal(t_rand))
    @test dh.mat == diagm(0=>[t_rand..., 1.0])

    # Scale constant
    c = 9.0
    dh = basic_dh(c)
    @test dh.mat == diagm(0=>[c, c, c, 1.0])

    # SMatrix only
    @test sm44_I == basic_dh(sm33_I).mat
    R, t = dh_R_t(basic_dh(sm33_rand))
    @test R == sm33_rand
    @test t == zero_3

    # inverse
    dh_rand = basic_dh(rot_rand, t_rand)
    @test inv(dh_rand).mat ≈ inv(dh_rand.mat)

    # multiply
    @test dh_rand.mat * dh_rand.mat ≈ (dh_rand * dh_rand).mat

    # povray_12
    @test all( povray_12(dh_rand) .== SVector(rot_rand..., t_rand...) )

    # one
    @test one(basic_dh{Float64}).mat == zeros(4, 4) + I

    # dh_vector_mul
    p_rand = rand(SVector{3,Float64})
    dh = basic_dh(rot_rand, t_rand)
    @test dh_vector_mul(dh, p_rand) == rot_rand * p_rand + t_rand

end


sig = [2 3; 3 2; 1 1]
S = random_S0Graph(sig)

w = random_S0_density(S.sig)
w /= tr(w)

#@show S
#display(w)

λ, x1, Z = dsw_schur2(S)
problem = maximize(trace_logm(x1, w), [ λ <= 1 ])
@time solve!(problem, () -> SCS.Optimizer(verbose=0, eps=solver_eps))
h1=problem.optval
x1=Hermitian(evaluate(x1))

λ, x2, Z = dsw_schur2(complement(S))
problem = maximize(trace_logm(x2, w), [ λ <= 1 ])
@time solve!(problem, () -> SCS.Optimizer(verbose=0, eps=solver_eps))
h2=problem.optval
x2=Hermitian(evaluate(x2))

@test x1*x2 ≈ x2*x1  atol=tol
@test x1*x2 ≈ Ψ(S, w)  atol=tol
# follows from the above
@test h1+h2 ≈ tr(w*log(Ψ(S, w)))  atol=tol


a=[.4 .6; .3 .7]
b=[.3 .1 .6; .5 .2 .3]
data=[1,2,3]
model=hmm((a,b))

function naiveprob(data)
	filter=transpose(model.mu[:,data[1]])
	for t=2:length(data)
		filter=sum(filter*slice(model.m,:,data[t-1],:,data[t]),1)
	end
	sum(filter)
end

@test vec(sum(model.m,(3,4)))≈fill(1,6)
@test reshape(sum(model.m[:,1,:,:],4),2,2)≈a
@test loglikelihood(model,data)≈log(naiveprob(data))/length(data)
data=Array(Array,2)
data[1]=[1,2,3]
data[2]=[3,2,1]
@test loglikelihood(model,data)≈(log(naiveprob(data[1]))/length(data[1])+log(naiveprob(data[2]))/length(data[2]))/2

viterbi(model,data)

data=rand(model,10000)
abhat=em(model,data)

@test vecnorm(abhat[1]-a)<5e-2
@test vecnorm(abhat[2]-b)<5e-2


using VoronoiDelaunay, Gadfly

points = [
 Point2D(-1.0563841812533212, -1.4606363138997696)
 Point2D(0.06312982975128989, -0.48031801152366027)
 Point2D(0.1624918689993189, -0.19919450833195906)
 Point2D(-1.5293344878962758, -0.7657808444340142)
 Point2D(0.5319064220493406, 0.6107808132476504)
 Point2D(-0.3670342825169435, 0.8207427582546951)
 Point2D(-1.9797019290444364, -0.5066353099040788)
 Point2D(-1.5811584920674324, 1.0346409888830976)
 Point2D(1.2185165319349451, 1.4177209167374605)
 Point2D(-1.5991536318191626, -1.3063986775765466) ];

tess, ranges = DelaunayTessellation( points )
xy = getplotxy( delaunayedges( tess, ranges ) )
l1 = layer( x=getx.(points), y=gety.(points), Theme(default_color=colorant"orange"), Geom.point )
l2 = layer( x=xy[1], y=xy[2], Geom.path )
plot( l1, l2, Coord.cartesian(fixed=true) )


using Turing

@model simplegaussmodel(x) = begin
  s ~ InverseGamma(2,3)
  m ~ Normal(0,sqrt(s))
  for i in 1:length(x)
    x[i] ~ Normal(m, sqrt(s))
  end
  s, m
end


using JuMP
using DelimitedFiles
import GLPK
import Cbc
# import Xpress

"""
For research purposes, a utility that finds the optimal stable matching using
integer programming, with commercial Xpress solver as default.
"""
function IP_stable_opt(students::Array{Int64, 2}, schools::Array{Int64, 2},
                    capacities::Array{Int64, 1})
    n, m = size(schools)
    @assert (m,) == size(capacities)
    @assert (m, n) == size(students) "Shape mismatch between schools and students"

    # Open-source, slow for this problem.
    # model = Model(GLPK.Optimizer)
    # set_optimizer_attribute(model, "msg_lev", GLPK.GLP_MSG_ALL)

    # Open source, fast for this problem for no reason.
    model = Model(Cbc.Optimizer)

    # Commercial option; need to have license in your path, but this problem is
    # small enough for the demo version. Fastest.
    # model = Model(Xpress.Optimizer)

    @variable(model, x[1:m, 1:n], Bin)
    @objective(model, Min, sum(x .* students))

    if sum(capacities) ≤ n
        @constraints(model, begin
            student_capacity[s in 1:n], sum(x[:, s]) ≤ 1
            school_capacity[c in 1:m], sum(x[c, :]) == capacities[c]
            stability[c in 1:m, s in 1:n], (capacities[c] * x[c, s] +
                                            capacities[c] * x[:, s]' * (students[:, s] .≤ students[c, s]) +
                                            x[c, :]' * (schools[:, c] .≤ schools[s, c])) ≥ capacities[c]
        end)
    else
        @constraints(model, begin
            student_capacity[s in 1:n], sum(x[:, s]) == 1
            school_capacity[c in 1:m], sum(x[c, :]) ≤ capacities[c]
            stability[c in 1:m, s in 1:n], (capacities[c] * x[c, s] +
                                            capacities[c] * x[:, s]' * (students[:, s] .≤ students[c, s]) +
                                            x[c, :]' * (schools[:, c] .≤ schools[s, c])) ≥ capacities[c]
        end)
    end

    optimize!(model)

    display("Total student disutility: $(objective_value(model))")
    out = value.(x)
    assn = vec(mapslices(col -> findfirst(y -> y > 0.9, col), out, dims = 1))
    return assn
end

"""
Same but without stability constraint.
"""
function LP_system_opt(students::Array{Int64, 2}, schools::Array{Int64, 2},
                    capacities::Array{Int64, 1})
    n, m = size(schools)
    @assert (m,) == size(capacities)
    @assert (m, n) == size(students) "Shape mismatch between schools and students"

    # Open source. Works great for LP case.
    model = Model(GLPK.Optimizer)
    set_optimizer_attribute(model, "msg_lev", GLPK.GLP_MSG_ALL)

    # Open source.
    # model = Model(Cbc.Optimizer)

    # Commercial option.
    # model = Model(Xpress.Optimizer)

    # Constraint matrix is TUM so no integrality constraint.
    @variable(model, 0 ≤ x[1:m, 1:n] ≤ 1)
    @objective(model, Min, sum(x .* students))
    if sum(capacities) ≤ n
        @constraints(model, begin
            student_capacity[s in 1:n], sum(x[:, s]) ≤ 1
            school_capacity[c in 1:m], sum(x[c, :]) == capacities[c]
        end)
    else
        @constraints(model, begin
            student_capacity[s in 1:n], sum(x[:, s]) == 1
            school_capacity[c in 1:m], sum(x[c, :]) ≤ capacities[c]
        end)
    end

    optimize!(model)
    display("Total student disutility: $(objective_value(model))")
    out = value.(x)
    @assert isempty(filter(x -> !(round(x, digits=5) in [0, 1]), out)) "Solution not integral"
    assn = vec(mapslices(col -> findfirst(y -> y > 0.9, col), out, dims = 1))
    return assn
end

students = readdlm("examples/sysopt/students.dat", Int)
schools = readdlm("examples/sysopt/schools.dat", Int)
n, m = size(schools)
capacities = ones(Int64, m)

assn_LP = LP_system_opt(students, schools, capacities)
# GLPK Simplex Optimizer, v4.64
# 200 rows, 10000 columns, 20000 non-zeros
#       0: obj =  0.000000000e+000 inf =  1.000e+002 (100)
#     199: obj =  4.704000000e+003 inf =  0.000e+000 (0)
# Perturbing LP to avoid stalling [400]...
# Removing LP perturbation [1301]...
# *  1301: obj =  2.407000000e+003 inf =  0.000e+000 (0) 8
# OPTIMAL LP SOLUTION FOUND

assn = IP_stable_opt(students, schools, capacities)
# Result - Optimal solution found
#
# Objective value:                2426.00000000
# Enumerated nodes:               0
# Total iterations:               0
# Time (CPU seconds):             15.34
# Time (Wallclock seconds):       15.34
#
# Total time (CPU seconds):       15.38   (Wallclock seconds):       15.38
#
# "Total student disutility: 2426.0"

# writedlm("examples/sysopt/LPSystemOpt.dat", assn_LP)
# writedlm("examples/sysopt/IPStableOpt.dat", assn)


include("../utils.jl")
include("group_utils.jl")

@testset "Special Orthogonal group" begin
    for n in [2, 3]
        G = SpecialOrthogonal(n)
        @test repr(G) == "SpecialOrthogonal($n)"
        M = base_manifold(G)
        @test M === Rotations(n)
        p = Matrix(I, n, n)
        @test is_default_metric(MetricManifold(G, EuclideanMetric()))

        types = [Matrix{Float64}]

        if n == 2
            ω = [[1.0], [-1.0], [3.0]]
            pts = [exp(M, p, hat(M, p, ωi)) for ωi in ω]
            Xpts = [hat(M, p, [-1.5]), hat(M, p, [0.5])]
        else
            ω = [[1.0, 2.0, 3.0], [3.0, 2.0, 1.0], [1.0, 3.0, 2.0]]
            pts = [exp(M, p, hat(M, p, ωi)) for ωi in ω]
            Xpts = [hat(M, p, [-1.0, 2.0, 0.5]), hat(M, p, [1.0, 0.0, 0.5])]
        end

        ge = allocate(pts[1])
        identity_element!(G, ge)
        @test isapprox(ge, I; atol=1e-10)

        gI = Identity(G)
        gT = allocate_result(G, exp, gI, log(G, pts[1], pts[2]))
        @test size(gT) == size(ge)
        @test eltype(gT) == eltype(ge)
        gT = allocate_result(G, log, gI, pts[1])
        @test size(gT) == size(ge)
        @test eltype(gT) == eltype(ge)

        for T in types
            gpts = convert.(T, pts)
            Xgpts = convert.(T, Xpts)
            @test compose(G, gpts[1], gpts[2]) ≈ gpts[1] * gpts[2]
            @test translate_diff(G, gpts[2], gpts[1], Xgpts[1], LeftAction()) ≈ Xgpts[1]
            @test translate_diff(G, gpts[2], gpts[1], Xgpts[1], RightAction()) ≈
                  transpose(gpts[2]) * Xgpts[1] * gpts[2]
            test_group(
                G,
                gpts,
                Xgpts,
                Xgpts;
                test_diff=true,
                test_invariance=true,
                test_lie_bracket=true,
                test_adjoint_action=true,
            )
        end

        @testset "Decorator forwards to group" begin
            DM = NotImplementedGroupDecorator(G)
            test_group(DM, pts, Xpts, Xpts; test_diff=true)
        end

        @testset "Group forwards to decorated" begin
            retraction_methods = [
                Manifolds.PolarRetraction(),
                Manifolds.QRRetraction(),
                Manifolds.GroupExponentialRetraction(LeftAction()),
                Manifolds.GroupExponentialRetraction(RightAction()),
            ]

            inverse_retraction_methods = [
                Manifolds.PolarInverseRetraction(),
                Manifolds.QRInverseRetraction(),
                Manifolds.GroupLogarithmicInverseRetraction(LeftAction()),
                Manifolds.GroupLogarithmicInverseRetraction(RightAction()),
            ]

            test_manifold(
                G,
                pts;
                test_injectivity_radius=false,
                test_project_tangent=true,
                test_musical_isomorphisms=false,
                retraction_methods=retraction_methods,
                inverse_retraction_methods=inverse_retraction_methods,
                exp_log_atol_multiplier=20,
                retraction_atol_multiplier=12,
                is_tangent_atol_multiplier=1.2,
                test_atlases=(Manifolds.RetractionAtlas(),),
            )

            @test injectivity_radius(G) == injectivity_radius(M)
            @test injectivity_radius(G, pts[1]) == injectivity_radius(M, pts[1])
            @test injectivity_radius(G, pts[1], PolarRetraction()) ==
                  injectivity_radius(M, pts[1], PolarRetraction())

            y = allocate(pts[1])
            exp!(G, y, pts[1], Xpts[1])
            @test isapprox(M, y, exp(M, pts[1], Xpts[1]))

            y = allocate(pts[1])
            retract!(G, y, pts[1], Xpts[1])
            @test isapprox(M, y, retract(M, pts[1], Xpts[1]))

            w = allocate(Xpts[1])
            inverse_retract!(G, w, pts[1], pts[2])
            @test isapprox(M, pts[1], w, inverse_retract(M, pts[1], pts[2]))

            w = allocate(Xpts[1])
            inverse_retract!(G, w, pts[1], pts[2], QRInverseRetraction())
            @test isapprox(
                M,
                pts[1],
                w,
                inverse_retract(M, pts[1], pts[2], QRInverseRetraction()),
            )

            if n == 2
                z = collect(reshape(1.0:4.0, 2, 2))
            else
                z = collect(reshape(1.0:9.0, 3, 3))
            end

            @test isapprox(M, project(G, z), project(M, z))
            z2 = allocate(pts[1])
            project!(G, z2, z)
            @test isapprox(M, z2, project(M, z))
        end

        @testset "vee/hat" begin
            X = Xpts[1]
            pe = Identity(G)

            Xⁱ = vee(G, identity_element(G), X)
            @test Xⁱ ≈ vee(G, pe, X)

            X2 = hat(G, pts[1], Xⁱ)
            @test isapprox(M, pe, X2, hat(G, pe, Xⁱ); atol=1e-6)
        end
        @testset "Identity and get_vector/get_coordinates" begin
            e = Identity(G)

            eF = Identity(SpecialEuclidean(n))
            if n == 2
                c = [1.0]
            else
                c = [1.0, 0.0, 0.0]
            end
            Y = zeros(representation_size(G))
            get_vector_lie!(G, Y, c, Manifolds.VeeOrthogonalBasis())
            @test Y ≈ get_vector_lie(G, c, Manifolds.VeeOrthogonalBasis())
            get_vector!(M, Y, identity_element(G), c, Manifolds.VeeOrthogonalBasis())
            @test Y ≈ get_vector(M, identity_element(G), c, Manifolds.VeeOrthogonalBasis())

            @test get_coordinates(
                M,
                identity_element(G),
                Y,
                Manifolds.VeeOrthogonalBasis(),
            ) == c
            c2 = similar(c)
            get_coordinates_lie!(G, c2, Y, Manifolds.VeeOrthogonalBasis())
            @test c == c2
            get_coordinates!(M, c2, identity_element(G), Y, Manifolds.VeeOrthogonalBasis())
            @test c == c2

            q = zeros(n, n)
            mul!(q, e, e)
            @test isone(q)
            e2 = Identity(G)
            @test mul!(e2, e, e) === e2
        end
    end
end


#=
JuMP helpers for constructing formulations for WSOS constraints
=#

# generate random polynomials nonegative on domain (may fail if domain not bounded)
function make_nonneg_polys(num::Int, Ps::Vector{Matrix{Float64}})
    # build JuMP model for finding minima (primal polymin)
    U = size(Ps[1], 1)
    K = Hypatia.WSOSInterpNonnegativeCone{Float64, Float64}(U, Ps)
    model = JuMP.Model(sep_hypatia)
    JuMP.@variable(model, a)
    JuMP.@objective(model, Max, a)
    con = JuMP.@constraint(model, ones(U) .- a in K)

    # generate polys
    return [make_nonneg_poly(U, model, con) for _ in 1:num]
end

function make_nonneg_poly(U::Int, model::JuMP.Model, con::JuMP.ConstraintRef)
    vals = Float64.(rand(0:9, U))
    MOI.modify(JuMP.backend(model), JuMP.index(con), MOI.VectorConstantChange(vals))

    JuMP.optimize!(model)
    stat = JuMP.termination_status(model)

    if stat in (MOI.OPTIMAL, MOI.ALMOST_OPTIMAL)
        # make poly nonnegative if minimum is negative
        min_val = JuMP.dual_objective_value(model)
        if min_val < 1e-5
            vals .+= abs(min_val)
        end
        return vals
    else
        # try again
        @warn("try again")
        return make_nonneg_poly(U, model, con)
    end
end

# find minimum value of a polynomial - solve primal polymin
function find_poly_min(vals::Vector{Float64}, Ps::Vector{Matrix{Float64}})
    U = size(Ps[1], 1)
    K = Hypatia.WSOSInterpNonnegativeCone{Float64, Float64}(U, Ps)
    model = JuMP.Model(sep_hypatia)
    JuMP.@variable(model, a)
    JuMP.@objective(model, Max, a)
    JuMP.@constraint(model, vals .- a in K)

    JuMP.optimize!(model)
    @assert JuMP.termination_status(model) in (MOI.OPTIMAL, MOI.ALMOST_OPTIMAL)
    return JuMP.dual_objective_value(model)
end

# check whether polynomial is nonnegative (actually SOS) - solve separation problem
function check_nonneg(vals::Vector{Float64}, Ps::Vector{Matrix{Float64}})
    U = size(Ps[1], 1)
    K = Hypatia.WSOSInterpNonnegativeCone{Float64, Float64}(U, Ps)
    model = JuMP.Model(sep_hypatia)
    JuMP.@constraint(model, vals in K)

    JuMP.optimize!(model)
    return (JuMP.termination_status(model) in (MOI.OPTIMAL, MOI.ALMOST_OPTIMAL))
end

# add a WSOS constraint
function add_wsos(
    use_nat::Bool,
    Ps::Vector{Matrix{Float64}},
    aff::AbstractVector{<:JuMPScalar},
    model::JuMP.Model,
)
    return (use_nat ? add_wsos_nat : add_wsos_ext)(Ps, aff, model)
end

# WSOS cone formulation
function add_wsos_nat(
    Ps::Vector{Matrix{Float64}},
    aff::AbstractVector{<:JuMPScalar},
    model::JuMP.Model,
)
    U = size(Ps[1], 1)
    @assert length(aff) == U
    K = Hypatia.WSOSInterpNonnegativeCone{Float64, Float64}(U, Ps)
    JuMP.@constraint(model, aff in K)
    return
end

# PSD extended formulation
function add_wsos_ext(
    Ps::Vector{Matrix{Float64}},
    aff::AbstractVector{<:JuMPScalar},
    model::JuMP.Model,
)
    U = size(Ps[1], 1)
    @assert length(aff) == U

    psd_vars = []
    for Pr in Ps
        Lr = size(Pr, 2)
        psd_r = JuMP.@variable(model, [1:Lr, 1:Lr], Symmetric)
        if Lr == 1
            JuMP.@constraint(model, psd_r[1, 1] >= 0)
        else
            JuMP.@constraint(model, psd_r in JuMP.PSDCone())
        end
        push!(psd_vars, psd_r)
    end

    coeffs_lhs = JuMP.@expression(
        model,
        [u in 1:U],
        sum(
            sum(
                Pr[u, k] * Pr[u, l] * psd_r[k, l] * (k == l ? 1 : 2) for
                k in 1:size(Pr, 2) for l in 1:k
            ) for (Pr, psd_r) in zip(Ps, psd_vars)
        )
    )
    JuMP.@constraint(model, coeffs_lhs .== aff)
    return
end


nt = 1000
seed = 99
ssm = acqr(0.8,1.5,2,3,0.5,2)
m = acqr_simula(ssm,nt,seed)
y = m["y"]
a = ssm.a
c = ssm.c
q = ssm.q
r = ssm.r
x10 = ssm.m1
P10 = ssm.P1

###########################
print("Testing acqr_ksmoother_s: ")
xtt,Ptt,xtt1,Ptt1,et,St,Kt,loglik = acqr_kfilter_s(y,a,c,q,r,x10)
xtN,PtN,Pt1tN = acqr_ksmoother_s(a,xtt,Ptt,xtt1,Ptt1)
if sum(xtN.^2) > 1e-6
    println("OK")
end


### CONCRETE TYPE: ITERATIVE PROX EVALUATION
# prox! is computed using CG on a system with matrix A'A + I/(lambda*gamma)
# or AA' + I/(lambda*gamma), according to which matrix is smaller.

using LinearAlgebra

struct LeastSquaresIterative{R <: Real, RC <: RealOrComplex{R}, M, V <: AbstractVector{RC}, O} <: LeastSquares
    A::M # m-by-n operator
    b::V
    Atb::V
    lambda::R
    shape::Symbol
    S::O
    res::Vector{RC} # m-sized buffer
    res2::Vector{RC} # m-sized buffer
    q::Vector{RC} # n-sized buffer
end

is_prox_accurate(f::LeastSquaresIterative) = false

function LeastSquaresIterative(A::M, b::V, lambda::R) where {R <: Real, RC <: RealOrComplex{R}, M, V <: AbstractVector{RC}}
    if size(A, 1) != length(b)
        error("A and b have incompatible dimensions")
    end
    if lambda <= 0
        error("lambda must be positive")
    end
    m, n = size(A)
    if m >= n
        shape = :Tall
        S = AcA(A)
        LeastSquaresIterative{R, RC, M, V, AcA}(A, b, A'*b, lambda, shape, S, zeros(RC, m), [], zeros(RC, n))
    else
        shape = :Fat
        S = AAc(A)
        LeastSquaresIterative{R, RC, M, V, AAc}(A, b, A'*b, lambda, shape, S, zeros(RC, m), zeros(RC, m), zeros(RC, n))
    end
end

function (f::LeastSquaresIterative{R, RC, M, V})(x::AbstractVector{RC}) where {R, RC, M, V}
    mul!(f.res, f.A, x)
    f.res .-= f.b
    return (f.lambda/2)*norm(f.res, 2)^2
end

function prox!(y::AbstractVector{D}, f::LeastSquaresIterative{R, RC, M, V}, x::AbstractVector{D}, gamma::R=R(1)) where {R, RC, M, V, D <: RealOrComplex{R}}
    lamgam = f.lambda*gamma
    f.q .= f.Atb .+ x./lamgam
    # two cases: (1) tall A, (2) fat A
    if f.shape == :Tall
        y .= x
        op = Shift(f.S, R(1)/lamgam)
        IterativeSolvers.cg!(y, op, f.q)
    else # f.shape == :Fat
        # y .= lamgam*(f.q - (f.A'*(f.fact\(f.A*f.q))))
        mul!(f.res, f.A, f.q)
        op = Shift(f.S, R(1)/lamgam)
        IterativeSolvers.cg!(f.res2, op, f.res)
        mul!(y, adjoint(f.A), f.res2)
        y .-= f.q
        y .*= -lamgam
    end
    mul!(f.res, f.A, y)
    f.res .-= f.b
    return (f.lambda/2)*norm(f.res, 2)^2
end

function gradient!(y::AbstractVector{D}, f::LeastSquaresIterative{R, RC, M, V}, x::AbstractVector{D}) where {R, RC, M, V, D <: Union{R, Complex{R}}}
    mul!(f.res, f.A, x)
    f.res .-= f.b
    mul!(y, adjoint(f.A), f.res)
    y .*= f.lambda
    fy = (f.lambda/2)*dot(f.res, f.res)
end

function prox_naive(f::LeastSquaresIterative, x::AbstractVector{D}, gamma::R=R(1)) where {R, D <: RealOrComplex{R}}
    lamgam = f.lambda*gamma
    y = IterativeSolvers.cg(f.A'*f.A + I/lamgam, f.Atb + x/lamgam)
    fy = (f.lambda/2)*norm(f.A*y-f.b)^2
    return y, fy
end


#-------------poor-julia.jl----------------------------------------------------#
#
# Purpose: This code is supposed to be slow in julia
#
#------------------------------------------------------------------------------#

type Loc
    x::Array{Float64}
    y::Array{Float64}
end

num = 1000
rad = 1

# this integrates the area of a circle
pos = Loc([0.0],[0.0])
count = 0
for i = 1:num
    push!(pos.x, rand())
    push!(pos.y, rand())
    if (pos.x[i] * pos.x[i] + pos.y[i] * pos.y[i] < rad*rad)
        count += 1.0
    end
end

println("The area of your circle is: ", (count / num) * rad*rad*4)


#=
CenterStrike
Number Of Strikes up and down
expiry

gender

market,

Vanna Volga Parameters

=#

function StrikeLadder(CallPutFlag, MarketPrice, CenterStrike, T, VVInput)

LowerStrike = CenterStrike - 5
UpperStrike = CenterStrike + 5
StrikePrices = collect(LowerStrike:0.5:UpperStrike)

SensitivityMatrix = zeros(Float64, length(StrikePrices), 7)

SensitivityMatrix[:,1] = StrikePrices
SensitivityMatrix[:,2] = 0.25

for i in 1:length(StrikePrices) SensitivityMatrix[i,3] = Black76(CallPutFlag, MarketPrice, StrikePrices[i], T, 0.05, SensitivityMatrix[i,2]) end

for i in 1:length(StrikePrices) SensitivityMatrix[i,4] = VannaVolga(VVInput.putStrike, VVInput.ATMStrike, VVInput.callStrike, StrikePrices[i], CallPutFlag, T, 0.05, 0, VVInput.putVol, VVInput.ATMVol, VVInput.callVol)[2] end

for i in 1:length(StrikePrices) SensitivityMatrix[i,5] = Black76(CallPutFlag, MarketPrice, StrikePrices[i], T, 0.05, SensitivityMatrix[i,4]) end

for i in 1:length(StrikePrices) SensitivityMatrix[i,6] = Delta(CallPutFlag, MarketPrice, StrikePrices[i], T, 0.05, 0, SensitivityMatrix[i,4]) end

for i in 1:length(StrikePrices) SensitivityMatrix[i,7] = Gamma(CallPutFlag, MarketPrice, StrikePrices[i], T, 0.05, 0, SensitivityMatrix[i,4]) end

SensitivityMatrix = DataFrame(SensitivityMatrix)
names!(SensitivityMatrix, [:Strike, :HVol, :ThPrice, :implVol, :implPrem, :implDelta, :implGamma])

SensitivityMatrix

end

#VVSep = VVParams(10, 11, 12, 0.25, 0.21, 0.3)

#StrikeLadder("c", 11, 11, 1/12, VVSep)


module MultivariateDiscretization

using DataFrames
using CSV
using StatsBase
using Discretizers
using Distributions
using Statistics
using LinearAlgebra
using MultivariateStats
using FreqTables
using Clustering
using InformationMeasures
using Distances

include("DoaneRule.jl")
include("BayesianBlocks.jl")
include("CPD.jl")
include("IPD.jl")
include("UniformWidth.jl")
include("benchmark_measures.jl")

export
    greedy_IPD,
    CPD,
    CPD_clustered,
    BayesianBlocks,
    DoaneRule,
    CJS_empirical,
    UW,
    greedy_IPD_clustered

end