"""
The advent of quantum technology requires new figures of merit to compare the performance
of quantum and classical systems.
The signaling dimension quantifies the classical
simulation cost of black-box devices.
That is, it specifies the minimum amount of noiseless, classical communication
needed to exactly simulate the input-output statistics of the device in question.
The signaling dimension is discussed in greater detail in the Background section
of this documentation.

## Features:
* Tools to compute signaling correlations and polytopes (see [Signaling Correlations](@ref) and [Signaling Polytopes](@ref)).
* A catalog of Bell inequalities for witnessing signaling dimension (see [Bell Inequalities](@ref)).
* Methods to certify the signaling dimension of signaling systems (see [Certifying Signaling Dimension](@ref)).
"""
module SignalingDimension

using LinearAlgebra
using Combinatorics
using BellScenario

include("./bell_inequalities.jl")
include("./affinely_independent_enumerations.jl")
include("./certifying_signaling_dimension.jl")
include("./verifications.jl")

end


function _one(params::Vector{Float64}, pop::Pathogen.Population, i::Int64)
  return 1.0
end

function _zero(params::Vector{Float64}, pop::Pathogen.Population, i::Int64)
  return 0.0
end

function _constant(params::Vector{Float64}, pop::Pathogen.Population, i::Int64)
  return params[1]
end

function _coefficient(params::Vector{Float64}, pop::Pathogen.Population, i::Int64)
  return params[1] * pop.risks[i, :riskfactor1]
end

function _linear(params::Vector{Float64}, pop::Pathogen.Population, i::Int64)
  return params[1] + params[2] * pop.risks[i, :riskfactor1]
end

function _powerlaw(params::Vector{Float64}, pop::Pathogen.Population, i::Int64, k::Int64)
  α = params[1]
  β = params[2]
  d = pop.distances[k, i]
  return α * (d^(-β))
end

function _powerlaw_w_intercept(params::Vector{Float64}, pop::Pathogen.Population, i::Int64, k::Int64)
  α = params[1]
  β = params[2]
  γ = params[3]
  d = pop.distances[k, i]
  return (α * (d^(-β))) + γ
end


function CBMIMMPSolver(A,b,C,d,VariableDef;XLB=[],XUB=[], rEPS = 1e-6,aEPS = 1e-6,TimeLim = 2e+10)
    t0 = time_ns();
    # Initialization
    const EPS = 0.000001;
    const inf = 9999999;

    nObj = size(C)[1];
    nVar = size(A)[2];
    nCons = length(b);

    if XLB == []
        XLB = -1e9*ones(nVar)
    end
    if XUB== []
        XUB = 1e9*ones(nVar)
    end

    NodeCounter = 1;
    GUB = Inf;
    GLB = 0;
    # Node 0
    OptX = zeros(nVar)
    YI = zeros(nObj);
    Var = zeros(nVar);
    PayoffTable = zeros(nObj,nObj);
    for i = 1:nObj
        ObjOrd = [j for j = i:i+nObj-1]
        ObjOrd[ObjOrd .> nObj] = ObjOrd[ObjOrd .> nObj]-nObj
        ObjVal, Var = ArgLexMin(A,b,C,d,XLB,XUB,VariableDef,ObjOrd; TimeLimit = TimeLim-(time_ns() - t0)/1e9)

        if (TimeLim-(time_ns() - t0)/1e9) < 0.001;
            println("Time Limit reached! The solution is not a proved optimal")
            return GUB, OptX;
        end
        YI[i] = ObjVal[i]
        PayoffTable[i,:] = ObjVal
        if GUB >= prod(PayoffTable[i,:])
            GUB = prod(PayoffTable[i,:])
            OptX = copy(Var)
        end
    end
    GLB = prod(YI);
    S = [NodeType2(YI,GLB,GUB)]
    Y = zeros(nObj)
    # Main Loop
    s = MinimalT1(S)[1];
    while (length(S) > 0) && (abs(GUB-GLB) >= aEPS) && ((GUB-GLB)/GUB >= rEPS)

        # Time commands ---------------------------------------------------
        if (TimeLim-(time_ns() - t0)/1e9) < 0.001;
            println("Time Limit reached! The solution is not a proved optimal")
            return GUB, OptX;
        end
        # -----------------------------------------------------------------
        # Explore node s
        Y_bar,Var,Y,IsExplored = Explore(A,b,C,d,XLB,XUB,VariableDef,S[s].Point)

        NodeCounter += 1

        if IsExplored
            if (prod(Y_bar) < GUB)
                GUB = prod(Y_bar)
                OptX = copy(Var)
            end

            # Branch
            for i = 1:nObj
                Point = copy(S[s].Point)
                Point[i] = Y[i];
                if (GUB - prod(Point) > aEPS) & ((GUB-prod(Point))/GUB >= rEPS) & !(Dominated(Point,S,s))
                    push!(S,NodeType2(Point,prod(Point),GUB))
                end
            end
        end
        deleteat!(S,s);
        if ~((length(S) > 0) && (abs(GUB-GLB) >= aEPS) && ((GUB-GLB)/GUB >= rEPS)); break; end
        s,GLB = MinimalT1(S)

    end

    return GUB, OptX;
end


using Test, Convex, SCS, SchattenNorms

import Base.kron
import LinearAlgebra

eye(n::Integer) = Matrix{Float64}(LinearAlgebra.I, (n, n))

x,y,z = [0 1; 1 0], [0 -1im; 1im 0], [1 0; 0 -1]

liou(x) = kron(conj(x),x)
liou(x,y) = kron(transpose(y),x)

function rand_unitary(d)
    rm = randn(d,d)+1im*randn(d,d)
    return LinearAlgebra.svd(rm).U
end

@testset "maximal dnorm examples" begin
    let # variable scope for cnot
        cnot = float([1 0 0 0; 0 1 0 0; 0 0 0 1; 0 0 1 0])
        cnotL = liou(cnot)
        @test isapprox(2.0, dnorm(cnotL-eye(16)), atol=1e-5)
        @test isapprox(2.0, ddist(cnotL,eye(16)), atol=1e-5)
        @test isapprox(2.0, ddistu(cnot,eye(4)), atol=1e-5)
    end

    @test isapprox(2.0, dnorm(eye(4)-liou(x)), atol=1e-5)
    @test isapprox(2.0, ddist(eye(4),liou(x)), atol=1e-5)
    @test isapprox(2.0, ddistu(eye(2),x), atol=1e-5)

    @test isapprox(2.0, dnorm(eye(4)-liou(z)), atol=1e-5)
    @test isapprox(2.0, ddist(eye(4),liou(z)), atol=1e-5)
    @test isapprox(2.0, ddistu(eye(2),z), atol=1e-5)

    @test isapprox(2.0, dnorm(eye(4)-liou(y)), atol=1e-5)
    @test isapprox(2.0, ddist(eye(4),liou(y)), atol=1e-5)
    @test isapprox(2.0, ddistu(eye(2),y), atol=1e-5)

    @test isapprox(2.0, dnorm(liou(x)-liou(y)), atol=1e-5)
    @test isapprox(2.0, ddist(liou(x),liou(y)), atol=1e-5)
    @test isapprox(2.0, ddistu(x,y), atol=1e-5)

    @test isapprox(2.0, dnorm(liou(x)-liou(z)), atol=1e-5)
    @test isapprox(2.0, ddist(liou(x),liou(z)), atol=1e-5)
    @test isapprox(2.0, ddistu(x,z), atol=1e-5)

    @test isapprox(2.0, dnorm(liou(z)-liou(y)), atol=1e-5)
    @test isapprox(2.0, ddist(liou(z),liou(y)), atol=1e-5)
    @test isapprox(2.0, ddistu(z,y), atol=1e-5)
end

@testset "maximal dnorm examples, random" begin
    global u = rand_unitary(2)
    global uu = liou(u)
    global v = rand_unitary(2)
    global vv = liou(v)

    @test isapprox(2.0, dnorm(uu*eye(4)-uu*liou(x)), atol=1e-5)
    @test isapprox(2.0, ddist(uu*eye(4),uu*liou(x)), atol=1e-5)
    @test isapprox(2.0, ddistu(u*eye(2),u*x), atol=1e-5)

    @test isapprox(2.0, dnorm(uu*eye(4)-uu*liou(z)), atol=1e-5)
    @test isapprox(2.0, ddist(uu*eye(4),uu*liou(z)), atol=1e-5)
    @test isapprox(2.0, ddistu(u*eye(2),u*z), atol=1e-5)

    @test isapprox(2.0, dnorm(uu*eye(4)-uu*liou(y)), atol=1e-5)
    @test isapprox(2.0, ddist(uu*eye(4),uu*liou(y)), atol=1e-5)
    @test isapprox(2.0, ddistu(u*eye(2),u*y), atol=1e-5)

    @test isapprox(2.0, dnorm(uu*liou(x)-uu*liou(y)), atol=1e-5)
    @test isapprox(2.0, ddist(uu*liou(x),uu*liou(y)), atol=1e-5)
    @test isapprox(2.0, ddistu(u*x,u*y), atol=1e-5)

    @test isapprox(2.0, dnorm(uu*liou(x)-uu*liou(z)), atol=1e-5)
    @test isapprox(2.0, ddist(uu*liou(x),uu*liou(z)), atol=1e-5)
    @test isapprox(2.0, ddistu(u*x,u*z), atol=1e-5)

    @test isapprox(2.0, dnorm(uu*liou(z)-uu*liou(y)), atol=1e-5)
    @test isapprox(2.0, ddist(uu*liou(z),uu*liou(y)), atol=1e-5)
    @test isapprox(2.0, ddistu(u*z,u*y), atol=1e-5)
end

@testset "dnorm for Pauli channels" begin
pauli = Matrix{ComplexF64}[eye(2), x, y, z]
    for i in 1:10
        p1 = rand(4); p1 = p1/sum(p1)
        p2 = rand(4); p2 = p2/sum(p2)
        pauli_dnorm = norm(p1-p2,1)

        pc1 = sum([ p1[ii+1]*liou(pauli[ii+1]) for ii in 0:3])
        pc2 = sum([ p2[ii+1]*liou(pauli[ii+1]) for ii in 0:3])

        calc_dnorm1 = ddist(pc1,pc2)

        @test isapprox(pauli_dnorm, calc_dnorm1, atol=1e-5)
    end
end

@testset "unitary invariance of dnorm" begin
    global u = rand_unitary(2)
    global uu = liou(u)
    global v = rand_unitary(2)
    global vv = liou(v)

    global duv  = dnorm(uu-vv)
    @time begin
        global duv2 = dnorm(uu*vv'-eye(4))
        global duv3 = dnorm(vv'*uu-eye(4))
        global duv4 = dnorm(eye(4)-uu'*vv)
        global duv5 = dnorm(eye(4)-vv*uu')
    end

    @test isapprox(duv, duv2, atol=1e-5)
    @test isapprox(duv, duv3, atol=1e-5)
    @test isapprox(duv, duv4, atol=1e-5)
    @test isapprox(duv, duv5, atol=1e-5)
end

@testset "ddist difference of unitary transformations" begin

    for i in 1:20
        for d in [2,3,4]
            global u = rand_unitary(d)
            global uu = liou(u)
            global v = rand_unitary(d)
            global vv = liou(v)
            @test isapprox(ddistu(u,v), ddist(uu,vv), atol=1e-4)
        end
    end

    for i in 1:20
        for d in [2,3,4]
            global u = rand_unitary(d)
            global uu = liou(u)
            global v = rand_unitary(d)
            global vv = liou(v)

            global duv  = ddistu(u,v)
            global duv2 = ddistu(u*v',eye(d))
            global duv3 = ddistu(v'u,eye(d))
            global duv4 = ddistu(eye(d),u'*v)
            global duv5 = ddistu(eye(d),v*u')

            @test isapprox(duv, duv2, atol=1e-10)
            @test isapprox(duv, duv3, atol=1e-10)
            @test isapprox(duv, duv4, atol=1e-10)
            @test isapprox(duv, duv5, atol=1e-10)
        end
    end
end

#def = dnorm(ee-ff)
#@time begin
#    def2 = dnorm(u*(ee-ff)*v)
#    def3 = dnorm(u*(ee-ff))
#    def4 = dnorm((ee-ff)*v)
#    def5 = dnorm(u'*(ee-ff))
#end

#println(def)
#println(def2)
#println(def3)
#println(def4)
#println(def5)


# External packages
using Revise, DelimitedFiles, SymPy

# Add Exasim to Julia search path
cdir = pwd(); ii = findlast("Exasim", cdir);
include(cdir[1:ii[end]] * "/Installation/setpath.jl");

# Exasim packages
using Preprocessing, Mesh, Gencode, Postprocessing

# create pde structure and mesh structure
pde, mesh = Preprocessing.initializeexasim();

# Define PDE model: governing equations, initial solutions, and boundary conditions
pde.model = "ModelC";            # ModelC, ModelD, ModelW
include("pdemodel.jl");          # include the PDE model file

# Set discretization parameters, physical parameters, and solver parameters
pde.porder = 4;          # polynomial degree
pde.torder = 3;          # time-stepping order of accuracy
pde.nstage = 3;          # time-stepping number of stages
pde.dt = 0.05*ones(200);   # time step sizes
pde.soltime = collect(1:length(pde.dt)); # steps at which solution are collected
pde.visdt = 0.05; # visualization timestep size

gam = 1.4;                      # specific heat ratio
M_ref = sqrt(1/gam);            # Mach number
pde.physicsparam = [gam M_ref];
pde.tau = [1+1/M_ref];          # DG stabilization parameter

# Choose computing platform and set number of processors
#pde.platform = "gpu";           # choose this option if NVIDIA GPUs are available
pde.mpiprocs = 1;                # number of MPI processors

# create a linear mesh for a square domain
mesh.p, mesh.t = Mesh.SquareMesh(10,10,1); # a mesh of 8 by 8 quadrilaterals
mesh.p = 10.0*mesh.p .- 5.0;
# expressions for disjoint boundaries
mesh.boundaryexpr = [p -> (p[2,:] .< -5+1e-3), p -> (p[1,:] .> 5-1e-3), p -> (p[2,:] .> 5-1e-3), p -> (p[1,:] .< -5+1e-3)];
mesh.boundarycondition = [1 1 1 1]; # Set boundary condition for each disjoint boundary
mesh.periodicexpr = [2 p->p[2,:] 4 p->p[2,:]; 1 p->p[1,:] 3 p->p[1,:]];

# call exasim to generate and run C++ code to solve the PDE model
sol, pde, mesh,~,~,~,~  = Postprocessing.exasim(pde,mesh);

# visualize the numerical solution of the PDE model using Paraview
pde.visscalars = ["density", 1, "energy", 4];  # list of scalar fields for visualization
pde.visvectors = ["momentum", [2, 3]]; # list of vector fields for visualization
Postprocessing.vis(sol,pde,mesh); # visualize the numerical solution
print("Done!");


map_str = ""
width, height = Int32(0), Int32(0)
open("day03/day03.in") do io
    global map_str, width, height
    for line in readlines(io)
        map_str = map_str * line
        width = length(line)
        height += 1
    end
end

function encounters(right::Int64, down::Int64)::Int64
    posx, posy = Int64(1+right), Int64(1+down)
    tree_count = Int64(0)
    while posy <= height
        global map_str
        if map_str[(posy-1)*width+posx] == '#'
            tree_count += 1
        end
        posx = (posx+right-1) % width + 1
        posy = posy+down

    end
    return tree_count
end

c = encounters(1, 1)
c *= encounters(3, 1)
c *= encounters(5, 1)
c *= encounters(7, 1)
c *= encounters(1, 2)
println(c)


######### Stan program example  ###########

using StanSample

ProjDir = @__DIR__

bernoullimodel = "
data {
  int<lower=0> N;
  array[N] int<lower=0,upper=1> y;
}
parameters {
  real<lower=0,upper=1> theta;
}
model {
  theta ~ beta(1,1);
  y ~ bernoulli(theta);
}
";

observed_data = Dict("N" => 10, "y" => [0, 1, 0, 1, 0, 0, 0, 0, 0, 1])

# Default for tmpdir is to create a new tmpdir location
# To prevent recompilation of a Stan progam, choose a fixed location,
tmpdir= ProjDir * "/tmp"

sm = SampleModel("bernoulli2", bernoullimodel, tmpdir);

rc = stan_sample(sm; data=observed_data,
  save_warmup=true, num_warmups=1000,
  num_samples=1000, thin=1, delta=0.85
);

if success(rc)
  df = read_summary(sm, true)
  df |> display
end


module  TestHeterozygosity

using PopGen
using Test

cats = @nancycats;
testarray = cats.genodata.genotype[1:10]

@testset "Hardy Weinberg" begin
    @test PopGen._chisqlocus(testarray) == (16.691358024691358, 12, 0.1615808310222504)
    tmp = hwetest(cats)
    @test tmp isa DataFrames.DataFrame
    @test size(tmp) == (9,4)
    tmp = hwetest(cats, correction = "bh")
    @test size(tmp) == (9,5)
    tmp = hwetest(cats, by = "population", correction = "bh")
    @test size(tmp) == (153,6)
end

end #module


@testset "entropy" begin
    ent = entropy(cars, 7, 4)
    ent2 = entropy(cars, "class", header, 4)
    @test ent >= 0 && ent <= 1
    @test ent2 >= 0 && ent2 <= 1
    @test ent == ent2
end

@testset "information gain" begin
    for i in 1:6
        ig = information_gain(cars, i, 7, 4)
        ig2 = information_gain(cars, header[i], "class", header, 4)
        ig3 = information_gain(cars, header[i], 7, header, 4)
        ig4 = information_gain(cars, i, "class", header, 4)
        @test ig >= 0 && ig <= 1
        @test ig2 >= 0 && ig2 <= 1
        @test ig3 >= 0 && ig3 <= 1
        @test ig4 >= 0 && ig4 <= 1
        @test ig == ig2 == ig3 == ig4
    end
end


# mobility.jl

"""
----------------------------------------------------------------------
The Moblity of the Polaron.
----------------------------------------------------------------------
"""

"""
polaron_mobility_ac(Ω::Float64, β::Float64, α::Float64, v::Float64, w::Float64)

    Calculate the ac moblity μ(Ω) of the polaron at finite temperature (equation (1) in Hellwarth 1999) for a given frequency Ω. β is the thermodynamic beta. v and w are the variational polaron parameters that minimise the free energy, for the supplied α Frohlich coupling.
"""
function polaron_mobility_ac(Ω, β, α, v, w)
    Ω / imag(χ(Ω, β, α, v, w))
end

"""
polaron_mobility_ac(Ω::Float64, α::Float64, v::Float64, w::Float64)

    Calculate the ac moblity μ(Ω) of the polaron at zero-temperatures (equation (46) in FHIP 1962) for a given frequency Ω. v and w are the variational polaron parameters that minimise the free energy, for the supplied α Frohlich coupling.
"""
function polaron_mobility_ac(Ω, α, v, w)
    Ω / imag(χ(Ω, α, v, w))
end

"""
polaron_mobility_dc(β::Float64, α::Float64, v::Float64, w::Float64)

    Calculate the dc moblity μ(β) of the polaron at finite temperatures (equation (46) in FHIP 1962) at zero frequenc. v and w are the variational polaron parameters that minimise the free energy, for the supplied α Frohlich coupling.
"""
function polaron_mobility_dc(β, α, v, w)
    1 / imag(χ_dc(β, α, v, w))
end


using WaveletInformation
using StatsBase

r = MersenneTwister(5)
testmat = rand(r, 100, 1024)
testids = sample(r, [1,2], 100)

@time WaveletInformation.mutual_info(testmat,testids)
@time WaveletInformation.mutual_info(testmat,testids)

@show  WaveletInformation.mutual_info(testmat,testids)[1:5]

L = [1; [2^n for n in 1:10]];
@time WaveletInformation.mutual_info_thresh(testmat,testids,5, 95, L)


using HOODESolver: filtred_f, PrepareU0
using Test

include("henon_heiles.jl")
include("dataprep_u0.jl")

# these 3 functions come from old version with sin and cos computation
# to test the new version
function reffltfct( u, s, c )
    a = c*u[1] + s*u[3]
    b = 2 * u[2] * a
    return [ s*b, u[4], -c*b, -u[2] - a^2 + u[2]^2, 1 ]
end
function reffltfctgen2( u::Array{Array{T,1},1}, s, c, n_tau ) where T <: Number
    f = reffltfct.( u, s, c)
    # convert an array of vector to a matrix
    f = collect(transpose(reshape(collect(Iterators.flatten(f)), 5, n_tau)))
    return f
end
function reffltfctgen( uMat::Array{T,2}, s, c, n_tau ) where T <: Number
    return reffltfctgen2(reshape(mapslices(x->[x],uMat,dims = (2,)), n_tau), s, c, n_tau)
end

function test_tau_A()
    epsilon = big"0.5"
    n_tau = 64
    A = [0 0 1 0; 0 0 0 0; -1 0 0 0; 0 0 0 0]
    parphi = HOODESolver.PreparePhi(epsilon, n_tau, A, henon_heiles)
    tol= 1e-77
    @testset "preparephi tau_A" begin
        for i = 1:n_tau
            s,c = setprecision(precision(BigFloat)+32) do
                v = (i-1)*2big(pi)/n_tau
                sin(v), cos(v)
            end

            res_ref = one(zeros(BigFloat,5,5))
            res_ref[1,1] = res_ref[3,3] = c
            res_ref[1,3] = s
            res_ref[3,1]= -res_ref[1,3]
            @test isapprox(res_ref, convert(Matrix, parphi.tau_Ap[i]), atol=tol)
            res_ref[3,1], res_ref[1,3] = res_ref[1,3], res_ref[3,1]
            @test isapprox(res_ref, convert(Matrix, parphi.tau_Ap_inv[i]), atol=tol)
        end
    end
end
function test_fct()
    epsilon = big"0.5"
    n_tau = 64
    A = [0 0 1 0; 0 0 0 0; -1 0 0 0; 0 0 0 0]
    parphi = HOODESolver.PreparePhi(epsilon, n_tau, A, henon_heiles)
    tol= 1e-76
    @testset "preparephi fct" begin
        for i = 1:n_tau
            s,c = setprecision(precision(BigFloat)+32) do
                v = (i-1)*2big(pi)/n_tau
                sin(v), cos(v)
            end
            u0 = rand(BigFloat,5)
            res_ref = reffltfct(u0, s, c)
            res = filtred_f(u0,parphi.tau_Ap_inv[i], parphi.tau_Ap[i], parphi)
            @test isapprox(res_ref, res, atol=tol)
        end
    end
end

function testpreparephi0()

    epsilon = big"0.5"
    n_tau = 64
    tol=1e-76
    @time @testset "preparephi and function for all tau" begin
        parphi = HOODESolver.PreparePhi(
    epsilon,
    n_tau,
    [0 0 1 0; 0 0 0 0; -1 0 0 0; 0 0 0 0],
    henon_heiles
)
        u_mat = rand(BigFloat, 5, n_tau)
        u_mat_tr = collect(transpose(u_mat))
        tau = LinRange(zero(BigFloat), one(BigFloat), n_tau + 1)[1:end - 1]
        s = sinpi.(2tau)
        c = cospi.(2tau)
        old_m = collect(transpose(reffltfctgen(u_mat_tr, s, c, n_tau)))
        new_m = HOODESolver.filtredfct(parphi,u_mat)
        println("norm=$(norm(old_m-new_m))")
        @test isapprox(old_m, new_m, atol=tol)
    end
end

function testpreparephi()
    @time @testset "phi and prepare u0" begin
        tab_ref, tab_eps = setprecision(1024) do
            get_prepare_u0_for_test()
        end
        for i_prec=1:3
            prec = 256*2^(i_prec-1)
            tol=max((1e-77)^i_prec, 1e-200)
            setprecision(prec) do
                for i_eps=1:size(tab_eps,1)
                   epsilon = BigFloat(tab_eps[i_eps])
                   n_tau = 32
                   parphi = HOODESolver.PreparePhi(
    epsilon,
    n_tau,
    [0 0 1 0; 0 0 0 0; -1 0 0 0; 0 0 0 0],
    henon_heiles
)
                    u0 = BigFloat.([0.125, 0.140625, 0.15625, 0.171875])
                    for ord=2:10
            #     paru0 = PrepareU0(parphi, ord, u0, 1024)
                        paru0 = PrepareU0(parphi, ord, u0)
 #                   println("prec=$prec ord=$ord norm=$(norm(tab_ref[:, :, ord]- paru0.ut0))")
                        @test isapprox(tab_ref[:, :, ord, i_eps], paru0.ut0[1:4,:], atol=tol, rtol=tol)
                    end
                end
            end
        end
    end
end

test_tau_A()
test_fct()
testpreparephi0()
testpreparephi()


struct SparseValueIterationSolver <: Solver
    max_iterations::Int64
    belres::Float64 # the Bellman Residual
    include_Q::Bool
    verbose::Bool
    init_util::Vector{Float64}
end

function SparseValueIterationSolver(;max_iterations=500,
                                    belres::Float64=1e-3,
                                    include_Q::Bool=true,
                                    verbose::Bool=false,
                                    init_util::Vector{Float64}=Vector{Float64}(undef, 0))
    return SparseValueIterationSolver(max_iterations, belres, include_Q, verbose, init_util)
end

@POMDP_require solve(solver::SparseValueIterationSolver, mdp::Union{MDP,POMDP}) begin
    P = typeof(mdp)
    S = statetype(P)
    A = actiontype(P)
    @req discount(::P)
    @req n_states(::P)
    @req n_actions(::P)
    @subreq ordered_states(mdp)
    @subreq ordered_actions(mdp)
    @req transition(::P,::S,::A)
    @req reward(::P,::S,::A,::S)
    @req stateindex(::P,::S)
    @req actionindex(::P, ::A)
    @req actions(::P, ::S)
    as = actions(mdp)
    ss = states(mdp)
    a = first(as)
    s = first(ss)
    dist = transition(mdp, s, a)
    D = typeof(dist)
    @req support(::D)
    @req pdf(::D,::S)
end

function qvalue!(m::Union{MDP,POMDP}, transition_A_S_S2, reward_S_A::AbstractMatrix{F}, value_S::AbstractVector{F}, out_qvals_S_A) where {F}
    @assert size(out_qvals_S_A) == (n_states(m), n_actions(m))
    for a in 1:n_actions(m)
        out_qvals_S_A[:, a] = view(reward_S_A, :, a) + discount(m) * transition_A_S_S2[a] * value_S
    end
end

function transition_matrix_a_s_sp(mdp::MDP)
    # Thanks to zach
    na = n_actions(mdp)
    ns = n_states(mdp)
    transmat_row_A = [Float64[] for _ in 1:n_actions(mdp)]
    transmat_col_A = [Float64[] for _ in 1:n_actions(mdp)]
    transmat_data_A = [Float64[] for _ in 1:n_actions(mdp)]

    for s in states(mdp)
        si = stateindex(mdp, s)
        for a in actions(mdp, s)
            ai = actionindex(mdp, a)
            if !isterminal(mdp, s, a) # if terminal, the transition probabilities are all just zero
                td = transition(mdp, s, a)
                for (sp, p) in weighted_iterator(td)
                    if p > 0.0
                        spi = stateindex(mdp, sp)
                        push!(transmat_row_A[ai], si)
                        push!(transmat_col_A[ai], spi)
                        push!(transmat_data_A[ai], p)
                    end
                end
            end
        end
    end
    transmats_A_S_S2 = [sparse(transmat_row_A[a], transmat_col_A[a], transmat_data_A[a], n_states(mdp), n_states(mdp)) for a in 1:n_actions(mdp)]
    # Note: not valid for terminal states
    # @assert all(all(sum(transmats_A_S_S2[a], dims=2) .≈ ones(n_states(mdp))) for a in 1:n_actions(mdp)) "Transition probabilities must sum to 1"
    return transmats_A_S_S2
end

function reward_s_a(mdp::MDP)
    reward_S_A = fill(-Inf, (n_states(mdp), n_actions(mdp))) # set reward for all actions to -Inf unless they are in actions(mdp, s)
    for s in states(mdp)
        for a in actions(mdp, s)
            td = transition(mdp, s, a)
            r = 0.0
            for (sp, p) in weighted_iterator(td)
                if p > 0.0
                    r += p*reward(mdp, s, a, sp)
                end
            end
            reward_S_A[stateindex(mdp, s), actionindex(mdp, a)] = r
        end
    end
    return reward_S_A
end

function solve(solver::SparseValueIterationSolver, mdp::Union{MDP, POMDP})
    nS = n_states(mdp)
    nA = n_actions(mdp)
    if isempty(solver.init_util)
        v_S = zeros(nS)
    else
        @assert length(solver.init_util) == nS "Input utility dimension mismatch"
        v_S = solver.init_util
    end

    transition_A_S_S2 = transition_matrix_a_s_sp(mdp)
    reward_S_A = reward_s_a(mdp)
    qvals_S_A = zeros(nS, nA)
    maxchanges_T = zeros(solver.max_iterations)

    total_time = 0.0
    for i in 1:solver.max_iterations
        iter_time = @elapsed begin
            qvalue!(mdp, transition_A_S_S2, reward_S_A, v_S, qvals_S_A)
            new_v_S = dropdims(maximum(qvals_S_A, dims=2), dims=2)
            @assert size(v_S) == size(new_v_S)
            maxchanges_T[i] = maximum(abs.(new_v_S .- v_S))
            v_S = new_v_S
        end
        total_time += iter_time
        if solver.verbose
            @info "residual: $(maxchanges_T[i]), time: $(iter_time), total time: $(total_time) " i
        end
        maxchanges_T[i] < solver.belres ? break : nothing
    end
    qvalue!(mdp, transition_A_S_S2, reward_S_A, v_S, qvals_S_A)
    # Rounding to avoid floating point error noise
    policy_S = dropdims(getindex.(argmax(round.(qvals_S_A, digits=20), dims=2), 2), dims=2)
    if solver.include_Q
        policy = ValueIterationPolicy(mdp, qvals_S_A, v_S, policy_S)
    else
        policy = ValueIterationPolicy(mdp, utility=v_S, policy=policy_S, include_Q=false)
    end
    return policy
end


"""
Bridges `CP.Element` to MILP constraints by using a unary encoding of the
index in the array.
"""
struct Element2MILPBridge{T} <: MOIBC.AbstractBridge
    vars::Vector{MOI.VariableIndex}
    vars_bin::Vector{MOI.ConstraintIndex{MOI.VariableIndex, MOI.ZeroOne}}
    con_unary::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}
    con_choose_one::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}
    con_value::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}
end

function MOIBC.bridge_constraint(
    ::Type{Element2MILPBridge{T}},
    model,
    f::MOI.VectorOfVariables,
    s::CP.Element{T},
) where {T}
    return MOIBC.bridge_constraint(
        Element2MILPBridge{T},
        model,
        MOI.VectorAffineFunction{T}(f),
        s,
    )
end

function MOIBC.bridge_constraint(
    ::Type{Element2MILPBridge{T}},
    model,
    f::MOI.VectorAffineFunction{T},
    s::CP.Element{T},
) where {T}
    vars, vars_bin = MOI.add_constrained_variables(model, [MOI.ZeroOne() for _ in 1:length(s.values)])

    f_scalars = MOIU.scalarize(f)
    f_value = f_scalars[1]
    f_index = f_scalars[2]

    con_unary = MOI.add_constraint(
        model,
        sum(T(i) * vars[i] for i in 1:length(s.values)) - f_index,
        MOI.EqualTo(zero(T))
    )

    con_choose_one = MOI.add_constraint(
        model,
        sum(one(T) .* vars),
        MOI.EqualTo(one(T))
    )

    con_value = MOI.add_constraint(
        model,
        sum(s.values[i] * vars[i] for i in 1:length(s.values)) - f_value,
        MOI.EqualTo(zero(T))
    )

    return Element2MILPBridge(vars, vars_bin, con_unary, con_choose_one, con_value)
end

function MOI.supports_constraint(
    ::Type{Element2MILPBridge{T}},
    ::Union{Type{MOI.VectorOfVariables}, Type{MOI.VectorAffineFunction{T}}},
    ::Type{CP.Element{T}},
) where {T}
    return true
end

function MOIB.added_constrained_variable_types(::Type{Element2MILPBridge{T}}) where {T}
    return [(MOI.ZeroOne,)]
end

function MOIB.added_constraint_types(::Type{Element2MILPBridge{T}}) where {T}
    return [
        (MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}),
    ]
end

function MOI.get(b::Element2MILPBridge{T}, ::MOI.NumberOfVariables) where {T}
    return length(b.vars)
end

function MOI.get(
    b::Element2MILPBridge{T},
    ::MOI.NumberOfConstraints{
        MOI.VariableIndex, MOI.ZeroOne,
    },
) where {T}
    return length(b.vars_bin)
end

function MOI.get(
    ::Element2MILPBridge{T},
    ::MOI.NumberOfConstraints{
        MOI.ScalarAffineFunction{T}, MOI.EqualTo{T},
    },
) where {T}
    return 3
end

function MOI.get(
    b::Element2MILPBridge{T},
    ::MOI.ListOfVariableIndices,
) where {T}
    return copy(b.vars)
end

function MOI.get(
    b::Element2MILPBridge{T},
    ::MOI.ListOfConstraintIndices{
        MOI.VariableIndex, MOI.ZeroOne,
    },
) where {T}
    return copy(b.vars_bin)
end

function MOI.get(
    b::Element2MILPBridge{T},
    ::MOI.ListOfConstraintIndices{
        MOI.ScalarAffineFunction{T}, MOI.EqualTo{T},
    },
) where {T}
    return [b.con_unary, b.con_choose_one, b.con_value]
end


# Boys.jl contains different implementations and approximations to the Boys function
using Interpolations

function boys_array_asymp(mmax,x)
    boys_array = zeros(Float64,mmax)
    denom = 0.5/x # Only used for large x, so don't check for small x
    boys_array[1] = sqrt(0.5*pi*denom)
    for m in 2:mmax
        boys_array[m] = boys_array[m-1]*(2m-1)*denom
    end
    return boys_array
end

function boys_array_gamma(mmax,x,SMALL=1e-18)
    x = max(x,SMALL) # needs underflow protection because of inverse
    boys_array = zeros(Float64,mmax)
    oox = 1/x
    denom = sqrt(oox)
    boys_array[1] = 0.5*denom*gammainc(0.5,x)
    for m in 2:mmax
        denom *= oox
        # Could speed this up more by expressing gamma(m) in terms of gamma(m±1)
        boys_array[m] = 0.5*denom*gammainc(m-0.5,x)
    end
    return boys_array
end

boys_array_fmref(mmax,x) = [fm_ref(m-1,x) for m in 1:mmax]
boys_array_Fgamma(mmax,x) = [Fgamma(m-1,x) for m in 1:mmax]

"Boys Fgamma function, using the lower incomplete gamma function."
@inline function Fgamma(m,T,SMALL=1e-18,Tcrit=20.0)
    # Note, most programs use a much larger value for Tcrit (117)
    mhalf = m+0.5
    T = max(T,SMALL) # Evidently needs underflow protection
    if T>Tcrit
        retval = sqrt(pi/2)*factorial2(2m-1)/(2T)^mhalf
    else
        #retval = 0.5*T^-mhalf*gammainc_nr(mhalf,T)
        retval = fmspline(m,T)
    end
    return retval
end

function Fm(m,T,SMALL=1e-18)
    mhalf = m+0.5
    T = max(T,SMALL)
    return 0.5*T^-mhalf*gammainc_nr(mhalf,T)
end

"gammainc returns the lower incomplete gamma function"
@inline gammainc(a,x) = gamma(a)*gamma_inc(a,x)[1]

# This is the ref function from libint
function fm_ref(m,T,eps = 1e-10)
    denom = (m + 0.5)
    term = exp(-T) /2denom
    old_term = 0.0
    sum = term
    while term > sum*eps || old_term < term
        denom += 1
        old_term = term
        term = old_term * T / denom
        sum += term
    end
    return sum
end

# These were taken from Numerical Recipes
@inline function gammainc_nr(a::Float64,x::Float64)
    if abs(a-1) < 1e-3
        println("Warning: gammainc_series is known to have problems for a ~ 1")
    end
    if x < (a+1.0)
        #Use the series representation
        gam,gln = gser(a,x)
    else
        #Use continued fractions
        gamc,gln = gcf(a,x)
        gam = 1-gamc
    end
    return exp(gln)*gam
end

@inline function gser(a,x,ITMAX=100,EPS=3e-9)
    # Series representation of Gamma. NumRec sect 6.1.
    gln=loggamma(a)
    if x == 0
        return 0,gln
    end
    ap = a
    delt = s = 1/a
    for i in 1:ITMAX
        ap += 1
        delt *= (x/ap)
        s += delt
        if abs(delt) < abs(s)*EPS
            break
        end
    end
    return s*exp(-x+a*log(x)-gln),gln
end

@inline function gcf(a::Float64,x::Float64,ITMAX::Int64=200,EPS::Float64=3e-9,FPMIN::Float64=1e-30)
    #Continued fraction representation of Gamma. NumRec sect 6.1"
    gln=loggamma(a)
    b=x+1.0-a
    c=1.0/FPMIN
    d=1.0/b
    h=d
    for i in 1:ITMAX
        an=-i*(i-a)
        b=b+2.
        d=an*d+b
        if abs(d) < FPMIN
            d=FPMIN
        end
        c=b+an/c
        if abs(c) < FPMIN
            c=FPMIN
        end
        d=1.0/d
        delt=d*c
        h=h*delt
        if abs(delt-1.) < EPS
            break
        end
    end
    gammcf = exp(-x+a*log(x)-gln)*h
    return gammcf,gln
end

function make_interpolator(mmax=10,Tmax=20.0)
    Tgrid = 0:0.005:Tmax
    mgrid = 0:mmax
    fmvalues = [Fm(m,T) for m in mgrid, T in Tgrid]
    itp = interpolate(fmvalues, BSpline(Cubic(Line(OnGrid()))))
    sitp = scale(itp,mgrid,Tgrid)
    return sitp
end
const fmspline = make_interpolator()


using DiffEqBase,
    DiffEqOperators, ForwardDiff, LinearAlgebra, SparseDiffTools, Test, LinearSolve
const A = rand(300, 300)
f(du, u) = mul!(du, A, u)
f(u) = A * u
x = rand(300)
v = rand(300)
du = similar(x)

cache1 = ForwardDiff.Dual{typeof(ForwardDiff.Tag(SparseDiffTools.DeivVecTag(),eltype(x))),eltype(x),1}.(x, ForwardDiff.Partials.(tuple.(reshape(v, size(x)))))
cache2 = ForwardDiff.Dual{typeof(ForwardDiff.Tag(SparseDiffTools.DeivVecTag(),eltype(x))),eltype(x),1}.(x, ForwardDiff.Partials.(tuple.(reshape(v, size(x)))))
@test num_jacvec!(du, f, x, v) ≈ ForwardDiff.jacobian(f, similar(x), x) * v rtol =
    1e-6
@test num_jacvec!(du, f, x, v, similar(v), similar(v)) ≈
      ForwardDiff.jacobian(f, similar(x), x) * v rtol = 1e-6
@test num_jacvec(f, x, v) ≈ ForwardDiff.jacobian(f, similar(x), x) * v rtol =
    1e-6

@test auto_jacvec!(du, f, x, v) ≈ ForwardDiff.jacobian(f, similar(x), x) * v
@test auto_jacvec!(du, f, x, v, cache1, cache2) ≈
      ForwardDiff.jacobian(f, similar(x), x) * v
@test auto_jacvec(f, x, v) ≈ ForwardDiff.jacobian(f, similar(x), x) * v

f(du, u, p, t) = mul!(du, A, u)
f(u, p, t) = A * u
L = JacVecOperator(f, x)
@test L * x ≈ auto_jacvec(f, x, x)
@test L * v ≈ auto_jacvec(f, x, v)
@test mul!(du, L, v) ≈ auto_jacvec(f, x, v)
DiffEqBase.update_coefficients!(L, v, nothing, nothing)
@test mul!(du, L, v) ≈ auto_jacvec(f, v, v)
# reshape
xx = [x x]
vv = [v v]
duu = [du du]
L = JacVecOperator(f, xx)
@test L * vec(xx) ≈ mul!(vec(duu), L, vec(xx))
@test vec(duu) ≈ vec(A * xx) rtol = 1e-6

L = JacVecOperator(f, x, autodiff = false)
DiffEqBase.update_coefficients!(L, x, nothing, nothing)
@test L * x ≈ num_jacvec(f, x, x)
@test L * v ≈ num_jacvec(f, x, v)
@test mul!(du, L, v) ≈ num_jacvec(f, x, v) rtol = 1e-6
DiffEqBase.update_coefficients!(L, v, nothing, nothing)
@test mul!(du, L, v) ≈ num_jacvec(f, v, v) rtol = 1e-6
L = JacVecOperator(f, xx, autodiff = false)
DiffEqBase.update_coefficients!(L, vv, nothing, nothing)
@test L * vec(xx) ≈ mul!(vec(duu), L, vec(xx)) rtol = 1e-6
@test vec(duu) ≈ vec(A * xx)

L2 = JacVecOperator{Float64}(f)
DiffEqBase.update_coefficients!(L2, x, nothing, nothing)
@test L2 * x ≈ auto_jacvec(f, x, x)
@test L2 * v ≈ auto_jacvec(f, v, v)
@test mul!(du, L2, x) ≈ auto_jacvec(f, x, x)
DiffEqBase.update_coefficients!(L2, xx, nothing, nothing)
@test L2 * vec(xx) ≈ auto_jacvec(f, xx, vec(xx))
@test L2 * vec(vv) ≈ auto_jacvec(f, vv, vec(vv))
@test mul!(vec(duu), L2, vec(xx)) ≈ auto_jacvec(f, xx, vec(xx))

L2 = JacVecOperator{Float64}(f, autodiff = false)
DiffEqBase.update_coefficients!(L2, x, nothing, nothing)
@test L2 * x ≈ num_jacvec(f, x, x)
@test L2 * v ≈ num_jacvec(f, v, v) rtol = 1e-6
DiffEqBase.update_coefficients!(L2, xx, nothing, nothing)
@test L2 * vec(xx) ≈ num_jacvec(f, xx, vec(xx))
@test L2 * vec(vv) ≈ num_jacvec(f, vv, vec(vv)) rtol = 1e-6

using OrdinaryDiffEq
function lorenz(du, u, p, t)
    du[1] = 10.0(u[2] - u[1])
    du[2] = u[1] * (28.0 - u[3]) - u[2]
    du[3] = u[1] * u[2] - (8 / 3) * u[3]
end
u0 = [1.0; 0.0; 0.0]
tspan = (0.0, 100.0)
ff1 = ODEFunction(lorenz, jac_prototype = JacVecOperator{Float64}(lorenz, u0))
ff2 = ODEFunction(
    lorenz,
    jac_prototype = JacVecOperator{Float64}(lorenz, u0, autodiff = false),
)

for ff in [ff1, ff2]
    prob = ODEProblem(ff, u0, tspan)
    @test solve(prob, TRBDF2()).retcode == :Success
    @test solve(prob, TRBDF2(linsolve = KrylovJL_GMRES())).retcode == :Success
    @test solve(prob, Exprb32()).retcode == :Success
    @test sol = solve(prob, Rosenbrock23()).retcode == :Success
    @test sol = solve(prob, Rosenbrock23(linsolve = KrylovJL_GMRES())).retcode == :Success
end


###
# Standard QR Pivot Optimizer
###

mutable struct QRPivot
	Ψ::AbstractArray #Basis matrix from SVD, RPCA, etc.
	pivots::AbstractArray #Ranked list of sensor locations
end

function QRPivot(Ψ)
	n,m = size(Ψ)
	pivots = zeros((max(n,m),1))
	return QRPivot(Ψ,pivots)
end

function QRPivot(Ψ,n_sensors::Int)
	n,m = size(Ψ)
	pivots = zeros((n_sensors,1))
	return QRPivot(Ψ,pivots)
end

function QRPivot(Ψ,pivots::AbstractArray)
	return QRPivot(Ψ,pivots)
end

function fit(qr_pivot::QRPivot;optimizer_kwargs...)
	"""
	Fits the QRPivot sensor placement.
	qrpivot::QRPivot
		QRPivot object.
	optimizer_kwargs: dictionary
		Optional settings for optimizer
	"""
	qr_pivot.pivots = sensor_placement(qr_pivot.Ψ)
end

function sensor_placement(Ψ)
	# --> Compute the QRPivot w/ column pivoting decomposition of Ψ.
	_, _, p = qr(conj(Ψ), Val(true))
	return p[1:size(Ψ, 2)]
end


using PkgBenchmark
using BenchmarkHelper

import Whirl: Systems, TimeMarching, Fields
using Fields
using Systems
using TimeMarching
#using SaddlePointSystems

#include(joinpath(Pkg.dir("Whirl"), "src/Whirl.jl"))
#import Whirl
#import Whirl:@get, Soln, IntFactSystems, TimeMarching, Fields

#using IntFactSystems
#include(joinpath(Pkg.dir("Whirl"), "src/systems/navier_stokes.jl"))
#import TimeMarching: ifrk!, RK31

@benchgroup "Navier Stokes" begin
  woseen(x::Tuple{Real,Real},t;Re=1.0,x0::Tuple{Real,Real}=(0,0),t0=1) =
                      exp(-((x[1]-x0[1])^2+(x[2]-x0[2])^2)/(4(t+t0)/Re))/(1+t/t0)

  nx = 260; ny = 130
  Re = 200
  U = 1.0
  U∞ = (U,0.0)
  Lx = 2.0
  Δx = Lx/(ny-1)
  Δt = min(0.5*Δx,0.5*Δx^2*Re)
  w₀ = Nodes(Dual,(nx,ny))

  xg,yg = coordinates(w₀,dx=Δx)
  x0 = (1,1)
  t0 = 1
  wexact(t) = [woseen((x,y),t;Re=Re,x0=x0.+U∞.*t,t0=t0) for x in xg, y in yg]

  sys = Systems.NavierStokes((nx,ny),Re,Δx,Δt,U∞ = U∞)

  ifrk = IFRK(w₀,sys.Δt,
            (t,w) -> plan_intfact(t,w,sys),
            (w,t) -> r₁(w,t,sys) ,rk=TimeMarching.RK31)

  t = 0.0
  w₀ .= wexact(t)
  w = deepcopy(w₀)
  @bench "Single-step NS solver" $ifrk($t,$w)

#=
    wexact(x, y, t) = exp(-((x-t)^2+y^2)/(0.2^2+4t/200))/(1+4t/(200*0.2^2))

    Re = 200
    Δx = 0.02
    x = linspace(-1-0.5Δx, 3+0.5Δx, 202)
    y = linspace(-1-0.5Δx, 1+0.5Δx, 102)
    Δt = min(0.5*Δx,0.5*Δx^2*Re)
    dims = length.((x,y))

    ns = NavierStokes(dims, Re, Δx, Δt, (1.0, 0.0))
    s = Soln(0.0, Fields.DualNodes(dims));
    s.u .= [wexact(xᵢ, yᵢ, 0)*Δx for xᵢ in x, yᵢ in y]
    s₊ = Soln(0.0, Fields.DualNodes(dims));

    @bench "Single-Step NS Solver" ifrk!($s₊, $s, $Δt, $RK31, $ns)
    =#
end

#=
@benchgroup "Fields"  begin
    celldims = (500, 500)

    nodes = DualNodes(celldims)
    nodes .= rand(Float64,size(nodes))

    edges = Edges(Primal, nodes)
    edges.u .= reshape(1:length(edges.u), size(edges.u))
    edges.v .= reshape(1:length(edges.v), size(edges.v))

    dual = Edges(Dual, nodes)

    L = Laplacian(celldims, with_inverse = true, fftw_flags = FFTW.PATIENT)

    @bench "Shift Edges to Edges" Fields.shift!($dual, $edges)
    @bench "Shift Nodes to Edges" Fields.shift!($dual, $nodes)

    @bench "Node to Edge Curl" curl!($edges, $nodes)
    @bench "Edge to Node Curl" curl!($nodes, $edges)

    out = DualNodes(celldims)
    @bench "Laplacian" A_mul_B!($out, $L, $nodes)
    @bench "Inverse Laplacian" A_ldiv_B!($out, $L, $nodes)
end
=#


module TestEcology
using Test

# Looking at relations to historical measures, updated with similarity
# and partitioning
using Diversity
using Diversity.Ecology
using Diversity.ShortNames

numspecies = 100;
numcommunities = 8;
communities = rand(numspecies, numcommunities);
communities /= sum(communities);
eco = Metacommunity(communities)
weights = rand(numspecies);
weights /= sum(weights);
Z1 = ones(typeof(weights[1]), (length(weights), length(weights)));

@testset "Standard ecological diversities" begin
    @test richness(communities)[!,:diversity] ≈ subdiv(ᾱ(eco), 0)[!,:diversity]
    @test richness(Metacommunity(communities))[!,:diversity] ==
        richness(communities)[!,:diversity]
    @test_throws ErrorException richness(Metacommunity(communities,
        GeneralTypes(rand(numspecies,numspecies))))

    @test shannon(communities)[!,:diversity] ≈ log.(subdiv(ᾱ(eco), 1)[!,:diversity])
    @test shannon(Metacommunity(communities))[!,:diversity] ==
        shannon(communities)[!,:diversity]
    @test_throws ErrorException shannon(Metacommunity(communities,
        GeneralTypes(rand(numspecies,numspecies))))

    @test simpson(communities)[!,:diversity] ≈ subdiv(ᾱ(eco), 2)[!,:diversity] .^ -1
    @test simpson(Metacommunity(communities))[!,:diversity] ==
        simpson(communities)[!,:diversity]
    @test_throws ErrorException simpson(Metacommunity(communities,
        GeneralTypes(rand(numspecies,numspecies))))

    @test jaccard([1 0 0; 0 1 1]'/3)[!,:diversity] .+ 1.0 ≈ [1.0]
    @test jaccard(Metacommunity([1 0 1; 0 1 1]'/4))[!,:diversity] ==
        jaccard([1 0 1; 0 1 1]'/4)[!,:diversity]
    @test_throws ErrorException jaccard(Metacommunity([1 0 1; 0 1 1]'/4,
                                                      GeneralTypes(rand(3, 3))))
    @test jaccard([1 0 1; 0 1 1]'/4)[!,:diversity] ≈ [1.0 / 3.0]
    @test_throws ErrorException jaccard([1 1 0; 0 1 1; 1 1 1]/7)
    @test_throws ErrorException jaccard(Metacommunity([1 1 0; 0 1 1; 1 1 1]/7))
end

@testset "Generalised ecological diversities" begin
    @test generalisedrichness(metacommunityDiversity,
                              communities, Z1)[!,:diversity] ≈ [1]
    @test generalisedrichness(subcommunityDiversity,
                              communities)[!,:diversity] ≈
          richness(communities)[!,:diversity]

    @test_throws ErrorException generalisedrichness(individualDiversity,
                                                    communities, Z1)

    @test generalisedshannon(metacommunityDiversity,
                             communities, Z1)[!,:diversity] .+ 1.0 ≈ [1.0]
    @test generalisedshannon(subcommunityDiversity,
                             communities)[!,:diversity] ≈
          shannon(communities)[!,:diversity]
    @test_throws ErrorException generalisedshannon(individualDiversity,
                                                   communities, Z1)

    @test generalisedsimpson(metacommunityDiversity,
                             communities, Z1)[!,:diversity] ≈ [1.0]
    @test generalisedsimpson(subcommunityDiversity,
                             communities)[!,:diversity] ≈
          simpson(communities)[!,:diversity]
    @test_throws ErrorException generalisedsimpson(individualDiversity,
                                                   communities, Z1)

    @test generalisedjaccard([1 0 1; 0 1 1]'/4, [0, Inf])[!,:diversity] ≈ [1.0/3.0, 1.0]
    @test generalisedjaccard([1 1 1; 1 1 1]'/6, [0, 1])[!,:diversity] ≈ [1.0, 1.0]
end

end


using Statistics

@testset "std" begin
    @test testf(std, rand(10))
    @test testf(std, rand(10,1,2))
    @test testf(std, rand(10,1,2), corrected=true)
    @test testf(std, rand(10,1,2), dims=1)
end
@testset "var" begin
    @test testf(var, rand(10))
    @test testf(var, rand(10,1,2))
    @test testf(var, rand(10,1,2), corrected=true)
    @test testf(var, rand(10,1,2), dims=1)
end


# A transformation for testing the local affine mapping
struct SquareMe <: Transformation; end
(::SquareMe)(x) = x.^2
CoordinateTransformations.transform_deriv(::SquareMe, x0) = Matrix(Diagonal(2*x0))

struct Ray37
    origin::Vector
    direction::Vector
end
function (m::LinearMap)(r::Ray37)
    Ray37(m(r.origin),
          m(r.direction))
end
function (t::Translation)(r::Ray37)
    Ray37(t(r.origin),
          r.direction)
end

@testset "Common Transformations" begin
    @testset "AffineMap" begin
        @testset "Simple" begin
            M = [1 2; 3 4]
            v = [-1, 1]
            x = [1,0]
            y = [0.0,1.0]
            A = AffineMap(M,v)
            @test A(x) == M*x + v
        end

        @testset "composition " begin
            M1 = [1 2; 3 4]
            v1 = [-1, 1]
            A1 = AffineMap(M1,v1)
            M2 = [0 1; 1 0]
            v2 = [-2, 0]
            A2 = AffineMap(M2,v2)
            x = [1,0]
            y = [0,1]
            @test A1(x) == M1*x + v1
            @test A1(y) == M1*y + v1
            @test (A2∘A1)(x) == M2*(M1*x + v1) + v2
            @test (A2∘A1)(y) == M2*(M1*y + v1) + v2
        end

        @testset "inverse" begin
            M = [1.0 2.0; 3.0 4.0]
            v = [-1.0, 1.0]
            x = [1,0]
            y = [0.0,1.0]
            A = AffineMap(M,v)
            @test inv(A)(A(x)) ≈ x
            @test inv(A)(A(y)) ≈ y
        end

        @testset "Affine approximation" begin
            S = SquareMe()
            x0 = [1,2,3]
            dx = 0.1*[1,-1,1]
            A = AffineMap(S, x0)
            @test isapprox(S(x0 + dx), A(x0 + dx), atol=maximum(2*dx.^2))
        end
    end

    @testset "LinearMap" begin
        M = [1 2; 3 4]
        x = [1,0]
        y = [0,1]
        L = LinearMap(M)
        @test L(x) == M*x
        @test inv(L)(x) == inv(M)*x
        @test inv(L)(y) == inv(M)*y
        @test inv(L)(L(x)) ≈ x
        @test inv(L)(L(y)) ≈ y
        @test (L∘L)(x) == (M*M)*x
    end

    @testset "Translation" begin
        x = SVector(1.0, 2.0)
        trans = Translation(2.0, -1.0)

        # Inverse
        @test inv(trans) == Translation(-2.0, 1.0)

        # Composition
        @test trans ∘ trans == Translation(4.0, -2.0)

        # Transform
        @test trans(x) === SVector(3.0, 1.0)
        @test trans(collect(x)) == [3.0, 1.0]

        # Transform derivative
        m1 = transform_deriv(trans, x)
        m2 = [m1[i,j] for i=1:2, j=1:2] # m1 might be a UniformScaling
        @test m2 == Matrix(I, 2, 2)  # in v0.7 and above, this can just be `== I`

        # Transform parameter derivative
        m1 = transform_deriv_params(trans, x)
        m2 = [m1[i,j] for i=1:2, j=1:2] # m1 might be a UniformScaling
        @test m2 == Matrix(I, 2, 2)  # In v0.7 and above, this can just be `== I`
    end

    @testset "Recenter" begin
        M = [1 2; 3 4]
        for origin in ([5,-3], @SVector([5,-3]))
            c = recenter(M, origin)
            @test c(origin) == origin
            @test c(zero(origin)) == [6,-6]
        end
    end

    @testset "application of AffineMap in terms of LinearMap and Translation" begin
        origin = randn(3)
        direction = randn(3)
        linear = randn(3,3)
        trans = randn(3)
        r = Ray37(origin, direction)
        expected = Ray37(linear*origin+trans, linear*direction)
        result   = AffineMap(linear, trans)(r)
        @test expected.origin ≈ result.origin
        @test expected.direction ≈ result.direction
    end
end


#=
Copyright (c) 2018-2022 Chris Coey, Lea Kapelevich, and contributors

This Julia package Hypatia.jl is released under the MIT license; see LICENSE
file in the root directory or at https://github.com/chriscoey/Hypatia.jl

D-optimal experiment design maximizes the determinant of the information matrix
adapted from Boyd and Vandenberghe, "Convex Optimization", section 7.5
maximize    F(V × Diagonal(x) × V')
subject to  sum(np) == n
            0 .<= np .<= n_max
where np is a vector of variables representing the number of experiment to run,
and the columns of V are the vectors representing each experiment

if logdet_obj or rootdet_obj is true, F is the logdet or rootdet function
if geomean_obj is true, we use a formulation from
https://picos-api.gitlab.io/picos/optdes.html that finds an equivalent minimizer
=#

struct DOptimalDesignJuMP{T <: Real} <: ExampleInstanceJuMP{T}
    q::Int
    p::Int
    n::Int
    n_max::Int
    logdet_obj::Bool # use formulation with logdet objective
    rootdet_obj::Bool # use formulation with rootdet objective
    geomean_obj::Bool # use formulation with geomean objective
end

function build(inst::DOptimalDesignJuMP{T}) where {T <: Float64}
    (q, p, n, n_max) = (inst.q, inst.p, inst.n, inst.n_max)
    @assert (p > q) && (n > q) && (n_max <= n)
    @assert +(inst.logdet_obj, inst.geomean_obj, inst.rootdet_obj) == 1
    V = randn(q, p)

    model = JuMP.Model()
    JuMP.@variable(model, np[1:p])
    JuMP.@constraint(model, sum(np) == n)
    mid = n_max / 2
    JuMP.@constraint(model, vcat(mid, np .- mid) in MOI.NormInfinityCone(p + 1))
    JuMP.@variable(model, hypo)
    JuMP.@objective(model, Max, hypo)

    if inst.logdet_obj || inst.rootdet_obj
        # information matrix lower triangle
        v1 = [JuMP.@expression(model,
            sum(V[i, k] * np[k] * V[j, k] for k in 1:p)) for i in 1:q for j in 1:i]
        if inst.logdet_obj
            JuMP.@constraint(model, vcat(hypo, 1, v1) in MOI.LogDetConeTriangle(q))
        else
            JuMP.@constraint(model, vcat(hypo, v1) in MOI.RootDetConeTriangle(q))
        end
    else
        # hypogeomean + epinormeucl formulation
        JuMP.@variable(model, L[i in 1:q, j in 1:i])
        JuMP.@variable(model, W[1:p, 1:q])
        VW = V * W
        JuMP.@constraints(model, begin
            [i in 1:q, j in 1:i], VW[i, j] == L[i, j]
            [i in 1:q, j in (i + 1):q], VW[i, j] == 0
            vcat(hypo, [L[i, i] for i in 1:q]) in MOI.GeometricMeanCone(1 + q)
            [i in 1:p], vcat(sqrt(q) * np[i], W[i, :]) in JuMP.SecondOrderCone()
        end)
    end

    return model
end


"""
    Code for a GaussianMixture mask shape for use with CCF
Author: Eric Ford
Created: September 2020
"""

import SpecialFunctions: erf
using StaticArrays

"""   GaussianMixtureCCFMask
A mixture of truncated GaussianMixtures mask with three parameters per mixture component: weight,  standard deviation and offset (in m/s)
and one parameter for the velocity width at which the whole thing is truncated.
Mask weights are stored separately in a line list.
TODO: Test before using
"""
struct GaussianMixtureMixtureCCFMask{NMix::Integer} <: AbstractCCFMaskShape
    weight::SVector{NMix,Float64}
    σ_sqrt2::SVector{NMix,Float64}
    v_offset::SVector{NMix,Float64}
    truncation_Δv::Float64
    normalization::SVector{NMix,Float64}
    cdf_normalization::SVector{NMix,Float64}

    """ GaussianMixtureCCFMask( σ ; half_truncation_width_in_σ=2 ) """
    function GaussianMixtureCCFMask(weight::AbstractVector{Float64}, σ::AbstractVector{Float64}, truncation_Δv::Real; v_offset::AbstractVector{Float64}=zeros(length(weight)) )
        n = legnth(weight)
        @assert 1 <= n <= 10   # Arbitrary upper limit
        @assert length(σ) == n
        @assert length(v_offset) == n
        @assert all(0 .<= weight .<= one(eltype(weight)))   #
        @assert all(0 .< σ .<= 300000)   # 300km/s is arbitrary choice for an upper limit
        @assert all(-1000 .<= v_offset .<= 1000)   # 1km/s is arbitrary choice for an upper limit
        @assert 0 < truncation_Δv <= 300000

        pdf_norm = zeros(n)
        cdf_norm = zeros(n)
        for i in 1:n
            integral = ( erf((truncation_Δv-v_offset[i])/(sqrt(2.0)*σ[i])) - erf((-truncation_Δv-v_offset[i])/(sqrt(2.0*σ[i]))) )
            pdf_norm[i] = weight[i]/(sqrt(2π)*σ[i]*integral)
            cdf_norm[i] = weight[i]/integral
        end
        new(weight, σ.*sqrt(2.0),v_offset, truncation_Δv,pdf_norm,cdf_norm)
    end

end

""" `GaussianMixtureCCFMask( inst ; scale_factor )`
For now just makes a single Gaussian, so for testing purposes only.
"""
function GaussianMixtureCCFMask(inst::AbstractInstrument; σ_scale_factor::Real = 1, truncation_Δv::Real = default_gaussian_mixture_ccf_truncation_Δv )
    σ = σ_scale_factor * default_ccf_mask_v_width(inst)
    GaussianMixtureCCFMask([1.0], [σ],truncation_Δv,v_offset=[0.])
end

λ_min(m::GaussianMixtureCCFMask,λ::Real) = λ/calc_doppler_factor(m.truncation_Δv)
λ_max(m::GaussianMixtureCCFMask,λ::Real) = λ*calc_doppler_factor(m.truncation_Δv)

function cdf(m::GaussianMixtureCCFMask, v_lo::Real,v_hi::Real, i::Integer)
    m.cdf_normalization[i]*( erf((v_hi-v_offset[i])/σ_sqrt2[i]) - erf((-v_lo-v_offset[i])/σ_sqrt2[i]) )
end

function pdf(m::GaussianMixtureCCFMask, Δv::Real, i::Integer)
    m.normalization[i]*exp(-(Δv-m.v_offset[i]/m.σ_sqrt2[i])^2)
end

function integrate(m::GaussianMixtureCCFMask, v_lo::Real,v_hi::Real)
    @assert -m.truncation_Δv <= v_lo < v_hi <= m.truncation_Δv
    #quadgk(m, v_lo, v_hi)[1]
    mapreduce(i->cdf(m,v_lo,v_hi,i),+,1:length(m.weights))
end

""" Functor for returning PSF for Δv <= half_width.  """
function (m::GaussianMixtureCCFMask)(Δv::Real)
    if abs2(Δv) > abs2(m.truncation_Δv)
        return zero(Δv)
    else
        return mapreduce(i->pdf(m,Δv,i),+,1:length(m.weights))
    end
end

function mask_with_increased_fwhm(m::GaussianMixtureCCFMask, Δfwhm::Real )
    fwhm_orig = 2*sqrt(log(2)) .* m.σ_sqrt2
    fwhm_new = sqrt.(fwhm_orig.^2 .+ Δfwhm^2)
    σ_new = 0.5 .* fwhm_new./sqrt(2*log(2))
    return GaussianMixtureCCFMask(m.weight, σ_new, m.truncation_Δv, v_offset=m.v_offset )
end


@testset ExtendedTestSet "mask_elements" begin
    @testset ExtendedTestSet "mask_elements" begin
        img = [
            1 10 1 1
            -2 -2 -2 -2
            1 1 1 1
            1 1 -2 1
            1 1 1 1
            -2 -2 -2 -2
        ]
        answer = Bool.([
            1 1 1 1
            0 0 0 0
            1 1 1 1
            1 1 1 1
            1 1 1 1
            0 0 0 0
        ])
        @test mask_elements(img, 0, 2) == answer
    end

    @testset ExtendedTestSet "mask_elements" begin
        img = [
            1 -2 -2 -2
            -2 -2 -2 -2
            1 -2 -2 -2
            -2 -2 -2 -2
            1 -2 -2 -2
            -2 -2 -2 -2
        ]
        answer = Bool.([
            1 0 0 0
            0 0 0 0
            1 0 0 0
            0 0 0 0
            1 0 0 0
            0 0 0 0
        ])
        @test mask_elements(img, 0, 2) == answer
    end
end


# Chapter 15. Structs and Objects

# export Point

# Composite Types
struct Point
    x
    y
end

# Exercise 17-2
function Point(x::Float64=0, y::Float64=0)
    Point(x,y)
end

# Mutable Structs
# https://benlauwens.github.io/ThinkJulia.jl/latest/book.html#_mutable_structs
mutable struct MPoint
    x
    y
end

# Rectangles
# https://benlauwens.github.io/ThinkJulia.jl/latest/book.html#_rectangles
"""
Represents a rectangle.

width: Width
height:
corner: Lower left corner
"""
struct Rectangle
    width
    height
    corner::MPoint
end

# Instances as Arguments
# https://benlauwens.github.io/ThinkJulia.jl/latest/book.html#_instances_as_arguments
function printpoint(p)
    println("($(p.x), $(p.y))")
end

# Exercise 15-1
function distancebetweenpoints(p, q)
    x2 = p.x^2 + q.x^2
    y2 = p.y^2 + q.y^2
    ret = sqrt(x2 + y2)

    return ret
end

function movepoint!(p, dx, dy)
    p.x += dx
    p.y += dy

    nothing
end

function moverectangle!(rect, dx, dy)
    movepoint!(rect.corner, dx, dy)
end

# Instances as Return Values
function findcenter(rect)
    Point(
    rect.corner.x + rect.width/2.,
    rect.corner.y + rect.height/2.)
end


# constructors.jl

const tupleTMs = (:TaylorModel1, :RTaylorModel1)
const NumberNotSeries = TaylorSeries.NumberNotSeries

#=
Structs `TaylorModel1` and `RTaylorModel1` are essentially identical, except
the way the remainder is handled; the remainder for `TaylorModel1`
must contain 0.
=#
for TM in tupleTMs
    @eval begin
        struct $(TM){T,S} <: AbstractSeries{T}
            pol  :: Taylor1{T}    # polynomial approx (of order `ord`)
            rem  :: Interval{S}   # remainder
            x0   :: Interval{S}   # expansion point
            dom  :: Interval{S}   # interval of interest

            # Inner constructor
            function $(TM){T,S}(pol::Taylor1{T}, rem::Interval{S},
                    x0::Interval{S}, dom::Interval{S}) where {T,S}
                if $(TM) == TaylorModel1
                    @assert zero(S) ∈ rem && x0 ⊆ dom
                else
                    @assert x0 ⊆ dom
                end
                return new{T,S}(pol, rem, x0, dom)
            end
        end

        # Outer constructors
        $(TM)(pol::Taylor1{T}, rem::Interval{S},
            x0, dom::Interval{S}) where {T,S} = $(TM){T,S}(pol, rem, interval(x0), dom)

        # Constructor just chainging the remainder
        $(TM)(u::$(TM){T,S}, Δ::Interval{S}) where {T,S} = $(TM){T,S}(u.pol, Δ, u.x0, u.dom)

        # Short-cut for independent variable
        $(TM)(ord::Integer, x0, dom::Interval{T}) where {T} =
            $(TM)(x0 + Taylor1(eltype(x0), ord), zero(dom), interval(x0), dom)

        # Short-cut for a constructor expanding around midpoint by default
        $(TM)(ord::Integer, dom::Interval{T}) where {T} =
            $(TM)(ord, Interval(mid(dom)), dom)

        # Short-cut for a constant TM
        $(TM)(a::T, ord::Integer, x0::Interval{S}, dom::Interval{S}) where
            {T,S} = $(TM)(Taylor1([a], ord), zero(dom), x0, dom)

        # Functions to retrieve the order and remainder
        @inline get_order(tm::$TM) = get_order(tm.pol)
        @inline remainder(tm::$TM) = tm.rem
        @inline polynomial(tm::$TM) = tm.pol
        @inline domain(tm::$TM) = tm.dom
        @inline expansion_point(tm::$TM) = tm.x0
    end
end

@doc doc"""
    TaylorModel1{T,S}

Absolute Taylor model in 1 variable, providing a rigurous polynomial approximation
given by a Taylor polynomial `pol` (around `x0`) and an absolute remainder
`rem` for a function `f(x)` in one variable, valid in the interval `dom`.
This corresponds to definition 2.1.3 of Mioara Joldes' thesis.

Fields:
- `pol`: polynomial approximation, represented as `TaylorSeries.Taylor1`
- `rem`: the interval bound
- `x0` : expansion point
- `dom`: domain, interval over which the Taylor model is defined / valid

The approximation ``f(x) = p(x) + \Delta`` is satisfied for all
``x\in \mathcal{D}`` (``0\in \Delta``); `n` is the order (degree)
of the polynomial ``p(x)=\sum_{i=0}^n p_i (x - x_0)^i``.

""" TaylorModel1

@doc doc"""
    RTaylorModel1{T,S}

Relative Taylor model in 1 variable, providing a rigurous polynomial approximation
given by a Taylor polynomial `pol` (around `x0`) and a relative remainder
`rem` for a function `f(x)` in one variable, valid in the interval `dom`.
This corresponds to definition 2.3.2 of Mioara Joldes' thesis.

Fields:
- `pol`: polynomial approximation, represented as `TaylorSeries.Taylor1`
- `rem`: the interval bound
- `x0` : expansion point
- `dom`: domain, interval over which the Taylor model is defined / valid

The approximation ``f(x) = p(x) + \delta (x - x_0)^{n+1}`` is satisfied for all
``x\in \mathcal{D}``; `n` is the order (degree) of the polynomial
``p(x)=\sum_{i=0}^n p_i (x - x_0)^i``.

""" RTaylorModel1

# TaylorModelN's struct
"""
    TaylorModelN{N,T,S}

Taylor Models with absolute remainder for `N` independent variables.

"""
struct TaylorModelN{N,T,S} <: AbstractSeries{T}
    pol  :: TaylorN{T}        # polynomial approx (of order `ord`)
    rem  :: Interval{S}       # remainder
    x0   :: IntervalBox{N,S}  # expansion point
    dom  :: IntervalBox{N,S}  # interval of interest

    # Inner constructor
    function TaylorModelN{N,T,S}(pol::TaylorN{T}, rem::Interval{S},
            x0::IntervalBox{N,S}, dom::IntervalBox{N,S}) where {N,T<:NumberNotSeries,S<:Real}

        @assert N == get_numvars()
        @assert zero(S) ∈ rem && x0 ⊆ dom

        return new{N,T,S}(pol, rem, x0, dom)
    end
end

# Outer constructors
TaylorModelN(pol::TaylorN{T}, rem::Interval{S}, x0::IntervalBox{N,S},
    dom::IntervalBox{N,S}) where {N,T,S} = TaylorModelN{N,T,S}(pol, rem, x0, dom)

# Constructor for just changing the remainder
TaylorModelN(u::TaylorModelN{N,T,S}, Δ::Interval{S}) where {N,T,S} =
    TaylorModelN{N,T,S}(u.pol, Δ, u.x0, u.dom)

# Short-cut for independent variable
TaylorModelN(nv::Integer, ord::Integer, x0::IntervalBox{N,T}, dom::IntervalBox{N,T}) where {N,T} =
    TaylorModelN(x0[nv] + TaylorN(Interval{T}, nv, order=ord), zero(dom[1]), x0, dom)

# Short-cut for a constant
TaylorModelN(a::Interval{T}, ord::Integer, x0::IntervalBox{N,T}, dom::IntervalBox{N,T}) where {N,T} =
    TaylorModelN(TaylorN(a, ord), zero(dom[1]), x0, dom)
TaylorModelN(a::T, ord::Integer, x0::IntervalBox{N,T}, dom::IntervalBox{N,T}) where {N,T} =
    TaylorModelN(TaylorN(a, ord), zero(dom[1]), x0, dom)

# Functions to retrieve the order and remainder
@inline get_order(tm::TaylorModelN) = tm.pol.order
@inline remainder(tm::TaylorModelN) = tm.rem
@inline polynomial(tm::TaylorModelN) = tm.pol
@inline domain(tm::TaylorModelN) = tm.dom
@inline expansion_point(tm::TaylorModelN) = tm.x0
@inline get_numvars(::TaylorModelN{N,T,S}) where {N,T,S} = N

"""
    TMSol{N,T,V1,V2,M}

Structure containing the solution of a validated integration.

# Fields
    `time :: AbstractVector{T}`  Vector containing the expansion time of the `TaylorModel1` solutions

    `fp   :: AbstractVector{IntervalBox{N,T}}`  IntervalBox vector representing the flowpipe

    `xTMv :: AbstractMatrix{TaylorModel1{TaylorN{T},T}}`  Matrix whose entry `xTMv[i,t]` represents
    the `TaylorModel1` of the i-th dependent variable, obtained at time time[t].
"""
struct TMSol{N,T<:Real,V1<:AbstractVector{T},V2<:AbstractVector{IntervalBox{N,T}},
        M<:AbstractMatrix{TaylorModel1{TaylorN{T},T}}}
    time :: V1
    fp   :: V2
    xTM  :: M

    function TMSol(time::V1, fp::V2, xTM::M) where
            {N,T<:Real,V1<:AbstractVector{T},V2<:AbstractVector{IntervalBox{N,T}},
            M<:AbstractMatrix{TaylorModel1{TaylorN{T},T}}}
        @assert length(time) == length(fp) == size(xTM,2) && N == size(xTM,1)
        return new{N,T,V1,V2,M}(time, fp, xTM)
    end
end

@inline expansion_point(a::TMSol) = getfield(a,:time)
@inline expansion_point(a::TMSol, n::Int) = getindex(getfield(a,:time),n)
@inline flowpipe(a::TMSol) = getfield(a,:fp)
@inline flowpipe(a::TMSol, n::Int) = getindex(getfield(a,:fp),n)
@inline get_xTM(a::TMSol) = getfield(a,:xTM)
@inline get_xTM(a::TMSol, n::Int) = getindex(getfield(a,:xTM),:,n)
@inline domain(a::TMSol) = domain.(getindex(getfield(a, :xTM), 1, :)) # vector!
@inline domain(a::TMSol, n::Int) = domain(getindex(getfield(a, :xTM), 1, n))
@inline get_numvars(::TMSol{N,T,V1,V2,M}) where {N,T,V1,V2,M} = N


using DoubleExponentialFormulas
using LinearAlgebra: norm
using Test

quadde32 = QuadDE(Float32)
quadde64 = QuadDE(Float64)
quaddeBF = QuadDE(BigFloat)

# Test Base.show works without error
let
    io = IOBuffer()
    @test (Base.show(io, MIME("text/plain"), quadde32); true)
    @test (Base.show(io, MIME("text/plain"), quadde64); true)
    @test (Base.show(io, MIME("text/plain"), quaddeBF); true)
end

# Abel transform
let
    f(r::AbstractFloat) = exp(-(r^2 - 1))
    expect = sqrt(BigFloat(π))

    y = 1.0
    g = r -> 2*f(r)*r/sqrt(r^2 - y^2)

    I, E = quadde32(g, 1, Inf)
    @test I isa Float32
    @test I ≈ expect
    @test E ≤ sqrt(eps(typeof(I)))*norm(I)

    I, E = quadde64(g, 1, Inf)
    @test I isa Float64
    @test I ≈ expect
    @test E ≤ sqrt(eps(typeof(I)))*norm(I)

    rtol = 1e-30
    I, E = quaddeBF(g, 1, Inf, rtol=rtol)
    @test I isa BigFloat
    @test isapprox(I, expect, rtol=10rtol)
    @test E ≤ rtol*norm(I)
end


"""
    make_hermitian(arr)

Takes an array `arr` and appends rows, cols, ... if necessary
so that `arr` is a hermitian array which preserves Parseval's theorem.

# Examples
```jldoctest
julia> FFTResampling.make_hermitian([1.0 2.0])
1×3 Array{Float64,2}:
 0.5  2.0  0.5

julia> FFTResampling.make_hermitian([1.0 2.0; 3.0 4.0])
3×3 Array{Float64,2}:
 0.5  1.0  0.0
 1.5  4.0  1.5
 0.0  1.0  0.5

julia> FFTResampling.make_hermitian([1im 2.0; 3.0im 4.0im; 5.0 6.0im])
3×3 Array{Complex{Float64},2}:
 0.0+0.5im  2.0+0.0im  2.5-0.0im
 0.0+1.5im  0.0+4.0im  0.0-1.5im
 2.5+0.0im  0.0+6.0im  0.0-0.5im
```
"""
function make_hermitian(arr::AbstractArray{T, N}) where {T, N}
    # if the size is odd, we need to add a slice to that dimension
    # at this slice we then fill new values, so that `arr` is hermitian
    mf(x) = x % 2 == 0 ? x +1 : x
    size_new = map(mf, size(arr))
    # new array with new size
    arr_new = zeros(eltype(arr), size_new)

    # fill the new array with the old one
    same_ind = map(x -> 1:x, size(arr))
    arr_new[same_ind...] = arr

    # copy again to have a out array (might be necessary because of reassigning)
    arr_out = copy(arr_new)

    # now modify values at the edges to get hermitian property
    # and Parsveval's theorem correct
    @inbounds for d = 1:N
        # special case when singleton dimension
        if size(arr_new)[d] == 1 || size(arr)[d] % 2 == 1
            continue
        end

        # extract indices represent the left slice we need to copy
        extract_indices = slice_indices(arr_new, d, 1)
        y = arr_new[extract_indices...] ./ 2
        y_rev = reverse_all(y)

        # assign indices represent the right sliuce we need to assign the
        # conjugated and halved one
        assign_indices = slice_indices(arr_new, d, size(arr_out)[d])

        # assign the halved
        arr_out[extract_indices...] = y
        # assign the conjugated, halved one
        arr_out[assign_indices...] = conj.(y_rev)
    end

    return arr_out
end

"""
    get_indices_around_center(i_in, i_out)
A function which provides two output indices `i1` and `i2`
where `i2 - i1 = i_out`
The indices are chosen in a way that the set `i1:i2`
cuts the interval `1:i_in` in a way that the center frequency
stays at the center position.
Works for both odd and even indices
"""
function get_indices_around_center(i_in, i_out)
    if (mod(i_in, 2) == 0 && mod(i_out, 2) == 0
     || mod(i_in, 2) == 1 && mod(i_out, 2) == 1)
        x = (i_in - i_out) ÷ 2
        return 1 + x, i_in - x
    elseif mod(i_in, 2) == 1 && mod(i_out, 2) == 0
        x = (i_in - 1 - i_out) ÷ 2
        return 1 + x, i_in - x - 1
    elseif mod(i_in, 2) == 0 && mod(i_out, 2) == 1
        x = (i_in - (i_out - 1)) ÷ 2
        return 1 + x, i_in - (x - 1)
    end
end

"""
    center_extract(arr, new_size_array)
Extracts a center of an array.
`new_size_array` must be list of sizes indicating the output
size of each dimension. Centered means that a center frequency
stays at the center position. Works for even and uneven.
If `length(new_size_array) < length(ndims(arr))` the remaining dimensions
are untouched and copied.
# Examples
```jldoctest
julia> FFTResampling.center_extract([1 2; 3 4], [1])
1×2 Array{Int64,2}:
 3  4

julia> FFTResampling.center_extract([1 2; 3 4], [1, 1])
1×1 Array{Int64,2}:
 4

julia> FFTResampling.center_extract([1 2 3; 3 4 5; 6 7 8], [2 2])
2×2 Array{Int64,2}:
 1  2
 3  4
```
"""
function center_extract(arr::AbstractArray, new_size_array)
    new_size_array = collect(new_size_array)

    # we construct two lists
    # the reason is, that we don't change higher dimensions which are not
    # specified in new_size_array
    out_indices1 = [get_indices_around_center(size(arr)[x], new_size_array[x])
                    for x = 1:length(new_size_array)]

    out_indices1 = [x[1]:x[2] for x = out_indices1]

    # out_indices2 contains just ranges covering the full size of each dimension
    out_indices2 = [1:size(arr)[i] for i = (1 + length(new_size_array)):ndims(arr)]
    return arr[out_indices1..., out_indices2...]
end

"""
    center_set!(arr_large, arr_small)
Puts the `arr_small` central into `arr_large`.
The convention, where the center is, is the same as the definition
as for FFT based centered.
Function works both for even and uneven arrays.
# Examples
```jldoctest
julia> FFTResampling.center_set!([1, 1, 1, 1, 1, 1], [5, 5, 5])
6-element Array{Int64,1}:
 1
 1
 5
 5
 5
 1
```
"""
function center_set!(arr_large, arr_small)
    out_is = []
    for i = 1:ndims(arr_large)
        a, b = get_indices_around_center(size(arr_large)[i], size(arr_small)[i])
        push!(out_is, a:b)
    end

    #rest = ones(Int, ndims(arr_large) - 3)
    arr_large[out_is...] = arr_small

    return arr_large
end

"""
    center_pos(x)
Calculate the position of the center frequency.
Size of the array is `x`
# Examples
```jldoctest
julia> FFTResampling.center_pos(3)
2
julia> FFTResampling.center_pos(4)
3
```
"""
function center_pos(x::Integer)
    # integer division
    return div(x, 2) + 1
end

"""
    slice(arr, dim, index)

Return a `N` dimensional slice (where one dimensions has size 1) of the N-dimensional `arr` at the index position
`index` in the `dim` dimension of the array.
It holds `size(out)[dim] == 1`.

# Examples
```jldoctest
julia> x = [1 2 3; 4 5 6; 7 8 9]
3×3 Array{Int64,2}:
 1  2  3
 4  5  6
 7  8  9

julia> FFTResampling.slice(x, 1, 1)
1×3 view(::Array{Int64,2}, 1:1, :) with eltype Int64:
 1  2  3

julia> FFTResampling.slice(x, 2, 3)
3×1 view(::Array{Int64,2}, :, 3:3) with eltype Int64:
 3
 6
 9

```

"""
function slice(arr::AbstractArray{T, N}, dim::Integer, index::Integer) where {T, N}
    inds = slice_indices(arr, dim, index)
    return @view arr[inds...]
end

function slice_indices(arr::AbstractArray{T, N}, dim::Integer, index::Integer) where {T, N}
    f(x) = x[1] == dim ? (index:index) : (1:x[2])
    inds = map(f, enumerate(size(arr)))
    return inds
end

"""
    reverse_all(arr)

Reverse an array `arr` over all dimensions.

# Examples
```jldoctest
julia> FFTResampling.reverse_all([1 2 3 4 5])
1×5 Array{Int64,2}:
 5  4  3  2  1

julia> FFTResampling.reverse_all([1 2 3 4 5; 7 8 9 10 11])
2×5 Array{Int64,2}:
 11  10  9  8  7
  5   4  3  2  1

julia> FFTResampling.reverse_all([1; 2; 3; 4; 5])
5-element Array{Int64,1}:
 5
 4
 3
 2
 1
```
"""
function reverse_all(arr::AbstractArray)
    return arr[reverse_all_indices(arr)...]
end

function reverse_all_indices(arr)
    out = []
    for i in size(arr)
        push!(out, i:-1:1)
    end
    return out
end

function dft_1D(arr)
    N = length(arr)
    X = zeros(ComplexF64, N)

    for k = 0:N-1
        s = zero(X[1])
        for n = 0:N-1
            s += arr[n + 1]  * exp(-1im*2*π*k *n/ N)
        end
        X[k+1] = s
    end
    return X
end

"""
This function ensures the constraints a spectrum must have so that after
iffting the result is purely real.

For example, inspect `ffshift(fft(randn((6,6))))` and you see that the highest negative
frequencies obey a certain symmetry. This can be generalized to N dimensions.
If we cut the array so that it'll be even in the that d dimension (new_size[d] % 2 ==0)
we need to manually restore the symmetry to guarantee a purely real result.
"""
function add_high_frequencies(size_arr, arr_out_f, new_size, N)
    fix_mul = false
    for d = 1:N
        if new_size[d] % 2 == 1 || new_size[d] == size_arr[d] ||
            (size_arr[d] % 2 == 0 && new_size[d] == (size_arr[d] + 1))
            continue
        end

        # construct the slice containing the highest positive frequency
        inds_extract = []
        inds_assign = []

        for i = 1:N
            a,b = get_indices_around_center(size_arr[i], new_size[i])
            if i == d
                # b+1 is the highest positive frequency which
                # will be cut off (under certain conditions)
                iend = min(b+1, size_arr[i])
                push!(inds_extract, iend:iend)
                push!(inds_assign, a:a)
            else
                push!(inds_extract, a:min(b+1, size_arr[i]))
                push!(inds_assign, a:min(b+1, size_arr[i]))
            end
        end

        # add the highest positive frequency slice to the highest negative
        # but use the same arr_out_f
        # in that way we create a matrix which has the demanded symmetry of FFT to produce a real result
        arr_out_f[inds_assign...] = (arr_out_f[inds_assign...] .+ arr_out_f[inds_extract...])
    end

    return arr_out_f
end


# Checks if probabilities are strictly positive and if not adjusts slightly.
# What adjustments can be justified have to be determined in each instance.

function checkTree(tree0::Tree, eps)

# For every parent node going backwards
tmp = reverse(unique(tree0.parent)); pop!(tmp)
for i in tmp
    #Note that the probs are the conditional ones
    prob = tree0.probability[tree0.children[i+1]]
    if any(x->x>0,prob)
      #do nothing
    else
      prob = prob .+ eps
      prob = prob./ sum(prob)
      println()
      println("updated tree")
    end
  end

end


export loadCanonicalFG_Kaess, loadCanonicalFG_TestSymbolic, loadCanonicalFG_CaesarRing1D

"""
    $SIGNATURES

Canonical example from literature, Kaess, et al.: ISAM2, IJRR, 2011.

Notes
- Paper variable ordering: p = [:l1;:l2;:x1;:x2;:x3]
"""
function loadCanonicalFG_Kaess(;graphinit::Bool=false)
  fg = initfg()

  addVariable!(fg,:x1, ContinuousScalar)
  addFactor!(fg, [:x1;], Prior(Normal()), autoinit=graphinit)

  addVariable!(fg,:x2, ContinuousScalar)
  addFactor!(fg,[:x1, :x2], LinearConditional(Normal()), autoinit=graphinit)

  addVariable!(fg, :x3, ContinuousScalar)
  addFactor!(fg,[:x2,:x3],LinearConditional(Normal()), autoinit=graphinit)

  addVariable!(fg, :l1, ContinuousScalar)
  addFactor!(fg, [:x1,:l1], LinearConditional(Normal()) , autoinit=graphinit)
  addFactor!(fg, [:x2,:l1], LinearConditional(Normal()) , autoinit=graphinit)

  addVariable!(fg, :l2, ContinuousScalar)
  addFactor!(fg, [:x3,:l2], LinearConditional(Normal()), autoinit=graphinit)

  return fg
end

"""
    $SIGNATURES

Canonical example introduced by Borglab.

Notes
- Known variable ordering: p = [:x1; :l3; :l1; :x5; :x2; :l2; :x4; :x3]
"""
function loadCanonicalFG_TestSymbolic(;graphinit::Bool=false)
  fg = initfg()

  addVariable!(fg, :x1, ContinuousScalar)
  addVariable!(fg, :x2, ContinuousScalar)
  addVariable!(fg, :x3, ContinuousScalar)
  addVariable!(fg, :x4, ContinuousScalar)
  addVariable!(fg, :x5, ContinuousScalar)
  addVariable!(fg, :l1, ContinuousScalar)
  addVariable!(fg, :l2, ContinuousScalar)
  addVariable!(fg, :l3, ContinuousScalar)

  addFactor!(fg, [:x1;:l1], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x1;:x2], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x2;:l1], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x2;:x3], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x3;:x4], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x4;:l2], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x4;:x5], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:l2;:x5], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x4;:l3], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x5;:l3], LinearConditional(Normal()), autoinit=graphinit)

  return fg
end

"""
    $SIGNATURES

Canonical example introduced originally as Caesar Hex Example.

Notes
- Paper variable ordering: p = [:x0;:x2;:x4;:x6;:x1;:l1;:x5;:x3;]
"""
function loadCanonicalFG_CaesarRing1D(;graphinit::Bool=false)

  fg = initfg()

  addVariable!(fg, :x0, ContinuousScalar)
  addVariable!(fg, :x1, ContinuousScalar)
  addVariable!(fg, :x2, ContinuousScalar)
  addVariable!(fg, :x3, ContinuousScalar)
  addVariable!(fg, :x4, ContinuousScalar)
  addVariable!(fg, :x5, ContinuousScalar)
  addVariable!(fg, :x6, ContinuousScalar)

  addFactor!(fg, [:x0], Prior(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x0;:x1], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x1;:x2], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x2;:x3], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x3;:x4], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x4;:x5], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x5;:x6], LinearConditional(Normal()), autoinit=graphinit)

  addVariable!(fg, :l1, ContinuousScalar)
  addFactor!(fg, [:x0;:l1], LinearConditional(Normal()), autoinit=graphinit)
  addFactor!(fg, [:x6;:l1], LinearConditional(Normal()), autoinit=graphinit)

  return fg
end


import QDLDL
using LightGraphs: SimpleGraph, maximal_cliques
import AMD, Metis, CuthillMcKee

function preprocess!(mats::AbstractVector{SparseMatrixCSC{T,S}}) where {T <: Number, S <: Integer}
    for mat in mats
        preprocess!(mat)
    end
end

"""
	preprocess!(mat::SparseMatrixCSC{T, <:Integer}) where {T}

Checks that `mat` is symmetric and drops numerical zeros.
"""
function preprocess!(mat::SparseMatrixCSC{T, <:Integer}) where {T}
    n = size(mat, 1)
    @assert size(mat) == (n, n)
	for i in 1:n, j in i+1:n
		@assert mat[i,j] .== mat[j,i]
	end
    dropzeros!(mat)
end

# Returns 0/1 sparsity pattern of a set of matrices
"""
	sparsity_pattern(mats::AbstractVector{SparseMatrixCSC{T,S}}) where {T, S <: Integer}

Returns the 0/1 aggregate sparsity pattern of the matrices in `mats` as a sparse matrix.
"""
function sparsity_pattern(mats::AbstractVector{SparseMatrixCSC{T,S}}) where {T, S <: Integer}
	n = size(mats[1], 1)
	for mat in mats
		!(n == size(mat, 1) && issymmetric(mat)) && error(ArgumentError(
			"Matrices must be symmetric and have the same dimensions."
		))
	end

	# Build 0/1 sparsity pattern matrix
	ret = spzeros(n,n)
	for mat in mats
	    @inbounds for col in 1:n, k in nzrange(mat, col)
	        ret[rowvals(mat)[k], col] = 1.0
		end
	end

	# Always assume diagonal can be nonzero
	ret[diagind(ret)] .= 1

    return ret
end
sparsity_pattern(mat::SparseMatrixCSC) = sparsity_pattern([mat])

# Gets chordal extension: a reordering + associated graph from cholesky
"""
	get_chordal_extension(sp_pattern::SparseMatrixCSC; perm="amd", verbose=false)

Returns `p, ip, L`, where `L` is a lower-triangular matrix representing the
chordal extension of the undirected graph `sp_pattern` after fill-reducing permutation
`p` (with inverse permutation `ip`).

Options for perm include
	- `"nothing"` (no permutation)
	- `amd` (approximate minimum degree)
	- `metis` (uses [METIS](http://glaros.dtc.umn.edu/gkhome/metis/metis/overview))
	- `cuthill-mckee` (Cuthill-McKee bandwidth reducing algorithm)

"""
function get_chordal_extension(sp_pattern::SparseMatrixCSC; perm="amd", cost=nothing, verbose=false)
	!issymmetric(sp_pattern) && error(ArgumentError("Matrix must be symmetric"))
	n = size(sp_pattern)[1]

	# Permutations: nothing, amd, vertex separators, TODO: add more options?
	# See survey Minimal triangulations of graphs: A survey
	# https://www.sciencedirect.com/science/article/pii/S0012365X05006060
	if perm == "none" || perm == "nothing" || isnothing(perm)
		p = nothing
	elseif perm == "amd"
		p = AMD.amd(sp_pattern)
	elseif perm == "metis"
		# Convert to Int64 for QDLDL (ret as Int32)
		# first arg = perm (second = iperm)
		p = Int.(Metis.permutation(sp_pattern)[1])
    elseif perm == "cuthill-mckee"
		p = cuthill_mckee(sp_pattern)
	elseif perm == "greedy" && !isnothing(cost)
		# NOTE: This part isn't really working well yet
		# Choose v according to criterion X
		# Let v be the eith vertex in ordering p
		# Let H be the graph obstained by eliminating v from H
		#  - make neighborhood of v a clique and then remove v
		# Return H, p
		# Criteria:
		# Greedy degree: 				v = argminᵤ d(u)
		# Greedy fill in				argminᵤ f(u) = num pairs of non-adj neighbors
		# Greedy degree + fill in		d(u) + f(u)
		# Greedy sparsest subgraph		f(u) - d(u)
		# Greedy fill in degree			d(u) + 1/n^2 * f(u)
		# Greedy degree fill in			f(u) + 1/n * d(u)
		n = size(sp_pattern, 1)
		p = Vector{Int}(undef, n)
		active_inds = Vector(1:n)
		for i in 1:n
			H = @view(sp_pattern[active_inds, active_inds])
			min_cost_vert = get_min_cost_vert(H, cost)
			p[i] = active_inds[min_cost_vert]
			deleteat!(active_inds, [min_cost_vert])
		end

	else
		error(ArgumentError("Invalid permutation"))
	end
	# TODO: add greedy
	# Algorithm: (summarized in Bodlaender & Koster, Treewith computations I. Upper bounds)
	# H = copy(G)
	# for i in 1:n

	F = QDLDL.qdldl(sp_pattern, perm=p, logical=true)

	if verbose
		num_nonzero = 2*nnz(F.L) + n
		num_nonzero_added = (num_nonzero - nnz(sp_pattern)) ÷ 2

		@info "Chordal Extension added $num_nonzero_added nonzeros."
		@info "Density: $(round(num_nonzero/n^2; digits=5))"
	end

    return F.perm, F.iperm, F.L
end

"""
	get_cliques(L::SparseMatrixCSC)

Returns the (maximal) cliques of the undirected graph represented by lower
triangular matrix `L`.
"""
function get_cliques(L::SparseMatrixCSC)
	G = SimpleGraph(L + L')
	return maximal_cliques(G)
end

function cuthill_mckee(sp::SparseMatrixCSC)
	# TODO: Custom implementation for efficiency?
	# Cuthill-McKee (https://en.wikipedia.org/wiki/Cuthill–McKee_algorithm)
	return CuthillMcKee.symrcm(sp)
end

function get_min_cost_vert(H, cost::Function)
	# NOTE: Removed check for performance
	# !issymmetric(H) && error("adj matrix must be symmetric")
	n = size(H, 1)

	min_cost = Inf
	amin_cost = 0
	for i in 1:n
		v_cost = cost(H, i)
		if v_cost < min_cost
			min_cost = v_cost
			amin_cost = i
		end
	end

	return amin_cost
end

# Some cost functions
# H is a (sparse) adj matrix of a graph (CSC format)
# i is the vertex to eval cost on
# https://www.sciencedirect.com/science/article/pii/S0890540109000947
function cost_deg(H::SparseMatrixCSC, i)
	# NOTE: assumes 0/1 matrix
	return length(H.colptr[i]:(H.colptr[i+1]-1))
end

function cost_fill_in(H, i)
	nna_neighbors = 0
	display(fieldnames(H))
	neighbors = @view(H.rowval[H.colptr[i]:(H.colptr[i+1]-1)])
	for i in neighbors, j in neighbors
		i == j && continue
		if H[i,j] == 0
			nna_neighbors += 1
		end
	end
	return nna_neighbors
end

cost_deg_plus_fill_in(H, i) = cost_deg(H, i) + cost_fill_in(H, i)
cost_sparsest_subset(H, i) 	= cost_fill_in(H, i) - cost_deg(H, i)
cost_fill_in_degree(H, i) 	= cost_deg(H, i) + 1/size(H,1)^2 * cost_fill_in(H, i)
cost_deg_fill_in(H, i) 		= 1/size(H,1) * cost_deg(H, i) + cost_fill_in(H, i)


# tools for displaying latin squares

# alphabets

abc = ["$ch" for ch = 'a':'z']
ABC = uppercase.(abc)
greek = ["$(Char(t))" for t in 945:969 if t != 962]
GREEK = uppercase.(greek)

export print_latin

"""
`print_latin(A)` prints out the Latin square `A` using uppercase
Latin letters.

`print_latin(A,B)` prints out the pair of (presumably orthogonal)
Latin squares using upper case Latin letters for `A` and lower case Greek
letters for `B`.

Both versions permit alternate character sets:
* `print_latin(A,charlist)`
* `print_latin(A,B,charlist1,charlist2)`

The following pre-built character sets are available:
* `LatinSquares.abc`: lower case Latin letters
* `LatinSquares.ABC`: upper case Latin letters
* `LatinSquares.greek`: lower case Greek letters
* `LatinSquares.GREEK`: upper case Greek letters
"""

function print_latin(A::Matrix{Int}, chars::Array{String,1} = ABC)
    r, c = size(A)
    for i = 1:r
        for j = 1:c
            print(chars[A[i, j]])
            if j < c
                print(" ")
            else
                println()
            end
        end
    end
    nothing
end

function print_latin(
    A::Matrix{Int},
    B::Matrix{Int},
    chars1::Array{String} = ABC,
    chars2::Array{String} = greek,
)
    r, c = size(A)
    for i = 1:r
        for j = 1:c
            print(chars1[A[i, j]] * chars2[B[i, j]])
            if j < c
                print(" ")
            else
                println()
            end
        end
    end
    nothing
end


# Compute the longest streak of `nuc` in vector `seq`
function longeststreak(seq::Vector{UInt8},nuc::UInt8)
    len=0
    current=0
    last=0
    count=0
    for i in linearindices(seq)
        if seq[i]==nuc
            count+=1
            current+=1
            if current > len
                len = current
                last = i
            end
        else
            current=0
        end
    end
    return len, last, count
end

# Compute a Hamming distance between `id1` and `id2`, assuming that
# both are of length `len`
function hamming(id1::UInt,id2::UInt,len)
    id1-=1
    id2-=1
    dist::UInt8 = 0
    for n in 1:len
        if (id1&0b11) $ (id2&0b11) != 0
            dist+=1
        end
        id1>>=2
        id2>>=2
    end
    return dist
end

function getminhammingdistance(codes::AbstractVector{UInt},N)
    nearest = zeros(UInt8,length(codes))
    @inbounds for i1 in 1:length(codes)
        nearest[i1] = N       # start with a maximal distance
        for i2 in 1:(i1-1)
            dist = hamming(codes[i1],codes[i2],N)
            nearest[i1] = min(nearest[i1],dist)
            # the minimum has been reached
            if dist == 1
                continue
            end
        end
    end
    return nearest
end


# ==============================================================================
# Calcul des generateurs avec 2 ϵ-contraintes alternees jusqu'a leur rencontre

function calculGenerateurs(A::Array{Int,2}, c1::Array{Int,1}, c2::Array{Int,1},
                           tailleSampling::Int64,
                           minf1RL::Float64, maxf2RL::Float64, maxf1RL::Float64, minf2RL::Float64,
                           d::tListDisplay)

    nbctr = size(A,1)
    nbvar = size(A,2)

    L1 = (tSolution{Float64})[]
    L2 = (tSolution{Float64})[]

    # Premiere generation de points (avec z1 la fonction a minimiser) ----------
    pasSample2 = (maxf2RL - minf2RL) / (tailleSampling-1) # pas de l'echantillonage sur z2
    j1 = 1

    # Seconde generation de points (avec z2 la fonction a minimiser) -----------
    pasSample1 = (maxf1RL-minf1RL) / (tailleSampling-1) # pas de l'echantillonage sur z1
    j2 = 1

    alternance = 1
    maxf2RLlimite = maxf2RL
    minf2RLlimite = minf2RL

    while maxf2RLlimite > minf2RLlimite
        alternance += 1
        if alternance % 2 == 0

            # minimise f1 avec ϵ-contrainte sur f2 -----------------------------
            verbose ? @printf("  z1 %2d : ϵ = %8.2f  ", j1, maxf2RL - (j1-1) * pasSample2) : nothing # echantillonage sur z2

            # calcul d'une solution epsilon-contrainte
            f1RL, xf1RL = computeLinearRelax2SPA(nbvar, nbctr, A, c1, c2, maxf2RL - (j1-1) * pasSample2, 1)

            # reconditionne les valeurs 0 et 1 et arrondi les autres valeurs
            nettoyageSolution!(xf1RL)
            verbose ? @printf("fRL = %8.2f  ",round(f1RL, digits=2)) : nothing

            # recalcule la solution au regard des 2 objectifs
            z1f1RLcourant, z2f1RLcourant = evaluerSolution(xf1RL, c1, c2)
            verbose ? @printf("[ %8.2f , %8.2f ] \n", z1f1RLcourant, z2f1RLcourant) : nothing

            # maj la valeur limite sur l'objectif 2 pour la solution courante
            maxf2RLlimite = z2f1RLcourant
            if maxf2RLlimite > minf2RLlimite
                push!(L1, (tSolution{Float64})(xf1RL,[z1f1RLcourant, z2f1RLcourant]))
                push!(d.xL,z1f1RLcourant);push!(d.yL,z2f1RLcourant)
                push!(d.xLf1,z1f1RLcourant);push!(d.yLf1,z2f1RLcourant)
            end
            j1 = j1+1

        else

            # minimise f2 avec ϵ-contrainte sur f1 -----------------------------
            verbose ? @printf("  z2 %2d : ϵ = %8.2f  ", j2, maxf1RL - (j2-1) * pasSample1) : nothing # echantillonage sur z1

            # calcul d'une solution epsilon-contrainte
            f2RL, xf2RL = computeLinearRelax2SPA(nbvar, nbctr, A, c1, c2, maxf1RL - (j2-1) * pasSample1, 2)

            # reconditionne les valeurs 0 et 1 et arrondi les autres valeurs
            nettoyageSolution!(xf2RL)
            verbose ? @printf("fRL = %8.2f  ",round(f2RL, digits=2)) : nothing

            # recalcule la solution au regard des 2 objectifs
            z1f2RLcourant, z2f2RLcourant = evaluerSolution(xf2RL, c1, c2)
            verbose ? @printf("[ %8.2f , %8.2f ] \n", z1f2RLcourant, z2f2RLcourant) : nothing

            # maj la valeur limite sur l'objectif 2 pour la solution courante
            minf2RLlimite = z2f2RLcourant
            if maxf2RLlimite > minf2RLlimite
                push!(L2, (tSolution{Float64})(xf2RL,[z1f2RLcourant, z2f2RLcourant]))
                push!(d.xL,z1f2RLcourant);push!(d.yL,z2f2RLcourant)
                push!(d.xLf2,z1f2RLcourant);push!(d.yLf2,z2f2RLcourant)
            end
            j2 = j2+1
        end
    end
    return  length(L1)+length(L2), vcat(L1,reverse(L2))
end


using IRKGaussLegendre, DiffEqProblemLibrary, DiffEqDevTools, Test
using DiffEqProblemLibrary.ODEProblemLibrary: importodeproblems; importodeproblems()
import DiffEqProblemLibrary.ODEProblemLibrary: prob_ode_2Dlinear, prob_ode_bigfloat2Dlinear

function NbodyODE!(du,u,Gm,t)
     N = length(Gm)
     du[1,:,:] .= 0
     for i in 1:N
        qi = u[2,:,i]
        Gmi = Gm[i]
        du[2,:,i] = u[1,:,i]
        for j in (i+1):N
           qj = u[2,:,j]
           Gmj = Gm[j]
           qij = qi - qj
           auxij = (qij[1]*qij[1]+qij[2]*qij[2]+qij[3]*qij[3])^(-3/2)
           du[1,:,i] -= Gmj*auxij*qij
           du[1,:,j] += Gmi*auxij*qij
        end
     end

    return
end

Gm = [5, 4, 3]
N=length(Gm)
q=[1,-1,0,-2,-1,0,1,3,0]
v=zeros(size(q))
q0 = reshape(q,3,:)
v0 = reshape(v,3,:)
u0 = Array{Float64}(undef,2,3,N)
u0[1,:,:] = v0
u0[2,:,:] = q0
tspan = (0.0,63.0)
prob=ODEProblem(NbodyODE!,u0,tspan,Gm)
sol1=solve(prob,IRKGL16(),reltol=1e-12, abstol=1e-12)

# Analytical tests

sol = solve(prob_ode_2Dlinear,IRKGL16())
@test sol.errors[:l2] < 1e-16

dts = (1//2) .^ (4:-1:2)
sim = test_convergence(dts,prob_ode_bigfloat2Dlinear,IRKGL16())
@test abs(sim.𝒪est[:final]-16) < 0.5


### A Pluto.jl notebook ###
# v0.12.4

using Markdown
using InteractiveUtils

# ╔═╡ d2ac9e5e-8bb6-4fa1-9cb3-957d238d7355
md"""
# Despacho múltiple / multiple dispatch
"""

# ╔═╡ 60cc9b65-62bd-4795-a9d9-30ff6a5fdc48
md"""
En este notebook vamos a explorar **multiple dispatch**, un concepto fundamental en Julia.

Multiple dispatch permite software:
- rápido
- extendible
- programable
- divertido para experimentar
"""

# ╔═╡ dda25ade-80d1-429a-a285-dbc477305966
md"""
Para entender el despacho múltiple en Julia, observemos el operador `+`.

Si llamamos `methods()` sobre `+`, podemos ver todas las definiciones de `+`
"""

# ╔═╡ d232a85b-62c8-4de5-8798-637af323b492
md"""
Podemos usar el macro de `@which` para saber qué método en particular estamos usando de `+`.

Distintos métodos se usan en cada uno de estos ejemplos.
"""

# ╔═╡ 495ee98c-e2b8-475f-8c36-b870c816d33b
md"""
Aún más, pues podemos definir nuevos métodos de `+`.

Primero tenemos que importar `+` de Base.
"""

# ╔═╡ 37b52b24-d323-47e4-87db-fcbe204ca96e
import Base: +

# ╔═╡ 19b8a372-4ea5-4ff4-8a6a-f48aa34430f4
methods(+);

# ╔═╡ 6426c7fa-c3ed-4d06-be67-355ac10ebfea
@which 3 + 3

# ╔═╡ b297c837-e937-4da0-acd8-933df7fee791
@which 3.0 + 3.0

# ╔═╡ 9896765a-8906-4877-b8e5-b5c0fc4b09f8
@which 3 + 3.0

# ╔═╡ 76151c1b-139f-40c6-b111-0b0634473e17
md"""
Digamos que queremos concatenar elementos con  `+`. Sin extender el método, no funciona
"""

# ╔═╡ 4f27b734-90ce-4502-9df2-b7d69c230266
"hello " + "world!"

# ╔═╡ 317c59a1-fe9d-47bc-acd6-d25f8a2bcbc7
@which "hello " + "world!"

# ╔═╡ 74997f65-4f75-4aa8-88f1-49a96a724e82
md"""
Entonces agregamos a `+` un método que toma dos cadenas y las concatena
"""

# ╔═╡ 1df76000-3434-4be6-b49d-6839436f1c6e
Base.:+(x::String, y::String) = string(x, y)

# ╔═╡ 37d06d98-4d74-4d29-9ec9-39fb0d31d31a
"hello " + "world!"

# ╔═╡ 1dbf2eb6-87ff-425e-b9bb-4039538c54ce
md"""
¡Funciona! Y si queremos más, podemos comprobarnos que Julia ha despachado sobre los tipos de "hello" y "world!", sobre el método que acabamos de definir
"""

# ╔═╡ 66dccffc-d0f1-4974-a673-4941cdc61429
@which "hello " + "world!"

# ╔═╡ b87d4755-802e-45a5-9478-2aa69af92a62
md"""
Vamos por un ejemplo más
"""

# ╔═╡ c5397d75-f538-4346-95e5-0c30f4d203e0
begin
	foo(x, y) = println("duck-typed foo!")
	foo(x::Int, y::Float64) = println("foo con entero y flotante!")
	foo(x::Float64, y::Float64) = println("foo con dos flotantes!")
	foo(x::Int, y::Int) = println("foo con dos enteros!")
end

# ╔═╡ b16e8931-5887-4335-bafa-8a06969d679f
foo(1, 1)

# ╔═╡ 33e60ea0-8788-477e-a45d-dd5c92f36929
foo(1., 1.)

# ╔═╡ a007b5cf-3ebf-473d-b640-32c1d9182fe9
foo(1, 1.0)

# ╔═╡ c135586a-4c72-4489-b143-c18226000bcc
foo(true, false)

# ╔═╡ 4a1d67f1-ee84-49e5-94ec-84a8c1ec965e
md"""
Nota que el último ejemplo aplica por default el caso de 'duck-typed foo' porque no había un método definido exclusivamente para dos booleanos.
"""

# ╔═╡ 976d280a-1474-47aa-9856-eb5e4ee6ab49
md"""
### Ejercicios

#### 9.1

Agrega un método para `+` que aplique un cifrado de César a una cadena (cómo en el notebook 6) tal que

```julia
"hello" + 4 == "lipps"
```
"""

# ╔═╡ ddd66730-c1d2-4a28-9b9e-c4aa66057e4e

# ╔═╡ 9cfd983b-0b80-4712-a394-d95b33e79ca8
md"""
#### 9.2

Checa que has extendido propiamente `+` recorriendo la próxima cadena para atrás por 3 letras:

"Gr#qrw#phggoh#lq#wkh#diidluv#ri#gudjrqv#iru#|rx#duh#fuxqfk|#dqg#wdvwh#jrrg#zlwk#nhwfkxs1"
"""

# ╔═╡ 09d662a9-9ce6-4718-9740-a79cf6ac18dd

# ╔═╡ Cell order:
# ╟─d2ac9e5e-8bb6-4fa1-9cb3-957d238d7355
# ╟─60cc9b65-62bd-4795-a9d9-30ff6a5fdc48
# ╟─dda25ade-80d1-429a-a285-dbc477305966
# ╠═19b8a372-4ea5-4ff4-8a6a-f48aa34430f4
# ╟─d232a85b-62c8-4de5-8798-637af323b492
# ╠═6426c7fa-c3ed-4d06-be67-355ac10ebfea
# ╠═b297c837-e937-4da0-acd8-933df7fee791
# ╠═9896765a-8906-4877-b8e5-b5c0fc4b09f8
# ╟─495ee98c-e2b8-475f-8c36-b870c816d33b
# ╠═37b52b24-d323-47e4-87db-fcbe204ca96e
# ╟─76151c1b-139f-40c6-b111-0b0634473e17
# ╠═4f27b734-90ce-4502-9df2-b7d69c230266
# ╠═317c59a1-fe9d-47bc-acd6-d25f8a2bcbc7
# ╟─74997f65-4f75-4aa8-88f1-49a96a724e82
# ╠═1df76000-3434-4be6-b49d-6839436f1c6e
# ╠═37d06d98-4d74-4d29-9ec9-39fb0d31d31a
# ╟─1dbf2eb6-87ff-425e-b9bb-4039538c54ce
# ╠═66dccffc-d0f1-4974-a673-4941cdc61429
# ╟─b87d4755-802e-45a5-9478-2aa69af92a62
# ╠═c5397d75-f538-4346-95e5-0c30f4d203e0
# ╠═b16e8931-5887-4335-bafa-8a06969d679f
# ╠═33e60ea0-8788-477e-a45d-dd5c92f36929
# ╠═a007b5cf-3ebf-473d-b640-32c1d9182fe9
# ╠═c135586a-4c72-4489-b143-c18226000bcc
# ╟─4a1d67f1-ee84-49e5-94ec-84a8c1ec965e
# ╟─976d280a-1474-47aa-9856-eb5e4ee6ab49
# ╠═ddd66730-c1d2-4a28-9b9e-c4aa66057e4e
# ╟─9cfd983b-0b80-4712-a394-d95b33e79ca8
# ╠═09d662a9-9ce6-4718-9740-a79cf6ac18dd


data(f::TaylorNModel) = (f.n, f.x0, f.I)

function +(f::TaylorNModel, g::TaylorNModel)
    @assert data(f) == data(g)

    return TaylorNModel(f.n, f.x0, f.I, f.p + g.p, f.Δ + g.Δ, f.order_bounds
     + g.order_bounds)
end

function -(f::TaylorNModel, g::TaylorNModel)
    @assert data(f) == data(g)

    return TaylorNModel(f.n, f.x0, f.I, f.p - g.p, f.Δ - g.Δ, f.order_bounds
     - g.order_bounds)
end

function +(α::Real, f::TaylorNModel)
    g = copy(f)

    g.p.coeffs[1].coeffs[1] += (α..α)

    return g
end

-(f::TaylorNModel) = (-1 * f)
-(α::Real, f::TaylorNModel) = α + (-f)

*(α::Real, f::TaylorNModel) = TaylorNModel(f.n, f.x0, f.I, α*f.p, (α..α)*f.Δ,
α .* f.order_bounds)

*(α::Interval, f::TaylorNModel) = TaylorNModel(f.n, f.x0, f.I, α*f.p, α*f.Δ,
α .* f.order_bounds)

"""
Truncate a TaylorNModel to order n
"""
function truncate(f::TaylorNModel, n)
    if n >= f.n  # nothing to do
        return f
    end

    coeffs = f.p.coeffs[1:n+1]
    order_bounds = f.order_bounds[1:n+1]

    # add in order_bounds to remainder:
    Δ = f.Δ + sum(f.order_bounds[n+2:end])

    TaylorNModel(n, f.x0, f.I, coeffs, Δ, order_bounds)
end

function *(f::TaylorNModel, g::TaylorNModel)
    @assert f.n == g.n  # not necessary
    @assert f.x0 == g.x0
    @assert f.I == g.I

    n, x0, I = f.n, f.x0, f.I

    # TODO

    product_series = f.p * g.p

    Δ = f.Δ * g.Δ

    for i in 0:n
        for j in (n-i+1):n  # j + k > n
            Δ += f.order_bounds[i] * g.order_bounds[j]
        end
    end

    Δ += (sum(f.order_bounds) * g.Δ)
    Δ += (sum(g.order_bounds) * f.Δ)

    return TaylorNModel(n, x0, I, product_series, Δ)
end