# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     formats: ipynb,jl:hydrogen
#     text_representation:
#       extension: .jl
#       format_name: hydrogen
#       format_version: '1.3'
#       jupytext_version: 1.10.3
#   kernelspec:
#     display_name: Julia 1.6.4
#     language: julia
#     name: julia-1.6
# ---

# %%
using Distributions
using Plots
using Optim

traceon = Optim.Options(
    store_trace = true,
    extended_trace = true
)

# model
Distributions.TDist(μ, ρ, ν) = LocationScale(μ, ρ, TDist(ν))

# test data
X = [-0.01, 0.01, 1.0]

# Warning: Very Slow!
lower = [-0.1, -15.0, -2.5]
upper = [ 0.4,  10.0,  8.0]
@show lower upper
r = @time optimize(x -> -loglikelihood(TDist(x[1], 10^x[2], 10^x[3]), X), lower, upper, [0.33, 0.0, 0.27], Fminbox(LBFGS()), traceon)

# %%
function plot_trace(r)
    x = r.initial_x
    c = hcat(x, (t.metadata["x"] for t in r.trace)...)
    l = -minimum(r)
    m = r.minimizer
    t = "initial = $x\nfinal = $(round.(m; digits=3))\nfinal loglikelihood = $(round(l; digits=3))"

    plot(; legend=:topleft)
    scatter3d!([x[1]], [x[2]], [x[3]]; label="initial", c=:blue, ms=3)
    plot!(c[1,:], c[2,:], c[3,:]; label="path", c=:darkcyan)
    scatter3d!([m[1]], [m[2]], [m[3]]; label="final", c=:yellow, ms=3)
    plot!(; xlabel="μ", ylabel="log₁₀ρ", zlabel="log₁₀ν")
    title!(t; titlefontsize=10)
end

plot_trace(r)
plot!(; fmt=:png)

# %%
function f(μ, log10ν, X)
    log10rhos = range(-15, 10; length=200)
    -maximum(loglikelihood(TDist(μ, 10^log10ρ, 10^log10ν), X) for log10ρ in log10rhos)
end

function plot_trace2d(r)
    x = r.initial_x
    c = hcat(x, (t.metadata["x"] for t in r.trace)...)
    l = -minimum(r)
    m = r.minimizer
    t = "initial = $x\nfinal = $(round.(m; digits=3))\nfinal loglikelihood = $(round(l; digits=3))"

    mus = range(-0.1, 0.4; length=200)
    log10nus = range(-2.5, 8.0; length=200)
    z = f.(mus', log10nus, Ref(X))

    plot(; legend=:topleft, colorbar=false)
    plot!(; xlim=extrema(mus), ylim=extrema(log10nus))
    heatmap!(mus, log10nus, z; clim=(-1, 5), c=reverse(cgrad(:CMRmap)))
    scatter!([x[1]], [x[3]]; label="initial", ms=4, c=:blue)
    plot!(c[1,:], c[3,:]; label="path", c=:cyan, lw=1.5)
    scatter!([m[1]], [m[3]]; label="final", ms=4, c=:yellow)
    plot!(; xlabel="μ", ylabel="log₁₀ν")
    title!(t; titlefontsize=10)
end

plot_trace2d(r)
plot!(; fmt=:png)

# %%

0024/TDist model is not log-convex - Fminbox with LBFGS.jl


using Revise
using ZonalFlow

dn      = "validate/stochastic/linear/"

tspan = (0.0,1000.0);
tsargs  = ( dt=0.001,
            adaptive=false,
            progress=true,
            progress_steps=100000,
            save_everystep=false,
            save_noise=false,
            saveat=5
           );

eqs     = [GQL(Λ=0),CE2(),CE2()];

"""
   Single mode excitation
"""

coeffs  = Coefficients(Ω=5.0,θ=0.0,μ=0.01,ν=0.0,ν₄=0.0,linear=true);
forcing = Stochastic(kf=3,dk=0,ε=0.01); #note dk = 0 => only kf is excited

labels  = ["ql_6x6_m3","ce2_6x6_m3","ce2_6x6_qlic_m3"];

domain  = Domain(extent=(2π,2π),res=(6,6));
prob    = BetaPlane(domain,coeffs,forcing);

ql     = integrate(prob,eqs[1],(0.0,5000.0);tsargs...);
write(prob,eqs[1],ql,dn=dn,fn=labels[1])

ce2     = integrate(prob,eqs[2],tspan;tsargs...);
write(prob,eqs[2],ce2,dn=dn,fn=labels[2])

ce2_qlic     = integrate(prob,eqs[3],tspan;u0=ql.u[end],tsargs...);
write(prob,eqs[3],ce2_qlic,dn=dn,fn=labels[3])

"""
   Three-mode excitations
"""

# 6x6

domain  = Domain(extent=(2π,2π),res=(6,6));
prob    = BetaPlane(domain,coeffs,forcing);

coeffs  = Coefficients(Ω=5.0,θ=0.0,μ=0.01,ν=0.0,ν₄=0.0,linear=true);
forcing = Stochastic(kf=3,dk=1,ε=0.01);

ob,eqs[1],(0.0,5000.0);tsargs...);
write(prob,eqs[1],ql,dn=dn,fn=labels[1])

ce2     = integrate(prob,eqs[2],tspan;tsargs...);
write(prob,eqs[2],ce2,dn=dn,fn=labels[2])

ce2_qlic     = integrate(prob,eqs[3],tspan;u0=ql.u[end],tsargs...);
write(prob,eqs[3],ce2_qlic,dn=dn,fn=labels[3])

# 6x6 with dt = 0.005
domain  = Domain(extent=(2π,2π),res=(6,6));
prob    = BetaPlane(domain,coeffs,forcing);

tsargs  = ( dt=0.0005,
            adaptive=false,
            progress=true,
            progress_steps=100000,
            save_everystep=false,
            saveat=5
           );

labels  = ["ql_6x6_dt0005","ce2_6x6_dt0005","ce2_6x6_qlic_dt0005"];

ql     = integrate(prob,eqs[1],(0.0,5000.0);tsargs...);
write(prob,eqs[1],ql,dn=dn,fn=labels[1])

ce2     = integrate(prob,eqs[2],tspan;tsargs...);
write(prob,eqs[2],ce2,dn=dn,fn=labels[2])

ce2_qlic     = integrate(prob,eqs[3],tspan;u0=ql.u[end],tsargs...);
write(prob,eqs[3],ce2_qlic,dn=dn,fn=labels[3])

# 8x8
domain  = Domain(extent=(2π,2π),res=(8,8));
prob    = BetaPlane(domain,coeffs,forcing);

tsargs  = ( dt=0.001,
            adaptive=false,
            progress=true,
            progress_steps=100000,
            save_everystep=false,
            saveat=5
           );

labels  = ["ql_8x8","ce2_8x8","ce2_8x8_qlic"];

ql     = integrate(prob,eqs[1],(0.0,5000.0);tsargs...);
write(prob,eqs[1],ql,dn=dn,fn=labels[1])

ce2     = integrate(prob,eqs[2],tspan;tsargs...);
write(prob,eqs[2],ce2,dn=dn,fn=labels[2])

ce2_qlic     = integrate(prob,eqs[3],tspan;u0=ql.u[end],tsargs...);
write(prob,eqs[3],ce2_qlic,dn=dn,fn=labels[3])

# 10x10

domain  = Domain(extent=(2π,2π),res=(10,10));
prob    = BetaPlane(domain,coeffs,forcing);

labels  = ["ql_10x10","ce2_10x10","ce2_10x10_qlic"];

ql     = integrate(prob,eqs[1],(0.0,5000.0);tsargs...);
write(prob,eqs[1],ql,dn=dn,fn=labels[1])

ce2     = integrate(prob,eqs[2],tspan;tsargs...);
write(prob,eqs[2],ce2,dn=dn,fn=labels[2])

ce2_qlic     = integrate(prob,eqs[3],tspan;u0=ql.u[end],tsargs...);
write(prob,eqs[3],ce2_qlic,dn=dn,fn=labels[3])

# 12x12

domain  = Domain(extent=(2π,2π),res=(12,12));
prob    = BetaPlane(domain,coeffs,forcing);

labels  = ["ql_12x12","ce2_12x12","ce2_12x12_qlic"];

ql     = integrate(prob,eqs[1],(0.0,5000.0);tsargs...);
write(prob,eqs[1],ql,dn=dn,fn=labels[1])

ce2     = integrate(prob,eqs[2],tspan;tsargs...);
write(prob,eqs[2],ce2,dn=dn,fn=labels[2])

ce2_qlic     = integrate(prob,eqs[3],tspan;u0=ql.u[end],tsargs...);
write(prob,eqs[3],ce2_qlic,dn=dn,fn=labels[3])

# GCE2
# eqs     = [NL(),GQL(Λ=prob.d.nx-1),GCE2(Λ=prob.d.nx-1)];
# labels  = ["nl_linear","gql_5_linear","gce2_5_linear"];
# sols    = integrate(prob,eqs,tspan;tsargs...);
# write(prob,eqs,sols,dn="validate/stochastic/6x6/",labels=labels)


# TODO: Make tests that vary the parameters and validate the output.

plot(exp, 0, 30)
plot(sinpi, -2, 2)
plot(rand(15))
plot(rand(15, 4))

plot(x -> cos(x), 0:1)
plot(cos, 0, 1, border=false)
plot(x -> sin(x), -15:15)
plot([x -> cos(x), x -> cos(x + pi)], 0:5)
plot(x -> x^3 - 2x^2 + 3x, -5, 5)
plot(x -> tanh(x), -1.5:1.5)
plot(z -> 3z^2 - 2z^3, 0:1)

plot([1, 3, 5])
plot(1:10)
plot([1, 3, 5, 7], [13, 11, 9, 7])
plot([1:2:20], rand(10, 3))

plot(exp, 0.1:20)
logplot(exp, 0.1:20)
plot(x -> exp(-x/5), 0.01:20)
logplot(x -> exp(-x/5), 0.01:20)

plot(sinpi, -2, 2, invert=false, gridlines=false)
plot(sinpi, -2, 2, invert=true, gridlines=false)
plot(sinpi, -2, 2, invert=false, gridlines=true)
plot(sinpi, -2, 2, invert=true, gridlines=true)

# Test rounding and symoblic label display.
plot(sin, -6pi, 6pi, cols=140)


const _SOCtoRSOCMap{T} =
    Union{Constraint.SOCtoRSOCBridge{T},Variable.RSOCtoSOCBridge{T}}

function map_set(::Type{<:_SOCtoRSOCMap}, set::MOI.SecondOrderCone)
    return MOI.RotatedSecondOrderCone(MOI.dimension(set))
end

function inverse_map_set(
    ::Type{<:_SOCtoRSOCMap},
    set::MOI.RotatedSecondOrderCone,
)
    return MOI.SecondOrderCone(MOI.dimension(set))
end

const _RSOCtoSOCMap{T} =
    Union{Constraint.RSOCtoSOCBridge{T},Variable.SOCtoRSOCBridge{T}}

function map_set(::Type{<:_RSOCtoSOCMap}, set::MOI.RotatedSecondOrderCone)
    return MOI.SecondOrderCone(MOI.dimension(set))
end

function inverse_map_set(::Type{<:_RSOCtoSOCMap}, set::MOI.SecondOrderCone)
    return MOI.RotatedSecondOrderCone(MOI.dimension(set))
end

const _RSOCMap{T} = Union{_RSOCtoSOCMap{T},_SOCtoRSOCMap{T}}

function map_function(::Type{<:_RSOCMap{T}}, func, i::IndexInVector) where {T}
    scalars = MOIU.eachscalar(func)
    if i.value == 1 || i.value == 2
        t = scalars[1]
        u = scalars[2]
        s2 = √T(2)
        ts = MOIU.operate!(/, T, t, s2)
        us = MOIU.operate!(/, T, u, s2)
        # Cannot use `operate!` here since `ts` and `us` are needed for the next
        # line
        if i.value == 1
            return t / s2 + u / s2
        else
            return t / s2 - u / s2
        end
    else
        return scalars[i.value]
    end
end

function map_function(::Type{<:_RSOCMap{T}}, func) where {T}
    scalars = MOIU.eachscalar(func)
    t = scalars[1]
    u = scalars[2]
    x = scalars[3:end]
    s2 = √T(2)
    ts = MOIU.operate!(/, T, t, s2)
    us = MOIU.operate!(/, T, u, s2)
    # Cannot use `operate!` here since `ts` and `us` are needed for the next
    # line
    y = ts - us
    z = MOIU.operate!(+, T, ts, us)
    return MOIU.operate(vcat, T, z, y, x)
end

# The map is an involution
function inverse_map_function(BT::Type{<:_RSOCMap}, func)
    return map_function(BT, func)
end

# The map is symmetric
function adjoint_map_function(BT::Type{<:_RSOCMap}, func)
    return map_function(BT, func)
end

# The map is a symmetric involution
function inverse_adjoint_map_function(BT::Type{<:_RSOCMap}, func)
    return map_function(BT, func)
end


function _circular_neighbor_offsets(npoints::Integer, radius::Real)
    radius >= 1 || throw(ArgumentError("radius >= 1.0 is expected."))

    ntuple(npoints) do p
        θ=2π*(p-1)/npoints
        pos = (-1, 1) .* radius .* sincos(θ)
        # rounding the positions for more stable result when applying interpolation
        round.(pos; digits=8)
    end
end

# A helper function to let `interpolation` keyword correctly understands `Degree` inputs
@inline wrap_BSpline(itp::Interpolations.InterpolationType) = itp
@inline wrap_BSpline(degree::Interpolations.Degree) = BSpline(degree)

@inline _inbounds_getindex(A, I) = @inbounds A[I...]
@inline _inbounds_getindex(A::Interpolations.AbstractInterpolation, I) = @inbounds A(I...)


using BandedMatrices
using LinearAlgebra
using Test

include("test_banded.jl")
include("test_subarray.jl")
include("test_gbmm.jl")
include("test_broadcasting.jl")
include("test_indexing.jl")
include("test_bandedlu.jl")
include("test_bandedqr.jl")
include("test_symbanded.jl")
include("test_tribanded.jl")
include("test_interface.jl")
include("test_miscs.jl")


println("Starting -- Julia seems to be working.")
println("")

using Random
Random.seed!(1)

###############################################################
println("Let's first create an initial cluster, with 16 star formation events")

include("./Cluster.jl")

lmax = 10

n_disks = 16
n_stars_per_disk = 64
σ = 0.006

m_min = 1.0
m_max = 100.0
γₘ = -2.0

a_min = 1.0
a_max = 100.0
γₐ = 0.0

e_min = 0.0
e_max = 0.3
γₑ = 1.0

clust = Cluster_initial(n_disks, n_stars_per_disk, lmax,
                        σ = σ,
                        m_min = m_min, m_max = m_max, γₘ = γₘ,
                        a_min = a_min, a_max = a_max, γₐ = γₐ,
                        e_min = e_min, e_max = e_max, γₑ = γₑ)

println("We can compute this cluster's binding energy E and spin s")
Etot_norm, s = get_ES_from_clust(clust) # Here, Etot_norm = Etot / (J N^2) = -E
                                        # J = G mₘᵢₙ² / aₘᵢₙ

###############################################################
println("The cluster has been created and its (E, s) computed")
println("Now let's plot it")
# It should reproduce the figure "Initial_cluster.png"

using Plots

my_plot_1 = plot_that_cluster(clust)

display(my_plot_1)

###############################################################
println("")
println("Now let's create the initial DF related to this initial cluster")
# This means we create all the Kₖ = (m, a, e), Nₖ, Lₖ and Jₗ[K, K'], and we translate (E, s) from the initial cluster into (Eₜₒₜ, Lₜₒₜ)

# First we create the m, a, e bins, and fill them with stars i.e. create the N(K) vector
include("./Miscellaneous.jl")

n_pop_m = 10
n_pop_a = 3
n_pop_e = 1

Bins_m, Bins_a, Bins_e, K, L, N = create_KLN(m_min, m_max, γₘ, n_pop_m,
                                            a_min, a_max, γₐ, n_pop_a,
                                            e_min, e_max, γₑ, n_pop_e)

# Then we create the matrix of coupling coefficients J
include("./Coupling_coefficients_interpolated.jl")

J = Coupling_coefficients_interpolated(K, lmax)

# Then we translate the normalized binding energy and spin into the pure energy and angular momentum
include("./Distribution.jl")

Etot, Ltot = get_EL_for_dist(Etot_norm, s,
                                L, N, m_min, a_min)

###############################################################
println("The initial DF realted to the initial cluster has been created")
println("")
println("Let's modify this DF so that it maximizes the entropy S")
# This means we relax the cluster

include("./Solve_by_energy_sequence.jl")
using LinearAlgebra

dist = solve_by_energy_sequence(J, L, N, Etot, Ltot)
println( norm(dist.cost) < 10^(-6) ) # This is to check that the optimization method converged

###############################################################
println("The entropy has been maximized")
println("Finally, let's plot the final DF")
# It should reproduce the figure "Relaxed_cluster.png"

my_plot_2 = plot_that_dist("m", # "m" means that we want to plot the segregation in mass.
                                # "a" or "e" would give the segregations in semi-major axis and eccentricity
                        Etot_norm, s, lmax,
                        dist,
                        Bins_m, n_pop_m,
                        Bins_a, n_pop_a,
                        Bins_e, n_pop_e)

display(my_plot_2)

readline() # This is just so that the figure panels remain open


using Flux: Chain, train!, params
using Flux.Losses:mse
using Flux.Data:DataLoader
using Flux.Optimise:ADAM
using IterTools:ncycle
using Statistics:mean

"""
    AE(encoder= Chain(),
       decoder = Chain(),
       batchsize= 32,
       epochs = 1,
       shuffle = false,
       partial = true,
       opt = ADAM(),
       loss = mse)

Calculate the anomaly score of an instance based on the reconstruction loss of an autoencoder, see [1] for an
explanation of auto encoders.

Parameters
----------
$_ae_params

$_ae_loss

Examples
--------
$(_score_unsupervised("AE"))

References
----------
[1] Aggarwal, Charu C. (2017): Outlier Analysis.
"""
MMI.@mlj_model struct AE <: UnsupervisedDetector
    encoder::Chain = Chain()
    decoder::Chain = Chain()
    batchsize::Integer = 32
    epochs::Integer = 1
    shuffle::Bool = false
    partial::Bool = true
    opt::Any = ADAM()
    loss::Function = mse
end

struct AEModel <: Model
    chain::Chain
end

function fit(detector::AE, X::Data)::Fit
    loader = DataLoader(X, batchsize=detector.batchsize, shuffle=detector.shuffle, partial=detector.partial)

    # Create the autoencoder
    model = Chain(detector.encoder, detector.decoder)

    # train the neural network model
    train!(x -> detector.loss(model(x), x), params(model), ncycle(loader, detector.epochs), detector.opt)

    scores = detector.loss(model(X), X, agg=instance_mean)
    Fit(AEModel(model), scores)
end

@score function score(detector::AE, model::Fit, X::Data)::Result
    detector.loss(model.chain(X), X, agg=instance_mean)
end


# This file is a part of AstroLib.jl. License is MIT "Expat".
# Copyright (C) 2016 Mosè Giordano.

const Mjprec =
    SMatrix{6,6}(+0.9999256782,     +0.0111820610,     +0.0048579479,
                 -0.000551,         +0.238514,         -0.435623,
                 -0.0111820611,     +0.9999374784,     -0.0000271474,
                 -0.238565,         -0.002667,         +0.012254,
                 -0.0048579477,     -0.0000271765,     +0.9999881997,
                 +0.435739,         -0.008541,         +0.002117,
                 +0.00000242395018, +0.00000002710663, +0.00000001177656,
                 +0.99994704,       +0.01118251,       +0.00485767,
                 -0.00000002710663, +0.00000242397878, -0.00000000006582,
                 -0.01118251,       +0.99995883,       -0.00002714,
                 -0.00000001177656, -0.00000000006587, 0.00000242410173,
                 -0.00485767,       -0.00002718,       1.00000956)

# Note: IDL version of `jprecess' changes in-place "muradec", "parallax" and
# "radvel".  We don't do anything like this, but calculations are below,
# commented, in case someone is interested.
function _jprecess(ra::T, dec::T, parallax::T, radvel::T, epoch::T,
                   muradec::Vector{T}) where {T<:AbstractFloat}
    @assert length(muradec) == 2
    sinra, cosra  = sincos(deg2rad(ra))
    sindec, cosdec = sincos(deg2rad(dec))
    if isfinite(epoch) && epoch != 1950
        A = A_precess .+ deg2rad.(A_dot_precess .* (epoch .- 1950) ./ 360000)
    else
        A = A_precess
    end
    r0 = SVector(cosra*cosdec,  sinra*cosdec,  sindec)
    r0_dot = SVector(-muradec[1]*sinra*cosdec - muradec[2]*cosra*sindec,
                     muradec[1]*cosra*cosdec - muradec[2]*sinra*sindec,
                     muradec[2]*cosdec) + 21.095 * radvel * parallax * r0
    r1 = r0 .- A .+ dot(r0, A) .* r0
    r1_dot = r0_dot .- A_dot_precess .+ dot(r0, A_dot_precess) .* r0
    R_1 = vcat(r1, r1_dot)
    R  = Mjprec*R_1
    if isfinite(epoch)
        t  = ((epoch - 1950) - 50.00021) / 100
        rr1 = @view(R[1:3]) .+ deg2rad.(@view(R[4:6]) .* t ./ 3600)
        x = rr1[1]
        y = rr1[2]
        z = rr1[3]
    else
        x = R[1]
        y = R[2]
        z = R[3]
        # x_dot = R[3]
        # y_dot= R[4]
        # z_dot = R[5]
    end
    rmag = norm((x, y, z))
    r2 = rmag*rmag
    dec2000 = asin(z / rmag)
    ra2000  = atan(y, x)
    # if isnan(epoch)
    #     muradec[1] = (x*y_dot - y*x_dot)/(x*x + y*y)
    #     muradec[2] = (z_dot*(x*x + y*y) - z*(x*x_dot + y*y_dot))/(r2*norm((x, y)))
    # end
    # if parallax > 0
    #     radvel   = (x*x_dot + y*y_dot + z*z_dot)/(21.095*parallax*rmag)
    #     parallax = parallax / rmag
    # end
    if ra2000 < 0
        ra2000 += 2 * T(pi)
    end
    ra2000  = rad2deg(ra2000)
    dec2000 = rad2deg(dec2000)
    return ra2000, dec2000
end

# Main interface.
jprecess(ra::Real, dec::Real, muradec::Vector{R};
         parallax::Real=0.0, radvel::Real=0.0) where {R<:Real} =
             _jprecess(promote(float(ra), float(dec), float(parallax),
                               float(radvel), NaN)...,
                       float(muradec))

jprecess(ra::Real, dec::Real, epoch::Real=1950.0) =
    _jprecess(promote(float(ra), float(dec), 0.0, 0.0, float(epoch))...,
              zeros(typeof(float(ra)), 2))

# Tuple arguments.
jprecess(radec::Tuple{Real,Real}, muradec::Vector{<:Real};
         parallax::Real=0.0, radvel::Real=0.0) =
             jprecess(radec..., muradec, parallax=parallax, radvel=radvel)

jprecess(radec::Tuple{Real,Real}, epoch::Real=1950.0) =
    jprecess(radec..., epoch)

# Vectorial arguments.
function jprecess(ra::AbstractArray{R}, dec::AbstractArray{<:Real},
                  muradec::AbstractArray{<:Real};
                  parallax::AbstractArray{<:Real}=zeros(R, length(ra)),
                  radvel::AbstractArray{<:Real}=zeros(R, length(ra))) where {R<:Real}
    @assert length(ra) == length(dec) == size(muradec)[2] == length(parallax) == length(radvel)
    typer = float(R)
    ra2000  = similar(ra, typer)
    dec2000 = similar(dec, typer)
    for i in eachindex(ra)
        ra2000[i], dec2000[i] = jprecess(ra[i], dec[i], muradec[:,i],
                                         parallax=parallax[i], radvel=radvel[i])
    end
    return ra2000, dec2000
end

function jprecess(ra::AbstractArray{R}, dec::AbstractArray{<:Real},
                  epoch::Real=1950.0) where {R<:Real}
    @assert length(ra) == length(dec)
    typer = float(R)
    ra2000  = similar(ra, typer)
    dec2000 = similar(dec, typer)
    for i in eachindex(ra)
        ra2000[i], dec2000[i] = jprecess(ra[i], dec[i], epoch[i])
    end
    return ra2000, dec2000
end

"""
    jprecess(ra, dec[, epoch]) -> ra2000, dec2000
    jprecess(ra, dec, muradec[, parallax=parallax, radvel=radvel]) -> ra2000, dec2000

### Purpose ###

Precess positions from B1950.0 (FK4) to J2000.0 (FK5).

### Explanation ###

Calculate the mean place of a star at J2000.0 on the FK5 system from the mean
place at B1950.0 on the FK4 system.

`jprecess` function has two methods, one for each of the following cases:

* the proper motion is known and non-zero
* the proper motion is unknown or known to be exactly zero (i.e. extragalactic
  radio sources).  Better precision can be achieved in this case by inputting
  the epoch of the original observations.

### Arguments ###

The function has 2 methods.  The common mandatory arguments are:

* `ra`: input B1950 right ascension, in degrees.
* `dec`: input B1950 declination, in degrees.

The two methods have a different third argument (see "Explanation" section for
more details).  It can be one of the following:

* `muradec`: 2-element vector containing the proper motion in seconds of arc
  per tropical *century* in right ascension and declination.
* `epoch`: scalar giving epoch of original observations.

If none of these two arguments is provided (so `jprecess` is fed only with right
ascension and declination), it is assumed that proper motion is exactly zero and
`epoch = 1950`.

If it is used the method involving `muradec` argument, the following keywords
are available:

* `parallax` (optional numerical keyword): stellar parallax, in seconds of arc.
* `radvel` (optional numerical keyword): radial velocity in km/s.

Right ascension and declination can be passed as the 2-tuple `(ra, dec)`.  You
can also pass `ra`, `dec`, `parallax`, and `radvel` as arrays, all of the same
length N.  In that case, `muradec` should be a matrix 2×N.

### Output ###

The 2-tuple of right ascension and declination in 2000, in degrees, of input
coordinates is returned.  If `ra` and `dec` (and other possible optional
arguments) are arrays, the 2-tuple of arrays `(ra2000, dec2000)` of the same
length as the input coordinates is returned.

### Method ###

The algorithm is taken from the Explanatory Supplement to the Astronomical
Almanac 1992, page 184.  See also Aoki et al (1983), A&A, 128, 263.  URL:
http://adsabs.harvard.edu/abs/1983A%26A...128..263A.

### Example ###

The SAO catalogue gives the B1950 position and proper motion for the
star HD 119288.   Find the J2000 position.

* RA(1950) = 13h 39m 44.526s
* Dec(1950) = 8d 38' 28.63''
* Mu(RA) = -.0259 s/yr
* Mu(Dec) = -.093 ''/yr

```jldoctest
julia> using AstroLib

julia> muradec = 100 * [-15*0.0259, -0.093]; # convert to century proper motion

julia> ra = ten(13, 39, 44.526)*15;

julia> decl = ten(8, 38, 28.63);

julia> adstring(jprecess(ra, decl, muradec), precision=2)
" 13 42 12.740  +08 23 17.69"
```

### Notes ###

"When transferring individual observations, as opposed to catalog mean place,
the safest method is to tranform the observations back to the epoch of the
observation, on the FK4 system (or in the system that was used to to produce the
observed mean place), convert to the FK5 system, and transform to the the epoch
and equinox of J2000.0" -- from the Explanatory Supplement (1992), p. 180

`bprecess` performs the precession to B1950 coordinates.

Code of this function is based on IDL Astronomy User's Library.
"""
jprecess


# https://en.wikipedia.org/wiki/Letter_frequency
function text_frequencies()
    (
        0.082,
        0.015,
        0.028,
        0.043,
        0.130,
        0.022,
        0.020,
        0.061,
        0.070,
        0.0015,
        0.0077,
        0.040,
        0.025,
        0.067,
        0.075,
        0.019,
        0.00095,
        0.060,
        0.063,
        0.091,
        0.028,
        0.0098,
        0.024,
        0.0015,
        0.020,
        0.00074,
    )
end

# https://en.wikipedia.org/wiki/Letter_frequency
function dictionary_frequencies()
    (
        0.078,
        0.02,
        0.04,
        0.038,
        0.11,
        0.014,
        0.03,
        0.023,
        0.082,
        0.0021,
        0.025,
        0.053,
        0.027,
        0.072,
        0.061,
        0.028,
        0.0024,
        0.073,
        0.087,
        0.067,
        0.033,
        0.01,
        0.0091,
        0.0027,
        0.016,
        0.0044,
    )
end

"""
Maximize text frequency of unique letters.
"""
function text_frequency_score(candidate::Candidate)
    letter_flags = LetterFlags(candidate)
    unnormalized_score = dot(letter_flags.flags, text_frequencies())
    # unnormalized_score / (5 * maximum(text_frequencies()))
    unnormalized_score / 0.65
end

"""
Maximize dictionary frequency of unique letters.
"""
function dictionary_frequency_score(candidate::Candidate)
    letter_flags = LetterFlags(candidate)
    unnormalized_score = dot(letter_flags.flags, dictionary_frequencies())
    # unnormalized_score / (5 * maximum(dictionary_frequencies()))
    unnormalized_score / 0.55
end

"""
Which letters have never been seen in previous trials?
"""
function never_seen(trials::Vector{Trial})
    previously_seen = fill(false, MVector{26, Bool})

    for trial in trials
        previously_seen .|= LetterFlags(trial.candidate).flags
    end

    .!previously_seen
end

"""
Maximize number of unique letters that were not already guessed.
"""
function novelty_score(candidate::Candidate, never_seen)
    sum(LetterFlags(candidate).flags .& never_seen) / 5
end

function score_candidates(
    candidates::Vector{Candidate},
    trials::Vector{Trial},
    weights::Vector,
)
    never_seen_chars = never_seen(trials)

    score_functions = (
        c -> text_frequency_score(c),
        c -> dictionary_frequency_score(c),
        c -> novelty_score(c, never_seen_chars),
    )

    @assert length(score_functions) == length(weights)

    scores = fill(0.0, length(candidates))

    for (i, f) in enumerate(score_functions)
        for (j, candidate) in enumerate(candidates)
            scores[j] += weights[i] * f(candidate)
        end
    end

    scores
end


D2hs = [Symmetry.Symel("E", [1 0 0; 0 1 0; 0 0 1]),
    Symmetry.Symel("sigmah", [1 0 0; 0 1 0; 0 0 -1]),
    Symmetry.Symel("i", [-1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2^1", [-1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("sigmav_1", [-1.0 0.0 -0.0; 0.0 1.0 0.0; -0.0 0.0 1.0]),
    Symmetry.Symel("sigmad_1", [1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_2(2)", [1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(1)", [1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0])]

D3hs = [Symmetry.Symel("E", [1 0 0; 0 1 0; 0 0 1]),
    Symmetry.Symel("sigmah", [1 0 0; 0 1 0; 0 0 -1]),
    Symmetry.Symel("C_3^1", [-0.4999999999999998 -0.8660254037844387 0.0; 0.8660254037844387 -0.4999999999999998 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_3^2", [-0.5000000000000003 0.8660254037844384 0.0; -0.8660254037844384 -0.5000000000000003 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("S_3^1", [-0.4999999999999998 -0.8660254037844387 -0.0; 0.8660254037844387 -0.4999999999999998 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("S_3^5", [-0.5000000000000003 0.8660254037844384 0.0; -0.8660254037844384 -0.5000000000000003 -0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("sigmav_2", [0.5000000000000002 0.8660254037844387 0.0; 0.8660254037844387 -0.5000000000000009 -0.0; 0.0 -0.0 1.0]),
    Symmetry.Symel("sigmav_3", [0.4999999999999991 -0.8660254037844394 0.0; -0.8660254037844394 -0.4999999999999998 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("sigmav_1", [-1.0 0.0 -0.0; 0.0 1.0 0.0; -0.0 0.0 1.0]),
    Symmetry.Symel("C_2(2)", [-0.5000000000000002 -0.8660254037844387 0.0; -0.8660254037844387 0.5000000000000009 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(3)", [-0.4999999999999991 0.8660254037844394 0.0; 0.8660254037844394 0.4999999999999998 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(1)", [1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0])]

D4hs = [Symmetry.Symel("E", [1 0 0; 0 1 0; 0 0 1]),
    Symmetry.Symel("sigmah", [1 0 0; 0 1 0; 0 0 -1]),
    Symmetry.Symel("i", [-1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_4^1", [0.0 -1.0 0.0; 1.0 0.0 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_2^1", [-1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_4^3", [0.0 1.0 0.0; -1.0 0.0 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("S_4^1", [0.0 -1.0 0.0; 1.0 0.0 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("S_4^3", [0.0 1.0 0.0; -1.0 0.0 -0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("sigmav_2", [-1.0 0.0 0.0; 0.0 1.0 -0.0; 0.0 -0.0 1.0]),
    Symmetry.Symel("sigmav_1", [-1.0 0.0 -0.0; 0.0 1.0 0.0; -0.0 0.0 1.0]),
    Symmetry.Symel("sigmad_2", [0.0 1.0 0.0; 1.0 0.0 -0.0; 0.0 -0.0 1.0]),
    Symmetry.Symel("sigmad_1", [0.0 -1.0 0.0; -1.0 0.0 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_2(2)", [-1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(3)", [1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(4)", [-1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(1)", [1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0])]

D5hs = [Symmetry.Symel("E", [1 0 0; 0 1 0; 0 0 1]),
    Symmetry.Symel("sigmah", [1 0 0; 0 1 0; 0 0 -1]),
    Symmetry.Symel("C_5^1", [0.30901699437494745 -0.9510565162951535 0.0; 0.9510565162951535 0.30901699437494745 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_5^2", [-0.8090169943749473 -0.5877852522924731 0.0; 0.5877852522924731 -0.8090169943749473 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_5^3", [-0.8090169943749475 0.587785252292473 0.0; -0.587785252292473 -0.8090169943749475 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_5^4", [0.30901699437494734 0.9510565162951535 0.0; -0.9510565162951535 0.30901699437494734 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("S_5^1", [0.30901699437494745 -0.9510565162951535 0.0; 0.9510565162951535 0.30901699437494745 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("S_5^7", [-0.8090169943749473 -0.5877852522924731 -0.0; 0.5877852522924731 -0.8090169943749473 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("S_5^3", [-0.8090169943749475 0.587785252292473 0.0; -0.587785252292473 -0.8090169943749475 -0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("S_5^9", [0.30901699437494734 0.9510565162951535 0.0; -0.9510565162951535 0.30901699437494734 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("sigmav_2", [0.8090169943749473 -0.5877852522924734 -0.0; -0.5877852522924734 -0.8090169943749481 -0.0; -0.0 -0.0 1.0]),
    Symmetry.Symel("sigmav_3", [-0.3090169943749479 0.951056516295154 0.0; 0.951056516295154 0.3090169943749471 -0.0; 0.0 -0.0 1.0]),
    Symmetry.Symel("sigmav_4", [-0.30901699437494834 -0.9510565162951539 0.0; -0.9510565162951539 0.30901699437494745 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("sigmav_5", [0.8090169943749475 0.5877852522924731 -0.0; 0.5877852522924731 -0.8090169943749481 0.0; -0.0 0.0 1.0]),
    Symmetry.Symel("sigmav_1", [-1.0 0.0 -0.0; 0.0 1.0 0.0; -0.0 0.0 1.0]),
    Symmetry.Symel("C_2(2)", [-0.8090169943749473 0.5877852522924734 0.0; 0.5877852522924734 0.8090169943749481 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(3)", [0.3090169943749479 -0.951056516295154 0.0; -0.951056516295154 -0.3090169943749471 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(4)", [0.30901699437494834 0.9510565162951539 0.0; 0.9510565162951539 -0.30901699437494745 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(5)", [-0.8090169943749475 -0.5877852522924731 0.0; -0.5877852522924731 0.8090169943749481 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(1)", [1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0])]

D6hs = [Symmetry.Symel("E", [1 0 0; 0 1 0; 0 0 1]),
    Symmetry.Symel("sigmah", [1 0 0; 0 1 0; 0 0 -1]),
    Symmetry.Symel("i", [-1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_6^1", [0.5000000000000001 -0.8660254037844386 0.0; 0.8660254037844386 0.5000000000000001 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_3^1", [-0.4999999999999998 -0.8660254037844388 0.0; 0.8660254037844388 -0.4999999999999998 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_2^1", [-1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_3^2", [-0.5000000000000006 0.8660254037844385 0.0; -0.8660254037844385 -0.5000000000000006 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_6^5", [0.49999999999999944 0.8660254037844392 0.0; -0.8660254037844392 0.49999999999999944 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("S_6^1", [0.5000000000000001 -0.8660254037844386 0.0; 0.8660254037844386 0.5000000000000001 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("S_3^1", [-0.4999999999999998 -0.8660254037844388 -0.0; 0.8660254037844388 -0.4999999999999998 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("S_3^8", [-0.5000000000000006 0.8660254037844385 0.0; -0.8660254037844385 -0.5000000000000006 -0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("S_6^5", [0.49999999999999944 0.8660254037844392 0.0; -0.8660254037844392 0.49999999999999944 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("sigmav_2", [0.5000000000000002 0.8660254037844387 0.0; 0.8660254037844387 -0.5000000000000009 -0.0; 0.0 -0.0 1.0]),
    Symmetry.Symel("sigmav_3", [0.4999999999999991 -0.8660254037844394 0.0; -0.8660254037844394 -0.4999999999999998 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("sigmav_1", [-1.0 0.0 -0.0; 0.0 1.0 0.0; -0.0 0.0 1.0]),
    Symmetry.Symel("sigmad_2", [1.0 0.0 -0.0; 0.0 -1.0 -0.0; -0.0 -0.0 1.0]),
    Symmetry.Symel("sigmad_3", [-0.4999999999999998 0.8660254037844392 0.0; 0.8660254037844392 0.49999999999999933 -0.0; 0.0 -0.0 1.0]),
    Symmetry.Symel("sigmad_1", [-0.5000000000000009 -0.8660254037844383 0.0; -0.8660254037844383 0.5000000000000008 0.0; 0.0 0.0 1.0]),
    Symmetry.Symel("C_2(2)", [-0.4999999999999998 0.8660254037844388 0.0; 0.8660254037844388 0.4999999999999998 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(3)", [-0.5000000000000004 -0.8660254037844385 0.0; -0.8660254037844385 0.5000000000000007 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(4)", [1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(5)", [-0.4999999999999989 0.8660254037844395 0.0; 0.8660254037844395 0.49999999999999933 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(6)", [-0.5000000000000013 -0.8660254037844378 0.0; -0.8660254037844378 0.5000000000000009 0.0; 0.0 0.0 -1.0]),
    Symmetry.Symel("C_2(1)", [1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0])]

D2hcn = ["Ag", "B1g", "B2g", "B3g", "Au", "B1u", "B2u", "B3u"]
D2hct = [1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0;
         1.0  1.0 -1.0 -1.0  1.0  1.0 -1.0 -1.0;
         1.0 -1.0  1.0 -1.0  1.0 -1.0  1.0 -1.0;
         1.0 -1.0 -1.0  1.0  1.0 -1.0 -1.0  1.0;
         1.0  1.0  1.0  1.0 -1.0 -1.0 -1.0 -1.0;
         1.0  1.0 -1.0 -1.0 -1.0 -1.0  1.0  1.0;
         1.0 -1.0  1.0 -1.0 -1.0  1.0 -1.0  1.0;
         1.0 -1.0 -1.0  1.0 -1.0  1.0  1.0 -1.0]
D3hcn = ["A1′", "A2′", "E′", "A1″", "A2″", "E″"]
D3hct = D3dct
D4hcn = ["A1g", "A2g", "B1g", "B2g", "Eg", "A1u", "A2u", "B1u", "B2u", "Eu"]
D4hct = [1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0;
         1.0  1.0  1.0 -1.0 -1.0  1.0  1.0  1.0 -1.0 -1.0;
         1.0 -1.0  1.0  1.0 -1.0  1.0 -1.0  1.0  1.0 -1.0;
         1.0 -1.0  1.0 -1.0  1.0  1.0 -1.0  1.0 -1.0  1.0;
         2.0  0.0 -2.0  0.0  0.0  2.0  0.0 -2.0  0.0  0.0;
         1.0  1.0  1.0  1.0  1.0 -1.0 -1.0 -1.0 -1.0 -1.0;
         1.0  1.0  1.0 -1.0 -1.0 -1.0 -1.0 -1.0  1.0  1.0;
         1.0 -1.0  1.0  1.0 -1.0 -1.0  1.0 -1.0 -1.0  1.0;
         1.0 -1.0  1.0 -1.0  1.0 -1.0  1.0 -1.0  1.0 -1.0;
         2.0  0.0 -2.0  0.0  0.0 -2.0  0.0  2.0  0.0  0.0]
D5hcn = ["A1′", "A2′", "E1′", "E2′", "A1″", "A2″", "E1″", "E2″"]
D5hct = D5dct
D6hcn = ["A1g", "A2g", "B1g", "B2g", "E1g", "E2g", "A1u", "A2u", "B1u", "B2u", "E1u", "E2u"]
D6hct = [1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0;
         1.0  1.0  1.0  1.0 -1.0 -1.0  1.0  1.0  1.0  1.0 -1.0 -1.0;
         1.0 -1.0  1.0 -1.0  1.0 -1.0  1.0 -1.0  1.0 -1.0  1.0 -1.0;
         1.0 -1.0  1.0 -1.0 -1.0  1.0  1.0 -1.0  1.0 -1.0 -1.0  1.0;
         2.0  1.0 -1.0 -2.0  0.0  0.0  2.0  1.0 -1.0 -2.0  0.0  0.0;
         2.0 -1.0 -1.0  2.0  0.0  0.0  2.0 -1.0 -1.0  2.0  0.0  0.0;
         1.0  1.0  1.0  1.0  1.0  1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0;
         1.0  1.0  1.0  1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0  1.0  1.0;
         1.0 -1.0  1.0 -1.0  1.0 -1.0 -1.0  1.0 -1.0  1.0 -1.0  1.0;
         1.0 -1.0  1.0 -1.0 -1.0  1.0 -1.0  1.0 -1.0  1.0  1.0 -1.0;
         2.0  1.0 -1.0 -2.0  0.0  0.0 -2.0 -1.0  1.0  2.0  0.0  0.0;
         2.0 -1.0 -1.0  2.0  0.0  0.0 -2.0  1.0  1.0 -2.0  0.0  0.0]


"""
    VonMisesFisher(M; params...)

The von Mises-Fisher (vMF) or Langevin distribution on the `Sphere` or `Stiefel` manifold `M`.

Given a matrix ``X ∈ 𝔽^{n × k}`` with IID entries ``X_{ij} ∼ \\mathrm{Normal}(F_{ij}, 1)``
for ``F ∈ 𝔽^{n × k}``, the restriction of the corresponding distribution in ``𝔽^{n × k}``
to the `Stiefel(n, k, 𝔽)` manifold, that is, the matrices for which ``X^\\mathrm{H} X = I_k``,
is the vMF distribution on the `Stiefel` manifold. The vMF distribution can also be specified
for any submanifold of the `Stiefel` manifold, including the `Sphere` and the `Circle`.

# Parameterizations

    VonMisesFisher(M::AbstractSphere{𝔽}; params...)
    VonMisesFisher(n::Int[, 𝔽]; params...)

Construct the vMF distribution on `Sphere(n-1,𝔽)==` ``𝔽𝕊^{n-1}```.

Implemented parameterizations are:

  - `(μ, κ)`: the modal direction ``μ ∈ 𝔽𝕊^{n-1}`` and concentration ``κ ∈ ℝ⁺``
  - `(c,)`: ``c = κ μ ∈ 𝔽^n``, the mean of the normal distribution in the embedded space.

The density of the vMF distribution on ``𝔽𝕊^{n-1}`` with respect to the normalized
Hausdorff measure is

```math
p(x | μ, κ) = \\frac{Γ(ν + 1)κ^ν}{2^ν I_ν(κ)} \\exp(κ \\Re⟨μ, x⟩)),
```

where ``ν = n/2-1``,  ``⟨⋅,⋅⟩`` is the Frobenius inner product, and ``I_ν(z)``
is the modified Bessel function of the first kind.

    VonMisesFisher(M::Stiefel{n,k,𝔽}; params...)
    VonMisesFisher(n::Int, k::Int[, 𝔽]; params...)

Construct the matrix vMF distribution on `Stiefel(n, k, 𝔽)=` ``\\mathrm{St}(n, k, 𝔽)``.

Implemented parameterizations are:

  - `(F,)`: a parameter matrix ``F ∈ 𝔽^{n × k}``, the mean of the normal distribution in the
    embedded space.
  - `(U, D, V)`: The SVD decomposition of ``F = U D V``, where ``U ∈ \\mathrm{St}(n, k, 𝔽)`` and
    ``V ∈ \\mathrm{U}(k, 𝔽)``.
  - `(H, P)`: The polar decomposition of ``F = H P``, where ``H ∈ \\mathrm{St}(n, k, 𝔽)`` is the
    mode, and ``P ∈ 𝔽^{k × k}`` is a Hermitian positive definite matrix.

The density of the vMF distribution on `Stiefel(n, k, 𝔽)` with respect to the normalized
Hausdorff measure is

```math
p(x | F) = \\frac{\\exp(\\Re⟨F, x⟩)}{_0 F_1(\\frac{n}{2}; \\frac{1}{4} F^\\mathrm{H}F)},
```

where ``_0 F_1(b; B)`` is a hypergeometric function with matrix argument ``B``.
"""
struct VonMisesFisher{M,N,T} <: ParameterizedMeasure{N}
    manifold::M
    par::NamedTuple{N,T}
end
VonMisesFisher(M; params...) = VonMisesFisher(M, NamedTuple(params))
VonMisesFisher(p::Int, 𝔽=ℝ; params...) = VonMisesFisher(Sphere(p - 1, 𝔽); params...)
function VonMisesFisher(n::Int, k::Int, 𝔽::AbstractNumbers=ℝ; params...)
    return VonMisesFisher(Stiefel(n, k, 𝔽); params...)
end

"""
    VonMises(𝔽=ℝ; params...)

The von Mises distribution on the real or complex `Circle`.

This is just a convenient alias for [`VonMisesFisher(Circle(𝔽); params...)`](@ref).

# Constructors

    VonMises(μ=π, κ=1)
    VonMises(ℂ; μ=im, κ=1)
    VonMises(ℂ; c=3+4im)
"""
const VonMises{𝔽,N,T} = VonMisesFisher{Circle{𝔽},N,T}
VonMises(𝔽=ℝ; params...) = VonMisesFisher(Circle(𝔽); params...)

function Base.show(io::IO, mime::MIME"text/plain", μ::VonMisesFisher)
    return show_manifold_measure(io, mime, μ)
end

Manifolds.base_manifold(d::VonMisesFisher) = getfield(d, :manifold)

function MeasureTheory.basemeasure(μ::VonMisesFisher)
    return normalize(Hausdorff(base_manifold(μ)))
end

function MeasureTheory.logdensity(d::VonMises{ℝ,(:μ, :κ)}, x)
    κ = d.κ
    return κ * cos(only(x) - only(d.μ)) - logbesseli(0, κ)
end

function MeasureTheory.logdensity(d::VonMisesFisher{M,(:μ, :κ)}, x) where {M}
    p = manifold_dimension(base_manifold(d)) + 1
    κ = d.κ
    return κ * real(dot(d.μ, x)) - lognorm_vmf(p, κ)
end
function MeasureTheory.logdensity(d::VonMisesFisher{M,(:c,)}, x) where {M}
    p = manifold_dimension(base_manifold(d)) + 1
    c = d.c
    κ = norm(c)
    return real(dot(c, x)) - lognorm_vmf(p, κ)
end
function MeasureTheory.logdensity(d::VonMisesFisher{M,(:F,)}, x) where {M}
    n, _ = representation_size(base_manifold(d))
    F = d.F
    return real(dot(F, x)) - logpFq((), (n//2,), (F'F) / 4)
end
function MeasureTheory.logdensity(d::VonMisesFisher{M,(:U, :D, :V)}, x) where {M}
    n, _ = representation_size(base_manifold(d))
    D = Diagonal(d.D)
    return real(dot(D * d.V', d.U' * x)) - logpFq((), (n//2,), D .^ 2 ./ 4)
end
function MeasureTheory.logdensity(d::VonMisesFisher{M,(:H, :P)}, x) where {M}
    n, _ = representation_size(base_manifold(d))
    P = d.P
    return real(dot(d.H, P, x)) - logpFq((), (n//2,), (P^2) / 4)
end

StatsBase.mode(d::VonMisesFisher{<:Any,(:μ, :κ)}) = d.μ
StatsBase.mode(d::VonMisesFisher{<:Any,(:c,)}) = (c = d.c; c / norm(c))
StatsBase.mode(d::VonMisesFisher{<:Any,(:F,)}) = (F = svd(d.F); F.U * F.Vt)
StatsBase.mode(d::VonMisesFisher{<:Any,(:U, :D, :V)}) = d.U * d.V'
StatsBase.mode(d::VonMisesFisher{<:Any,(:H, :P)}) = d.H

# ₀F₁(p//2; κ²/4) = 2ᵛ Iᵥ(κ) / κᵛ / Γ(v + 1) for v = p/2 - 1
# Note that the usual vMF constant Cₚ(κ) is defined wrt the un-normalized Hausdorff
# measure, whereas we use the normalized Hausdorff measure here.
function lognorm_vmf(p, κ)
    iszero(κ) && return log(one(κ))
    ν = p//2 - 1
    r = logbesseli(ν, κ) + ν * (logtwo - log(κ))
    return r + loggamma(oftype(r, p//2))
end

#
# Random sampling
#

function Base.rand(rng::AbstractRNG, T::Type, d::VonMisesFisher)
    p = default_point(d, T)
    return Random.rand!(rng, p, d)
end

##
## von Mises
##

# Best, D. J., and Nicholas I. Fisher. "Efficient simulation of the von Mises distribution."
# Journal of the Royal Statistical Society: Series C (Applied Statistics) 28.2 (1979): 152-157.
# doi: 10.2307/2346732
function Base.rand(rng::AbstractRNG, T::Type, d::VonMises{ℝ,(:μ, :κ)})
    κ = T(d.κ)
    tκ = 2κ
    τ = 1 + sqrt(1 + tκ^2)
    ρ = (τ - sqrt(2τ)) / tκ
    r = (1 + ρ^2) / 2ρ
    f = zero(T)
    while true
        z = cospi(rand(rng, T))
        f = T((1 + r * z) / (r + z))
        c = κ * (r - f)
        u = rand(rng, T)
        (c * (2 - c) > u || log(c / u) + 1 ≥ c) && break
    end
    θ₀ = acos(f)
    θ = (rand(rng, (θ₀, -θ₀)))
    return mod2pi(θ + T(d.μ) + π) - π
end
function Base.rand(rng::AbstractRNG, T::Type, d::VonMises{ℂ,(:μ, :κ)})
    θ = rand(rng, T, VonMises(ℝ; μ=angle(d.μ), κ=d.κ))
    return cis(θ)
end
function Base.rand(rng::AbstractRNG, T::Type, d::VonMises{ℂ,(:c,)})
    c = d.c
    θ = rand(rng, T, VonMises(ℝ; μ=angle(c), κ=abs(c)))
    return cis(θ)
end

##
## von Mises-Fisher on the sphere
##

function Random.rand!(
    rng::AbstractRNG, p::AbstractArray, d::VonMisesFisher{<:AbstractSphere,(:μ, :κ)}
)
    n = manifold_dimension(base_manifold(d)) + 1
    return _rand_vmf_sphere!(rng, p, n, d.μ, d.κ)
end
function Random.rand!(
    rng::AbstractRNG, p::AbstractArray, d::VonMisesFisher{<:AbstractSphere,(:c,)}
)
    n = manifold_dimension(base_manifold(d)) + 1
    return _rand_vmf_sphere!(rng, p, n, d.c)
end

# Andrew T.A Wood (1994) Simulation of the von mises fisher distribution,
# Communications in Statistics - Simulation and Computation, 23:1, 157-164
# doi: 10.1080/0361091940881316
function _rand_vmf_sphere!(rng, p, n, μ, κ)
    eltype(p) <: Real && isone(n) && return _rand_vmf_0sphere!(rng, p, κ * μ)
    T = real(eltype(p))
    t = _rand_normal_scale_vmf_sphere(rng, n, T(κ))
    _rand_tangent_vmf_sphere!(rng, p)
    _combine_tangent_normal_sphere!(p, t)
    _reflect_from_xaxis_to_c!(p, μ, 1)
    return p
end
function _rand_vmf_sphere!(rng, p, n, c)
    eltype(p) <: Real && isone(n) && return _rand_vmf_0sphere!(rng, p, c)
    T = real(eltype(p))
    κ = T(norm(c))
    t = _rand_normal_scale_vmf_sphere(rng, n, κ)
    _rand_tangent_vmf_sphere!(rng, p)
    _combine_tangent_normal_sphere!(p, t)
    _reflect_from_xaxis_to_c!(p, c, κ)
    return p
end
function _rand_vmf_0sphere!(rng, p, c)
    p[1] = rand(rng, Bernoulli(; logitp=2 * c[1])) ? 1 : -1
    return p
end

# in the tangent-normal parameterization
# p = t x + √(1 - t²) [0; ξ], for x the x-axis, ξ ∈ 𝕊ⁿ⁻², and t ∈ [-1, 1],
# then ξ follows the normalized Hausdorff measure, and p(t) ∝ (1 - t^2)^((n-3)/2).
# so we draw t and ξ, compose p, then then transform it using the reflection that
# takes the x to μ.
# Method due to Wood, 1994. Adapted also for complex and quaternionic spheres.
function _rand_normal_scale_vmf_sphere(rng, n, κ)
    T = eltype(κ)
    twoκ = 2κ
    if n == 3
        # 2t+1 follows an exponential distribution truncated to [0, 1]
        # so we use inverse transform sampling
        u = rand(rng, T)
        return 1 + log(u + exp(-twoκ) * (1 - u)) / κ
    end
    m = T((n - 1)//2)
    a = twoκ / (n - 1)
    b = sqrt(a^2 + 1) - a
    x = (1 - b) / (1 + b)
    c = κ * x + (n - 1) * log1p(-x^2)
    βdist = Beta(m, m)

    z = rand(rng, T, βdist)
    t = (1 - (1 + b) * z) / (1 - (1 - b) * z)
    while κ * t + (n - 1) * log1p(-x * t) - c < log(rand(rng))
        z = rand(rng, T, βdist)
        t = (1 - (1 + b) * z) / (1 - (1 - b) * z)
    end
    return t
end
function _rand_tangent_vmf_sphere!(rng, p)
    randn!(rng, p)
    p[1] -= real(p[1])
    rdiv!(p, norm(p))
    return p
end
function _combine_tangent_normal_sphere!(p, t)
    rmul!(p, sqrt(1 - t^2))
    p[1] += t
    return p
end
# in-place apply Householder reflection p ↦ p - q 2𝕽⟨q,p⟩/‖q‖², for q=e₁-c/‖c‖
function _reflect_from_xaxis_to_c!(p, c, cnorm=norm(c))
    num = real(p[1]) - real(dot(c, p)) / cnorm
    den = cnorm - real(c[1])
    α = num / den
    p .+= c .* α
    p[1] -= α * cnorm
    return p
end

##
## von Mises-Fisher on the Stiefel manifold
##

function Random.rand!(
    rng::AbstractRNG, p::AbstractArray, d::VonMisesFisher{<:Stiefel{n,k},(:U, :D, :V)}
) where {n,k}
    dim𝔽 = real_dimension(number_system(base_manifold(d)))
    return _rand_vmf_stiefel!(rng, p, dim𝔽, n, k, d.U, d.D, d.V)
end
function Random.rand!(
    rng::AbstractRNG, p::AbstractArray, d::VonMisesFisher{<:Stiefel{n,k},(:F,)}
) where {n,k}
    U, D, V = svd(d.F)
    dim𝔽 = real_dimension(number_system(base_manifold(d)))
    return _rand_vmf_stiefel!(rng, p, dim𝔽, n, k, U, D, V)
end
function Random.rand!(
    rng::AbstractRNG, p::AbstractArray, d::VonMisesFisher{<:Stiefel{n,k},(:H, :P)}
) where {n,k}
    D, V = eigen(Hermitian(d.P))
    U = d.H * V
    dim𝔽 = real_dimension(number_system(base_manifold(d)))
    return _rand_vmf_stiefel!(rng, p, dim𝔽, n, k, U, D, V)
end

# Peter Hoff. Simulation of the Matrix Bingham—von Mises—Fisher Distribution,
# With Applications to Multivariate and Relational Data.
# Journal of Computational and Graphical Statistics. 18(2). 2009.
function _rand_vmf_stiefel!(rng, p, dim𝔽, n, k, U, D, V)
    if isone(k)
        _rand_vmf_sphere!(rng, vec(p), dim𝔽 * n, vec(U), D[1])
        rmul!(p, V[1]')
        return p
    end
    T = real(eltype(p))
    Z = fill!(similar(p), zero(T))
    Z₁ = @view Z[:, 1]
    U₁ = @view U[:, 1]
    y = similar(Z₁)
    z = similar(U₁)
    while true
        _rand_vmf_sphere!(rng, Z₁, dim𝔽 * n, U₁, D[1])
        lcrit = zero(T)
        for j in 2:k
            s = n - j + 1
            r = dim𝔽 * s
            @views begin
                N = _nullbasis(Z[:, 1:(j - 1)])
                Uⱼ = U[:, j]
                Zⱼ = Z[:, j]
                zⱼ = z[1:s]
                yⱼ = y[1:s]
            end
            Dⱼ = D[j]
            if Dⱼ > 0
                mul!(zⱼ, N', Uⱼ, Dⱼ, false)
                _rand_vmf_sphere!(rng, yⱼ, r, zⱼ)
                mul!(Zⱼ, N, yⱼ)
                nzⱼ = norm(zⱼ)
                ν = r//2 - 1
                lcrit += T(
                    logbesseli(ν, nzⱼ) - logbesseli(ν, Dⱼ) + ν * (log(Dⱼ) - log(nzⱼ))
                )
            else  # sample from uniform distribution, lcrit contribution is zero
                randn!(rng, yⱼ)
                mul!(Zⱼ, N, yⱼ, inv(norm(yⱼ)), false)
            end
        end
        log(rand(rng)) < lcrit && break
    end
    mul!(p, Z, V')
    return p
end

# basis N of null space of A, such that N'A=0
# compute basis of null space of matrix A
function _nullbasis(A)
    F = qr(A)
    rank = size(F.R, 1)
    return F.Q[:, (rank + 1):end]
end


"""
    smssvd(X, d, stdThresholds=10 .^ range(-2,stop=0,length=100); nbrIter=10, maxSignalDim=typemax(Int)) -> (U,Σ,V,ps,signalDimensions,selectedVariables)

Computes the SubMatrix Selection Singular Value Decomposition (SMSSVD) of a matrix X.

**Inputs**

* `X`: PxN data matrix (P variables and N samples).
* `d`: Number of dimension in the result.
* `stdThresholds`: Standard deviation filtering thresholds at which the Projection Score will be evaluated. Expressed as fractions of the maximum standard deviation of a variable in X. Should be a vector of increasing values between 0 and 1.

**Outputs**

* `U`: Pxd matrix of left singular vectors (variable representation).
* `S`: d-vector of singular values.
* `V`: Nxd matrix of right singular vectors (sample representation).
* `ps`: d x length(stdThresholds) matrix with all Projection Scores. Useful for plotting.
* `signalDimensions`: Vector with the number of dimensions in each signal detected by SMSSVD.
* `selectedVariables`: For each signal, a bitmask showing the variables selected by Projection Score.
"""
function smssvd(X, d::Integer, stdThresholds=10 .^ range(-2,stop=0,length=100); nbrIter=10, maxSignalDim=typemax(Int))
    σMax = maximum(std(X,dims=2)) # Always base the variable filtering on the original σ's

    U = zeros(size(X,1),d)
    Σ = zeros(d)
    V = zeros(size(X,2),d)

    ps = zeros(d, length(stdThresholds))

    signalDimensions = Int[]
    selectedVariables = Vector{BitArray}()

    k = 1
    while k<=d
        # Filter variables and choose dimension by optimizing over Projection Score
        σ = dropdims(std(X,dims=2),dims=2) / σMax # always keep the same scale

        dmax = min(d-k+1, maxSignalDim)
        PS = projectionscorefiltered(X, 1:dmax, stdThresholds, nbrIter=nbrIter, σ=σ)
        dims,σInd = Tuple(CartesianIndices(size(PS))[argmax(PS)]) # Use projection score both to choose dimension and threshold for this signal
        σThresh = stdThresholds[σInd]

        r = k:k+dims-1
        ps[r,:] = PS[1:dims,:]
        push!(signalDimensions, dims)

        push!(selectedVariables,σ.>=σThresh)
        Y = X[selectedVariables[end],:]

        # Find the subspace Π that we are interested in.
        K = Symmetric(Y'Y)
        M = size(K,1)
        _,Π = eigen(K, M-dims+1:M) # only get the largest eigenvalues and vectors

        # Project X onto the subspace v and compute SVD. For dims=1, this is identical to uσ:=Xv.
        F = svd(X*Π) # solve for smaller matrix expressed in the basis of the subspace Π
        UΠ,ΣΠ,VΠ = F.U,F.S,F.V

        U[:,r] = UΠ
        Σ[r]   = ΣΠ
        V[:,r] = Π*VΠ # expand to original basis

        # make matrix orthogonal to previous component
        X = X - UΠ*(UΠ'X) # multiplication order avoiding big matrices

        k += dims
    end

    U,Σ,V,ps,signalDimensions,selectedVariables
end

# convenience method useful when calling smssvd from R
function smssvd(X, d::Vector{T}, stdThresholds=10 .^ range(-2,stop=0,length=100); kwargs...) where T<:Integer
    @assert length(d)==1
    smssvd(X, d[1], stdThresholds; kwargs...)
end


#
println("Fd) Symbolic simplification of spherical tensor operators, matrix elements and spherical amplitudes.")
#
using SymEngine
j1  = Basic(:j1);    j2  = Basic(:j2);    j3  = Basic(:j3);    j4  = Basic(:j4);    j5  = Basic(:j5)
m1  = Basic(:m1);    m2  = Basic(:m2);    m3  = Basic(:m3);    m4  = Basic(:m4);    m5  = Basic(:m5)
ja  = Basic(:ja);    jb  = Basic(:jb);    jc  = Basic(:jc);    jd  = Basic(:jd);    je  = Basic(:je)
ma  = Basic(:ma);    mb  = Basic(:mb);    mc  = Basic(:mc);    md  = Basic(:md);    me  = Basic(:me)
k   = Basic(:k);     q   = Basic(:q );    K   = Basic(:K);     Q   = Basic(:Q);
kk  = Basic(:kk);    qq  = Basic(:qq);    KK  = Basic(:KK);    QQ  = Basic(:QQ);

if  true
# Define simple electronic and field operators
Ck      = SphericalTensor.CkOperator(k);                 println("$Ck");         @show Ck
Coulomb = SphericalTensor.CoulombOperator();             println("$Coulomb");    @show Coulomb
Dop     = SphericalTensor.DipoleOperator(false);         println("$Dop");        @show Dop
Tk      = SphericalTensor.TkOperator(k);                 println("$Tk");         @show Tk

gConst  = SphericalTensor.ScalarConstant(false, Basic(:g));                 println("$gConst");        @show gConst
uVector = SphericalTensor.UVector(false);                                   println("$uVector");       @show uVector

# Define spherical tensor product
prod1   = SphericalTensor.TensorProduct(false, j2, Ck, Tk);                 println("$prod1");         @show prod1
prod2   = SphericalTensor.TensorProduct(false, Basic(0), uVector, Dop);     println("$prod2");         @show prod2
prod3   = SphericalTensor.TensorProduct(false, K, prod1, prod2);            println("$prod3");         @show prod3
prod4   = SphericalTensor.TensorProduct(false, K, uVector, Tk);             println("$prod4");         @show prod4

# Define spherical and reduced states
jma     = SphericalTensor.SphericalState(false, ja, ma);                    println("$jma");           @show jma
jmb     = SphericalTensor.SphericalState(false, jb, mb);                    println("$jmb");           @show jmb
j2      = SphericalTensor.ReducedState(j2);                                 println("$j2");            @show j2
j3      = SphericalTensor.ReducedState(j3);                                 println("$j3");            @show j3

# Define spherical matrix elements
me1     = SphericalTensor.SphericalMatrixElement(jma, Tk, q, jmb);          println("$me1");           @show me1
me2     = SphericalTensor.SphericalMatrixElement(jma, prod2, q, jmb);       println("$me2");           @show me2
me3     = SphericalTensor.SphericalMatrixElement(jma, prod3, q, jmb);       println("$me3");           @show me3
me4     = SphericalTensor.SphericalMatrixElement(false, jma, gConst, prod3, q, jmb);
                                                                            println("$me4");           @show me4
me2s    = SphericalTensor.SphericalMatrixElement(true,  jma, gConst, prod2, q, jmb);
                                                                            println("$me2s");          @show me2s
me5     = SphericalTensor.SphericalMatrixElement(false, jma, gConst, prod4, q, jmb);
                                                                            println("$me5");           @show me5
me5s    = SphericalTensor.SphericalMatrixElement(true,  jma, gConst, prod4, q, jmb);
                                                                            println("$me5s");          @show me5s

# Expand spherical operators
we1     = SphericalTensor.expandSphericalTensorComponent(Coulomb, q);       println("$Coulomb");       @show we1
we2     = SphericalTensor.expandSphericalTensorComponent(uVector, q);       println("$uVector");       @show we2
we3     = SphericalTensor.expandSphericalTensorComponent(prod1, q);         println("$prod1");         @show we3
we4     = SphericalTensor.expandSphericalTensorComponent(prod2, q);         println("$prod2");         @show we4
we5     = SphericalTensor.expandSphericalTensorComponent(prod3, q);         println("$prod3");         @show we5

# Expand spherical operators
wm1     = SphericalTensor.expandSphericalMatrixElements([me1]);             println("\n $me1");        @show wm1
wm2     = SphericalTensor.expandSphericalMatrixElements([me2]);             println("\n $me2");        @show wm2
wm3     = SphericalTensor.expandSphericalMatrixElements([me3]);             println("\n $me3");        @show wm3
wm4     = SphericalTensor.expandSphericalMatrixElements([me4]);             println("\n $me4");        @show wm4
wm5     = SphericalTensor.expandSphericalMatrixElements([me2s, me2]);       println("\n $me2s $me2");  @show wm5
wm6     = SphericalTensor.expandSphericalMatrixElements([me5s, me5]);       println("\n $me5s $me5");  @show wm6

elseif  false

end


include("../src/delta.jl")
include("../src/vm_basis.jl")
include("../src/hecke.jl")
include("../src/prime_range.jl")

function test_hecke_on_qexp()
   print("hecke.qexp...")

   delta = delta_qexp(100)
   h1 = hecke_operator_on_qexp(delta, 1, 12, 30)
   @test h1 - delta_qexp(30) == 0
   h2 = truncate(hecke_operator_on_qexp(delta, 3, 12, 10), 7)
   S = parent(h2)
   q = gen(S)
   @test h2 - (252q - 6048q^2 + 63504q^3 - 370944q^4 +
               1217160q^5 - 1524096q^6 + O(q^7)) == 0

   println("PASS")
end

function test_hecke_on_basis()
   print("hecke.basis...")

   B = victor_miller_basis(32, 10, false)
   H_basis = hecke_operator_on_basis(B, 3, 32)
   b0, b1, b2 = B
   h0 = hecke_operator_on_qexp(b0, 3, 32)
   R = base_ring(h0)
   S = MatrixSpace(R, 3, 3)
   matrix = S([617673396283948 2611200 5615943999897600; 0 50220 965671206912; 0 432 17312940])

   println("PASS")
end

function test_hecke_vm()
   print("hecke.victormiller...")

   B = victor_miller_basis(24, 20, true)
   b1, b2 = B
   h1 = hecke_operator_on_qexp(b1, 3, 24, 7)
   S = parent(h1)
   q = gen(S)
   @test h1 == 195660*q - 982499328*q^2 + 85442803344*q^3 + 1302498570240*q^4 +
               23514204375720*q^5 - 333538871869440*q^6 + O(q^7)
   h2 = hecke_operator_on_qexp(b2, 3, 24, 7)
   S = parent(h2)
   q = gen(S)
   @test h2 == -48*q + 143820*q^2 - 16295040*q^3 - 424520544*q^4 -
               4306546080*q^5 + 67844160144*q^6 + O(q^7)

   #test hecke_operator_on_basis
   H_basis = hecke_operator_on_basis(B, 3, 24)
   R = base_ring(h1)
   S = MatrixSpace(R, 2, 2)
   matrix = S([195660 -982499328; -48 143820])

   println("PASS")
end

function test_hecke()
   test_hecke_on_qexp()
   test_hecke_on_basis()
   test_hecke_vm()

   println("")
end


using LinearAlgebra

struct ValueOne; end
ValueOne()

# Compute X <- a X + b I.
function matfun_axpby!(X,a,b,Y::UniformScaling)
    m,n=size(X)
    if ~(a isa ValueOne)
        rmul!(X,a)
    end
    @inbounds for i=1:n
        X[i,i]+=(b isa ValueOne) ? 1 : b
    end
end

@inline function exp_mono_m8_opt_rho13_5(A)
    T=promote_type(eltype(A),Float64)
    A_copy=similar(A,T); A_copy .= A;
    return exp_mono_m8_opt_rho13_5!(A_copy)
end

@inline function exp_mono_m8_opt_rho13_5!(A)
    T=promote_type(eltype(A),Float64) # Make it work for many 'bigger' types (matrices and scalars)
    # max_memslots=10
    n=size(A,1)
    # The first slots are precomputed nodes [:A]
    memslots2 = similar(A,T)
    memslots3 = similar(A,T)
    memslots4 = similar(A,T)
    memslots5 = similar(A,T)
    memslots6 = similar(A,T)
    memslots7 = similar(A,T)
    memslots8 = similar(A,T)
    memslots9 = similar(A,T)
    memslots10 = similar(A,T)
    # Assign precomputed nodes memslots
    memslots1=A # overwrite A
    # Uniform scaling is exploited.
    # No matrix I explicitly allocated.
    # Computation order: B2 Ba3 Bb3 B3 Ba4 Bb4 B4 Ba5 Bb5 B5 Ba6 Bb6 B6 Ba7 Bb7 B7 Ba8 Bb8 B8 Ba9 Bb9 B9 T2k11
    # Computing B2 with operation: mult
    mul!(memslots2,memslots1,memslots1)
    # Computing Ba3 = x*I+x*A+x*B2
    coeff1=-0.003584018414055765
    coeff2=0.16376260367565243
    coeff3=0.04917059370539749
    memslots3 .= coeff2.*memslots1 .+ coeff3.*memslots2
    mul!(memslots3,true,I*coeff1,true,true)
    # Computing Bb3 = x*I+x*A+x*B2
    coeff1=0.30528232877254763
    coeff2=0.23746768414771152
    coeff3=0.0023273769915510135
    memslots4 .= coeff2.*memslots1 .+ coeff3.*memslots2
    mul!(memslots4,true,I*coeff1,true,true)
    # Computing B3 with operation: mult
    mul!(memslots5,memslots3,memslots4)
    # Deallocating Ba3 in slot 3
    # Deallocating Bb3 in slot 4
    # Computing Ba4 = x*I+x*A+x*B2+x*B3
    coeff1=0.0005515398938947843
    coeff2=0.02533967378579724
    coeff3=0.017067262331527198
    coeff4=0.9638185168573307
    memslots3 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5
    mul!(memslots3,true,I*coeff1,true,true)
    # Computing Bb4 = x*I+x*A+x*B2+x*B3
    coeff1=-0.008872840493334803
    coeff2=0.25571769129339744
    coeff3=0.0037256194747560907
    coeff4=0.002052240135641998
    memslots4 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5
    mul!(memslots4,true,I*coeff1,true,true)
    # Computing B4 with operation: mult
    mul!(memslots6,memslots3,memslots4)
    # Deallocating Ba4 in slot 3
    # Deallocating Bb4 in slot 4
    # Computing Ba5 = x*I+x*A+x*B2+x*B3+x*B4
    coeff1=-0.0012630062267903097
    coeff2=-0.0010962696861478022
    coeff3=0.00022839563887310042
    coeff4=0.013673005502563394
    coeff5=0.8216219421030205
    memslots3 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6
    mul!(memslots3,true,I*coeff1,true,true)
    # Computing Bb5 = x*I+x*A+x*B2+x*B3+x*B4
    coeff1=0.8293727566445452
    coeff2=0.1675033881979259
    coeff3=0.0030839993650848517
    coeff4=0.0031897875289976277
    coeff5=3.234759791150232e-5
    memslots4 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6
    mul!(memslots4,true,I*coeff1,true,true)
    # Computing B5 with operation: mult
    mul!(memslots7,memslots3,memslots4)
    # Deallocating Ba5 in slot 3
    # Deallocating Bb5 in slot 4
    # Computing Ba6 = x*I+x*A+x*B2+x*B3+x*B4+x*B5
    coeff1=4.437908455542293e-5
    coeff2=0.0003258145596094315
    coeff3=0.00022081927999800417
    coeff4=-0.13039438638797307
    coeff5=0.8475672106088131
    coeff6=0.8145974867536132
    memslots3 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6 .+ coeff6.*memslots7
    mul!(memslots3,true,I*coeff1,true,true)
    # Computing Bb6 = x*I+x*A+x*B2+x*B3+x*B4+x*B5
    coeff1=-0.10275230374603168
    coeff2=0.25648676493721656
    coeff3=0.005628536962635935
    coeff4=0.008329230127484285
    coeff5=0.00021583757875408033
    coeff6=-1.0968604718569746e-7
    memslots4 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6 .+ coeff6.*memslots7
    mul!(memslots4,true,I*coeff1,true,true)
    # Computing B6 with operation: mult
    mul!(memslots8,memslots3,memslots4)
    # Deallocating Ba6 in slot 3
    # Deallocating Bb6 in slot 4
    # Computing Ba7 = x*I+x*A+x*B2+x*B3+x*B4+x*B5+x*B6
    coeff1=-2.8201037195155804e-6
    coeff2=4.8728643050646066e-6
    coeff3=2.2723646529748017e-6
    coeff4=-0.0061294802470251075
    coeff5=0.0688513777099278
    coeff6=-0.12038470670165373
    coeff7=0.9948034699390058
    memslots3 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6 .+ coeff6.*memslots7 .+ coeff7.*memslots8
    mul!(memslots3,true,I*coeff1,true,true)
    # Computing Bb7 = x*I+x*A+x*B2+x*B3+x*B4+x*B5+x*B6
    coeff1=0.004650329563627054
    coeff2=0.2598813154789629
    coeff3=0.006290014251994485
    coeff4=0.010576806709956765
    coeff5=0.0003949349245182783
    coeff6=8.856309878298438e-6
    coeff7=7.553245375703866e-9
    memslots4 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6 .+ coeff6.*memslots7 .+ coeff7.*memslots8
    mul!(memslots4,true,I*coeff1,true,true)
    # Computing B7 with operation: mult
    mul!(memslots9,memslots3,memslots4)
    # Deallocating Ba7 in slot 3
    # Deallocating Bb7 in slot 4
    # Computing Ba8 = x*I+x*A+x*B2+x*B3+x*B4+x*B5+x*B6+x*B7
    coeff1=1.0002604649982476
    coeff2=0.2422828857323093
    coeff3=0.022001533431922154
    coeff4=0.15933029209747282
    coeff5=0.03184541272812379
    coeff6=0.007635550247315368
    coeff7=0.0008615033049494069
    coeff8=3.134369854398072e-5
    memslots3 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6 .+ coeff6.*memslots7 .+ coeff7.*memslots8 .+ coeff8.*memslots9
    mul!(memslots3,true,I*coeff1,true,true)
    # Computing Bb8 = x*I+x*A+x*B2+x*B3+x*B4+x*B5+x*B6+x*B7
    coeff1=1.0002604649982476
    coeff2=0.2422828857323093
    coeff3=0.022001533431922154
    coeff4=0.15933029209747282
    coeff5=0.03184541272812379
    coeff6=0.007635550247315368
    coeff7=0.0008615033049494069
    coeff8=3.134369854398072e-5
    memslots4 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6 .+ coeff6.*memslots7 .+ coeff7.*memslots8 .+ coeff8.*memslots9
    mul!(memslots4,true,I*coeff1,true,true)
    # Computing B8 with operation: mult
    mul!(memslots10,memslots3,memslots4)
    # Deallocating Ba8 in slot 3
    # Deallocating Bb8 in slot 4
    # Computing Ba9 = x*I+x*A+x*B2+x*B3+x*B4+x*B5+x*B6+x*B7+x*B8
    coeff1=0.1346603332119568
    coeff2=-5.9198518169123926e-5
    coeff3=-8.660244928553117e-6
    coeff4=-8.731780915688725e-5
    coeff5=-2.1711606510903095e-5
    coeff6=-5.9723490061415135e-6
    coeff7=-7.70252692547925e-7
    coeff8=-3.1855167508881976e-8
    coeff9=1.0081137407871135
    memslots3 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6 .+ coeff6.*memslots7 .+ coeff7.*memslots8 .+ coeff8.*memslots9 .+ coeff9.*memslots10
    mul!(memslots3,true,I*coeff1,true,true)
    # Computing Bb9 = x*I+x*A+x*B2+x*B3+x*B4+x*B5+x*B6+x*B7+x*B8
    coeff1=-0.06540541282818779
    coeff2=6.846710342127941e-5
    coeff3=2.738382227960167e-6
    coeff4=-6.3730674335546956e-6
    coeff5=-5.739005263969626e-6
    coeff6=-2.157568588204766e-6
    coeff7=-3.3636703984819904e-7
    coeff8=-1.5592924041261544e-8
    coeff9=0.9946441317159572
    # Smart lincomb recycle B7
    memslots9 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6 .+ coeff6.*memslots7 .+ coeff7.*memslots8 .+ coeff8.*memslots9 .+ coeff9.*memslots10
    mul!(memslots9,true,I*coeff1,true,true)
    # Computing B9 with operation: mult
    mul!(memslots4,memslots3,memslots9)
    # Deallocating Ba9 in slot 3
    # Deallocating Bb9 in slot 9
    # Computing T2k11 = x*I+x*A+x*B2+x*B3+x*B4+x*B5+x*B6+x*B8+x*B9
    coeff1=0.008776543046053789
    coeff2=-1.3078408314728238e-5
    coeff3=-9.364829337290504e-7
    coeff4=-4.881187067247219e-6
    coeff5=-6.561388347938557e-7
    coeff6=-1.0279110807751132e-7
    coeff7=-5.458270135394687e-9
    coeff8=-0.06726697857924235
    coeff9=0.9964850858948753
    # Smart lincomb recycle A
    memslots1 .= coeff2.*memslots1 .+ coeff3.*memslots2 .+ coeff4.*memslots5 .+ coeff5.*memslots6 .+ coeff6.*memslots7 .+ coeff7.*memslots8 .+ coeff8.*memslots10 .+ coeff9.*memslots4
    mul!(memslots1,true,I*coeff1,true,true)
    # Deallocating B2 in slot 2
    # Deallocating B3 in slot 5
    # Deallocating B4 in slot 6
    # Deallocating B5 in slot 7
    # Deallocating B6 in slot 8
    # Deallocating B8 in slot 10
    # Deallocating B9 in slot 4
    return memslots1 # Returning T2k11
end


__precompile__(true)

module QPnorm

using LinearAlgebra
using Polynomials
using GeneralQP
using TRS
using DataFrames
using Printf
using SparseArrays
using CSV

include("algebra.jl")
include("qp_norm_boundary.jl")
include("printing.jl")
include("qp_norm.jl")
include("find_feasible_point.jl")
export solve, find_feasible_point

end


export generate, ShipNoise, Pinger

function generate(signal::AbstractArray, nsamples, fs)
  length(signal) >= nsamples && return signal[1:nsamples]
  padded(signal, (0,nsamples-signal))
end

Base.@kwdef struct Pinger
  spl = db2amp(180.0)
  frequency = 38000.0
  duration = 0.02
  start = 0.0
  interval = 1.0
  window = nothing
end

function generate(pinger::Pinger, nsamples, fs)
  x = zeros(nsamples)
  ping = cw(pinger.frequency, pinger.duration; fs=fs, window=pinger.window)
  t = pinger.start
  while t < nsamples/fs
    ndx = round(Int, t*fs)
    n = length(ping)
    ndx+n-1 > nsamples && n = 1+nsamples-ndx
    x[ndx:ndx+n-1] .= ping[1:n]
    t += pinger.interval
  end
  pinger.spl * x
end

Base.@kwdef struct ShipNoise
  spl = db2amp(172.0)
  spec = [(50.0, 1.0)]
end

function generate(ship::ShipNoise, nsamples, fs)
  x = rand(PinkGaussianNoise(), nsamples)
  y = copy(x)
  for s in ship.spec
    y += s[2]*cw(s[1], nsamples/fs; fs=fs) .* x
  end
  lpf = digitalfilter(Lowpass(3000.0, fs=fs), FIRWindow(hanning(15)))
  y = filtfilt(lpf, y)
  √ship.spl * y/rms(y)
end


using SRCWA,Test

@testset "reflection" begin

    n1=rand()*10
    n2=rand()*10
nx,ny,dnx,dny=grid_n(0,0)
lambda=1000
theta=1E-30
phi=0
k0,Kx,Ky,kin=grid_k(nx,ny,theta,phi,lambda,100,100,n1^2)
upper=halfspace(Kx,Ky,n1^2)
lower=halfspace(Kx,Ky,n2^2)
V0,Kz0=modes_freespace(Kx,Ky)
Su=matrix_ref(upper,V0)
Sl=matrix_tra(lower,V0)
S=concatenate(Su,Sl)
a0te,a0tm=prepare_source(kin,upper.W,0,0)
cref=S.S11*a0tm#+a0tm
ctra=S.S21*a0tm
R=a2p(cref,upper,Kx,Ky,kin[3])
T=a2p(ctra,lower,Kx,Ky,kin[3])

    @test abs(((n1-n2)/(n1+n2))^2-R)<1E-6
    @test abs(1-((n1-n2)/(n1+n2))^2-T)<1E-6
end


using ValidatedNumerics
using FactCheck

setprecision(Interval, Float64)

facts("displaymode tests") do

    context("Interval") do

        a = 1..2
        b = -1.1..1.3
        c = Interval(pi)
        d = @interval(π)

        context("6 sig figs") do
            displaymode(format=:standard, sigfigs=6)

            @fact string(a) --> "[1, 2]"
            @fact string(b) --> "[-1.10001, 1.30001]"
            @fact string(c) --> "[3.14159, 3.1416]"
            @fact string(d) --> "[3.14159, 3.1416]"
        end

        context("10 sig figs") do
            displaymode(sigfigs=10)

            @fact string(a) --> "[1, 2]"
            @fact string(b) --> "[-1.100000001, 1.300000001]"
            @fact string(c) --> "[3.141592653, 3.141592654]"
            @fact string(d) --> "[3.141592653, 3.141592654]"
        end

        context("20 sig figs") do
            displaymode(sigfigs=20)

            @fact string(a) --> "[1, 2]"
            @fact string(b) --> "[-1.1000000000000000889, 1.3000000000000000445]"
            @fact string(c) --> "[3.1415926535897931159, 3.141592653589793116]"
            @fact string(d) --> "[3.1415926535897931159, 3.1415926535897935601]"
        end

        context("Full") do
            displaymode(format=:full)

            @fact string(a) --> "Interval(1.0, 2.0)"
            @fact string(b) --> "Interval(-1.1, 1.3)"
            @fact string(c) --> "Interval(3.141592653589793, 3.141592653589793)"
            @fact string(d) --> "Interval(3.141592653589793, 3.1415926535897936)"
        end

        context("Midpoint") do
            displaymode(format=:midpoint, sigfigs=6)

            @fact string(a) --> "1.5 ± 0.5"
            @fact string(b) --> "0.1 ± 1.20001"
            @fact string(c) --> "3.14159 ± 0"
            @fact string(d) --> "3.14159 ± 4.4409e-16"
        end

    end

    context("DecoratedInterval") do
        a = @decorated(1, 2)

        displaymode(format=:standard, decorations=false)

        @fact string(a) --> "[1, 2]"

        displaymode(format=:standard, decorations=true)

        @fact string(a) --> "[1, 2]_com"

    end

    setprecision(Interval, 128)

    context("BigFloat intervals") do
        displaymode(format=:standard, decorations=false)

        a = @interval big(1)
        @fact string(a) --> "[1, 1]₁₂₈"

        displaymode(format=:full)
        @fact string(a) --> "Interval(1.000000000000000000000000000000000000000, 1.000000000000000000000000000000000000000)"

        a = DecoratedInterval(big(2), big(3), com)

        displaymode(format=:standard, decorations=false)
        @fact string(a) --> "[2, 3]₁₂₈"

        displaymode(format=:standard, decorations=true)
        @fact string(a) --> "[2, 3]₁₂₈_com"

        displaymode(format=:full)
        @fact string(a) --> "DecoratedInterval(Interval(2.000000000000000000000000000000000000000, 3.000000000000000000000000000000000000000), com)"

    end

    setprecision(Interval, Float64)

    context("IntervalBox") do

        displaymode(format=:standard, sigfigs=6)

        X = IntervalBox(1..2, 3..4)
        @fact string(X) --> "[1, 2] × [3, 4]"

        X = IntervalBox(1.1..1.2, 2.1..2.2)
        @fact string(X) --> "[1.09999, 1.20001] × [2.09999, 2.20001]"

        X = IntervalBox(-Inf..Inf, -Inf..Inf)
        @fact string(X) --> "[-∞, ∞] × [-∞, ∞]"

        displaymode(format=:full)
        @fact string(X) --> "IntervalBox(Interval(-Inf, Inf), Interval(-Inf, Inf))"

    end
end


@testset "product of integer" begin

    @test fact2(0)==1
    @test fact2(1)==1

    @test fact2(2)==2

    @test fact2(8)==2*4*6*8
    @test fact2(11)==3*5*7*9*11
end


# Depth-first visit / traversal

#################################################
#
#  Depth-first visit
#
#################################################

type DepthFirst <: AbstractGraphVisitAlgorithm
end

function depth_first_visit_impl!{V,E}(
    graph::AbstractGraph{V,E},      # the graph
    stack,                          # an (initialized) stack of vertex
    vertexcolormap::Vector{Int},    # an (initialized) color-map to indicate status of vertices
    edgecolormap::Vector{Int},      # an (initialized) color-map to indicate status of edges
    visitor::AbstractGraphVisitor)  # the visitor

    while !isempty(stack)
        u, uegs, tstate = pop!(stack)
        found_new_vertex = false

        while !done(uegs, tstate) && !found_new_vertex
            v_edge, tstate = next(uegs, tstate)
            v = v_edge.target
            v_color = vertexcolormap[vertex_index(v, graph)]
            e_color = edgecolormap[edge_index(v_edge, graph)]
            examine_neighbor!(visitor, u, v, v_color, e_color)

            if e_color == 0
                edgecolormap[edge_index(v_edge, graph)] = 1
            end

            if v_color == 0
                found_new_vertex = true
                vertexcolormap[vertex_index(v, graph)] = 1
                if !discover_vertex!(visitor, v)
                    return
                end
                push!(stack, (u, uegs, tstate))

                open_vertex!(visitor, v)
                vegs = out_edges(v, graph)
                push!(stack, (v, vegs, start(vegs)))
            end
        end

        if !found_new_vertex
            close_vertex!(visitor, u)
            vertexcolormap[vertex_index(u, graph)] = 2
        end
    end
end

function traverse_graph{V,G <: AbstractGraph}(
    graph::G,
    alg::DepthFirst,
    s::V,
    visitor::AbstractGraphVisitor;
    vertexcolormap = zeros(Int, num_vertices(graph)),
    edgecolormap = zeros(Int, num_edges(graph)))

    @graph_requires graph incidence_list vertex_map

    vertexcolormap[vertex_index(s, graph)] = 1
    if !discover_vertex!(visitor, s)
        return
    end

    segs = out_edges(s, graph)
    sstate = start(segs)
    stack = [(s, segs, sstate)]

    depth_first_visit_impl!(graph, stack, vertexcolormap, edgecolormap, visitor)
end

#################################################
#
#  Useful applications
#
#################################################

# Test whether a graph is cyclic

type DFSCyclicTestVisitor <: AbstractGraphVisitor
    found_cycle::Bool

    DFSCyclicTestVisitor() = new(false)
end

function examine_neighbor!{V}(
    vis::DFSCyclicTestVisitor,
    u::V,
    v::V,
    vcolor::Int,
    ecolor::Int)

    if vcolor == 1 && ecolor == 0
        vis.found_cycle = true
    end
end

discover_vertex!(vis::DFSCyclicTestVisitor, v) = !vis.found_cycle

function test_cyclic_by_dfs{G <: AbstractGraph}(graph::G)
    @graph_requires graph vertex_list incidence_list vertex_map

    cmap = zeros(Int, num_vertices(graph))
    visitor = DFSCyclicTestVisitor()

    for s in vertices(graph)
        if cmap[vertex_index(s, graph)] == 0
            traverse_graph(graph, DepthFirst(), s, visitor, vertexcolormap=cmap)
        end

        if visitor.found_cycle
            return true
        end
    end
    return false
end

# Topological sort using DFS

type TopologicalSortVisitor{V} <: AbstractGraphVisitor
    vertices::Vector{V}

    function TopologicalSortVisitor(n::Int)
        vs = Array(Int, 0)
        sizehint!(vs, n)
        new(vs)
    end
end

function examine_neighbor!{V}(visitor::TopologicalSortVisitor{V}, u::V, v::V, vcolor::Int, ecolor::Int)
    if vcolor == 1 && ecolor == 0
        throw(ArgumentError("The input graph contains at least one loop."))
    end
end

function close_vertex!{V}(visitor::TopologicalSortVisitor{V}, v::V)
    push!(visitor.vertices, v)
end

function topological_sort_by_dfs{V}(graph::AbstractGraph{V})
    @graph_requires graph vertex_list incidence_list vertex_map

    cmap = zeros(Int, num_vertices(graph))
    visitor = TopologicalSortVisitor{V}(num_vertices(graph))

    for s in vertices(graph)
        if cmap[vertex_index(s, graph)] == 0
            traverse_graph(graph, DepthFirst(), s, visitor, vertexcolormap=cmap)
        end
    end

    reverse(visitor.vertices)
end


"""
create (n x n) identity matrix

# Arguments
`T`: type of the matrix elements

`n`: dimensions
"""
function eye(T, n::Int)
  return Matrix{T}(LinearAlgebra.I, n, n)
end

"""
left order a binary matrix (Z)

convenience function for:
```julia
julia> sortslices(Z, dims=2, rev=true)
```
"""
function leftOrder(Z::Matrix{T}) where T
  return sortslices(Z, dims=2, rev=true)
end

"""
Generate a simple Z matrix. For simulation
studies. J, K are integers and dimensions
of the (binary) Z matrix. J must be
a multiple of K.
"""
function genSimpleZ(J::Int, K::Int)
  g = div(J, K)
  @assert g == J / K

  return kron(eye(Int, K), fill(1,g))
end

"""
Assert that a Z matrix is valid
"""
function isValidZ(Z)
  hasRepeatedColumns = size(unique(Z, dims=2), 2) < size(Z, 2)
  hasColOfZero =  any(sum(Z, dims=1) .== 0)
  return  !(hasRepeatedColumns || hasRepeatedColumns)
end

"""
Generate a (J x K) Z matrix for simulation studies.

`prob1` is the desired proportion of 1's in the matrix.
"""
function genZ(J::Int, K::Int, prob1::Float64)::Matrix{Int}
  @assert 0 < prob1 < 1
  Z = Int.(rand(J, K) .> prob1)
  Z = sortslices(Z, dims=1, rev=true)
  Z = leftOrder(Z)

  # if size(unique(Z, dims=2), 2) < K || any(sum(Z, dims=1) .== 0) || all(sum(Z, dims=2) .== 0)
  if isValidZ(Z)
    return Z
  else
    return genZ(J, K, prob1)
  end
end

#= Test
Z = Int.(randn(3,5) .> 0)
=#

function genData(J::Int, N::Vector{Int}, K::Int, L::Dict{Int, Int};
                 useSimpleZ::Bool=true, prob1::Float64=.6,
                 sortLambda::Bool=false, propMissingScale::Float64=0.7)

  I = length(N)
  Z = useSimpleZ ? genSimpleZ(J, K) : genZ(J, K, prob1)

  genData(J=J, N=N, K=K, L=L, Z=Z,
          beta=[-9.2, -2.3], # linear missing mechanism
          sig2=fill(0.1, I),
          mus=Dict(0=>collect(range(-5, length=L[0], stop=-1)),
                   1=>collect(range(1, length=L[1], stop=5))),
          a_W=[float(i) for i in 1:K],
          a_eta=Dict([ z => [float(l) for l in 1:L[z]] for z in 0:1 ]),
          sortLambda=sortLambda, propMissingScale=propMissingScale)
end

function genData(; J::Int, N::Vector{Int}, K::Int, L::Dict{Int, Int},
                 Z::Matrix{Int}, beta::Vector{Float64},
                 sig2::Vector{Float64}, mus::Dict{Int, Vector{Float64}},
                 a_W::Vector{Float64}, a_eta::Dict{Int, Vector{Float64}},
                 sortLambda::Bool=false, propMissingScale::Float64=0.7,
                 eps=zeros(length(N)))

  # Check eps
  @assert length(eps) == length(N)
  @assert all(0 .<= eps .<= 1)

  # Check Z dimensions
  @assert ncol(Z) == K && nrow(Z) == J

  # Check N dimensions
  @assert all(N .> 0)

  I = length(N)

  # Check sig2 dimensions
  @assert all(sig2 .> 0)
  @assert length(sig2) == I

  # Check mus dimensions
  @assert all(mus[0] .< 0) && all(mus[1] .> 0)
  @assert length(mus[0]) == L[0] && length(mus[1]) == L[1]

  # Check a_W dimensions
  @assert length(a_W) == K
  @assert all(a_W .> 0)

  # Check a_eta dimensions
  @assert length(a_eta[0]) == L[0]
  @assert length(a_eta[1]) == L[1]
  @assert all(a_eta[0] .> 0)
  @assert all(a_eta[1] .> 0)

  # Simulate W
  W = zeros(I, K)
  for i in 1:I
    W[i,:] = rand(Dirichlet(Random.shuffle(a_W)))
  end

  # Simulate lambda
  lam = [begin
           p = [eps[i]; (1 - eps[i]) * W[i, :]]
           rand(Categorical(p), N[i]) .- 1
         end for i in 1:I]
  if sortLambda
    lam = [sort(lami) for lami in lam]
  end

  # Simulate eta
  eta = Dict([ z => zeros(Float64, I, J, L[z]) for z in 0:1 ])
  for i in 1:I
    for j in 1:J
      eta[0][i, j, :] = rand(Dirichlet(Random.shuffle(a_eta[0])))
      eta[1][i, j, :] = rand(Dirichlet(Random.shuffle(a_eta[1])))
    end
  end

  # Simulate gam
  gam = [zeros(Int, Ni, J) for Ni in N]
  for i in 1:I
    for j in 1:J
      for n in 1:N[i]
        k = lam[i][n]
        if k > 0
          z = Z[j, k]
          gam[i][n,j] = rand(Categorical(eta[z][i, j, :]))
        else
          gam[i][n,j] = 0
        end
      end
    end
  end

  # Generate y
  #y = [zeros(Union{Float64, Missing}, Ni, J) for Ni in N]
  y = [zeros(Float64, Ni, J) for Ni in N]
  y_complete = [zeros(Float64, Ni, J) for Ni in N]

  function z_get(i, n, j)
    return Z[j, lam[i][n]]
  end

  function mu_get(i, n, j)
    l = gam[i][n, j]
    if l > 0
      z = z_get(i, n, j)
      out = mus[z][l]
    else
      out = 0.0
    end
    return out
  end

  for i in 1:I
    for j in 1:J
      for n in 1:N[i]
        sig_i = lam[i][n] > 0 ? sqrt(sig2[i]) : 3.0
        y_complete[i][n, j] = rand(Normal(mu_get(i, n, j), sig_i))
      end

      # Set some to be missing
      p_miss = [prob_miss(y_complete[i][n, j], beta) for n in 1:N[i]]
      prop_missing = rand() * propMissingScale * sum(W[i,:] .* (1 .- Z[j,:]))
      num_missing = Int(round(N[i] * prop_missing))
      idx_missing = Distributions.wsample(1:N[i], p_miss, num_missing, replace=false)
      y[i][:, j] .= y_complete[i][:, j] .+ 0
      y[i][idx_missing, j] .= NaN
    end
  end

  return Dict(:y=>y, :y_complete=>y_complete, :Z=>Z, :W=>W,
              :eta=>eta, :mus=>mus, :sig2=>sig2, :lam=>lam, :gam=>gam,
              :beta=>beta, :eps=>eps)
end # genData

#precompile(genData, (String, Int, Vector{Int}, Int, Int, Bool, Float64, Bool, Float64))


function repstring(r::AbstractString)
    n = length(r)
    replst = String[]
    for m in 1:n÷2
        s = r[1:chr2ind(r, m)]
        if (s ^ cld(n, m))[1:chr2ind(r, n)] != r continue end
        push!(replst, s)
    end
    return replst
end

tests = ["1001110011", "1110111011", "0010010010", "1010101010", "1111111111",
         "0100101101", "0100100", "101", "11", "00", "1",
         "\u2200\u2203\u2200\u2203\u2200\u2203\u2200\u2203"]

for r in tests
    replst = repstring(r)
    if isempty(replst)
        println("$r is not a rep-string.")
    else
        println("$r is a rep-string of ", join(replst, ", "), ".")
    end
end


export spolynomial, gröbnerbasis, groebnerbasis
export Buchberger, presort!, dummyselection, normalselection

"""
    spolynomial(p::AbstractPolynomialLike, q::AbstractPolynomialLike)

Computes the S-polynomial of `p` and `q` defined by
```math
S(p, q) =  \\frac{m}{\\mathrm{\\mathsc{LT}}(p)} \\cdot p - \\frac{m}{\\mathrm{\\mathsc{LT}}(q)} \\cdot q
```
where ``m = \\mathrm{lcm}(\\mathrm{\\mathsc{LM}}(p), \\mathrm{\\mathsc{LM}}(q))``.
"""
function spolynomial(p::APL, q::APL)
    m = lcm(leadingmonomial(p), leadingmonomial(q))
    MultivariatePolynomials._div(m, leadingterm(p)) * p - MultivariatePolynomials._div(m, leadingterm(q)) * q
end

function reducebasis!(F::AbstractVector{<:APL}; kwargs...)
    changed = true
    while changed
        changed = false
        keep = collect(eachindex(F))
        del = Int[]
        for j in eachindex(F)
            G = @view F[setdiff(keep, j)]
            r = rem(F[j], G; kwargs...)
            if isapproxzero(r; kwargs...)
                deleteat!(keep, findfirst(isequal(j), keep))
                push!(del, j)
                changed = true # Should probably not set that, no need to do one more loop int this case
            elseif leadingmonomial(r) != leadingmonomial(F[j])
                F[j] = r
                changed = true
            end
        end
        deleteat!(F, del)
    end
end

ext(i, j) = (min(i, j), max(i, j))

lcmlm(F, i, j) = lcm(leadingmonomial(F[i]), leadingmonomial(F[j]))

function criterion(F, B, i, j)
    m = lcmlm(F, i, j)
    for k in eachindex(F)
        if k != i && k != j &&
            !(ext(i, k) in B) && !(ext(j, k) in B) &&
            divides(leadingmonomial(F[k]), m)
            return true
        end
    end
    return false
end

# Preprocessing of F

# "In practice, some effort could be saved on average if we arranged the f_i so that their
# leading terms are listed in increasing order with respect to the chosen monomial ordering.
# Since the smaller LT(fi) are more likely to be used during the division algorithm,
# listing them earlier means that fewer comparisons will be required."
#
# Ideals, Varieties, and Algorithms
# Cox, Little and O'Shea, Fourth edition p. 116
function presort!(F)
    sort!(F, by=leadingmonomial)
end

# Selection of (i, j) ∈ B

function dummyselection(F, B)
    first(B)
end
# "BUCHBERGER (1985) suggests that there will often be some savings if we pick
# (i, j) ∈ B such that lcm(LM(fi), LM(fj)) is as small as possible.
# The corresponding S-polynomials will tend to yield any nonzero remainders
# (and new elements of the Gröbner basis) sooner in the process,
# so there will be more of a chance that subsequent remainders rem(S(fi, fj), G) will be zero.
# This normal selection strategy is discussed in more detail in BECKER and WEISPFENNING (1993),
# BUCHBERGER (1985) and GEBAUER and MÖLLER (1988)."
#
# Ideals, Varieties, and Algorithms
# Cox, Little and O'Shea, Fourth edition p. 116
function normalselection(F, B)
    best = first(B)
    m = lcmlm(F, best...)
    for (i, j) in Iterators.drop(B, 1)
        cur = lcmlm(F, i, j)
        if cur < m
            best = (i, j)
            m = cur
        end
    end
    best
end
# TODO sugar and double sugar selections

defaultgröbnerbasisalgorithm(p) = Buchberger()
abstract type AbstractGröbnerBasisAlgorithm end

struct Buchberger <: AbstractGröbnerBasisAlgorithm
    ztol::Float64
    pre!::Function
    sel::Function
end
#Buchberger() = Buchberger(presort!, dummyselection)
Buchberger(ztol=Base.rtoldefault(Float64)) = Buchberger(ztol, presort!, normalselection)

# Taken from Theorem 9 of
# Ideals, Varieties, and Algorithms
# Cox, Little and O'Shea, Fourth edition
function gröbnerbasis!(F::AbstractVector{<:APL}, algo=defaultgröbnerbasisalgorithm(F))
    algo.pre!(F)
    B = Set{NTuple{2, Int}}(Iterators.filter(t -> t[1] < t[2], (i, j) for i in eachindex(F), j in eachindex(F)))
    while !isempty(B)
        (i, j) = algo.sel(F, B)
        lmi = leadingmonomial(F[i])
        lmj = leadingmonomial(F[j])
        if !isconstant(gcd(lmi, lmj)) && !criterion(F, B, i, j)
            #r = rem(spolynomial(F[i], F[j]), F; ztol=algo.ztol)
            r = rem(spolynomial(F[i], F[j]), F)
            if !isapproxzero(r; ztol=algo.ztol)
                I = eachindex(F)
                push!(F, r)
                n = last(eachindex(F))
                for ii in I
                    push!(B, (ii, n))
                end
            end
        end
        delete!(B, (i, j))
    end
    reducebasis!(F, ztol=algo.ztol)
    map!(monic, F, F)
    F
end
function gröbnerbasis(F::Vector{<:APL}, args...)
    T = Base.promote_op(rem, eltype(F), typeof(F))
    gröbnerbasis!(copyto!(similar(F, T), F), args...)
end
const groebnerbasis = gröbnerbasis


#
# This file is part of the Simulate.jl Julia package, MIT license
#
# Paul Bayer, 2019
#
# This is a Julia package for discrete event simulation
#
# Benchmark adopted from the channel1.jl example
#

using Simulate, BenchmarkTools, Random
import Dates.now

function take(id::Int64, ch::Channel, step::Int64, qpi::Array{Float64,1})
    if isready(ch)
        take!(ch)                                            # take something from common channel
        event!(Fun(put, id, ch, step, qpi), after, rand())    # timed event after some time
    else
        event!(Fun(take, id, ch, step, qpi), Fun(isready, ch)) # conditional event until channel is ready
    end
end

function put(id::Int64, ch::Channel, step::Int64, qpi::Array{Float64,1})
    put!(ch, 1)
    qpi[step] += (-1)^(id+1)/(2id -1)      # Machin-like series (slow approximation to pi)
    step > 3 || take(id, ch, step+1, qpi)
end

function setup(n::Int)                     # a setup he simulation
    reset!(𝐶)
    Random.seed!(123)
    global ch = Channel{Int64}(32)  # create a channel
    global qpi = zeros(4)
    si = shuffle(1:n)
    for i in 1:n
        take(si[i], ch, 1, qpi)
    end
    for i in 1:min(n, 32)
        put!(ch, 1) # put first tokens into channel 1
    end
end

# first run to compile
setup(250)
run!(𝐶, 500)

# then take measurements
println(now())
@time setup(250)
println(@time run!(𝐶, 500))
println("result=", sum(qpi))

t = run(@benchmarkable run!(𝐶, 500) setup=setup(250) evals=1 seconds=15.0 samples=100)


# Math module
module Math

using Dierckx

# types for derivatives, etc
abstract FunctionType

type Derivative <: FunctionType end

type BernsteinPolynomial end

# misc function for comparison
function is_close{T <: Number}(x::T, y::T, n::Int = 42)
  if x == y
    return true
  end

  diff = abs(x - y)
  tol = n * eps(Float64) # machine epsilon

  if (x * y == 0.0) # x or y is 0
    return diff < (tol * tol)
  end

  return diff <= tol * abs(x) && diff <= tol * abs(y)
end

function close_enough{T <: Number}(x::T, y::T, n::Int = 42)
  if x == y
    return true
  end

  diff = abs(x -y)
  tol = n * eps()

  if x * y == 0
    return diff < (tol * tol)
  end

  return diff <= tol * abs(x) || diff <= tol * abs(y)
end

# misc functions - prob put in own file
function divide_array_by_self!{T, N <: Number}(a::Vector{T}, x::N)
  for i = 1:length(a)
    a[i] = a[i] / x
  end

  return a
end

function multiply_array_by_self!{T, N <: Number}(a::Vector{T}, x::N)
  for i = 1:length(a)
    a[i] = a[i] * x
  end

  return a
end

function get_factorial{I <: Integer}(i::I)
  if i > 20
    return Float64(factorial(BigInt(i)))
  else
    return factorial(i)
  end
end

function get_polynomial(::BernsteinPolynomial, i::Int, n::Int, x::Float64)
  coeff = get_factorial(n) / (get_factorial(n-1) * get_factorial(i))

  return coeff * (x ^ i) * (1.0 - x)^(n - i)
end

export is_close, close_enough, divide_array_by_self!, multiply_array_by_self!, get_factorial, get_polynomial, FunctionType, Derivative, BernsteinPolynomial

# Splines
type BSpline
  p::Integer
  n::Integer
  knots::Vector{Float64}
end

function spline_oper{I <: Integer}(spline::BSpline, i::I, x::Float64)
  i <= spline.n || error("i must not be greater than spline.n $i $(spline.n)")
  return N(spline, i, spline.p, x)
end

function N{I <: Integer}(spline::BSpline, i::I, p::I, x::Float64)
  if p == 0
    return (spline.knots[i] <= x && x < spline.knots[i + 1]) ? 1.0 : 0.0
  else
    return ((x - spline.knots[i]) / (spline.knots[i + p] - spline.knots[i])) * N(spline, i, p - 1, x) +
            ((spline.knots[i + p + 1] - x) / (spline.knots[i + p + 1] - spline.knots[i + 1])) * N(spline, i + 1, p - 1, x)
  end
end

export BSpline, spline_oper, N

# Constants
const EPS_VAL = eps()

# utilities.jl
include("utilities.jl")

# grid.jl
export bounded_log_grid
include("grid.jl")

# lmdif2.jl
export lmdif2!
include("lmdif2.jl")

# distribution.jl
export distribution_derivative, peizer_pratt_method_2_inversion
include("distributions.jl")

# integral.jl
export Integrator, IntegrationFunction, SegmentIntegral, operator, integrate, GaussLaguerreIntegration, get_order
include("integral.jl")

# svd.jl
export SVD
include("svd.jl")

# tridiagonal_operator.jl
export TridiagonalOperator, TridiagIdentity, set_mid_row!, set_last_row!, set_first_row!, apply_to, solve_for!
include("tridiagonal_operator.jl")

# matrix.jl
export NoneSalvagingAlgo, rank_reduced_sqrt, OrthogonalProjection, get_vector
include("matrix.jl")

# transformed_grid.jl
export TransformedGrid, LogGrid
include("transformed_grid.jl")

# rng.jl
export AbstractRandomSequenceGenerator, PseudoRandomRSG, InverseCumulativeRSG, SobolRSG, SobolInverseCumulativeRSG, next_sequence!, last_sequence, init_sequence_generator!
include("rng.jl")

# statistics.jl
export NonWeightedStatistics, GenericRiskStatistics, RiskStatistics, gen_RiskStatistics, GenericSequenceStats, reset!, add_sample!, adding_data!, error_estimate, stats_mean, stats_std_deviation,
stats_skewness, stats_kurtosis, stats_covariance
include("statistics.jl")

# sampled_curve.jl
export SampledCurve, get_size, set_log_grid!, set_grid!, sample!, value_at_center, first_derivative_at_center, second_derivative_at_center
include("sampled_curve.jl")

# general_linear_least_squares.jl
export GeneralLinearLeastSquares, get_coefficients
include("general_linear_least_squares.jl")

# interpolation.jl
export Interpolation, Spline, Lagrange, LogInterpolation, LogLinear, BicubicSpline, NaturalCubicSpline, CubicInterpolation, BackwardFlatInterpolation, update!, locate, initialize!, value, get_primitive,
derivative

include("interpolation/interpolation.jl")
include("interpolation/linear_interpolation.jl")
include("interpolation/log_interpolation.jl")
include("interpolation/backward_flat_interpolation.jl")
include("interpolation/spline_interpolation.jl")

# solvers.jl
export Solver1D, BrentSolver, NewtonSolver, FiniteDifferenceNewtonSafe, solve
include("solvers/solver.jl")
include("solvers/brent_solver.jl")
include("solvers/finite_difference.jl")
include("solvers/newton_solver.jl")

# optimization.jl
export lmdif!, Projection, CostFunction, Constraint, NoConstraint, PositiveConstraint, BoundaryConstraint, ProjectedConstraint, OptimizationMethod, LevenbergMarquardt, Simplex, Problem, EndCriteria,
project, minimize!, minimize_2!, include_params
include("optimization/lmdif.jl")
include("optimization/optimization.jl")
include("optimization/problem.jl")
include("optimization/simplex.jl")
include("optimization/levenberg_marquardt.jl")

end


@testset "SymSparseMatrixCSR" begin
    maxnz=5
    maxrows=5
    maxcols=5
    int_types=(Int32,Int64)
    float_types=(Float32,Float64)
    Bi_types=(0,1)

    for Ti in int_types
        for Tv in float_types
            for Bi in Bi_types
                I = Vector{Ti}()
                J = Vector{Ti}()
                V = Vector{Tv}()
                for (ik, jk, vk) in zip(rand(1:maxrows, maxnz), rand(1:maxcols, maxnz), rand(1:Tv(maxnz), maxnz) )
                    push_coo!(SymSparseMatrixCSR{Bi,Tv,Ti},I,J,V,ik,jk,vk)
                end
                finalize_coo!(SymSparseMatrixCSR{Bi,Tv,Ti},I,J,V,maxrows, maxcols)
                SYMCSC = Symmetric(sparse(I, J, V, maxrows, maxcols),:U)
                SYMCSR = symsparsecsr(SymSparseMatrixCSR{Bi,Tv,Ti},I, J, V, maxrows, maxcols)

                @test size(SYMCSC)==size(SYMCSR)
                @test SYMCSC == SYMCSR

                @test convert(SymSparseMatrixCSR{Bi}, SYMCSR) === SYMCSR

                @test hasrowmajororder(SYMCSR) == true
                @test hascolmajororder(SYMCSR) == false
                @test getptr(SYMCSR)           == SYMCSR.uppertrian.rowptr
                @test getindices(SYMCSR)       == colvals(SYMCSR)

                @test nnz(SYMCSC.data) == nnz(SYMCSR.uppertrian) <= nnz(SYMCSR)
                @test count(!iszero, SYMCSC.data) == count(!iszero, SYMCSR.uppertrian)

                ICSC,JCSC,VCSC= findnz(SYMCSC.data)
                ICSR,JCSR,VCSR= findnz(SYMCSR)

                @test sort(ICSC)==sort(JCSR) && sort(JCSC)==sort(ICSR) && sort(VCSC)==sort(VCSR)

                v = rand(size(SYMCSC)[2])
                @test SYMCSC*v == SYMCSR*v

                for cBi in Bi_types
                    if Bi == cBi
                        @test convert(SymSparseMatrixCSR{Bi}, SYMCSR) === SYMCSR
                    else
                        SYMCSRC = convert(SymSparseMatrixCSR{cBi}, SYMCSR)
                        @test SYMCSRC == SYMCSR
                        @test SYMCSRC !== SYMCSR
                    end
                end

                for cTi in int_types
                    for cTv in float_types
                        if (Ti,Tv) == (cTi,cTv)
                            @test convert(SymSparseMatrixCSR{Bi,Tv,Ti}, SYMCSR) === SYMCSR
                        else
                            SYMCSRC = convert(SymSparseMatrixCSR{Bi,cTv,cTi}, SYMCSR)
                            @test SYMCSRC == SYMCSR
                            @test SYMCSRC !== SYMCSR
                        end
                    end
                end
            end
        end
    end

end


# Given a container `net`, propagates the input `x` through all layers in `net`.
# Returns the final output of the network.
function forward(net, x)
    for layer in net
        x = fprop!(layer, x)
    end
    return x
end

# Given a container `net`, back-propagates the error-signal ∂y through all layers
# in `net` in reverse order. ∂y is typically a vector filled with 1/N.
# Returns the gradient of the output wrt. the input(s).
function backward(net, ∂y)
    for layer in reverse(net)
        ∂y = bprop!(layer, ∂y)
    end
    return ∂y
end

# Sets all gradients of a given layer to zero.
function zero_grads!(layer)
    for ∂θ in grads(layer)
        ∂θ[:] = 0
    end
end

# Calls `update_fn` on all (parameter, gradient) pairs in the container `net`.
function update_grads!(net, update_fn)
    for layer in net
        map(update_fn, params(layer), grads(layer))
    end
end

# Does a full batch of training: forward, backward, update.
function train_batch(net, crit, xb, yb; λ=0.01)
    target!(crit, yb)
    map(zero_grads!, net)

    # Forward pass
    pred = forward(net, xb)
    costs = fprop!(crit, pred)
    # Or alternatively, the following works just as well:
    # costs = forward([net ; crit], xb)

    # Backward pass
    # ∂pred = bprop!(crit, 1/size(xb, 1))
    ∂pred = bprop!(crit, ones(costs) ./ size(xb, 1))
    ∂x = backward(net, ∂pred)
    # Again, the following works just as well:
    # ∂x = backward([net ; crit], ones(costs) ./ size(xb, 1))

    # Update the parameters using the computed gradients, via SGD.
    update_grads!(net, (θ, ∂θ) -> sgd!(θ, ∂θ, λ))

    return mean(costs)
end

# Updates the parameters `θ` in a single step of SGD using their gradients `∂θ`
# and the learning-rate (update ratio) `λ`.
function sgd!(θ, ∂θ, λ)
    θ[:] = θ - λ*∂θ
end

# Loads train/valid/test split of data used for the exercise.
# Also pre-processes it by converting to float and dividing by 255 to get into [0,1] range.
function loadMNIST(prefix, T::Type)
    Xtr = readdlm("$prefix-train-data.csv", T)
    ytr = readdlm("$prefix-train-probas.csv", T)
    Xva = readdlm("$prefix-valid-data.csv", T)
    yva = readdlm("$prefix-valid-probas.csv", T)
    Xte = readdlm("$prefix-test-data.csv", T)
    yte = readdlm("$prefix-test-probas.csv", T)

    return (Xtr/255-0.5, ytr), (Xva/255-0.5, yva), (Xte/255-0.5, yte)
end