# eq 4.9
# Gilbert, J.C., Lemaréchal, C. Some numerical experiments with variable-storage quasi-Newton algorithms.
# Mathematical Programming 45, 407–435 (1989). https://doi.org/10.1007/BF01589113
function gilbert_init(α, s, y)
    a = dot(y, Diagonal(α), y)
    b = dot(y, s)
    c = dot(s, inv(Diagonal(α)), s)
    return @. b / (a / α + y^2 - (a / c) * (s / α)^2)
end

"""
    lbfgs_inverse_hessians(θs, ∇logpθs; Hinit=gilbert_init, history_length=5, ϵ=1e-12) -> Vector{WoodburyPDMat}

From an L-BFGS trajectory and gradients, compute the inverse Hessian approximations at each point.

Given positions `θs` with gradients `∇logpθs`, construct LBFGS inverse Hessian
approximations with the provided `history_length`.
"""
function lbfgs_inverse_hessians(θs, ∇logpθs; Hinit=gilbert_init, history_length=5, ϵ=1e-12)
    L = length(θs) - 1
    θ = θs[1]
    ∇logpθ = ∇logpθs[1]
    n = length(θ)

    # allocate caches/containers
    history_ind = 0 # index of last set history entry
    history_length_effective = 0 # length of history so far
    s = similar(θ) # cache for BFGS update, i.e. sₗ = θₗ₊₁ - θₗ = -λ Hₗ ∇logpθₗ
    y = similar(∇logpθ) # cache for yₗ = ∇logpθₗ₊₁ - ∇logpθₗ = Hₗ₊₁ \ s₁ (secant equation)
    S = similar(s, n, min(history_length, L)) # history of s
    Y = similar(y, n, min(history_length, L)) # history of y
    α = fill!(similar(θ), true) # diag(H₀)
    H = lbfgs_inverse_hessian(Diagonal(α), S, Y, history_ind, history_length_effective) # H₀ = I
    Hs = [H] # trace of H

    for l in 1:L
        θlp1, ∇logpθlp1 = θs[l + 1], ∇logpθs[l + 1]
        (any(isnan, θlp1) || any(isnan, ∇logpθlp1)) && break
        s .= θlp1 .- θ
        y .= ∇logpθ .- ∇logpθlp1
        if dot(y, s) > ϵ * sum(abs2, y)  # curvature is positive, safe to update inverse Hessian
            # add s and y to history
            history_ind = mod1(history_ind + 1, history_length)
            history_length_effective = max(history_ind, history_length_effective)
            S[1:n, history_ind] .= s
            Y[1:n, history_ind] .= y

            # initial diagonal estimate of H
            α = Hinit(α, s, y)
        else
            @debug "Skipping inverse Hessian update from iteration $l to avoid negative curvature."
        end

        θ, ∇logpθ = θlp1, ∇logpθlp1
        H = lbfgs_inverse_hessian(Diagonal(α), S, Y, history_ind, history_length_effective)
        push!(Hs, H)
    end

    return Hs
end

"""
    lbfgs_inverse_hessian(H₀, S₀, Y₀, history_ind, history_length) -> WoodburyPDMat

Compute approximate inverse Hessian initialized from `H₀` from history stored in `S₀` and `Y₀`.

`history_ind` indicates the column in `S₀` and `Y₀` that was most recently added to the
history, while `history_length` indicates the number of first columns in `S₀` and `Y₀`
currently being used for storing history.
`S = S₀[:, history_ind+1:history_length; 1:history_ind]` reorders the columns of `₀` so that the
oldest is first and newest is last.

From Theorem 2.2 of [^Byrd1994], the expression for the inverse Hessian ``H`` is

```math
\\begin{align}
B &= \\begin{pmatrix}H_0 Y & S\\end{pmatrix}\\\\
R &= \\operatorname{triu}(S^\\mathrm{T} Y)\\\\
E &= I \\circ R\\\\
D &= \\begin{pmatrix}
    0 & -R^{-1}\\\\
    -R^{-\\mathrm{T}} & R^\\mathrm{-T} (E + Y^\\mathrm{T} H_0 Y ) R^\\mathrm{-1}\\\\
H &= H_0 + B D B^\\mathrm{T}
\\end{pmatrix}
\\end{align}
```

[^Byrd1994]: Byrd, R.H., Nocedal, J. & Schnabel, R.B.
             Representations of quasi-Newton matrices and their use in limited memory methods.
             Mathematical Programming 63, 129–156 (1994).
             doi: [10.1007/BF01582063](https://doi.org/10.1007/BF01582063)
"""
function lbfgs_inverse_hessian(H₀::Diagonal, S0, Y0, history_ind, history_length)
    J = history_length
    α = H₀.diag
    B = similar(α, size(α, 1), 2J)
    D = fill!(similar(α, 2J, 2J), false)
    iszero(J) && return WoodburyPDMat(H₀, B, D)

    hist_inds = [(history_ind + 1):history_length; 1:history_ind]
    @views begin
        S = S0[:, hist_inds]
        Y = Y0[:, hist_inds]
        B₁ = B[:, 1:J]
        B₂ = B[:, (J + 1):(2J)]
        D₁₁ = D[1:J, 1:J]
        D₁₂ = D[1:J, (J + 1):(2J)]
        D₂₁ = D[(J + 1):(2J), 1:J]
        D₂₂ = D[(J + 1):(2J), (J + 1):(2J)]
    end

    mul!(B₁, Diagonal(α), Y)
    copyto!(B₂, S)
    mul!(D₂₂, S', Y)
    triu!(D₂₂)
    R = UpperTriangular(D₂₂)
    nRinv = UpperTriangular(D₁₂)
    copyto!(nRinv, -I)
    ldiv!(R, nRinv)
    nRinv′ = LowerTriangular(copyto!(D₂₁, nRinv'))
    tril!(D₂₂) # eliminate all but diagonal
    mul!(D₂₂, Y', B₁, true, true)
    LinearAlgebra.copytri!(D₂₂, 'U', false, false)
    rmul!(D₂₂, nRinv)
    lmul!(nRinv′, D₂₂)

    return WoodburyPDMat(H₀, B, D)
end


module transformations

export transform

import ..particle: transform

using ..polynomials, ..particle, ..operators_dense, ..fock

"""
    transform(b1::PositionBasis, b2::FockBasis; x0=1)
    transform(b1::FockBasis, b2::PositionBasis; x0=1)

Transformation operator between position basis and fock basis.

The coefficients are connected via the relation
```math
ψ(x_i) = \\sum_{n=0}^N ⟨x_i|n⟩ ψ_n
```
where ``⟨x_i|n⟩`` is the value of the n-th eigenstate of a particle in
a harmonic trap potential at position ``x``, i.e.:
```math
⟨x_i|n⟩ = π^{-\\frac{1}{4}} \\frac{e^{-\\frac{1}{2}\\left(\\frac{x}{x_0}\\right)^2}}{\\sqrt{x_0}}
            \\frac{1}{\\sqrt{2^n n!}} H_n\\left(\\frac{x}{x_0}\\right)
```
"""
function transform(b1::PositionBasis, b2::FockBasis; x0::Real=1)
    T = Matrix{ComplexF64}(undef, length(b1), length(b2))
    xvec = samplepoints(b1)
    A = hermite.A(b2.N)
    delta_x = particle.spacing(b1)
    c = 1.0/sqrt(x0*sqrt(pi))*sqrt(delta_x)
    for i in 1:length(b1)
        u = xvec[i]/x0
        C = c*exp(-u^2/2)
        for n=0:b2.N
            T[i,n+1] = C*horner(A[n+1], u)
        end
    end
    DenseOperator(b1, b2, T)
end

function transform(b1::FockBasis, b2::PositionBasis; x0::Real=1)
    T = Matrix{ComplexF64}(undef, length(b1), length(b2))
    xvec = samplepoints(b2)
    A = hermite.A(b1.N)
    delta_x = particle.spacing(b2)
    c = 1.0/sqrt(x0*sqrt(pi))*sqrt(delta_x)
    for i in 1:length(b2)
        u = xvec[i]/x0
        C = c*exp(-u^2/2)
        for n=0:b1.N
            T[n+1,i] = C*horner(A[n+1], u)
        end
    end
    DenseOperator(b1, b2, T)
end

end # module


using .RationalFunctionFields: interpolate_rational_function,
        interpolate_multivariate_rational_function, polynomial_reconstruction,
        random_linear_shift, decompose_by_degrees, interpolate_polynomial,
        generate_kronecker_points, backward_kronecker,
        decompose_by_good_degrees, unpackfrac

function test_rational_function_interpolation(f)

    R = parent(denominator(f))

    # hope we have multivariate ring here
    vs = collect(AA.gens(R))
    nvs = length(vs)

    npoints = 100

    varidx = 1

    points = [ [ one(AA.QQ) for _ in 1:nvs ] for _ in 1:npoints ]
    for i in 1:npoints
        points[i][varidx] = AA.QQ(i)
    end

    xs = []
    ys = []
    for i in 1:npoints
        push!(xs, points[i][varidx])
        push!(ys, AA.evaluate(numerator(f), points[i]) // AA.evaluate(denominator(f), points[i]))
    end

    uniring, x = AA.PolynomialRing(AA.QQ, "x")

    inter = interpolate_rational_function(uniring, xs, ys)

    degf = AA.degree(numerator(f), vs[varidx]), AA.degree(denominator(f), vs[varidx])
    degi = AA.degree(numerator(inter)), AA.degree(denominator(inter))

    @test degi == degf
end

@testset "Polynomial Reconstruction tests" begin
    R, a = Nemo.PolynomialRing(Nemo.QQ, "a")

    ms = [
        (a - 2)*(a - 4)*(a + 5//3)*(a - 10//21)*(a - 1//8),
        (a^2 + a + 1)*(a + 10)^3*(a - 1)*(a - 2)
    ]
    fs = [
        a^2 + a + 1,
        (a - 3)^2,
        a,
        R(1)
    ]

    for f in fs
        for m in ms
            q = polynomial_reconstruction(f, m)
            n, d = unpackfrac(q)

            @test mod(n, m) == mod(d*f, m)
            @test AA.degree(n) + AA.degree(d) <= AA.degree(m)
        end
    end

    #=
    # should throw as long as the factor in f vanishes
    f = (a - 2)*(a - 1)*(a - 4)
    @test_throws DomainError polynomial_reconstruction(f, ms[1])
    @test_throws DomainError polynomial_reconstruction(f, ms[2])
    =#

end

#=
@testset "Rational Interpolation tests" begin

    R, (x1, x2, x3) = AA.PolynomialRing(AA.QQ, ["x1", "x2", "x3"])

    fs = [
      (x1 + x2) // x2,
      x1^5 // (x2 + x1 + x3),
      (x2 + x3) // (x1^3 + x1*x2 + x1*x3),
      (x1^4*x2^3 + x1^4*x3 + x2) // x1,
      (x1^6 + x2) // (x1^9 + x3),
      (x1^36 + x3^12 + x1^23 - x2^2 - x2*x3^2 + x1^2*x2^3+(2*x3^4)) // (x1^2 + x2 + x1*x2 + x3)
    ]

    for f in fs
        test_rational_function_interpolation(f)
    end

end
=#

###############################################################################

#=
@testset "Interpolation + shift tests" begin

    R, (x1, x2, x3) = AA.PolynomialRing(AA.QQ, ["x1", "x2", "x3"])

    f = (x1^2*x2 + x3) // (x2 + 3)

    npoints = 100
    ndeg = 3
    nvs = length(AA.gens(R))

    points = [ [ BigInt(j)^i for i in [ (ndeg+1)^k for k in 1:nvs ] ] for j in 1:npoints ]

    xs = []
    ys = []

    shifts, inverses = random_linear_shift(AA.QQ, length(points[1]))
    fp = [ fj(pj) for (fj, pj) in zip(shifts, AA.gens(R)) ]
    g = AA.evaluate(numerator(f), fp) // AA.evaluate(denominator(f), fp)

    for i in 1:npoints
        p = points[i]

        push!(xs, p[1])
        push!(ys,
            AA.evaluate(numerator(g), p) // AA.evaluate(denominator(g), p)
        )
    end

    uniring, x = AA.PolynomialRing(AA.QQ, "x")

    inter = interpolate_multivariate_rational_function(R, uniring, xs, ys)

    lol = [R(0), R(0)]
    for (i, D) in [ [1, numerator(inter)], [2, denominator(inter)] ]
        polybuilder = AA.MPolyBuildCtx(R)
        for (e, c) in zip(0:AA.degree(D), AA.coefficients(D))
            if c == 0
                continue
            end
            AA.push_term!(polybuilder, c, decompose_by_degrees(e, ndeg+1, nvs))
        end
        lol[i] = AA.finish(polybuilder)
    end

    F = lol[1] // lol[2]

    shiftedvariables = [ fj(pj) for (fj, pj) in zip(inverses, AA.gens(R)) ]

    F = AA.evaluate(numerator(F), shiftedvariables) // AA.evaluate(denominator(F), shiftedvariables)

    F = AA.leading_coefficient(numerator(F)) *inv(AA.leading_coefficient(denominator(F))) * (numerator(F) * inv(AA.leading_coefficient(numerator(F)))) // (denominator(F) * inv(AA.leading_coefficient(denominator(F))))

    @test isone(denominator(F // f)) && isone(numerator(F // f))

end
=#

#=
@testset "Degree prediction tests" begin

    fs0 = [
        (x) -> 2,
        (x) -> x,
        (x) -> 1 // x,
        (x) -> (x^2 + 11) // (x + 111)
    ]
    answers0 = [
        (0, 0),
        (1, 0),
        (0, 1),
        (2, 1)
    ]

    fs = [
        (x) -> (x^3 + 1),
        (x) -> (x^16 - 23x^11 + 123),
        (x) -> (x^2 // (x + 1)^4),
        (x) -> ((x - 1) // (x^18 + x^17 - 123)),
        (x) -> ((5x^113 + 2x^55 + 3x^33 + 1) // (x^11 + x + 22))
    ]

    answers = [
        (3, 0),
        (16, 0),
        (2, 4),
        (1, 18),
        (113, 11)
    ]

    FF = AA.GF(2^31-1)

    for (i, f) in enumerate(fs0)
        d = predict_degrees(f, AA.QQ)
        @test answers0[i] == d
    end

    for (i, f) in enumerate(fs)
        d = predict_degrees(f, FF)
        @test answers[i] == d
    end

end
=#

#################################################################

#=
@testset "Kronecker points generation tests" begin
    ground = Sing.GF(2^31 - 1)
    R, (a, b, c) = AA.PolynomialRing(ground, ["a", "b", "c"])

    maxexp = 10
    nvariables = 3

    polys = [
        a,
        a + b + c,
        a^2 + b^2 + c^3,
        a*c + b*c + c^3,
        a*b*c + a^4 + b,
        a^1*b^2*c^3 - 2a - 3b - 4c,
        b^10
    ]

    xs, points = generate_kronecker_points(ground, maxexp, nvariables)

    for poly in polys
        for (point, x) in zip(points, xs)
            symbolic_sub = [a, a^(maxexp+1), a^((maxexp+1)^2)]
            ans = AA.evaluate(AA.evaluate(poly, symbolic_sub), [x, ground(0), ground(0)])
            @test ans == AA.evaluate(poly, point)
        end
    end

end
=#

#=
@testset "Good Kronecker tests" begin
    ns = [1, 5, 37]
    prods = [[1, 2], [1, 2, 6], [1, 5, 25]]

    anses = [
        [[1, 0],  [1, 0, 0], [1, 0, 0]],
        [[1, 2],  [1, 2, 0], [0, 1, 0]],
        [[1, 18], [1, 0, 6], [2, 2, 1]]
    ]

    for (n, ans) in zip(ns, anses)
        hmm = map(p -> decompose_by_good_degrees(n, p, length(p)), prods)

        @test hmm == ans
    end
end
=#

#=
@testset "Kronecker backward substitution tests" begin
    FF = Sing.QQ
    R, (x1, x2, x3) = AA.PolynomialRing(FF, ["x1", "x2", "x3"])
    uni, x = AA.PolynomialRing(FF, "x")

    truefs = [
        x1,
        x2,
        x3,
        x1^2 + x1*x2^2 + x1*x2*x3 + x3 + 1,
        x1 + x2^5 + x3,
    ]
    fs = [
        x,
        x^2,
        x^4,
        x^2 + x*x^8 + x*x^4*x^16 + x^16 + 1,
        x + x^30 + x^36
    ]
    maxexps = [
        1,
        1,
        1,
        3,
        5
    ]

    for (truef, f, maxexp) in zip(truefs, fs, maxexps)
        @test backward_kronecker(f, R, maxexp) == truef
    end
end
=#

@testset "Polynomial interpolation tests" begin

    # over Singular finite field
    FF = Sing.GF(2^31 - 1)

    uniring, x = AA.PolynomialRing(FF, "x")
    test_cases = [
        uniring(1),
        x,
        x^2 + x + 1,
        x^4 - x,
        x^18 + x^17 + 22x^10 + 11
    ]

    for case in test_cases
        numpoints = AA.degree(case) + 1
        xs = [rand(FF) for _ in 1:numpoints]
        ys = [AA.evaluate(case, t) for t in xs]
        ans = interpolate_polynomial(uniring, xs, ys)
        @test ans == case
    end

    # over Singular QQ
    FF = Sing.QQ

    uniring, x = AA.PolynomialRing(FF, "x")
    test_cases = [
        uniring(1),
        x,
        x^2 + x + 1,
        x^4 - x,
        x^18 + x^17 + 22x^10 + 11
    ]

    for case in test_cases
        numpoints = AA.degree(case) + 5
        xs = [rand(FF) for _ in 1:numpoints]
        ys = [AA.evaluate(case, t) for t in xs]
        ans = interpolate_polynomial(uniring, xs, ys)
        @test ans == case
    end
end

@testset "Interpolation tests" begin

    FF = AA.GF(2^31 - 1)
    uniring, x = AA.PolynomialRing(FF, "x")
    test_cases = [
        1 // x,
        (x + 1) // (x + 2),
        (x^3 - 7) // (3 * x^2 + 1),
        (x^18 - 16) // (x + 1),
        (x + 1)^100 // (x + 2)^100,
    ]

    for case in test_cases
        n, d = unpackfrac(case)
        numpoints = 2 * (AA.degree(n) + AA.degree(d))
        xs = [rand(FF) for _ in 1:numpoints]
        ys = [AA.evaluate(n, xval) // AA.evaluate(d, xval) for xval in xs]
        ans = interpolate_rational_function(uniring, xs, ys)
        @test ans == case
    end

    xs = [FF(2), FF(4), FF(8), FF(16)]
    ys = [FF(1), FF(1), FF(1), FF(1)]
    ans = interpolate_rational_function(uniring, xs, ys)
    @test ans == FF(1)

    xs = [FF(2), FF(3)]
    ys = [FF(1), FF(1)]
    ans = interpolate_rational_function(uniring, xs, ys)
    @test ans == FF(1)

end

#=
# for future

using Logging

logger = Logging.SimpleLogger(stderr, Logging.Debug)

Logging.global_logger(logger)
@debug "lol"

uniring, x = AA.PolynomialRing(AA.QQ, "x")
f = (x) -> (x + 1) // (x + 2),

ans = interpolate_rational_function(uniring, xs, ys)
println(ans)

=#


mutable struct State{A<:AbstractVector{<:Complex}}
  ψ::A
end

State(ψ::AbstractVector{<:Real}) = State(float(complex(ψ)))

function State(n::Integer, vs::NTuple{N,Integer}) where N
  ψ = zeros(ComplexF64, 2^n)
  foreach(v -> ψ[v+1] = 1/√length(vs), vs)
  return State(ψ)
end

State(n::Integer) = State(n, (0,))

nbits(s::State) = nbits(s.ψ)
Base.length(s::State) = nbits(s)

labels(s::State) = map(n -> Base.bitstring(n-1)[end-nbits(s)+1:end], 1:length(s.ψ))

probabilities(s::State) = abs2.(s.ψ)

function Base.show(io::IO, s::State; n = 5)
  basis = reverse(collect(Iterators.filter(x -> x[2] ≉ 0, zip(labels(s), s.ψ))))
  length(basis) == 1 && basis[1][2] ≈ 1 && return print(io, "|$(basis[1][1])⟩")
  join(io, ["|$(l)⟩ * ($p)" for (l, p) in take(basis, n)], " +\n")
  length(basis) > n && print(io, "\n    ⋮")
end

function apply!(U::AbstractMatrix, s::State)
  s.ψ = U * s.ψ
  return s
end

function apply!(U::AbstractMatrix, s::State, is...)
  @assert nbits(U) == length(is)
  U = pad(U, 0, nbits(s)-nbits(U))
  S = headfirst(nbits(s), is...)
  apply!(S'*U*S, s)
end

function probabilities(s::State, is...)
  n = length(is)
  M = measure(n, nbits(s)-length(is))*headfirst(nbits(s), is...)
  M*probabilities(s)
end

function sample(ps)
  x = rand()
  s = zero(eltype(ps))
  for i = 1:length(ps)
    s += ps[i]
    s > x && return i
  end
end

function measure!(s::State, is...)
  n = length(is)
  M = measure(n, nbits(s)-length(is))*headfirst(nbits(s), is...)
  ps = M*probabilities(s)
  i = sample(ps)
  s.ψ .*= M[i,:]./sqrt(ps[i])
  bits(i-1, Val{length(is)})
end


#-----------------------------------------------------------------------# Mean
struct Mean
    μ::Float64
    n::Int
end
Mean() = Mean(0.0, 0)
function update(o::Mean, y)
    n = o.n + 1
    Mean(o.μ + (1 / n) * (y - o.μ), n)
end

#-----------------------------------------------------------------------# Sum
struct Sum
    value::Float64
    n::Int
end
Sum() = Sum(0.0, 0)
update(o::Sum, y) = Sum(o.value + y, o.n + 1)


using Distributions, Klara

p = BasicContUnvParameter(:p, pdf=Normal())

model = likelihood_model([p], isindexed=false)

mcsampler = AM(1.)

tuner = VanillaMCTuner()

mcrange = BasicMCRange(nsteps=10000, burnin=1000)

v0 = Dict(:p=>3.11)

outopts = Dict(:monitor=>[:value, :logtarget], :diagnostics=>[:accept])

job = BasicMCJob(model, mcsampler, mcrange, v0, outopts=outopts)

@time run(job)

chain = output(job)

mean(chain)

acceptance(chain)


@testset "S Updates" begin
    S = orth(randn(n, 2w - 1))
    Y = randn(n, 2w - 1)
    p = randn(n)

    S_new = S_update_a(S, Y, p)

    @test S_new' * S_new ≈ I

    S_new = S_update_b(S, Y, p)

    @test S_new' * S_new ≈ I

    # test fails, 0.0 ≉ 1e-17 - the average order of lhs
    # @test S_new'*S ≈ zeros(2w-1, 2w-1) # orthoganlity test 6.1.b

    # resort to a weaker orthogonality test
    @test norm(S_new' * S) ≤ (2w - 1)^2eps(Float64) # orthoganlity test 6.1.b

    S_new = S_update_c(S, Y, p)

    @test S_new' * S_new ≈ I

    # test fails, 0.0 ≉ 1e-17 - the average order of lhs
    # @test S_new'*S ≈ zeros(2w-1, 2w-1) # orthoganlity test 6.1.b

    # resort to a weaker orthogonality test
    @test norm(S_new' * S) ≤ (2w - 1)^2eps(Float64) # orthoganlity test 6.1.c

    S_new = S_update_d(S, Y, p)

    @test S_new' * S_new ≈ I

    S_new = S_update_e(S, Y, p)

    @test S_new' * S_new ≈ I

    S_new = S_update_f(S, Y, p)

    @test S_new' * S_new ≈ I
end

@testset "QN Updates" begin
    U = orth(randn(n, 2w + 1))
    V = ∇²f * U
    x = randn(n)
    y = ∇²f * x
    H = zeros(n, n) + I

    @test U'V ≈ V'U

    @test H' ≈ H

    Hnew = SR1(H, U, V, δ)
    @test Hnew * V ≈ U

    Hnew = PSB(H, U, V, δ)
    @test Hnew * V ≈ U

    Hnew = SR1(H, x, y, δ)
    @test Hnew * y ≈ x

    Hnew = PSB(H, x, y, δ)
    @test Hnew * y ≈ x
end

@testset "testing constructor" begin

    @test S_update(test_driver) ≡ S_UPDATE

    @test QN_update(test_driver) ≡ QN_UPDATE

    @test pflag(test_driver) ≡ PFLAG

    @test Δ_max(test_driver) ≡ Δ_max(test_options)

    @test δ_tol(test_driver) ≡ δ_tol(test_options)

    @test ϵ_tol(test_driver) ≡ ϵ_tol(test_options)

    @test samples(test_driver) ≡ samples(test_options)

    @test max_iterations(test_driver) ≡ max_iterations(test_options)

    @test weave_level(test_driver) ≡ weave_level(test_options)

    @test log_level(test_driver) ≡ log_level(test_options)

    restricted_type_test(test_driver)
end

@testset "testing behavior" begin

    # Mutating test_options from test_driver - test update in test_options
    Δ_max!(test_driver, 1.0)
    @test Δ_max(test_options) ≡ 1.0

    δ_tol!(test_driver, 1.0)
    @test δ_tol(test_options) ≡ 1.0

    ϵ_tol!(test_driver, 1.0)
    @test ϵ_tol(test_options) ≡ 1.0

    samples!(test_driver, 2)
    @test samples(test_options) ≡ 2

    max_iterations!(test_driver, 0)
    @test max_iterations(test_options) ≡ 0

    weave_level!(test_driver, NONE)
    @test weave_level(test_options) ≡ NONE

    log_level!(test_driver, ERROR)
    @test log_level(test_options) ≡ ERROR

    # Mutating test_options from test_options - test update in test_driver
    Δ_max!(test_options, Δₘ)
    @test Δ_max(test_driver) ≡ Δₘ

    δ_tol!(test_options, δ)
    @test δ_tol(test_driver) ≡ δ

    ϵ_tol!(test_options, ϵ)
    @test ϵ_tol(test_driver) ≡ ϵ

    samples!(test_options, 2w)
    @test samples(test_driver) ≡ 2w

    max_iterations!(test_options, N)
    @test max_iterations(test_driver) ≡ N

    weave_level!(test_options, WEAVE_LEVEL)
    @test weave_level(test_driver) ≡ WEAVE_LEVEL

    log_level!(test_options, LOG_LEVEL)
    @test log_level(test_driver) ≡ LOG_LEVEL
end


using JuMP, GLPK, LinearAlgebra, Random, HiGHS
using MKL
using LinearOptimizationSolver
using BenchmarkTools, BenchmarkPlots, StatsPlots

optimizer = () -> GLPK.Optimizer()

function build_problem(p::Integer, k::Integer)
    Random.seed!(123)
    e_p = rand(1:5,p)
    cc_p = rand(1:9,p)
    oc_p = rand(1:2,p)
    ic_k = rand(1:30,k)
    dd_k = rand(1:5,k)
    du_k = rand(1:1,k)
    r_k = [dd_k[i]*du_k[i] for i in 1:k]

    lenvars = [p, p*k, k];
    n = sum(lenvars)

    I_pp = Matrix(I,p,p)
    I_kk = Matrix(I,k,k)

    Z_pk = zeros(p,k)
    Z_kp = zeros(k,p)

    D = zeros(p,p*k)
    for i in 1:p
        D[i,1+(i-1)*k:i*k] .= 1
    end

    E = zeros(k,p*k)
    for i in 1:p
        E[:,1+(i-1)*k:i*k] = I_kk
    end

    A = [-I_pp D Z_pk; Z_kp E I_kk; Z_kp -E -I_kk]

    b = vcat(e_p, r_k, -r_k)

    c_y = zeros(p*k)

    for i in 1:p, j in 1:k
        c_y[(i-1)*k+j] = du_k[j]*oc_p[i]
    end

    c = -[cc_p' c_y' ic_k']

    m = p+2*k

    A = [A Matrix(I,m,m)]
    c = vcat(c',zeros(m))
    return A, b, c, n, m
end

p = 30
k = 10
A, b, c, n, m = build_problem(p, k)

input = create(A, b, c, solver = 1, verbose=false)
bench1 = @benchmark output = solve(input)
input = create(A, b, c, solver = 0, verbose=false)
bench2 = @benchmark output = solve(input)

function GLPK!(A, b, c)
    model_pl = Model(GLPK.Optimizer);
    m = size(A,1);
    n = size(A,2) - length(b);
    @variable(model_pl, X[1:n + m] >= 0);
    @constraint(model_pl, A*X .== b);
    @objective(model_pl, Max, c'X);
    optimize!(model_pl);
end

function HiGHS!(A, b, c)
    model_pl = Model(HiGHS.Optimizer);
    set_silent(model_pl)
    m = size(A,1);
    n = size(A,2) - length(b);
    @variable(model_pl, X[1:n + m] >= 0);
    @constraint(model_pl, A*X .== b);
    @objective(model_pl, Max, c'X);
    optimize!(model_pl);
end

bench3 = @benchmark GLPK!($A, $b, $c)
bench4 = @benchmark HiGHS!($A, $b, $c)

plotd = plot(bench1,yaxis=:log10,st=:violin)
plot!(bench2,yaxis=:log10,st=:violin,xticks=(1:2,["IP" "Simplex"]))
plot!(bench3,yaxis=:log10,st=:violin,xticks=(1:3,["IP" "Simplex" "GLPK"]))
plot!(bench4,yaxis=:log10,st=:violin,xticks=(1:4,["IP" "Simplex" "GLPK" "HiGHS"]))

savefig(plotd,"examples\\p=$(p),k=$(k).png")


function spline_basis(domain, p)
    R_spline = R"splines::ns($domain, $p)"
    Q = convert(Matrix{Float64}, R_spline)
    return Q
end

struct Atomic <: Distributions.ValueSupport end
struct ExpoSpline{T<:Real} <: UnivariateDistribution{Atomic}
    domain::Vector{Float64}
    Qbasis::Matrix{Float64} # basis matrix
    αcoef::Vector{T} # α
    Qα::Vector{T}
    φ_α::T # normalization constant
end
function ExpoSpline(domain, Qbasis::Matrix{Float64}, αcoef::Vector{T}) where {T <: Real}
    Qα = Qbasis * αcoef
    φ_α = log_sum_exp(Qα) # equation (10)
    return ExpoSpline(domain, Qbasis, αcoef, Qα, φ_α)
end
function ExpoSpline(domain, αcoef::Vector{T}) where {T <: Real}
    p = length(αcoef)
    Q = spline_basis(domain, p)
    return ExpoSpline(domain, Q, αcoef)
end
## doesn't work for ExpoSpline immutable ##
# function update_coefs!(d::ExpoSpline{T}, α::Vector{T}) where {T <: Real}
    # d.αcoef = α
    # LinAlg.A_mul_B!(d.Qα, d.Qbasis, α)
    # d.φ_α = log_sum_exp(d.Qα)
    # return d
# end

####################################
# Expo Spline distribution methods #
####################################

params(d::ExpoSpline) = (d.αcoef, )
coefs(d::ExpoSpline) = d.αcoef
partype(d::ExpoSpline{T}) where T = T

function log_sum_exp(x)
    # https://en.wikipedia.org/wiki/LogSumExp
    max_x = maximum(x)
    log_sum_exp_x = max_x + log(sum(xi -> exp(xi-max_x), x))
    return log_sum_exp_x
end
logpdf(d::ExpoSpline) = d.Qα .- d.φ_α             # equation (9)
pdf(d::ExpoSpline) = exp.(logpdf(d))
logpdf(d::ExpoSpline, j::Int) = d.Qα[j] - d.φ_α # equation (9)
function logpdf(d::ExpoSpline, x::Real)
    j = argmin(abs.(d.domain .- x)) # not the fastest way to do this
    return logpdf(d, j)
end
function pdf(d::ExpoSpline, x::Real)
    return exp(logpdf(d, x))
end

function logcdf(d::ExpoSpline)
    max_x = maximum(d.Qα)
    cumsum_exp_x = accumulate((s,xi) -> s + exp(xi - max_x), d.Qα; init=zero(max_x))
    log_last = log(cumsum_exp_x[end])
    lcdf = cumsum_exp_x # to modify in place
    @inbounds for (i, x) in enumerate(cumsum_exp_x)
        lcdf[i] = log(x) - log_last
    end
    return lcdf
end
cdf(d::ExpoSpline) = exp.(logcdf(d))
cdf(d::ExpoSpline, j::Int) = exp(logcdf(d)[j]) # wasteful?
function cdf(d::ExpoSpline, x::Real)
    j = argmin(abs.(d.domain .- x)) # inefficient
    return cdf(d, j)
end

function rand(d::ExpoSpline)
    lcdf = logcdf(d)
    logu = -Random.randexp()
    i = searchsortedfirst(lcdf, logu)
    return d.domain[min(i,length(lcdf))]
end
function rand(d::ExpoSpline, n::Int)
    lcdf = logcdf(d)
    logu = -Random.randexp(n)
    r = similar(d.domain, n)
    for j in 1:n
        logu = -Random.randexp()
        i = searchsortedfirst(lcdf, logu)
        r[j] = d.domain[min(i,length(lcdf))]
    end
    return r
end

#################
# Likelihood ####
#################
function get_logPi(domain::Vector{Float64}, noise::UnivariateDistribution, X_i::Float64)
    # unnormalized
    return logpdf.(noise, X_i.-domain)
end
function get_Pi(domain::Vector{Float64}, noise::UnivariateDistribution, X_i::Float64)
    # unnormalized
    return pdf.(noise, X_i.-domain)
end
get_logPi(d::ExpoSpline, noise, X_i) = get_logPi(d.domain, noise, X_i)
get_Pi(d::ExpoSpline, noise, X_i) = get_Pi(d.domain, noise, X_i)
function loglikelihood(logPi::AbstractVector{Float64}, loggα::Vector{T}) where T<:Real
    max_x = maximum(logPi) + maximum(loggα)
    sum_exp_x = zero(max_x)
    @inbounds for (logPi_j, loggα_j) in zip(logPi, loggα)
        sum_exp_x += exp(logPi_j + loggα_j - max_x)
    end
    return max_x + log(sum_exp_x)
end
function loglikelihood(d::ExpoSpline, logPi::AbstractVector{Float64})
    loggα = logpdf(d)
    return loglikelihood(logPi, loggα)
end
function getWi(d::ExpoSpline, logPi::AbstractVector{Float64})
    # equation (14)-(15)
    loggα = logpdf(d)
    logfi = loglikelihood(logPi, loggα)
    Wi = exp.(loggα) .* (exp.(logPi .- logfi) .- 1)
    return Wi
end
function dloglik(d::ExpoSpline, logPi::AbstractVector{Float64})
    # equation (16)
    return d.Qbasis'getWi(d, logPi)
end
function d2loglik(d::ExpoSpline, logPi::AbstractVector{Float64})
    gα = pdf(d)
    Wi = getWi(d, logPi)
    return -d.Qbasis'*(Wi*Wi' + Wi*gα' + gα*Wi' - diagm(Wi))*d.Qbasis
end
function loglikelihood(d::ExpoSpline, noise::UnivariateDistribution, X_i::Float64)
    logPi = get_logPi(d.domain, noise, X_i)
    return loglikelihood(d, logPi)
end

function make_loglik_w_noise_pre(d::ExpoSpline, noise, X::Vector{Float64})
    logP = get_logPi.(d, noise, X)
    mloglik_w_noise_pre = function (α)
        dα = ExpoSpline(d.domain, d.Qbasis, α)
        loglik = sum(Pi -> loglikelihood(dα, Pi), logP)
        return -loglik
    end
    return mloglik_w_noise_pre
end
function make_loglik_noisefree(d::ExpoSpline, θ::Vector{Float64})
    mloglik = (α) -> -sum(
        logpdf.( # compute the log-PDF under the…
            ExpoSpline(d.domain, d.Qbasis, α), # …candidate distribution…
            θ # …of the noise-free data
            )
        )
    return mloglik
end
##############
# with Prior #
##############
function make_penalized_w_noise_pre(d::ExpoSpline, noise, X::Vector{Float64}, c0::Real)
    logP = get_logPi.(d, noise, X)
    target = function (α)
        dα = ExpoSpline(d.domain, d.Qbasis, α)
        loglik = sum(Pi -> loglikelihood(dα, Pi), logP)
        penalty = c0 * norm(α, 2)
        return -loglik + penalty
    end
    return target
end

################
# Optimization #
################
function decon(d, noise, X::Vector{Float64}, c0::Real)
    f = make_penalized_w_noise_pre(d, noise, X, c0)
    td = Optim.TwiceDifferentiable(f, copy(coefs(d)); autodiff=:forward)
    opt_reg = Optim.optimize(td, zero(coefs(d)), Optim.Newton())
    best_x = Optim.minimizer(opt_reg)
    best_y = Optim.minimum(opt_reg)
    for _ in 1:5
        if Optim.converged(opt_reg)
            break
        end
        init_x = randn(length(coefs(d)))
        opt_reg = Optim.optimize(td, init_x, Optim.Newton())
        if Optim.minimum(opt_reg) < best_y
            best_x = Optim.minimizer(opt_reg)
            best_y = Optim.minimum(opt_reg)
        end
    end
    for _ in 1:5
        if Optim.converged(opt_reg)
            break
        end
        init_x = randn(length(coefs(d)))
        opt_reg = Optim.optimize(td, init_x, Optim.ConjugateGradient())
        if Optim.minimum(opt_reg) < best_y
            best_x = Optim.minimizer(opt_reg)
            best_y = Optim.minimum(opt_reg)
        end
    end
    if !Optim.converged(opt_reg)
        @warn("decon failed to converge")
    end
    d_optim = ExpoSpline(d.domain, d.Qbasis, best_x)
    return d_optim
end


# ===============================
# Written by AAE
# TU Delft, Spring 2014
# simulkade.com
# ===============================

# ================================================================
# Changes:
#    2014-12-30 added 2D radial and 3D cylindrical grids
#    2015-01-10 extended to accept nonuniform grids
# ================================================================

# %% ======================= DIFFUSION TERM ================================
"""
"""
function diffusionTerm(D::FaceValue)
    d = D.domain.dimension
    if d==1
      M = diffusionTerm1D(D)
    elseif d==1.5
      M = diffusionTermCylindrical1D(D)
    elseif d==2
      M, Mx, My = diffusionTerm2D(D)
    elseif d==2.5
      M, Mx, My = diffusionTermCylindrical2D(D)
    elseif d==2.8
      M, Mx, My = diffusionTermRadial2D(D)
    elseif d==3
      M, Mx, My, Mz = diffusionTerm3D(D)
    elseif d==3.2
      M, Mx, My, Mz = diffusionTermCylindrical3D(D)
    end
    M
end

# ======================== 1D CARTESIAN DIFFUSION =========================
function diffusionTerm1D(D::FaceValue)
# D is a face variable

# extract data from the mesh structure
Nx = D.domain.dims[1]
G = [1:Nx+2;]
DX = D.domain.cellsize.x
dx = 0.5*(DX[1:end-1]+DX[2:end])

# define the vectors to store the sparse matrix data
iix = zeros(Int64, 3*(Nx+2))
jjx = zeros(Int64, 3*(Nx+2))
sx = zeros(Float64, 3*(Nx+2))

# reassign the east, west for code readability
De = D.xvalue[2:Nx+1]./(dx[2:Nx+1].*DX[2:Nx+1])
Dw = D.xvalue[1:Nx]./(dx[1:Nx].*DX[2:Nx+1])

# calculate the coefficients for the internal cells
AE = reshape(De,Nx)
AW = reshape(Dw,Nx)
APx = -(AE+AW)

# build the sparse matrix based on the numbering system
rowx_index = reshape(G[2:Nx+1],Nx) # main diagonal x
iix[1:3*Nx] = repmat(rowx_index,3)
jjx[1:3*Nx] = [reshape(G[1:Nx],Nx); reshape(G[2:Nx+1],Nx); reshape(G[3:Nx+2],Nx)]
sx[1:3*Nx] = [AW; APx; AE]

# build the sparse matrix
kx = 3*Nx
M = sparse(iix[1:kx], jjx[1:kx], sx[1:kx], Nx+2, Nx+2)
end

# ======================== 2D CARTESIAN DIFFUSION =========================
function diffusionTerm2D(D::FaceValue)
# D is a face variable
# extract data from the mesh structure
Nx = D.domain.dims[1]
Ny = D.domain.dims[2]
G=reshape([1:(Nx+2)*(Ny+2);], Nx+2, Ny+2)

DX = D.domain.cellsize.x
DY = Array(Float64, 1, Ny+2)
DY[:] = D.domain.cellsize.y
dx = 0.5*(DX[1:end-1]+DX[2:end])
dy = Array(Float64, 1, Ny+1)
dy[:] = 0.5*(DY[1:end-1]+DY[2:end])

# define the vectors to store the sparse matrix data
iix = zeros(Int64, 3*(Nx+2)*(Ny+2))
iiy = zeros(Int64, 3*(Nx+2)*(Ny+2))
jjx = zeros(Int64, 3*(Nx+2)*(Ny+2))
jjy = zeros(Int64, 3*(Nx+2)*(Ny+2))
sx = zeros(Float64, 3*(Nx+2)*(Ny+2))
sy = zeros(Float64, 3*(Nx+2)*(Ny+2))
mnx = Nx*Ny
mny = Nx*Ny

# reassign the east, west for code readability (use broadcasting in Julia)
De = D.xvalue[2:Nx+1,:]./(dx[2:Nx+1].*DX[2:Nx+1])
Dw = D.xvalue[1:Nx,:]./(dx[1:Nx].*DX[2:Nx+1])
Dn = D.yvalue[:,2:Ny+1]./(dy[:,2:Ny+1].*DY[:,2:Ny+1])
Ds = D.yvalue[:,1:Ny]./(dy[:,1:Ny].*DY[:,2:Ny+1])

# calculate the coefficients for the internal cells
AE = reshape(De,mnx)
AW = reshape(Dw,mnx)
AN = reshape(Dn,mny)
AS = reshape(Ds,mny)
APx = -(AE+AW)
APy = -(AN+AS)

# build the sparse matrix based on the numbering system
rowx_index = reshape(G[2:Nx+1,2:Ny+1],mnx) # main diagonal x
iix[1:3*mnx] = repmat(rowx_index,3)
rowy_index = reshape(G[2:Nx+1,2:Ny+1],mny) # main diagonal y
iiy[1:3*mny] = repmat(rowy_index,3)
jjx[1:3*mnx] = [reshape(G[1:Nx,2:Ny+1],mnx); reshape(G[2:Nx+1,2:Ny+1],mnx); reshape(G[3:Nx+2,2:Ny+1],mnx)]
jjy[1:3*mny] = [reshape(G[2:Nx+1,1:Ny],mny); reshape(G[2:Nx+1,2:Ny+1],mny); reshape(G[2:Nx+1,3:Ny+2],mny)]
sx[1:3*mnx] = [AW; APx; AE]
sy[1:3*mny] = [AS; APy; AN]

# build the sparse matrix
kx = 3*mnx
ky = 3*mny
Mx = sparse(iix[1:kx], jjx[1:kx], sx[1:kx], (Nx+2)*(Ny+2), (Nx+2)*(Ny+2))
My = sparse(iiy[1:ky], jjy[1:ky], sy[1:ky], (Nx+2)*(Ny+2), (Nx+2)*(Ny+2))
M = Mx + My
(M, Mx, My)
end

# ======================== 3D CARTESIAN DIFFUSION =========================
function diffusionTerm3D(D::FaceValue)
# D is a face variable
# extract data from the mesh structure
Nx = D.domain.dims[1]
Ny = D.domain.dims[2]
Nz = D.domain.dims[3]
G=reshape([1:(Nx+2)*(Ny+2)*(Nz+2);], Nx+2, Ny+2, Nz+2)

# DX = repeat(D.domain.cellsize.x, outer=(1, Ny, Nz))
# DY = repeat(D.domain.cellsize.y.', outer=(Nx, 1, Nz))
# DZ = zeros(1,1,Nz+2);
# DZ(1,1,:) = D.domain.cellsize.z;
# DZ=repmat(DZ, Nx, Ny, 1);
# dx = 0.5*(DX(1:end-1,:,:)+DX(2:end,:,:));
# dy = 0.5*(DY(:,1:end-1,:)+DY(:,2:end,:));
# dz = 0.5*(DZ(:,:,1:end-1)+DZ(:,:,2:end));

DX = D.domain.cellsize.x
DY = Array(Float64, 1, Ny+2)
DY[:] = D.domain.cellsize.y
DZ = Array(Float64, 1,1,Nz+2)
DZ[:] = D.domain.cellsize.z
dx = 0.5*(DX[1:end-1]+DX[2:end])
dy = 0.5*(DY[:,1:end-1]+DY[:,2:end])
dz = 0.5*(DZ[:,:,1:end-1]+DZ[:,:,2:end])

# define the vectors to store the sparse matrix data
iix = zeros(Int64, 3*(Nx+2)*(Ny+2)*(Nz+2))
jjx = zeros(Int64, 3*(Nx+2)*(Ny+2)*(Nz+2))
sx = zeros(Float64, 3*(Nx+2)*(Ny+2)*(Nz+2))
iiy = zeros(Int64, 3*(Nx+2)*(Ny+2)*(Nz+2))
jjy = zeros(Int64, 3*(Nx+2)*(Ny+2)*(Nz+2))
sy = zeros(Float64, 3*(Nx+2)*(Ny+2)*(Nz+2))
iiz = zeros(Int64, 3*(Nx+2)*(Ny+2)*(Nz+2))
jjz = zeros(Int64, 3*(Nx+2)*(Ny+2)*(Nz+2))
sz = zeros(Float64, 3*(Nx+2)*(Ny+2)*(Nz+2))
mnx = Nx*Ny*Nz
mny = Nx*Ny*Nz
mnz = Nx*Ny*Nz

# reassign the east, west, north, and south velocity vectors for the
# code readability (use broadcasting)
De = D.xvalue[2:Nx+1,:,:]./(dx[2:Nx+1].*DX[2:Nx+1])
Dw = D.xvalue[1:Nx,:,:]./(dx[1:Nx].*DX[2:Nx+1])
Dn = D.yvalue[:,2:Ny+1,:]./(dy[:,2:Ny+1].*DY[:,2:Ny+1])
Ds = D.yvalue[:,1:Ny,:]./(dy[:,1:Ny].*DY[:,2:Ny+1])
Df = D.zvalue[:,:,2:Nz+1]./(dz[:,:,2:Nz+1].*DZ[:,:,2:Nz+1])
Db = D.zvalue[:,:,1:Nz]./(dz[:,:,1:Nz].*DZ[:,:,2:Nz+1])

# calculate the coefficients for the internal cells
AE = reshape(De,mnx)
AW = reshape(Dw,mnx)
AN = reshape(Dn,mny)
AS = reshape(Ds,mny)
AF = reshape(Df,mnz)
AB = reshape(Db,mnz)
APx = reshape(-(De+Dw),mnx)
APy = reshape(-(Dn+Ds),mny)
APz = reshape(-(Df+Db),mnz)

# build the sparse matrix based on the numbering system
rowx_index = reshape(G[2:Nx+1,2:Ny+1,2:Nz+1],mnx)  # main diagonal x
iix[1:3*mnx] = repmat(rowx_index,3)
rowy_index = reshape(G[2:Nx+1,2:Ny+1,2:Nz+1],mny)  # main diagonal y
iiy[1:3*mny] = repmat(rowy_index,3);
rowz_index = reshape(G[2:Nx+1,2:Ny+1,2:Nz+1],mnz)  # main diagonal z
iiz[1:3*mnz] = repmat(rowz_index,3)
jjx[1:3*mnx] = [reshape(G[1:Nx,2:Ny+1,2:Nz+1],mnx);
		reshape(G[2:Nx+1,2:Ny+1,2:Nz+1],mnx);
		reshape(G[3:Nx+2,2:Ny+1,2:Nz+1],mnx)]
jjy[1:3*mny] = [reshape(G[2:Nx+1,1:Ny,2:Nz+1],mny);
		reshape(G[2:Nx+1,2:Ny+1,2:Nz+1],mny);
		reshape(G[2:Nx+1,3:Ny+2,2:Nz+1],mny)]
jjz[1:3*mnz] = [reshape(G[2:Nx+1,2:Ny+1,1:Nz],mnz);
		reshape(G[2:Nx+1,2:Ny+1,2:Nz+1],mnz);
		reshape(G[2:Nx+1,2:Ny+1,3:Nz+2],mnz)];
sx[1:3*mnx] = [AW; APx; AE]
sy[1:3*mny] = [AS; APy; AN]
sz[1:3*mnz] = [AB; APz; AF]

# build the sparse matrix
kx = 3*mnx
ky = 3*mny
kz = 3*mnz
Mx = sparse(iix[1:kx], jjx[1:kx], sx[1:kx], (Nx+2)*(Ny+2)*(Nz+2), (Nx+2)*(Ny+2)*(Nz+2))
My = sparse(iiy[1:ky], jjy[1:ky], sy[1:ky], (Nx+2)*(Ny+2)*(Nz+2), (Nx+2)*(Ny+2)*(Nz+2))
Mz = sparse(iiz[1:kz], jjz[1:kz], sz[1:kz], (Nx+2)*(Ny+2)*(Nz+2), (Nx+2)*(Ny+2)*(Nz+2))
M = Mx + My + Mz
(M, Mx, My, Mz)
end

# ======================== 1D Cylindrical DIFFUSION =========================
function diffusionTermCylindrical1D(D::FaceValue)
# D is a face variable

# extract data from the mesh structure
Nx = D.domain.dims[1]
G = [1:Nx+2;]
DX = D.domain.cellsize.x
dx = 0.5*(DX[1:end-1]+DX[2:end])
rp = D.domain.cellcenters.x
rf = D.domain.facecenters.x

# define the vectors to store the sparse matrix data
iix = zeros(Int64, 3*(Nx+2))
jjx = zeros(Int64, 3*(Nx+2))
sx = zeros(Float64, 3*(Nx+2))

# reassign the east, west for code readability
De = rf[2:Nx+1].*D.xvalue[2:Nx+1]./(rp.*dx[2:Nx+1].*DX[2:Nx+1])
Dw = rf[1:Nx].*D.xvalue[1:Nx]./(rp.*dx[1:Nx].*DX[2:Nx+1])

# calculate the coefficients for the internal cells
AE = reshape(De,Nx)
AW = reshape(Dw,Nx)
APx = reshape(-(De+Dw),Nx)

# build the sparse matrix based on the numbering system
rowx_index = reshape(G[2:Nx+1],Nx) # main diagonal x
iix[1:3*Nx] = repmat(rowx_index,3)
jjx[1:3*Nx] = [reshape(G[1:Nx],Nx); reshape(G[2:Nx+1],Nx); reshape(G[3:Nx+2],Nx)]
sx[1:3*Nx] = [AW; APx; AE]

# build the sparse matrix
kx = 3*Nx
M = sparse(iix[1:kx], jjx[1:kx], sx[1:kx], Nx+2, Nx+2)
end

# ======================== 2D RADIAL DIFFUSION =========================
function diffusionTermRadial2D(D::FaceValue)
# D is a face variable
# extract data from the mesh structure

Nr = D.domain.dims[1]
Ntheta = D.domain.dims[2]
G=reshape([1:(Nr+2)*(Ntheta+2);], Nr+2, Ntheta+2)
DR = D.domain.cellsize.x
DTHETA = Array(Float64, 1, Ntheta+2)
DTHETA[:] = D.domain.cellsize.y
dr = 0.5*(DR[1:end-1]+DR[2:end])
dtheta = Array(Float64, 1, Ntheta+1)
dtheta[:] = 0.5*(DTHETA[1:end-1]+DTHETA[2:end])
rp = D.domain.cellcenters.x
rf = D.domain.facecenters.x

# define the vectors to store the sparse matrix data
iix = zeros(Int64, 3*(Nr+2)*(Ntheta+2))
iiy = zeros(Int64, 3*(Nr+2)*(Ntheta+2))
jjx = zeros(Int64, 3*(Nr+2)*(Ntheta+2))
jjy = zeros(Int64, 3*(Nr+2)*(Ntheta+2))
sx = zeros(Float64, 3*(Nr+2)*(Ntheta+2))
sy = zeros(Float64, 3*(Nr+2)*(Ntheta+2))
mnx = Nr*Ntheta
mny = Nr*Ntheta

# reassign the east, west for code readability
De = rf[2:Nr+1].*D.xvalue[2:Nr+1,:]./(rp.*dr[2:Nr+1].*DR[2:Nr+1])
Dw = rf[1:Nr].*D.xvalue[1:Nr,:]./(rp.*dr[1:Nr].*DR[2:Nr+1])
Dn = D.yvalue[:,2:Ntheta+1]./(rp.*rp.*dtheta[:,2:Ntheta+1].*DTHETA[:,2:Ntheta+1])
Ds = D.yvalue[:,1:Ntheta]./(rp.*rp.*dtheta[:,1:Ntheta].*DTHETA[:,2:Ntheta+1])

# calculate the coefficients for the internal cells
AE = reshape(De,mnx)
AW = reshape(Dw,mnx)
AN = reshape(Dn,mny)
AS = reshape(Ds,mny)
APx = reshape(-(De+Dw),mnx)
APy = reshape(-(Dn+Ds),mny)

# build the sparse matrix based on the numbering system
rowx_index = reshape(G[2:Nr+1,2:Ntheta+1],mnx) # main diagonal x
iix[1:3*mnx] = repmat(rowx_index,3)
rowy_index = reshape(G[2:Nr+1,2:Ntheta+1],mny) # main diagonal y
iiy[1:3*mny] = repmat(rowy_index,3)
jjx[1:3*mnx] = [reshape(G[1:Nr,2:Ntheta+1],mnx); reshape(G[2:Nr+1,2:Ntheta+1],mnx); reshape(G[3:Nr+2,2:Ntheta+1],mnx)]
jjy[1:3*mny] = [reshape(G[2:Nr+1,1:Ntheta],mny); reshape(G[2:Nr+1,2:Ntheta+1],mny); reshape(G[2:Nr+1,3:Ntheta+2],mny)]
sx[1:3*mnx] = [AW; APx; AE]
sy[1:3*mny] = [AS; APy; AN]

# build the sparse matrix
kx = 3*mnx
ky = 3*mny
Mx = sparse(iix[1:kx], jjx[1:kx], sx[1:kx], (Nr+2)*(Ntheta+2), (Nr+2)*(Ntheta+2))
My = sparse(iiy[1:ky], jjy[1:ky], sy[1:ky], (Nr+2)*(Ntheta+2), (Nr+2)*(Ntheta+2))
M = Mx + My
(M, Mx, My)
end

# ======================== 2D CYLINDRICAL DIFFUSION =========================
function diffusionTermCylindrical2D(D::FaceValue)
# D is a face variable
# extract data from the mesh structure
Nr = D.domain.dims[1]
Nz = D.domain.dims[2]
G=reshape([1:(Nr+2)*(Nz+2);], Nr+2, Nz+2)
DR = D.domain.cellsize.x
DZ = Array(Float64, 1, Nz+2)
DZ[:] = D.domain.cellsize.y
dr = 0.5*(DR[1:end-1]+DR[2:end])
dz = Array(Float64, 1, Nz+1)
dz[:] = 0.5*(DZ[1:end-1]+DZ[2:end])
rp = repmat(D.domain.cellcenters.x, 1, Nz)
rf = repmat(D.domain.facecenters.x, 1, Nz)

# define the vectors to store the sparse matrix data
iix = zeros(Int64, 3*(Nr+2)*(Nz+2))
iiy = zeros(Int64, 3*(Nr+2)*(Nz+2))
jjx = zeros(Int64, 3*(Nr+2)*(Nz+2))
jjy = zeros(Int64, 3*(Nr+2)*(Nz+2))
sx = zeros(Float64, 3*(Nr+2)*(Nz+2))
sy = zeros(Float64, 3*(Nr+2)*(Nz+2))
mnx = Nr*Nz
mny = Nr*Nz

# extract the diffusion coefficient data
Dx = D.xvalue
Dy = D.yvalue

# reassign the east, west for code readability
De = rf[2:Nr+1,:].*D.xvalue[2:Nr+1,:]./(rp.*dr[2:Nr+1].*DR[2:Nr+1])
Dw = rf[1:Nr,:].*D.xvalue[1:Nr,:]./(rp.*dr[1:Nr].*DR[2:Nr+1])
Dn = D.yvalue[:,2:Nz+1]./(dz[:,2:Nz+1].*DZ[:,2:Nz+1])
Ds = D.yvalue[:,1:Nz]./(dz[:,1:Nz].*DZ[:,2:Nz+1])

# calculate the coefficients for the internal cells
AE = reshape(De,mnx)
AW = reshape(Dw,mnx)
AN = reshape(Dn,mny)
AS = reshape(Ds,mny)
APx = reshape(-(De+Dw),mnx)
APy = reshape(-(Dn+Ds),mny)

# build the sparse matrix based on the numbering system
rowx_index = reshape(G[2:Nr+1,2:Nz+1],mnx) # main diagonal x
iix[1:3*mnx] = repmat(rowx_index,3)
rowy_index = reshape(G[2:Nr+1,2:Nz+1],mny) # main diagonal y
iiy[1:3*mny] = repmat(rowy_index,3)
jjx[1:3*mnx] = [reshape(G[1:Nr,2:Nz+1],mnx); reshape(G[2:Nr+1,2:Nz+1],mnx); reshape(G[3:Nr+2,2:Nz+1],mnx)]
jjy[1:3*mny] = [reshape(G[2:Nr+1,1:Nz],mny); reshape(G[2:Nr+1,2:Nz+1],mny); reshape(G[2:Nr+1,3:Nz+2],mny)]
sx[1:3*mnx] = [AW; APx; AE]
sy[1:3*mny] = [AS; APy; AN]

# build the sparse matrix
kx = 3*mnx
ky = 3*mny
Mx = sparse(iix[1:kx], jjx[1:kx], sx[1:kx], (Nr+2)*(Nz+2), (Nr+2)*(Nz+2))
My = sparse(iiy[1:ky], jjy[1:ky], sy[1:ky], (Nr+2)*(Nz+2), (Nr+2)*(Nz+2))
M = Mx + My
(M, Mx, My)
end

# ======================== 3D CYLINDRICAL DIFFUSION =========================
function diffusionTermCylindrical3D(D::FaceValue)
# extract data from the mesh structure

Nr = D.domain.dims[1]
Ntheta = D.domain.dims[2]
Nz = D.domain.dims[3]
G=reshape([1:(Nr+2)*(Ntheta+2)*(Nz+2);], Nr+2, Ntheta+2, Nz+2)
DR = D.domain.cellsize.x
DTHETA = Array(Float64, 1, Ntheta+2)
DTHETA[:] = D.domain.cellsize.y
DZ = Array(Float64, 1,1,Nz+2)
DZ[:] = D.domain.cellsize.z
dr = 0.5*(DR[1:end-1]+DR[2:end])
dtheta = 0.5*(DTHETA[:,1:end-1]+DTHETA[:,2:end])
dz = 0.5*(DZ[:,:,1:end-1]+DZ[:,:,2:end])
rp = D.domain.cellcenters.x # use broadcasting
rf = D.domain.facecenters.x

# define the vectors to stores the sparse matrix data
iix = zeros(Int64, 3*(Nr+2)*(Ntheta+2)*(Nz+2))
jjx = zeros(Int64, 3*(Nr+2)*(Ntheta+2)*(Nz+2))
sx = zeros(Float64, 3*(Nr+2)*(Ntheta+2)*(Nz+2))
iiy = zeros(Int64, 3*(Nr+2)*(Ntheta+2)*(Nz+2))
jjy = zeros(Int64, 3*(Nr+2)*(Ntheta+2)*(Nz+2))
sy = zeros(Float64, 3*(Nr+2)*(Ntheta+2)*(Nz+2))
iiz = zeros(Int64, 3*(Nr+2)*(Ntheta+2)*(Nz+2))
jjz = zeros(Int64, 3*(Nr+2)*(Ntheta+2)*(Nz+2))
sz = zeros(Float64, 3*(Nr+2)*(Ntheta+2)*(Nz+2))
mnx = Nr*Ntheta*Nz
mny = Nr*Ntheta*Nz
mnz = Nr*Ntheta*Nz

# extract the velocity data
# note: size(ux) = [1:m+1, 1:n] and size(uy) = [1:m, 1:n+1]
Dx = D.xvalue
Dy = D.yvalue
Dz = D.zvalue

# reassign the east, west, north, and south velocity vectors for the
# code readability
De = rf[2:Nr+1].*D.xvalue[2:Nr+1,:,:]./(rp.*dr[2:Nr+1].*DR[2:Nr+1])
Dw = rf[1:Nr].*D.xvalue[1:Nr,:,:]./(rp.*dr[1:Nr].*DR[2:Nr+1])
Dn = D.yvalue[:,2:Ntheta+1,:]./(rp.*rp.*dtheta[:,2:Ntheta+1].*DTHETA[:,2:Ntheta+1])
Ds = D.yvalue[:,1:Ntheta,:]./(rp.*rp.*dtheta[:,1:Ntheta].*DTHETA[:,2:Ntheta+1])
Df = D.zvalue[:,:,2:Nz+1]./(dz[:,:,2:Nz+1].*DZ[:,:,2:Nz+1])
Db = D.zvalue[:,:,1:Nz]./(dz[:,:,1:Nz].*DZ[:,:,2:Nz+1])

# calculate the coefficients for the internal cells
AE = reshape(De,mnx)
AW = reshape(Dw,mnx)
AN = reshape(Dn,mny)
AS = reshape(Ds,mny)
AF = reshape(Df,mnz)
AB = reshape(Db,mnz)
APx = reshape(-(De+Dw),mnx)
APy = reshape(-(Dn+Ds),mny)
APz = reshape(-(Df+Db),mnz)

# build the sparse matrix based on the numbering system
rowx_index = reshape(G[2:Nr+1,2:Ntheta+1,2:Nz+1],mnx)  # main diagonal x
iix[1:3*mnx] = repmat(rowx_index,3)
rowy_index = reshape(G[2:Nr+1,2:Ntheta+1,2:Nz+1],mny)  # main diagonal y
iiy[1:3*mny] = repmat(rowy_index,3)
rowz_index = reshape(G[2:Nr+1,2:Ntheta+1,2:Nz+1],mnz)  # main diagonal z
iiz[1:3*mnz] = repmat(rowz_index,3)
jjx[1:3*mnx] = [reshape(G[1:Nr,2:Ntheta+1,2:Nz+1],mnx);
		reshape(G[2:Nr+1,2:Ntheta+1,2:Nz+1],mnx);
		reshape(G[3:Nr+2,2:Ntheta+1,2:Nz+1],mnx)]
jjy[1:3*mny] = [reshape(G[2:Nr+1,1:Ntheta,2:Nz+1],mny);
		reshape(G[2:Nr+1,2:Ntheta+1,2:Nz+1],mny);
		reshape(G[2:Nr+1,3:Ntheta+2,2:Nz+1],mny)]
jjz[1:3*mnz] = [reshape(G[2:Nr+1,2:Ntheta+1,1:Nz],mnz);
		reshape(G[2:Nr+1,2:Ntheta+1,2:Nz+1],mnz);
		reshape(G[2:Nr+1,2:Ntheta+1,3:Nz+2],mnz)]
sx[1:3*mnx] = [AW; APx; AE]
sy[1:3*mny] = [AS; APy; AN]
sz[1:3*mnz] = [AB; APz; AF]

# build the sparse matrix
kx = 3*mnx
ky = 3*mny
kz = 3*mnz
Mx = sparse(iix[1:kx], jjx[1:kx], sx[1:kx], (Nr+2)*(Ntheta+2)*(Nz+2), (Nr+2)*(Ntheta+2)*(Nz+2))
My = sparse(iiy[1:ky], jjy[1:ky], sy[1:ky], (Nr+2)*(Ntheta+2)*(Nz+2), (Nr+2)*(Ntheta+2)*(Nz+2))
Mz = sparse(iiz[1:kz], jjz[1:kz], sz[1:kz], (Nr+2)*(Ntheta+2)*(Nz+2), (Nr+2)*(Ntheta+2)*(Nz+2))
M = Mx + My + Mz
(M, Mx, My, Mz)
end


### Wrapping elliptic group from ECC
using ECC

struct EllipticGroup{T} <: CyclicGroup
    G::T
    q
    t::Int
end

EllipticGroup{T}(sec,G::EllipticGroup{T}) where T <: S256Point = EllipticGroup(sec2point(sec),G.q,G.t)
EllipticGroup(sec,G::EllipticGroup) = typeof(G)(sec,G)

binary(G::EllipticGroup{T}) where T <: S256Point = point2sec(G.G)
value(G::EllipticGroup{T}) where T <: S256Point = G.G.𝑥.𝑛
security(G::EllipticGroup) = G.t
order(G::EllipticGroup) = G.q

function Scep256k1Group()
    G = ECC.G # G the scep256k1 generator point
    t = 256
    q = ECC.N
    EllipticGroup(G,q,t)
end

^(G::EllipticGroup,n) = EllipticGroup(n*G.G,G.q,G.t)

# When more elliptic groups appears then check for the elements should become important.
import Base.*
*(X::EllipticGroup,Y::EllipticGroup) = EllipticGroup(X.G + Y.G,X.q,X.t)

import Base.==
==(X::EllipticGroup,Y::EllipticGroup) = X.G==Y.G


"""
    integer_contractor(x::Interval)

Return the integers enclosed in the interval `x`.
"""

function integer_contractor(x::Interval)
    a = floor(x.lo)+1
    b = floor(x.hi)

    a > b && return emptyinterval(x)

    return Interval(a, b)
end

### Transformations on IntervalBoxes

"""Reflect in mirror at position x_mirror"""
function reflect_x(x_mirror)
    X -> begin
        x, y = X
        x = 2*x_mirror - x

        return IntervalBox(x, y)
    end
end

"""
    translate(α)

Returns a function that shifts (translates) a 2D `IntervalBox` in `x` (the first coordinate).
"""
function translate(α)
    X -> begin
        x, y = X
        return IntervalBox(x - α, y)
    end
end

odd(X::IntervalBox) = ( (x,y) = X; IntervalBox(-x, -y) )

## Transformations on Contractors

"""
Translation of a Contractor `C` by `α`.
Uses `inv(op) ∘ C ∘ op`
"""
translate(C, α) = translate(-α) ∘ C ∘ translate(α)

"""
Symmetric part of a Contractor, via an involution `op`
(i.e. such that `inv(op) == op`).
"""
symmetrise(C, op) = op ∘ C ∘ op

"Periodize the contractor C"
function periodise(C, period)

    X -> begin
        x, y = X

        x2 = -∞..∞
        x2, y = C(IntervalBox(x2, y))

        isempty(IntervalBox(x2, y)) && return(IntervalBox(∅, ∅))

        # periods where the periodization of x intersects with x2:
        periods = integer_contractor((x - x2) / period)

        isempty(periods) && return(IntervalBox(∅, ∅))

        x3 = x2 + periods*period
        x = x ∩ x3

        return IntervalBox(x, y)
    end

end


# f(x)=vcat(x...)
Fvcat(x...)=(vcat(x...),nothing)
Fvcat_inplace(value::Array,auxvalue,x...)=copy!(value,vcat(x...))

function Dvcat(derivativeIDX,f_c,faux_c,grad_c,grad_n,x...)
    startind=1
    for i=1:length(x)
        endind=startind+length(x[i])-1
        if pointer(x[i])==pointer(x[derivativeIDX]) # use pointers so we can deal with vcat([A B A])
            axpy!(1.0,grad_c[startind:endind],grad_n)
        end
        startind=endind+1
    end
end

if PROC=="GPU"

    function Fvcat_inplace(value::CudaArray,auxvalue,x::CudaArray...) # inplace
        totallength=1
        for i in 1:length(x)
            copyinto!(value,x[i],totallength)
            totallength+=length(x[i])
        end
    end

    function Dvcat(derivativeIDX,f_c,faux_c,grad_c,grad_n::CudaArray,x...)
        startind=1
        for i=1:length(x)
            endind=startind+length(x[i])-1
            if pointer(x[i])==pointer(x[derivativeIDX]) # use pointers so we can deal with vcat([A B A])
                copyfrom_update!(grad_n,grad_c,startind)
            end
            startind=endind+1
        end
    end

end

Derivative[Fvcat]=Dvcat # Define dictionary lookup
Inplace[Fvcat]=Fvcat_inplace

import Base.vcat
vcat(n::ADnode)=ADnode(Fvcat,n)

vcat(n::Array{ADnode,1})=n
vcat(n::ArrayADnode)=ADnode(Fvcat,n)

export vcat


function mean_sqrt_of_positive_diagonals(x)
    vals = diag(x)
    return mean(sqrt.(vals[vals .> 0]))^2
end

"""
Note that this is not exported with a different name to avoid confusion with
eigenvalue_clean(mat::Hermitian, eigenvalue_threshold::Real).
"""
function _eigenvalue_clean(mat::Hermitian, obs::Real)
    if sum(isnan.(mat)) > 0 return mat end # If someone inputs a matrix involving a NaN
    N = size(mat)[1]
    eigenvalues, eigenvectors = eigen(mat)
    sigma2 = 1 - maximum(eigenvalues)/N
    q = obs/N
    eigenvalue_threshold = (sigma2 * (1 + 1/q + 2*sqrt(1/q))) * mean_sqrt_of_positive_diagonals(mat)
    return eigenvalue_clean(eigenvalues, eigenvectors, eigenvalue_threshold)
end

"""
    eigenvalue_clean(eigenvalues::Vector{<:Real}, eigenvectors::Matrix{<:Real},
                     eigenvalue_threshold::Real)

This takes the small eigenvalues (with values below eigenvalue_threshold). It sets them to the
greater of their average or eigenvalue_threshold/(4*number_of_small_eigens). Then the matrix is reconstructed and returned (as a `Hermitian`)
### Inputs
* `eigenvalues` - The eigenvalues of a matrix.
* `eigenvectors` - The eigenvectors  of a matrix.
* `eigenvalue_threshold` - The threshold for a eigenvalue to be altered.
### Returns
* A `Hermitian`.

    eigenvalue_clean(mat::Hermitian, eigenvalue_threshold::Real)

This splits a matrix into its eigenvalues and eigenvectors. Then takes the small eigenvalues (with values below `eigenvalue_threshold`). It sets them to the
greater of their average or `eigenvalue_threshold/(4*number_of_small_eigens)`. Then the matrix is reconstructed and returned (as a `Hermitian`)
### Inputs
* `mat` - A matrix that you want to regularise with eigenvalue regularisation.
* `eigenvalue_threshold` - The threshold for a eigenvalue to be altered.
### Returns
* A `Hermitian`.

    eigenvalue_clean(mat::Hermitian, ts::SortedDataFrame)

Similarly to the above two methods these functions regularise a matrix by setting small eigenvalues to near zero.
The method of Laloux, Cizeau, Bouchaud & Potters 2000 is used to choose a threshold.
### Inputs
* `mat` - A matrix that you want to regularise with eigenvalue regularisation.
* `ts` - The tick data.
### Returns
* A `Hermitian`.

    eigenvalue_clean(covariance_matrix::CovarianceMatrix, ts::SortedDataFrame;
                     apply_to_covariance::Bool = true)
### Inputs
* `mat` - A matrix that you want to regularise with eigenvalue regularisation.
* `ts` - The tick data.
* `apply_to_covariance` Should regularisation be applied to the covariance matrix or the correlation matrix.
### Returns
* A `CovarianceMatrix`.

Note that if the input matrices include any NaN terms then regularisation is not possible. The matrix will be silently returned (as these NaNs will generally be
from upstream problems so it is useful to return the matrix rather than throw at this point).As a result outputs should be checked.

## References
Laloux, L., Cizeau, P., Bouchaud J. , Potters, M. 2000. "Random matrix theory and financial correlations" International Journal of Theoretical Applied FInance, 3, 391-397.
"""
function eigenvalue_clean(eigenvalues::Vector{<:Real},
                          eigenvectors::Matrix{<:Real}, eigenvalue_threshold::Real)
    number_of_small_eigens = sum(eigenvalues .< eigenvalue_threshold)
    av_small_eigens = max(eigenvalue_threshold/(4*number_of_small_eigens) , mean(eigenvalues[eigenvalues .< eigenvalue_threshold]) )
    eigenvalues[eigenvalues .< eigenvalue_threshold] .= av_small_eigens
    regularised_mat = construct_matrix_from_eigen(eigenvalues, eigenvectors)
    return regularised_mat
end
function eigenvalue_clean(mat::Hermitian, eigenvalue_threshold::Real)
    if sum(isnan.(mat)) > 0 return mat end # If someone inputs a matrix involving a NaN
    eigenvalues, eigenvectors = eigen(mat)
    return eigenvalue_clean(eigenvalues, eigenvectors, eigenvalue_threshold)
end
function eigenvalue_clean(mat::Hermitian, ts::SortedDataFrame)
    dims = size(mat)[1]
    obs = nrow(ts.df)/dims
    regularised_mat = _eigenvalue_clean(mat, obs)
    return Hermitian(regularised_mat)
end
function eigenvalue_clean(covariance_matrix::CovarianceMatrix, ts::SortedDataFrame;
                          apply_to_covariance::Bool = true)
    if apply_to_covariance
        regularised_covariance = eigenvalue_clean(covariance(covariance_matrix), ts)
        corr, vols = cov2cor_and_vol(regularised_covariance, covariance_matrix.time_period_per_unit, covariance_matrix.time_period_per_unit)
        return CovarianceMatrix(corr, vols, covariance_matrix.labels, covariance_matrix.time_period_per_unit)
    else
        return CovarianceMatrix(Hermitian(eigenvalue_clean(covariance_matrix.correlation, ts)), covariance_matrix.volatility, covariance_matrix.labels, covariance_matrix.time_period_per_unit)
    end
end


# Write a program that takes the duration of a year (in fractional days) for an imaginary planet as
# an input and produces a leap-year rule that minimizes the difference to the planet’s solar year.

planets = [["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"],
	  [87.97, 224.70, 365.2422, 1.88, 11.86, 29.46, 84.01, 164.79, 248.59 ]]

function leapYear(num)
    y = 0
    p = 0
    while p < 1
	p = planets[2][num] % 1 * y
        y += 1
    end
  println("$(planets[1][num]) Leap Year Every $(y-2) Years")
end

for i in 1:9
  leapYear(i)
end


using NonlinearEigenproblems;
using Test

@testset "dtn_dimer" begin
    # This requires that the FEM-files are downloaded
    nep=nep_gallery("dtn_dimer","/home/jarl/archive/dtn_umea_collab_nosync_julia/dimer_TM_ref2_p10_g0_a1",40);

    n=size(nep,1);
    # Reference eigvals computed with MATLAB code
    λv=[4.370036082655539 - 1.526561686254116im
        2.165520379441414 - 0.537312290190438im
        3.958068685793646 - 0.528959556453958im
        4.397326877370154 - 0.671081974763288im
        1.098616611079115 - 1.005745095689971im]

    λ=λv[end];

    # Solve it:
    (λs,vs)=quasinewton(nep,λ=1.099+1.006im,v=ones(n),logger=displaylevel,tol=1e-5)
    (λss,vss)=quasinewton(nep,λ=λs,v=vs,logger=displaylevel)

    @test minimum(abs.(λss.-λv))<1e-10 # gives 1.4512316607747323e-12

end


module AdaptiveDesignOptimization
    using Distributions, Parameters, StatsFuns
    export Optimizer, Model, Optimize, Randomize
    export Dynamic, Static
    export update!, mean_post, std_post, get_best_design

    include("structs.jl")
    include("functions.jl")
end


using Debug

type FactorAnalysis
  Ψ::Vector{Float64}
  F::Matrix{Float64}
  Vbar::Vector{Float64}
end

n_components(fa::FactorAnalysis) = size(fa.F, 2)

const SMALL = 1e-12

@debug function fit(::Type{FactorAnalysis}, V::Matrix{Float64}, n_components::Int, tol::Float64=1e-2, max_iter::Int=1000, Ψ::Vector{Float64} = ones(Float64, size(V,1)))
  Vbar = vec(mean(V, 2))
  σ2 = vec(var(V, 2, corrected=false)) # corrected = false used to get same results as scikit-learn
  D, N = size(V)
  X = centralize(V, Vbar)
  L_old = -Inf
  L = -Inf
  F = zeros(D, n_components)
  k = 0
  for k = 1:max_iter
    Ψ_sqrt = sqrt(Ψ) + SMALL
    X_tilde = 1./Ψ_sqrt .* X/(sqrt(N))
    U, Λ_tilde, _ = svd(X_tilde, thin=true)
    Λ = Λ_tilde.^2
    U_H = U[:,1:n_components]
    Λ_H = Λ[1:n_components]
    # Factor update
    @bp
    F = Ψ_sqrt .* U_H * diagm(sqrt(max(Λ_H .- 1, 0)))
    # Log likelihood
    L_old = L
    L = n_components + log(prod(2π*Ψ))
    for i = 1:n_components
      @inbounds L += log(Λ[i])
    end
    for i = n_components+1:D
      @inbounds L += Λ[i]
    end
    L *= -N/2
    # Noise update
    Ψ = vec(max(σ2 - sum(F.^2, 2), SMALL))

    if L - L_old < tol
      info("Factor analysis converged, log-likelihood = $L")
      break
    end
  end
  if k == max_iter
    info("Factor analysis did not converge in $max_iter iterations.")
  end
  FactorAnalysis(Ψ, F, Vbar), L
end

function transform(fa::FactorAnalysis, data::Matrix{Float64})
# FIXME
  X = centralize(data, fa.Vbar)
  F_Ψ = fa.F ./ fa.Ψ
  cov_z = inv(eye(n_components(fa)) .+ F_Ψ' * fa.Ψ)
  cov_z' * F_Ψ' * X
end


test = "target area: x=20..30, y=-10..-5"
puzzle = "target area: x=253..280, y=-73..-46"

function parse_input(input::AbstractString)
    re = r"^target area: x=(-?[0-9]+)..(-?[0-9]+), y=(-?[0-9]+)..(-?[0-9]+)$"
    m = match(re, input)
    @assert m !== nothing
    coords = parse.(Int, m.captures)
    return ((coords[3], coords[4]), (coords[1], coords[2]))
end

function q1((ymin, ymax), (xmin, xmax))
    @assert ymax < 0
    vx = round(Int, - 1/2 + sqrt(1/4 + 2xmin), RoundUp)
    vy = -ymin - 1
    @assert vx * (vx - 1) / 2 < xmin <= vx * (vx + 1) / 2
    return vy * (vy + 1) ÷ 2
end

function q2((ymin, ymax), (xmin, xmax))
    @assert xmin > 0 && ymax < 0
    vxmin = round(Int, - 1/2 + sqrt(1/4 + 2xmin), RoundUp)
    vxmax = xmax
    @assert vxmin * (vxmin - 1) / 2 < xmin <= vxmin * (vxmin + 1) / 2
    vymin = ymin
    vymax = -ymin - 1

    return count(vymin:vymax .=> (vxmin:vxmax)') do v
        return shoot(v, (ymin, ymax), (xmin, xmax))
    end
end

function shoot((vy, vx), (ymin, ymax), (xmin, xmax))::Bool
    (x, y) = (0, 0)
    while x <= xmax && y >= ymin
        x >= xmin && y <= ymax && return true
        x += vx
        y += vy
        vx = max(vx - 1, 0)
        vy -= 1
    end
    return false
end

let v = parse_input(test)
    @assert q1(v...) == 45
    @assert q2(v...) == 112
end

v = parse_input(puzzle)
println("Q1: ", q1(v...))
println("Q2: ", q2(v...))


using Jchemo

n = 50 ; p = 7 ; q = 2
Xtrain = rand(n, p) ; Ytrain = rand(n, q)
ytrain = Ytrain[:, 1]
m = 3
Xtest = rand(m, p) ; Ytest = rand(m, q)
ytest = Ytest[:, 1]

########### PLSR

nlv = 0:2
pars = mpars(nlv = nlv)
gridscore(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = plskern, pars = pars)

# Faster
gridscorelv(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = plskern, nlv = nlv)

########### KPLSR

nlv = 0:2
pars = mpars(nlv = nlv, gamma = [1; 10])
#pars = mpars(nlv = 0:nlv, kern = ["kpol";], degree = 1:2)
gridscore(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = kplsr, pars = pars)

# Faster
pars = mpars(gamma = [1, 10])
gridscorelv(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = kplsr, nlv = nlv, pars = pars)

########### DKPLSR

nlv = 0:2
pars = mpars(nlv = nlv, gamma = [1; 10])
gridscore(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = dkplsr, pars = pars)

# Faster
pars = mpars(gamma = [1; 10])
gridscorelv(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = dkplsr, nlv = nlv, pars = pars)

############ RR

lb = [.01; .1]
pars = mpars(lb = lb)
gridscore(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = rr, pars = pars)

gridscore(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = rrchol, pars = pars)

# Faster (only for rr, not for rrchol)
gridscorelb(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = rr, lb = lb)

############ KRR

lb = [.01; .1]
pars = mpars(lb = lb, gamma = [1; 10])
gridscore(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = krr, pars = pars)

# Faster
pars = mpars(gamma = [1; 10])
gridscorelb(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = krr, lb = lb, pars = pars)

############ kNN-R

nlvdis = 5 ; metric = ["mahal"; "eucl"]
h = [1; 3] ; k = [20; 10]
pars = mpars(nlvdis = nlvdis, metric = metric, h = h, k = k)
gridscore(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = knnr, pars = pars, verbose = true)

############ kNN-LWPLSR

nlvdis = 5 ; metric = ["mahal";]
h = [1.; 3.] ; k = [100; 20]
nlv = 1:2
pars = mpars(nlv = nlv, nlvdis = nlvdis, metric = metric, h = h, k = k)
gridscore(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = lwplsr, pars = pars, verbose = true)

# Faster
pars = mpars(nlvdis = nlvdis, metric = metric, h = h, k = k)
gridscorelv(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = lwplsr, nlv = nlv, pars = pars, verbose = false)

########### PLSR-AGG

# Here there is no sense to use gridscorelv

pars = mpars(nlv = ["1:2"; "1:3"])
gridscore(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = plsr_agg, pars = pars)

############ kNN-LWPLSR-AGG

# Here there is no sense to use gridscorelv

nlvdis = 5 ; metric = ["mahal";] ;
h = [1.; 3.] ; k = [20; 10]
nlv = ["1:2"; "2:5"] ;
pars = mpars(nlv = nlv, nlvdis = nlvdis, metric = metric, h = h, k = k) ;
gridscore(Xtrain, Ytrain, Xtest, Ytest;
    score = msep, fun = lwplsr_agg, pars = pars, verbose = true)


export Unscented

struct Unscented <: AbstractNonLinearApproximation
    L   :: Int64
    α   :: Float64
    β   :: Float64
    κ   :: Float64
    λ   :: Float64
    Wm  :: Vector{Float64}
    Wc  :: Vector{Float64}
end

function Unscented(dim::Int64; α::Float64=1e-3, β::Float64=2.0, κ::Float64=0.0)
    λ = α^2*(dim + κ) - dim
    Wm = ones(2*dim + 1)
    Wc = ones(2*dim + 1)
    Wm ./= (2*(dim+λ))
    Wc ./= (2*(dim+λ))
    Wm[1] = λ/(dim+λ)
    Wc[1] = λ/(dim+λ) + (1 - α^2 + β)
    return Unscented(dim, α, β, κ, λ, Wm, Wc)
end

# get-functions for the Unscented structure
getL(approximation::Unscented)  = approximation.L
getα(approximation::Unscented)  = approximation.α
getβ(approximation::Unscented)  = approximation.β
getκ(approximation::Unscented)  = approximation.κ
getλ(approximation::Unscented)  = approximation.λ
getWm(approximation::Unscented) = approximation.Wm
getWc(approximation::Unscented) = approximation.Wc


function solve(laplace::AnalyticalLaplace)
    (; D, T₂, dim, eiglim, eigstep) = laplace

    # TODO: Add support for dim = 1 (PlateSetup)
    L = length(D)

    # Compute radial and angular eigenvalues (λ and ν), with α^2 = λ and n^2 = ν for
    # cylinders and n(n+1) = ν for spheres
    dα = √eigstep * 1e3
    α_min = dα / 100
    α_max = √eiglim * 1e3
    α, n = find_α(laplace, α_min, α_max, dα)

    N = length(α)
    λ = α .^ 2
    Λ = diagm(λ)

    β = zeros(N)
    J = zeros(N)
    for m = 1:N
        β[m] = compute_β(laplace, α[m], n[m])
        if n[m] == 0
            J[m] = compute_int_J(laplace, α[m])[1]
        end
    end

    if dim == 2
        coeff = 2
    else
        coeff = √6
    end
    U = coeff * J .* β

    B = zeros(N, N)
    for m1 = 1:N
        for m2 = m1:N
            if abs(n[m1] - n[m2]) == 1
                K = compute_int_K(laplace, α[m1], n[m1], α[m2], n[m2])
                if dim == 2
                    ε = √(1 + (n[m1] == 0) + (n[m2] == 0))
                else
                    ε = (n[m1] + n[m2] + 1) / √((2 * n[m1] + 1) * (2 * n[m2] + 1))
                end
                B[m1, m2] = ε * β[m1] * β[m2] * K
                B[m2, m1] = B[m1, m2]
            end
        end
    end

    Bri = zeros(N, N, L)
    for m1 = 1:N
        for m2 = m1:N
            if n[m1] == n[m2]
                int_I = compute_int_I(laplace, α[m1], α[m2], n[m1])
                Bri[m1, m2, :] = 2 * β[m1] * β[m2] * int_I
                Bri[m2, m1, :] = Bri[m1, m2, :]
            end
        end
    end

    Br = zeros(N, N)
    for i = 1:L
        Br .+= Bri[:, :, i] / T₂[i]
    end

    (; Λ, β, U, B, Bri, Br)
end


abstract type AbstractIFParameter end
@snn_kw struct IFParameter{FT=Float32} <: AbstractIFParameter
    τm::FT = 20ms
    τe::FT = 5ms
    τi::FT = 10ms
    Vt::FT = -50mV
    Vr::FT = -60mV
    El::FT = Vr
end

abstract type AbstractIF end

@snn_kw mutable struct IF{VFT=Vector{Float32},VBT=Vector{Bool}} <: AbstractIF
    param::IFParameter = IFParameter()
    N::Int32 = 100
    v::VFT = param.Vr .+ rand(N) .* (param.Vt - param.Vr)
    ge::VFT = zeros(N)
    gi::VFT = zeros(N)
    fire::VBT = zeros(Bool, N)
    I::VFT = zeros(N)
    records::Dict = Dict()
end

"""
    [Integrate-And-Fire Neuron](https://neuronaldynamics.epfl.ch/online/Ch1.S3.html)
"""
IF

function integrate!(p::IF, param::IFParameter, dt::Float32)
    @unpack N, v, ge, gi, fire, I = p
    @unpack τm, τe, τi, Vt, Vr, El = param
    @inbounds for i = 1:N
        v[i] += dt * (ge[i] + gi[i] - (v[i] - El) + I[i]) / τm
        ge[i] += dt * -ge[i] / τe
        gi[i] += dt * -gi[i] / τi
    end
    @inbounds for i = 1:N
        fire[i] = v[i] > Vt
        v[i] = ifelse(fire[i], Vr, v[i])
    end
end


@testset "modify" begin
    model = Model(GLPK_DUAL_FACTORY)
    @variable(model, x[1:2] >= 0)
    @constraint(model, 2x[1] + x[2] <= 4)
    @constraint(model, x[1] + 2x[2] <= 4)
    @objective(model, Max, 4x[1] + 3x[2])
    optimize!(model)
    @test objective_value(model) ≈ 28 / 3
    set_objective_coefficient(model, x[1], 5.0)
    optimize!(model)
    @test objective_value(model) ≈ 10.6666666666

    model = Model(GLPK_DUAL_FACTORY)
    @variable(model, x[1:2] >= 0)
    @constraint(model, 2x[1] + x[2] <= 4)
    @constraint(model, x[1] + 2x[2] <= 4)
    @objective(model, Min, -4x[1] + -3x[2])
    optimize!(model)
    @test objective_value(model) ≈ -28 / 3
    set_objective_coefficient(model, x[1], -5.0)
    optimize!(model)
    @test objective_value(model) ≈ -10.6666666666

    model = Model(SCS_DUAL_FACTORY)
    @variable(model, x[1:2] >= 0)
    @constraint(model, 2x[1] + x[2] <= 4)
    @constraint(model, x[1] + 2x[2] <= 4)
    @objective(model, Max, 4x[1] + 3x[2])
    optimize!(model)
    @test objective_value(model) ≈ 28 / 3 atol = 1e-3
    set_objective_coefficient(model, x[1], 5.0)
    optimize!(model)
    @test objective_value(model) ≈ 10.6666666666 atol = 1e-3

    model = Model(SCS_DUAL_FACTORY)
    @variable(model, x[1:2] >= 0)
    @constraint(model, 2x[1] + x[2] <= 4)
    @constraint(model, x[1] + 2x[2] <= 4)
    @objective(model, Min, -4x[1] + -3x[2])
    optimize!(model)
    @test objective_value(model) ≈ -28 / 3 atol = 1e-3
    set_objective_coefficient(model, x[1], -5.0)
    optimize!(model)
    @test objective_value(model) ≈ -10.6666666666 atol = 1e-3

    model = Model(SCS_DUAL_FACTORY)
    @variable(model, x)
    @variable(model, y)
    @variable(model, z)
    @constraint(model, x + 2y + 3z >= 4)
    @constraint(model, x + y >= 1)
    @objective(model, Min, x^2 + x * y + y^2 + y * z + z^2)
    optimize!(model)
    @test objective_value(model) ≈ 1.8571 atol = 1e-3
    # Now the objective is @objective(model, Min, x^2 + x*y + y^2 + y*z + z^2 - 5x)
    set_objective_coefficient(model, x, -5.0)
    optimize!(model)
    @test objective_value(model) ≈ -9.15 atol = 1e-3
end


func(x) = cos.(x)
Inputs = 1.1
fp_simple   = fixed_point(func, Inputs; Algorithm = Simple)
fp_simple.Convergence_ < 1e-10
fp_anderson = fixed_point(func, Inputs; Algorithm = Anderson)
fp_anderson.Convergence_ < 1e-10
fp_aitken   = fixed_point(func, Inputs; Algorithm = Aitken)
fp_aitken.Convergence_ < 1e-10
fp_newton   = fixed_point(func, Inputs; Algorithm = Newton)
fp_newton.Convergence_ < 1e-10
fp_VEA      = fixed_point(func, Inputs; Algorithm = VEA)
fp_VEA.Convergence_ < 1e-10
fp_SEA      = fixed_point(func, Inputs; Algorithm = SEA)
fp_SEA.Convergence_ < 1e-10
fp_MPE      = fixed_point(func, Inputs; Algorithm = MPE)
fp_MPE.Convergence_ < 1e-10
fp_RRE      = fixed_point(func, Inputs; Algorithm = RRE)
fp_RRE.Convergence_ < 1e-10


@testset "CSP_graph" begin
    @testset "csp_graph" begin
        @polyvar x y z
        p = x*y + y*z
        S = @set x^2 + y^2 == 1 && y^2 + z^2 == 1
        G = SumOfSquares.Certificate.Sparsity.csp_graph(p, S)
        @test sort(G.int2n) == [z, y, x]
    end
    @testset "chordal_csp" begin
        @polyvar x y z
        p = x*y + y*z
        S = @set x^2 + y^2 == 1 && y^2 + z^2 == 1
        H, cliques = SumOfSquares.Certificate.Sparsity.chordal_csp_graph(p, S)
        @test length(cliques) == 2
        svar = cliques[1] ∪ cliques[2]
        @test H.int2n ⊆ svar
        @test svar ⊆ H.int2n
        @test sort!(svar)== sort!(H.int2n)
        I, cliquesI = SumOfSquares.Certificate.Sparsity.chordal_csp_graph(p, FullSpace())
        @test sort!(I.int2n) == sort!(H.int2n)
        @test sort!(collect.(I.graph.neighbors)) == sort!(collect.(H.graph.neighbors))
    end
end


# Gauss quadrature
import Ferrite: QuadratureRule
import FastGaussQuadrature: gausslegendre, gausslobatto
import Base.Cartesian: @nloops, @nref, @ntuple, @nexprs

# Generate Gauss quadrature rules on Lines
for dim in (1)
    @eval begin
        function (::Type{QuadratureRule{$dim,RefLine}})(quad_type::Symbol, order::Int)
            if quad_type == :legendre
                p, w = gausslegendre(order)
            elseif quad_type == :lobatto
                if order == 1
                    p, w = gausslegendre(order)
                else
                    p, w = gausslobatto(order)
                end
            else
                throw(ArgumentError("unsupported quadrature rule"))
            end
            weights = Vector{Float64}(undef, order^($dim))
            points  = Vector{Vec{$dim,Float64}}(undef, order^($dim))
            count = 1
            @nloops $dim i j->(1:order) begin
                t = @ntuple $dim q-> p[$(Symbol("i"*"_q"))]
                points[count] = Vec{$dim,Float64}(t)
                weight = 1.0
                @nexprs $dim j->(weight *= w[i_{j}])
                weights[count] = weight
                count += 1
            end
            return QuadratureRule{$dim,RefLine,Float64}(weights, points)
        end
    end
end


# LP programming using MATLAB-like construction
#
#   maximize 1000 x + 350 y
#
#       s.t. x >= 30, y >= 0
#            x - 1.5y >= 0  (i.e. -x + 1.5 y <= 0)
#            12 x + 8 y <= 1000
#            1000 x + 300 y <= 70000
#
#   solution: (59.0909, 36.3636)
#   objv: 71818.1818
#

using Gurobi, Test

@testset "LP 02" begin

	env = Gurobi.Env()
	setparam!(env, "OutputFlag", 0)

	model = gurobi_model(env;
		name="lp_02",
		sense=:maximize,
		f = [1000., 350.],
		A = [-1. 1.5; 12. 8.; 1000. 300.],
		b = [0., 1000., 70000.],
		lb = [0., 30.])

	optimize(model)

	@test isapprox(get_solution(model), [59.0909, 36.3636], atol=1e-4)
	@test isapprox(get_objval(model), 71818.1818, atol=1e-4)
end


"""
    TruncSVD

ITensor factorization type for a truncated singular-value
decomposition, returned by `svd`.
"""
struct TruncSVD
  U::ITensor
  S::ITensor
  V::ITensor
  spec::Spectrum
  u::Index
  v::Index
end

# iteration for destructuring into components `U,S,V,spec,u,v = S`
iterate(S::TruncSVD) = (S.U, Val(:S))
iterate(S::TruncSVD, ::Val{:S}) = (S.S, Val(:V))
iterate(S::TruncSVD, ::Val{:V}) = (S.V, Val(:spec))
iterate(S::TruncSVD, ::Val{:spec}) = (S.spec, Val(:u))
iterate(S::TruncSVD, ::Val{:u}) = (S.u, Val(:v))
iterate(S::TruncSVD, ::Val{:v}) = (S.v, Val(:done))
iterate(S::TruncSVD, ::Val{:done}) = nothing

@doc """
    svd(A::ITensor, inds::Index...; <keyword arguments>)

Singular value decomposition (SVD) of an ITensor `A`, computed
by treating the "left indices" provided collectively
as a row index, and the remaining "right indices" as a
column index (matricization of a tensor).

The first three return arguments are `U`, `S`, and `V`, such that
`A ≈ U * S * V`.

Whether or not the SVD performs a trunction depends on the keyword
arguments provided.

# Examples

```julia
i = Index(2)
j = Index(5)
k = Index(2)

A = randomITensor(i, j, k)
U, S, V = svd(A, i, k);
@show norm(A - U * S * V) <= 10 * eps() * norm(A)

# This will truncate the last 2 singular values.
# The norm of the difference with the original tensor
# will be the sqrt root of the sum of the squares of the
# singular values that get truncated.
Utrunc, Strunc, Vtrunc = svd(A, i, k; maxdim=2);
@show norm(A - Utrunc * Strunc * Vtrunc) ≈ sqrt(S[3, 3]^2 + S[4, 4]^2)

# Alternatively we can specify that we want to truncate
# the weights of the singular values up to a certain cutoff,
# so the error will be no larger than the cutoff.
Utrunc2, Strunc2, Vtrunc2 = svd(A, i, k; cutoff=1e-10);
@show norm(A - Utrunc2 * Strunc2 * Vtrunc2) <= 1e-10
```

# Keywords
- `maxdim::Int`: the maximum number of singular values to keep.
- `mindim::Int`: the minimum number of singular values to keep.
- `cutoff::Float64`: set the desired truncation error of the SVD, by default defined as the sum of the squares of the smallest singular values.
- `lefttags::String = "Link,u"`: set the tags of the Index shared by `U` and `S`.
- `righttags::String = "Link,v"`: set the tags of the Index shared by `S` and `V`.
- `alg::String = "divide_and_conquer"`. Options:
  - `"divide_and_conquer"` - A divide-and-conquer algorithm. LAPACK's gesdd. Fast, but may lead to some innacurate singular values for very ill-conditioned matrices. Also may sometimes fail to converge, leading to errors (in which case "qr_iteration" or "recursive" can be tried).
  - `"qr_iteration"` - Typically slower but more accurate for very ill-conditioned matrices compared to `"divide_and_conquer"`. LAPACK's gesvd.
  - `"recursive"` - ITensor's custom svd. Very reliable, but may be slow if high precision is needed. To get an `svd` of a matrix `A`, an eigendecomposition of ``A^{\\dagger} A`` is used to compute `U` and then a `qr` of ``A^{\\dagger} U`` is used to compute `V`. This is performed recursively to compute small singular values.
- `use_absolute_cutoff::Bool = false`: set if all probability weights below the `cutoff` value should be discarded, rather than the sum of discarded weights.
- `use_relative_cutoff::Bool = true`: set if the singular values should be normalized for the sake of truncation.

See also: [`factorize`](@ref), [`eigen`](@ref)
"""
function svd(A::ITensor, Linds...; kwargs...)
  utags::TagSet = get(kwargs, :lefttags, get(kwargs, :utags, "Link,u"))
  vtags::TagSet = get(kwargs, :righttags, get(kwargs, :vtags, "Link,v"))

  # Keyword argument deprecations
  #if haskey(kwargs, :utags) || haskey(kwargs, :vtags)
  #  @warn "Keyword arguments `utags` and `vtags` are deprecated in favor of `leftags` and `righttags`."
  #end

  Lis = commoninds(A, indices(Linds))
  Ris = uniqueinds(A, Lis)

  if length(Lis) == 0 || length(Ris) == 0
    error(
      "In `svd`, the left or right indices are empty (the indices of `A` are ($(inds(A))), but the input indices are ($Lis)). For now, this is not supported. You may have accidentally input the wrong indices.",
    )
  end

  CL = combiner(Lis...)
  CR = combiner(Ris...)

  AC = A * CR * CL

  cL = combinedind(CL)
  cR = combinedind(CR)
  if inds(AC) != (cL, cR)
    AC = permute(AC, cL, cR)
  end

  USVT = svd(tensor(AC); kwargs...)
  if isnothing(USVT)
    return nothing
  end
  UT, ST, VT, spec = USVT
  UC, S, VC = itensor(UT), itensor(ST), itensor(VT)

  u = commonind(S, UC)
  v = commonind(S, VC)

  if hasqns(A)
    # Fix the flux of UC,S,VC
    # such that flux(UC) == flux(VC) == QN()
    # and flux(S) == flux(A)
    for b in nzblocks(UC)
      i1 = inds(UC)[1]
      i2 = inds(UC)[2]
      newqn = -dir(i2) * flux(i1 => Block(b[1]))
      setblockqn!(i2, newqn, b[2])
      setblockqn!(u, newqn, b[2])
    end

    for b in nzblocks(VC)
      i1 = inds(VC)[1]
      i2 = inds(VC)[2]
      newqn = -dir(i2) * flux(i1 => Block(b[1]))
      setblockqn!(i2, newqn, b[2])
      setblockqn!(v, newqn, b[2])
    end
  end

  U = UC * dag(CL)
  V = VC * dag(CR)

  settags!(U, utags, u)
  settags!(S, utags, u)
  settags!(S, vtags, v)
  settags!(V, vtags, v)

  u = settags(u, utags)
  v = settags(v, vtags)

  return TruncSVD(U, S, V, spec, u, v)
end

"""
    TruncEigen

ITensor factorization type for a truncated eigenvalue
decomposition, returned by `eigen`.
"""
struct TruncEigen
  D::ITensor
  V::ITensor
  Vt::ITensor
  spec::Spectrum
  l::Index
  r::Index
end

# iteration for destructuring into components `D, V, spec, l, r = E`
iterate(E::TruncEigen) = (E.D, Val(:V))
iterate(E::TruncEigen, ::Val{:V}) = (E.V, Val(:spec))
iterate(E::TruncEigen, ::Val{:spec}) = (E.spec, Val(:l))
iterate(E::TruncEigen, ::Val{:l}) = (E.l, Val(:r))
iterate(E::TruncEigen, ::Val{:r}) = (E.r, Val(:done))
iterate(E::TruncEigen, ::Val{:done}) = nothing

"""
    eigen(A::ITensor[, Linds, Rinds]; <keyword arguments>)

Eigendecomposition of an ITensor `A`, computed
by treating the "left indices" `Linds` provided collectively
as a row index, and remaining "right indices" `Rinds` as a
column index (matricization of a tensor).

If no indices are provided, pairs of primed and unprimed indices are
searched for, with `Linds` taken to be the primed indices and
`Rinds` taken to be the unprimed indices.

The return arguments are the eigenvalues `D` and eigenvectors `U`
as tensors, such that `A * U ∼ U * D` (more precisely they are approximately
equal up to proper replacements of indices, see the example for details).

Whether or not `eigen` performs a trunction depends on the keyword
arguments provided. Note that truncation is only well defined for
positive semidefinite matrices.

# Arguments
- `maxdim::Int`: the maximum number of singular values to keep.
- `mindim::Int`: the minimum number of singular values to keep.
- `cutoff::Float64`: set the desired truncation error of the eigenvalues, by default defined as the sum of the squares of the smallest eigenvalues. For now truncation is only well defined for positive semi-definite eigenspectra.
- `ishermitian::Bool = false`: specify if the matrix is Hermitian, in which case a specialized diagonalization routine will be used and it is guaranteed that real eigenvalues will be returned.
- `plev::Int = 0`: set the prime level of the Indices of `D`. Default prime levels are subject to change.
- `leftplev::Int = plev`: set the prime level of the Index unique to `D`. Default prime levels are subject to change.
- `rightplev::Int = leftplev+1`: set the prime level of the Index shared by `D` and `U`. Default tags are subject to change.
- `tags::String = "Link,eigen"`: set the tags of the Indices of `D`. Default tags are subject to change.
- `lefttags::String = tags`: set the tags of the Index unique to `D`. Default tags are subject to change.
- `righttags::String = tags`: set the tags of the Index shared by `D` and `U`. Default tags are subject to change.
- `use_absolute_cutoff::Bool = false`: set if all probability weights below the `cutoff` value should be discarded, rather than the sum of discarded weights.
- `use_relative_cutoff::Bool = true`: set if the singular values should be normalized for the sake of truncation.

# Examples
```julia
i, j, k, l = Index(2, "i"), Index(2, "j"), Index(2, "k"), Index(2, "l")
A = randomITensor(i, j, k, l)
Linds = (i, k)
Rinds = (j, l)
D, U = eigen(A, Linds, Rinds)
dl, dr = uniqueind(D, U), commonind(D, U)
Ul = replaceinds(U, (Rinds..., dr) => (Linds..., dl))
A * U ≈ Ul * D # true
```

See also: [`svd`](@ref), [`factorize`](@ref)
"""
function eigen(A::ITensor, Linds, Rinds; kwargs...)
  @debug_check begin
    if hasqns(A)
      @assert flux(A) == QN()
    end
  end

  N = ndims(A)
  NL = length(Linds)
  NR = length(Rinds)
  NL != NR && error("Must have equal number of left and right indices")
  N != NL + NR &&
    error("Number of left and right indices must add up to total number of indices")

  ishermitian::Bool = get(kwargs, :ishermitian, false)

  tags::TagSet = get(kwargs, :tags, "Link,eigen")
  lefttags::TagSet = get(kwargs, :lefttags, tags)
  righttags::TagSet = get(kwargs, :righttags, tags)

  plev::Int = get(kwargs, :plev, 0)
  leftplev::Int = get(kwargs, :leftplev, plev)
  rightplev::Int = get(kwargs, :rightplev, plev)

  if lefttags == righttags && leftplev == rightplev
    leftplev = rightplev + 1
  end

  # Linds, Rinds may not have the correct directions
  Lis = indices(Linds)
  Ris = indices(Rinds)

  # Ensure the indices have the correct directions,
  # QNs, etc.
  # First grab the indices in A, then permute them
  # correctly.
  Lis = permute(commoninds(A, Lis), Lis)
  Ris = permute(commoninds(A, Ris), Ris)

  for (l, r) in zip(Lis, Ris)
    if space(l) != space(r)
      error("In eigen, indices must come in pairs with equal spaces.")
    end
    if hasqns(A)
      if dir(l) == dir(r)
        error("In eigen, indices must come in pairs with opposite directions")
      end
    end
  end

  CL = combiner(Lis...; dir=Out, tags="CMB,left")
  CR = combiner(dag(Ris)...; dir=Out, tags="CMB,right")

  AC = A * dag(CR) * CL

  cL = combinedind(CL)
  cR = dag(combinedind(CR))
  if inds(AC) != (cL, cR)
    AC = permute(AC, cL, cR)
  end

  AT = ishermitian ? Hermitian(tensor(AC)) : tensor(AC)

  DT, VT, spec = eigen(AT; kwargs...)
  D, VC = itensor(DT), itensor(VT)

  V = VC * dag(CR)

  # Set right index tags
  l = uniqueind(D, V)
  r = commonind(D, V)
  l̃ = setprime(settags(l, lefttags), leftplev)
  r̃ = setprime(settags(l̃, righttags), rightplev)

  replaceinds!(D, (l, r), (l̃, r̃))
  replaceind!(V, r, r̃)

  l, r = l̃, r̃

  # The right eigenvectors, after being applied to A
  Vt = replaceinds(V, (Ris..., r), (Lis..., l))

  @debug_check begin
    if hasqns(A)
      @assert flux(D) == QN()
      @assert flux(V) == QN()
      @assert flux(Vt) == QN()
    end
  end

  return TruncEigen(D, V, Vt, spec, l, r)
end

function eigen(A::ITensor; kwargs...)
  Ris = filterinds(A; plev=0)
  Lis = Ris'
  return eigen(A, Lis, Ris; kwargs...)
end

function noinds_error_message(decomp::String)
  return "$decomp without any input indices is currently not defined. In the future it may be defined as performing a $decomp decomposition treating the ITensor as a matrix from the primed to the unprimed indices."
end

qr(A::ITensor; kwargs...) = error(noinds_error_message("qr"))

# TODO: write this in terms of combiners and then
# call qr on the order-2 tensors directly
function qr(A::ITensor, Linds...; kwargs...)
  tags::TagSet = get(kwargs, :tags, "Link,qr")
  Lis = commoninds(A, indices(Linds))
  Ris = uniqueinds(A, Lis)
  Lpos, Rpos = NDTensors.getperms(inds(A), Lis, Ris)
  QT, RT = qr(tensor(A), Lpos, Rpos; kwargs...)
  Q, R = itensor(QT), itensor(RT)
  q = commonind(Q, R)
  settags!(Q, tags, q)
  settags!(R, tags, q)
  q = settags(q, tags)
  return Q, R, q
end

polar(A::ITensor; kwargs...) = error(noinds_error_message("polar"))

# TODO: allow custom tags in internal indices?
# TODO: return the new common indices?
function polar(A::ITensor, Linds...; kwargs...)
  U, S, V = svd(A, Linds...; kwargs...)
  u = commoninds(S, U)
  v = commoninds(S, V)
  δᵤᵥ′ = δ(u..., v'...)
  Q = U * δᵤᵥ′ * V'
  P = dag(V') * dag(δᵤᵥ′) * S * V
  return Q, P, commoninds(Q, P)
end

function factorize_qr(A::ITensor, Linds...; kwargs...)
  ortho::String = get(kwargs, :ortho, "left")
  if ortho == "left"
    L, R, q = qr(A, Linds...; kwargs...)
  elseif ortho == "right"
    Lis = uniqueinds(A, indices(Linds))
    R, L, q = qr(A, Lis...; kwargs...)
  else
    error(
      "In factorize using qr decomposition, ortho keyword $ortho not supported. Supported options are left or right.",
    )
  end
  return L, R
end

function factorize_svd(A::ITensor, Linds...; kwargs...)
  ortho::String = get(kwargs, :ortho, "left")
  alg::String = get(kwargs, :svd_alg, "divide_and_conquer")
  USV = svd(A, Linds...; kwargs..., alg=alg)
  if isnothing(USV)
    return nothing
  end
  U, S, V, spec, u, v = USV
  if ortho == "left"
    L, R = U, S * V
  elseif ortho == "right"
    L, R = U * S, V
  elseif ortho == "none"
    sqrtS = S
    sqrtS .= sqrt.(S)
    L, R = U * sqrtS, sqrtS * V
    replaceind!(L, v, u)
  else
    error(
      "In factorize using svd decomposition, ortho keyword $ortho not supported. Supported options are left, right, or none.",
    )
  end
  return L, R, spec
end

function factorize_eigen(A::ITensor, Linds...; kwargs...)
  ortho::String = get(kwargs, :ortho, "left")
  delta_A2 = get(kwargs, :eigen_perturbation, nothing)
  if ortho == "left"
    Lis = commoninds(A, indices(Linds))
  elseif ortho == "right"
    Lis = uniqueinds(A, indices(Linds))
  else
    error(
      "In factorize using eigen decomposition, ortho keyword $ortho not supported. Supported options are left or right.",
    )
  end
  simLis = sim(Lis)
  A2 = A * replaceinds(dag(A), Lis, simLis)
  if !isnothing(delta_A2)
    # This assumes delta_A2 has indices:
    # (Lis..., prime(Lis)...)
    delta_A2 = replaceinds(delta_A2, Lis, dag(simLis))
    noprime!(delta_A2)
    A2 += delta_A2
  end
  F = eigen(A2, Lis, simLis; ishermitian=true, kwargs...)
  D, _, spec = F
  L = F.Vt
  R = dag(L) * A
  if ortho == "right"
    L, R = R, L
  end
  return L, R, spec
end

factorize(A::ITensor; kwargs...) = error(noinds_error_message("factorize"))

"""
    factorize(A::ITensor, Linds::Index...; <keyword arguments>)

Perform a factorization of `A` into ITensors `L` and `R` such that `A ≈ L * R`.

# Arguments
- `ortho::String = "left"`: Choose orthogonality properties of the factorization.
  - `"left"`: the left factor `L` is an orthogonal basis such that `L * dag(prime(L, commonind(L,R))) ≈ I`.
  - `"right"`: the right factor `R` forms an orthogonal basis.
  - `"none"`, neither of the factors form an orthogonal basis, and in general are made as symmetrically as possible (depending on the decomposition used).
- `which_decomp::Union{String, Nothing} = nothing`: choose what kind of decomposition is used.
  - `nothing`: choose the decomposition automatically based on the other arguments. For example, when `nothing` is chosen and `ortho = "left"` or `"right"`, and a cutoff is provided, `svd` or `eigen` is used depending on the provided cutoff (`eigen` is only used when the cutoff is greater than `1e-12`, since it has a lower precision). When no truncation is requested `qr` is used for dense ITensors and `svd` for block-sparse ITensors (in the future `qr` will be used also for block-sparse ITensors in this case).
  - `"svd"`: `L = U` and `R = S * V` for `ortho = "left"`, `L = U * S` and `R = V` for `ortho = "right"`, and `L = U * sqrt.(S)` and `R = sqrt.(S) * V` for `ortho = "none"`. To control which `svd` algorithm is choose, use the `svd_alg` keyword argument. See the documentation for `svd` for the supported algorithms, which are the same as those accepted by the `alg` keyword argument.
  - `"eigen"`: `L = U` and ``R = U^{\\dagger} A`` where `U` is determined from the eigendecompositon ``A A^{\\dagger} = U D U^{\\dagger}`` for `ortho = "left"` (and vice versa for `ortho = "right"`). `"eigen"` is not supported for `ortho = "none"`.
  - `"qr"`: `L=Q` and `R` an upper-triangular matrix when `ortho = "left"`, and `R = Q` and `L` a lower-triangular matrix when `ortho = "right"` (currently supported for dense ITensors only).
In the future, other decompositions like QR (for block-sparse ITensors), polar, cholesky, LU, etc. are expected to be supported.

For truncation arguments, see: [`svd`](@ref)
"""
function factorize(A::ITensor, Linds...; kwargs...)
  ortho::String = get(kwargs, :ortho, "left")
  tags::TagSet = get(kwargs, :tags, "Link,fact")
  plev::Int = get(kwargs, :plev, 0)
  which_decomp::Union{String,Nothing} = get(kwargs, :which_decomp, nothing)
  cutoff = get(kwargs, :cutoff, nothing)
  eigen_perturbation = get(kwargs, :eigen_perturbation, nothing)
  if !isnothing(eigen_perturbation)
    if !(isnothing(which_decomp) || which_decomp == "eigen")
      error("""when passing a non-trivial eigen_perturbation to `factorize`,
               the which_decomp keyword argument must be either "automatic" or
               "eigen" """)
    end
    which_decomp = "eigen"
  end

  # Deprecated keywords
  if haskey(kwargs, :dir)
    error(
      """dir keyword in factorize has been replace by ortho.
Note that the default is now `left`, meaning for the results L,R = factorize(A), L forms an orthogonal basis.""",
    )
  end

  if haskey(kwargs, :which_factorization)
    error("""which_factorization keyword in factorize has been replace by which_decomp.""")
  end

  # Determines when to use eigen vs. svd (eigen is less precise,
  # so eigen should only be used if a larger cutoff is requested)
  automatic_cutoff = 1e-12
  dL, dR = dim(indices(Linds)), dim(indices(setdiff(inds(A), Linds)))
  maxdim = get(kwargs, :maxdim, min(dL, dR))
  might_truncate = !isnothing(cutoff) || maxdim < min(dL, dR)

  if isnothing(which_decomp)
    if !might_truncate && !hasqns(A) && ortho != "none"
      which_decomp = "qr"
    elseif isnothing(cutoff) || cutoff ≤ automatic_cutoff
      which_decomp = "svd"
    elseif cutoff > automatic_cutoff
      which_decomp = "eigen"
    end
  end

  if which_decomp == "svd"
    LR = factorize_svd(A, Linds...; kwargs...)
    if isnothing(LR)
      return nothing
    end
    L, R, spec = LR
  elseif which_decomp == "eigen"
    L, R, spec = factorize_eigen(A, Linds...; kwargs...)
  elseif which_decomp == "qr"
    hasqns(A) && error("QR factorization of an ITensor with QNs is not yet supported.")
    L, R = factorize_qr(A, Linds...; kwargs...)
    spec = Spectrum(nothing, 0.0)
  else
    throw(
      ArgumentError(
        """In factorize, factorization $which_decomp is not currently supported. Use `"svd"`, `"eigen"`, `"qr"` or `nothing`.""",
      ),
    )
  end

  # Set the tags and prime level
  l = commonind(L, R)
  l̃ = setprime(settags(l, tags), plev)
  replaceind!(L, l, l̃)
  replaceind!(R, l, l̃)
  l = l̃

  return L, R, spec, l
end


export PartialRandomization, PartialRandomizationAAFT

"""
    PartialRandomization(α = 0.5)
`PartialRandomization` surrogates[^Ortega1998] are similar to [`RandomFourier`](@ref) phase
surrogates, but during the phase randomization step, instead of drawing phases from `[0, 2π]`,
phases are drawn from `[0, 2π]*α`, where `α ∈ [0, 1]`. The authors refers to `α` as the
"degree" of phase randomization, where `α = 0` means `0 %` randomization and
`α = 1` means `100 %` randomization.

[^Ortega1998]: Ortega, Guillermo J.; Louis, Enrique (1998). Smoothness Implies Determinism in Time Series: A Measure Based Approach. Physical Review Letters, 81(20), 4345–4348. doi:10.1103/PhysRevLett.81.4345
"""
struct PartialRandomization{T} <: Surrogate
    α::T

    function PartialRandomization(α::T) where T <: Real
        @assert 0 <= α <= 1
        return new{T}(α)
    end
end

function surrogenerator(x::AbstractVector, rf::PartialRandomization, rng = Random.default_rng())
    forward = plan_rfft(x)
    inverse = plan_irfft(forward*x, length(x))
    m = mean(x)
    𝓕 = forward*(x .- m)
    shuffled𝓕 = zero(𝓕)
    s = similar(x)
    n = length(𝓕)
    r = abs.(𝓕)
    ϕ = angle.(𝓕)
    coeffs = zero(r)

    init = (inverse = inverse, m = m, coeffs = coeffs, n = n, r = r,
            ϕ = ϕ, shuffled𝓕 = shuffled𝓕)
    return SurrogateGenerator(rf, x, s, init, rng)
end

function (sg::SurrogateGenerator{<:PartialRandomization})()
    inverse, m, coeffs, n, r, ϕ, shuffled𝓕 =
        getfield.(Ref(sg.init),
        (:inverse, :m, :coeffs, :n, :r, :ϕ, :shuffled𝓕))
    s, rng = sg.s, sg.rng
    α = sg.method.α

    coeffs .= rand(rng, Uniform(0, 2π), n)
    shuffled𝓕 .= r .* exp.(coeffs .* 1im .* α)
    s .= inverse * shuffled𝓕 .+ m
    return s
end

"""
    PartialRandomizationAAFT(α = 0.5)

`PartialRandomizationAAFF` surrogates are similar to [`PartialRandomization`](@ref)
surrogates[^Ortega1998], but adds a rescaling step, so that the surrogate has
the same values as the original time series (analogous to the rescaling done for
[`AAFT`](@ref) surrogates).
Partial randomization surrogates have, to the package authors' knowledge, not been
published in scientific literature.

[^Ortega1998]: Ortega, Guillermo J.; Louis, Enrique (1998). Smoothness Implies Determinism in Time Series: A Measure Based Approach. Physical Review Letters, 81(20), 4345–4348. doi:10.1103/PhysRevLett.81.4345
"""
struct PartialRandomizationAAFT{T} <: Surrogate
    α::T

    function PartialRandomizationAAFT(α::T) where T <: Real
        @assert 0 <= α <= 1
        return new{T}(α)
    end
end

function surrogenerator(x::AbstractVector, rf::PartialRandomizationAAFT, rng = Random.default_rng())
    init = (
        gen = surrogenerator(x, PartialRandomization(rf.α), rng),
        ix = zeros(Int, length(x)),
        x_sorted = sort(x),
    )
    s = similar(x)
    return SurrogateGenerator(rf, x, s, init, rng)
end

function (sg::SurrogateGenerator{<:PartialRandomizationAAFT})()
    gen, ix, x_sorted = sg.init.gen, sg.init.ix, sg.init.x_sorted
    s = sg.s

    # Surrogate starts out as a PartialRandomization surrogate
    s .= gen()

    # Rescale to obtain a AAFT-like surrogate
    sortperm!(ix, s)
    s[ix] .= x_sorted

    return s
end


using BenchmarkTools

"""
    look_and_say_sequence(startsequence, n)

Construct the look and say sequence of order n and starting sequence startsequence.
"""
#function look_and_say_sequence(startsequence::String, n::Integer)
function look_and_say_sequence(startsequence, n)
    currentsequence = startsequence

    i = 2

    while i <= n
        count = 1
        tempseries = ""
        for j in 2:length(currentsequence)
            if currentsequence[j] == currentsequence[j-1]
                count += 1
            else
                tempseries = string(tempseries, count, currentsequence[j-1])
                count = 1
            end
        end
        tempseries = string(tempseries, count, currentsequence[length(currentsequence)])
        currentsequence = tempseries
        i += 1
    end
    return currentsequence
end

N = parse(Int, ARGS[1])

println("--------------------------")
println(@sprintf "Look and say sequence: %d" N)
println("--------------------------")

@btime begin
    r = look_and_say_sequence("1223334444", N)
end
#   println(r)


# M is the number of positions to keep
# 𝓟 is a collection of candidate positions.
function removepointssimple!(𝓟::Vector{T}, 𝑒::Function, M::Int, h::T, tol::T) where T
    @assert length(𝓟) > M

    abs_𝑒_𝓟 = abs.(𝑒.(𝓟))

    if length(𝓟) > M
        # remove the positions associated with the smallest errors.
        ind_sort = sortperm(abs_𝑒_𝓟,rev=true)

        out = 𝓟[ind_sort[1:M]]

        resize!(𝓟,length(out))
        𝓟[:] = out
    end

    @assert length(𝓟) == M

    return nothing
end

function lg7(a::Vector{T}, N::Int) where T
    𝓟 = Vector{T}(undef,0)
    removepointsAlg7!(𝓟,a,N)

    return 𝓟
end

# Algorithm 7 from Filip's thesis.
# N is the number of points to keep.
function removepointsAlg7!(𝓟::Vector{T}, 𝑒::Function, N) where T

    ### preparation steps before removing positions.
    sort!(𝓟)

    # for each interval, find the position that has the maximum absolute error.
    𝓐 = forcealternating(𝓟, 𝑒)

    # This should be true since 𝓟 should include the previous reference set, which
    #   has N alternating error signs.
    @assert length(𝓐) >= N

    if length(𝓐) == N
        return 𝓐
    end

    ##
    N_remove = length(𝓐) - N

    # remove the first or last position if N_remove cannot be fulfilled by
    #   removing only pairs of positions.
    if isodd(N_remove)
        if abs( 𝑒(𝓐[1]) ) > abs( 𝑒(𝓐[end]) )
            deleteat!(𝓐, length(𝓐))
        else
            deleteat!(𝓐, 1)
        end
        N_remove -= 1
    end
    @assert iseven(N_remove)

    # iteratively prune pairs of values.
    while N_remove > 0

        tmp = collect( max( abs(𝑒(𝓐[i])), abs(𝑒(𝓐[i+1])) ) for i = 1:length(𝓐)-1 )
        min_e, min_ind = findmin(tmp)

        # check if the wrap-around pair has the smallest absolute error.
        if max( abs(𝑒(𝓐[1])), abs(𝑒(𝓐[end])) ) < min_e
            # remove the abs error from wrap-around positions.
            deleteat!(𝓐, (1, length(𝓐)))
        else
            # remove smallest abs error.
            deleteat!(𝓐, (min_ind, min_ind+1))
        end

        N_remove -= 2
    end

    @assert length(𝓐) == N
    return 𝓐
end

function addpointsinwithinterval(a, N)
    𝓟 = Vector{T}(undef,0)
    addpointsinwithinterval!(𝓟, a, N)

    return 𝓟
end

# N is the number of points to add to a.
# assumes a is sorted in ascending order.
function addpointsinwithinterval!(𝓟::Vector{T}, a::Vector{T}, N)::Nothing where T
    M = length(a)

    ### add at least s number of points between every pair of extrema_positions
    s = div(N,M-1)
    r = N - s*(M-1)

    # reset container.
    resize!(𝓟, 0)

    # add points in the first interval.
    push!(𝓟, a[1])

    st = a[1]

    fin = a[2]
    tmp = collect(LinRange(st,fin,s+r+2)) # +2 since we want the end points, which are the extrema.

    push!(𝓟, tmp[2:end]...)

    st = fin

    # add points for the rest of the intervals.
    for i = 2:M-1
        # add points.
        fin = a[i+1]
        tmp = collect(LinRange(st,fin,s+2)) # +2 since we want the end points, which are the extrema.
        push!(𝓟, tmp[2:end]...)

        # update.
        st = fin
    end
    @assert length(𝓟) == N+M

    return nothing
end

function isbettercandidate(a::T, b::T, f::Function, η::T = one(T)) where T
    score_a = abs(Calculus.derivative(f,a)) + one(T)/abs(f(a))
    score_b = abs(Calculus.derivative(f,b)) + one(T)/abs(f(b))

    return score_a < score_b
end