export maximum_likelihood_lower_bound, ambiguous_lower_bound

export upper_bound, trivial_upper_bound, attains_trivial_upper_bound

"""
    maximum_likelihood_lower_bound( P :: BellScenario.AbstractStrategy ) :: Int64

Uses maximum likelihood estimation to efficiently compute the lower bound of the signaling dimension.
For a channel ``\\mathbf{P}\\in\\mathcal{P}^{X \\to Y}``, the maximum likelihood
lower bound on the signaling dimension is expressed,

```math
\\kappa(\\mathbf{P}) \\geq \\sum_{y\\in[Y]} \\max_{x\\in[X]} P(y|x).
```

Since the [`maximum_likelihood_facet`](@ref) is present on all signaling polytopes,
a lower bound can always be found with efficiency.
"""
function maximum_likelihood_lower_bound(P :: BellScenario.AbstractStrategy) :: Int64
    ml_score = sum(map(row -> max(row...), eachrow(P)))

    isapprox(ml_score%1,0, atol=1e-7) ? round(Int64, ml_score) : ceil(Int64, ml_score)
end

"""
    ambiguous_lower_bound( P :: BellScenario.AbstractStrategy ) :: Int64

Returns the lower bound on the signaling dimension as
witnessed by family of [`ambiguous_guessing_game`](@ref) Bell inequalities.
The intersection of the family of ambiguous guessing games forms the ambiguous
polytope ``\\mathcal{A}_{k,d}^{X\\to Y}`` where a violation to this polytope means
that ``\\kappa(\\mathbf{P}) \\geq d``.
Hence this method  computes the smallest ``d`` such that
``\\mathbf{P}\\in\\mathcal{A}_{k,d}^{X\\to Y}`` for all ``k``.
Formally, the  following equality is violated if ``\\mathbf{P}\\notin\\mathcal{A}_{k,d}^{X \\to Y}``

```math
d \\geq \\max_{k\\in[Y]}\\max_{\\sigma\\in \\Omega_{Y}} \\sum_{y=1}^{k} \\max_{x\\in[X]} P(\\sigma(y)|x) + \\frac{1}{X - d + 1}\\sum_{y=k+1}^{Y}\\sum_{x\\in [n]} P(\\sigma(y)|x)
```

where ``\\Omega_{Y}`` is the set of all permutations of ``[Y]``.

!!! "note" Note:
    Considering all permutations of ``k`` guessing rows can be costly. This performance
    is greatly improved by sorting the rows of ``\\mathbf{P}`` by their difference
    `max(row...) - sum(row)/(X - d + 1)` in non-increasing order.

In general, it may not be necessary to consider the entire range of ``k``.
For more specialized cases we provide the following methods:

"""
function ambiguous_lower_bound(P :: BellScenario.AbstractStrategy) :: Int64
    ambiguous_lower_bound(P, 1:size(P,1))
end

"""
    ambiguous_lower_bound(
        P :: BellScenario.AbstractStrategy,
        k :: Int64
    ) :: Int64

Finds the ambiguous lower bound on ``\\kappa(\\mathbf{P})`` for fixed ``k``.

* `P` - Channel ``\\mathbf{P}\\in\\mathcal{P}^{X\\to Y}``, a column stochastic matrix.
* `k` - The number of guessing rows.

A `DomainError` is thrown if:
* `k < 1`
* `k > size(P)[1]` (number of rows in `P`).
"""
function ambiguous_lower_bound(P :: BellScenario.AbstractStrategy, k :: Int64) :: Int64
    (num_rows, num_cols) = size(P)

    ambiguous_lower_bound(P, k:k)
end

"""
    ambiguous_lower_bound(P :: BellScenario.Strategy, k_range :: UnitRange{Int64}) :: Int64

Returns the smallest integer `d` such that `P` is contained by all ambiguous polytopes
with `k in k_range`.

A `DomainError` is thrown if `k_range` is not contained by the range `[1:size(P,1)]`.
"""
function ambiguous_lower_bound(P :: BellScenario.AbstractStrategy, k_range :: UnitRange{Int64}) :: Int64
    (num_rows, num_cols) = size(P)

    k_min = k_range[1]
    k_max = k_range[end]

    if !(1 ≤ k_min ≤ k_max ≤ num_rows)
        throw(DomainError(k_range, "input `k_range = k_min:k_max`  must have `1 ≤ k_min ≤  k_max ≤ num_rows`."))
    end

    # computing a raw lower bound using maximum likelihood  estimation
    P_ml_sort = sortslices(P, dims=1, by=row -> max(row...), rev=true)
    ml_min = sum(y -> max(P_ml_sort[y,:]...), 1:k_min)
    d_min = isapprox(ml_min%1, 0, atol=1e-7) ? round(Int64, ml_min) : ceil(Int64, ml_min)

    lower_bound = 1
    d = d_min
    while d <= min(num_rows, num_cols)
        P_sorted = _ambiguous_lower_bound_sort(P,d)

        ambiguous_scores = map( k -> begin
            ml_sum = sum(y -> max(P_sorted[y,:]...), 1:k)
            ambiguous_sum = (k < num_rows) ? sum(y -> sum(P_sorted[y,:]), k+1:num_rows) : 0

            ambiguous_score = ml_sum + ambiguous_sum/(num_cols - d + 1)

            ambiguous_score
        end,  k_range)

        (max_ambiguous_score, max_id) = findmax(ambiguous_scores)

        max_k = k_range[max_id]
        max_ml_sum = sum(y -> max(P_sorted[y,:]...), 1:max_k)

        d_min = isapprox(max_ml_sum%1, 0, atol=1e-7) ? round(Int64, max_ml_sum) : ceil(Int64, max_ml_sum)

        if (max_ambiguous_score < d) || (max_ambiguous_score ≈ d)
            lower_bound = d
            break
        else
            d = (d_min > d) ? d_min : d + 1
        end
    end

    lower_bound
end

"""
    _ambiguous_lower_bound_sort(P :: AbstractMatrix;  d=1::Int64) :: AbstractMatrix

Sort rows matrix ``\\mathbf{P}`` by ``\\max_{x\\in[X]} P(y|x) - \\sum_{x\\in[X]}P(y|x)/(X-d+1)``.
"""
function _ambiguous_lower_bound_sort(P :: AbstractMatrix, d::Int64)
    num_cols = size(P,2)

    max_sum_diff(row) = max(row...)- sum(row)/(num_cols - d + 1)
    sorted_rows = sortslices(P, dims=1, by=max_sum_diff, rev=true)

    sorted_rows
end

"""
    trivial_upper_bound( P :: BellScenario.AbstractStrategy ) :: Int64

The signaling dimension of channel cannot exceed the number of inputs or outputs.
Therefore, the trivial upper bound for the signaling dimension of a channel
``\\mathbf{P}\\in\\mathcal{P}^{X \\to Y}`` is simply,

```math
\\kappa(\\mathbf{P}) \\leq \\min\\{X,Y\\}.
```
"""
function trivial_upper_bound(P :: BellScenario.AbstractStrategy) :: Int64
    min(size(P)...)
end

"""
    attains_trivial_upper_bound( P :: BellScenario.Strategy ) :: Bool

Returns `true` if the channel `P` attains the [`trivial_upper_bound`](@ref).
This method relies on the fact:
* When ``d = X - 1``, the signaling polytope ``\\mathcal{C}_d^{X  \\to Y}`` is only
    bound by maximum likelihood facets.
* When ``d = Y - 1``, the  signaling polytope ``\\mathcal{C}_d^{X \\to Y}`` is only
    bound by maximum likelihood and ambiguous facets.
"""
function attains_trivial_upper_bound(P :: BellScenario.AbstractStrategy) :: Bool
    (num_rows, num_cols) = size(P)

    d = min(num_rows, num_cols) - 1

    κ_ML = maximum_likelihood_lower_bound(P)
    upper_bound_attained = (κ_ML > d)

    # if ML estimation doesn't attain the trivial upper bound ambiguous estimation is used
    if num_rows ≥ num_cols && !upper_bound_attained
        κ_ambiguous = ambiguous_lower_bound(P, num_cols:num_rows)
        upper_bound_attained = (κ_ambiguous > d)
    end

    upper_bound_attained
end

"""
    upper_bound( P :: BellScenario.AbstractStrategy ) :: Int64

Returns an upper bound for the signaling dimension of a channel `P`.
If [`attains_trivial_upper_bound`](@ref) returns `true`, then this upper bound
is tight and designates the signaling dimension.
Otherwise, a loose upper bound is provided.
A lower bound can be found with the [`maximum_likelihood_lower_bound`](@ref) or
[`ambiguous_lower_bound`](@ref) methods.
"""
function upper_bound(P :: BellScenario.AbstractStrategy) :: Int64
    trivial_bound = trivial_upper_bound(P)
    attains_trivial_upper_bound(P) ? trivial_bound : trivial_bound - 1
end


using Plots

# Frequency domain analysis of a simple system with time delays

s = tf("s")

P = delay(0.2) * ss(1/(s+1))

K = 3; Ti = 0.3;
C = ss(K*(1 + 1/s))

ω = exp10.(LinRange(-2,2,500))

nyquistplot(P*C, ω, xlims=(-2,1), ylims=(-2,2), gaincircles=false)

G_yd = feedback(P, C)
bodeplot(G_yd, ω, plotphase=false, title="Transfer function from load disturbances to output.")


module SimpleNeuralNetwork

import Base: repr, show

export NeuralLayer, NeuralNetwork, train!, predict, repr, show

function derivative(fun::Function; epsilon = 1e-3)
    function (x)
        (fun(x + epsilon) - fun(x - epsilon)) / (2*epsilon)
    end
end

function sqr_cost(y_predict::Array, y::Array)
    sqrt(sum((y_predict - y).^2))
end

type NeuralLayer
    weight::Union(Matrix, Array{None, 1}) # Let conversion do the work....
    _nodes_value::Array{Float64, 1}

    function NeuralLayer(w, nv)
        new(w, nv)
    end
end

type NeuralNetwork
    structure::Array{Int, 1}
    act_fun::Function
    act_diff::Function
    layers::Array{NeuralLayer, 1}
    out_fun::Function
    out_diff::Function
    _deltas::Array{Vector{FloatingPoint}, 1} # Array for storing dels

    # Constructor for NeuralNetwork type
    function NeuralNetwork(struct; act_fun = tanh, act_diff = None, out_fun = x-> x, out_diff = None)
        if act_diff == None
            act_diff = derivative(act_fun)
        end
        if out_diff == None
            out_diff = derivative(out_fun)
        end
        layers = NeuralLayer[]
        for ind = 1:(length(struct)-1)
            dim_in = struct[ind]
            dim_out = struct[ind+1]
            b = sqrt(6) / sqrt(dim_in + dim_out)
            w = 2b*rand(dim_out, dim_in + 1) - b
            nodes_value = push!([1.0], rand(dim_in)...)
            temp_layer = NeuralLayer(w, nodes_value)
            push!(layers, temp_layer)
        end
        # append the output layer.
        w = []
        nodes_value = rand(struct[end])
        temp_layer = NeuralLayer(w, nodes_value)
        push!(layers, temp_layer)
        _deltas = Vector{Float64}[[1.] for i = 1:length(struct)]
        nn = new(struct, act_fun, act_diff, layers, out_fun, out_diff, _deltas)
    end
end

function repr(nn::NeuralNetwork)
    struct = join([string(i) for i in nn.structure], "x")
    msg = join(["It is a ", struct, " NeuralNetwork.\n"] , "")
    msg = join([msg, "Activate Function: ", string(nn.act_fun), '\n'], "")
    msg = join([msg, "Output Function: ", string(nn.out_fun), '\n'], "")
    msg
end

function show(nn::NeuralNetwork)
    print(repr(nn))
    println()
end

function predict(nn::NeuralNetwork, data::Array{Float64, 2})
    predict_results = Array{Float64, 1}[]
    for data_id = 1:size(data)[1]
        v = data[data_id, :][:]
        #println(v)
        forward_prob!(nn, v)
        push!(predict_results, nn.out_fun(nn.layers[end]._nodes_value))
    end
    return predict_results
end

function forward_prob!(nn::NeuralNetwork, x)
    # forward_prob! will update nodes_value for all layers.
    nn.layers[1]._nodes_value = [1, x]
    n_layers = length(nn.structure)
    for layer_id = 1:(n_layers-2)
        current_layer = nn.layers[layer_id]
        next_layer = nn.layers[layer_id + 1]
        temp = current_layer.weight * current_layer._nodes_value
        next_layer._nodes_value = [1., nn.act_fun(temp)]
    end
    # Compute the node values of the last layer without pass through activation function.
    current_layer = nn.layers[end - 1]
    next_layer = nn.layers[end]
    temp = current_layer.weight * current_layer._nodes_value
    next_layer._nodes_value = temp[:]
    return
end

function back_prob!(nn::NeuralNetwork, x, y)
    # back_prob! will update nn._detas.
    forward_prob!(nn, x)
    nn._deltas[end] = -2.*(y - nn.out_fun(nn.layers[end]._nodes_value)).*nn.out_diff(nn.layers[end]._nodes_value)

    n_layers = length(nn.structure)
    for layer_id = n_layers-1:-1:1
        delta_next = nn._deltas[layer_id + 1]
        w_this = nn.layers[layer_id].weight[:, 2:end]
        nodes_value_this = nn.layers[layer_id]._nodes_value
        dd = nn.act_diff(nodes_value_this[2:end])
        temp = transpose(w_this) * delta_next
        nn._deltas[layer_id] = temp[:] .* dd
    end
    return
end

function train!(nn::NeuralNetwork, X::Matrix, Y::Array; epos = 10000, cost_fun = sqr_cost, tol = 0.0001, learning_rate = 0.1)
    y_predict = predict(nn, X)
    #return y_predict
    err_p = cost_fun(y_predict, Y)
    #return err_p
    n_obs = size(X)[1]
    #return n_obs
    errors = Float64[]
    for iter = 1:epos
        ind = rand(1:n_obs)
        x = X[ind, :][:]
        y = Y[ind]
        back_prob!(nn, x, y)
        #return nn
        for layer_id in 1:(length(nn.layers)-1)
            next_id = layer_id + 1
            nl = nn.layers[layer_id]
            gradient = nn._deltas[next_id]*nl._nodes_value'
            nl.weight -= learning_rate .* gradient
        end

        y_predict = predict(nn, X)
        err_t = cost_fun(y_predict, Y)
        push!(errors, err_t)
        if iter % 1000 == 0
            if err_p - err_t > 0 && err_p - err_t < tol
                println("Terminating training process due to no significant improvement.")
                println("At iteration No. ", iter)
                break
            end
        end
    end
    return errors
end

end # module


using ITensors

function heisenberg(N)
  os = Sum{Op}()
  for j in 1:(N - 1)
    os += "Sz", j, "Sz", j + 1
    os += 0.5, "S+", j, "S-", j + 1
    os += 0.5, "S-", j, "S+", j + 1
  end
  return os
end

function main(N; nsteps, order)
  ℋ = heisenberg(N)
  s = siteinds("S=1/2", N)
  ψ₀ = MPS(s, n -> isodd(n) ? "↑" : "↓")
  t = 1.0
  𝒰 = exp(im * t * ℋ; alg=Trotter{order}(nsteps))
  U = Prod{ITensor}(𝒰, s)
  H = ITensor(ℋ, s)
  𝒰ʳᵉᶠ = exp(im * t * ℋ)
  Uʳᵉᶠ = ITensor(𝒰ʳᵉᶠ, s)
  Uʳᵉᶠψ₀ = replaceprime(Uʳᵉᶠ * prod(ψ₀), 1 => 0)
  return norm(prod(U(ψ₀)) - Uʳᵉᶠψ₀)
end

@show main(4; nsteps=10, order=1)
@show main(4; nsteps=10, order=2)
@show main(4; nsteps=10, order=4)
@show main(4; nsteps=100, order=1)
@show main(4; nsteps=100, order=2)
@show main(4; nsteps=100, order=4)


inpt = """
1
2
3
7
11
13
17
19
23
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97
101
103
107
109
113"""

packs = Int64[]
for item in split(inpt)
	push!(packs, parse(Int, item))
end
# only need to look for the smallest subset that sums to 520 mass (total/3) and leaves two other subsets that sum to total/3
i = 3
found = false
total = div(sum(packs), 4)
println(total)
while true
	# the input is sorted, so the smallest quantum entanglement is going to be found first
	for best in combinations(packs, i)
		if sum(best) == total
			remainder = setdiff(packs, best)
			for parts in partitions(remainder, 3)
				# now try all possibilities of the remaining combinations
				# if they don't all 3 add up to total, we're not done
				found = true
				for p in parts
					if sum(p) != total
						found = false
						break
					end
				end
				found && break
			end
		end
		if found
			println(best, " ", sum(best), " ", prod(best))
			break
		end
	end
	found && break
	i += 1
end


@doc doc"""
    Metric

Abstract type for the pseudo-Riemannian metric tensor $g$, a family of smoothly
varying inner products on the tangent space. See [`inner`](@ref).
"""
abstract type Metric end

@doc doc"""
    RiemannianMetric <: Metric

Abstract type for Riemannian metrics, a family of positive definite inner
products. The positive definite property means that for $v \in T_x M$, the
inner product $g(v, v) > 0$ whenever $v$ is not the zero vector.
"""
abstract type RiemannianMetric <: Metric end

@doc doc"""
    LorentzMetric <: Metric

Abstract type for Lorentz metrics, which have a single time dimension. These
metrics assume the spacelike convention with the time dimension being last,
giving the signature $(++...+-)$.
"""
abstract type LorentzMetric <: Metric end

"""
    MetricManifold{M<:Manifold,G<:Metric} <: Manifold

Equip a manifold with a metric. Such a manifold is generally called pseudo-
or semi-Riemannian. Each `MetricManifold` must implement
[`local_metric`](@ref).

# Constructor

    MetricManifold(manifold, metric)
"""
struct MetricManifold{M<:Manifold,G<:Metric} <: Manifold
    manifold::M
    metric::G
end

convert(::Type{MT},M::MetricManifold{MT,GT}) where {MT,GT} = base_manifold(M)

is_decorator_manifold(M::MetricManifold) = Val(true)

@doc doc"""
    metric(M::MetricManifold)

Get the metric $g$ of the manifold `M`.
"""
metric(M::MetricManifold) = M.metric

@doc doc"""
    local_metric(M::MetricManifold, x)

Local matrix representation at the point `x` of the metric tensor $g$ on the
manifold `M`, usually written $g_{ij}$. The matrix has the property that
$g(v, w)=v^T [g_{ij}] w = g_{ij} v^i w^j$, where the latter expression uses
Einstein summation convention.
"""
function local_metric(M::MetricManifold, x)
    error("Local metric not implemented on $(typeof(M)) for point $(typeof(x))")
end

@doc doc"""
    inverse_local_metric(M::MetricManifold, x)

Local matrix representation of the inverse metric (cometric) tensor, usually
written $g^{ij}$
"""
inverse_local_metric(M::MetricManifold, x) = inv(local_metric(M, x))

@doc doc"""
    det_local_metric(M::MetricManifold, x)

Determinant of local matrix representation of the metric tensor $g$
"""
det_local_metric(M::MetricManifold, x) = det(local_metric(M, x))

@doc doc"""
    log_local_metric_density(M::MetricManifold, x)

Return the natural logarithm of the metric density $\rho$ of `M` at `x`, which
is given by $\rho=\log \sqrt{|\det [g_{ij}]|}$.
"""
log_local_metric_density(M::MetricManifold, x) = log(abs(det_local_metric(M, x))) / 2

@doc doc"""
    local_metric_jacobian(M::MetricManifold, x)

Get partial derivatives of the local metric of `M` at `x` with respect to the
coordinates of `x`, $\frac{\partial}{\partial x^k} g_{ij} = g_{ij,k}$. The
dimensions of the resulting multi-dimensional array are ordered $(i,j,k)$.
"""
function local_metric_jacobian(M, x)
    error("local_metric_jacobian not implemented on $(typeof(M)) for point $(typeof(x)). For a suitable default, enter `using ForwardDiff`.")
end

@doc doc"""
    christoffel_symbols_first(M::MetricManifold, x)

Compute the Christoffel symbols of the first kind in local coordinates.
The Christoffel symbols are (in Einstein summation convention)

$\Gamma_{ijk} = \frac{1}{2} \left[g_{kj,i} + g_{ik,j} - g_{ij,k}\right],$

where $g_{ij,k}=\frac{\partial}{\partial x^k} g_{ij}$ is the coordinate
derivative of the local representation of the metric tensor. The dimensions of
the resulting multi-dimensional array are ordered $(i,j,k)$.
"""
function christoffel_symbols_first(M::MetricManifold, x)
    ∂g = local_metric_jacobian(M, x)
    n = size(∂g, 1)
    Γ = similar(∂g, Size(n, n, n))
    @einsum Γ[i,j,k] = 1 / 2 * (∂g[k,j,i] + ∂g[i,k,j] - ∂g[i,j,k])
    return Γ
end

@doc doc"""
    christoffel_symbols_second(M::MetricManifold, x)

Compute the Christoffel symbols of the second kind in local coordinates.
The Christoffel symbols are (in Einstein summation convention)

$\Gamma^{l}_{ij} = g^{kl} \Gamma_{ijk},$

where $\Gamma_{ijk}$ are the Christoffel symbols of the first kind, and
$g^{kl}$ is the inverse of the local representation of the metric tensor.
The dimensions of the resulting multi-dimensional array are ordered $(l,i,j)$.
"""
function christoffel_symbols_second(M::MetricManifold, x)
    ginv = inverse_local_metric(M, x)
    Γ₁ = christoffel_symbols_first(M, x)
    Γ₂ = similar(Γ₁)
    @einsum Γ₂[l,i,j] = ginv[k,l] * Γ₁[i,j,k]
    return Γ₂
end

@doc doc"""
    christoffel_symbols_second_jacobian(M::MetricManifold, x)

Get partial derivatives of the Christoffel symbols of the second kind
for manifold `M` at `x` with respect to the coordinates of `x`,
$\frac{\partial}{\partial x^l} \Gamma^{k}_{ij} = \Gamma^{k}_{ij,l}.$
The dimensions of the resulting multi-dimensional array are ordered $(i,j,k,l)$.
"""
function christoffel_symbols_second_jacobian(M, x)
    error("christoffel_symbols_second_jacobian not implemented on $(typeof(M)) for point $(typeof(x)). For a suitable default, enter `using ForwardDiff`.")
end

@doc doc"""
    riemann_tensor(M::MetricManifold, x)

Compute the Riemann tensor $R^l_{ijk}$, also known as the Riemann curvature
tensor, at the point `x`. The dimensions of the resulting multi-dimensional
array are ordered $(l,i,j,k)$.
"""
function riemann_tensor(M::MetricManifold, x)
    n = size(x, 1)
    Γ = christoffel_symbols_second(M, x)
    ∂Γ = christoffel_symbols_second_jacobian(M, x) ./ n
    R = similar(∂Γ, Size(n, n, n, n))
    @einsum R[l,i,j,k] = ∂Γ[l,i,k,j] - ∂Γ[l,i,j,k] + Γ[s,i,k] * Γ[l,s,j] - Γ[s,i,j] * Γ[l,s,k]
    return R
end

"""
    ricci_tensor(M::MetricManifold, x)

Compute the Ricci tensor, also known as the Ricci curvature tensor,
of the manifold `M` at the point `x`.
"""
function ricci_tensor(M::MetricManifold, x)
    R = riemann_tensor(M, x)
    n = size(R, 1)
    Ric = similar(R, Size(n, n))
    @einsum Ric[i,j] = R[l,i,l,j]
    return Ric
end

"""
    ricci_curvature(M::MetricManifold, x)

Compute the Ricci scalar curvature of the manifold `M` at the point `x`.
"""
function ricci_curvature(M::MetricManifold, x)
    ginv = inverse_local_metric(M, x)
    Ric = ricci_tensor(M, x)
    S = sum(ginv .* Ric)
    return S
end

"""
    gaussian_curvature(M::MetricManifold, x)

Compute the Gaussian curvature of the manifold `M` at the point `x`.
"""
gaussian_curvature(M::MetricManifold, x) = ricci_curvature(M, x) / 2

"""
    einstein_tensor(M::MetricManifold, x)

Compute the Einstein tensor of the manifold `M` at the point `x`.
"""
function einstein_tensor(M::MetricManifold, x)
    Ric = ricci_tensor(M, x)
    g = local_metric(M, x)
    ginv = inverse_local_metric(M, x)
    S = sum(ginv .* Ric)
    G = Ric - g .* S / 2
    return G
end

@doc doc"""
    solve_exp_ode(M::MetricManifold, x, v, tspan; solver=AutoVern9(Rodas5()), kwargs...)

Approximate the exponential map on the manifold over the provided timespan
assuming the Levi-Civita connection by solving the ordinary differential
equation

$\frac{d^2}{dt^2} x^k + \Gamma^k_{ij} \frac{d}{dt} x_i \frac{d}{dt} x_j = 0,$

where $\Gamma^k_{ij}$ are the Christoffel symbols of the second kind, and
the Einstein summation convention is assumed. The arguments `tspan` and
`solver` follow the `OrdinaryDiffEq` conventions. `kwargs...` specify keyword
arguments that will be passed to `OrdinaryDiffEq.solve`.

Currently, the numerical integration is only accurate when using a single
coordinate chart that covers the entire manifold. This excludes coordinates
in an embedded space.
"""
function solve_exp_ode(M, x, v, tspan; kwargs...)
    error("solve_exp_ode not implemented on $(typeof(M)) for point $(typeof(x)), vector $(typeof(v)), and timespan $(typeof(tspan)). For a suitable default, enter `using OrdinaryDiffEq`.")
end

function exp(M::MMT, x, v, T::AbstractVector) where {MMT<:MetricManifold}
    sol = solve_exp_ode(M, x, v, extrema(T); dense=false, saveat=T)
    n = length(x)
    return map(i -> sol.u[i][n+1:end], 1:length(T))
end

"""
    exp(M::MetricManifold, x, v, args...)

Numerically integrate the exponential map assuming the Levi-Civita connection.
See [`solve_exp_ode`](@ref)

Currently, the numerical integration is only accurate when using a single
coordinate chart that covers the entire manifold. This excludes coordinates
in an embedded space.
"""
function exp!(M::MMT, y, x, v) where {MMT<:MetricManifold}
    tspan = (0.0, 1.0)
    sol = solve_exp_ode(M, x, v, tspan; dense=false, saveat=[1.0])
    n = length(x)
    y .= sol.u[1][n+1:end]
    return y
end

# Introduce the default to fall back to just base without metric.
#
# Most of these can be reduced as soon as we have the final THTT decorator

inner(M::MMT, x, v, w) where {MMT<:MetricManifold} = dot(v, local_metric(M, x) * w)
norm(M::MMT, x, v) where {MMT<:MetricManifold} = sqrt(inner(M,x,v,v))
distance(M::MMT, x, y) where {MMT<:MetricManifold} = distance(base_manifold(M), x, y)
zero_tangent_vector(M::MMT, x, v) where {MMT<:MetricManifold} = zero_tangent_vector(base_manifold(M), x, v, v)
log!(M::MMT, v, x, y) where {MMT<:MetricManifold} = log!(base_manifold(M), v, x, y)
retract!(M::MMT, y, x, v, t::Real,args...) where {MMT<:MetricManifold} = retract!(base_manifold(M), y, x,v,t::Real, args...)
retract!(M::MMT, y, x, v) where {MMT<:MetricManifold} = retract!(base_manifold(M), y, x, v)
project_tangent!(M::MMT, w, x, v) where {MMT<:MetricManifold} = project_tangent!(base_manifold(M), w, x, v)
is_approx(M::MMT,args...; kwargs...) where {MMT<:MetricManifold} = is_approx(base_manifold(M),args...; kwargs...)
function vector_transport_to!(M::MMT, vto, x, v, y, m::AbstractVectorTransportMethod) where {MMT<:MetricManifold}
    return vector_transport_to!(base_manifold(M), vto, x, v, y, m)
end
function injectivity_radius(M::MMT, args...) where {MMT<:MetricManifold}
    return injectivity_radius(base_manifold(M), args...)
end
function zero_tangent_vector!(M::MMT, v, x) where {MMT<:MetricManifold}
    return zero_tangent_vector!(base_manifold(M), v, x)
end
function check_manifold_point(M::MMT, x; kwargs...) where {MMT<:MetricManifold}
    return check_manifold_point(base_manifold(M), x; kwargs...)
end
function check_tangent_vector(M::MMT, x, v; kwargs...) where {MMT<:MetricManifold}
    return check_tangent_vector(base_manifold(M), x, v; kwargs...)
end
function inner(B::VectorBundleFibers{<:CotangentSpaceType, MMT}, x, v, w) where {MMT<:MetricManifold}
    ginv = inverse_local_metric(B.M, x)
    return dot(v, ginv * w)
end

function flat!(M::MMT, v::FVector{CotangentSpaceType}, x, w::FVector{TangentSpaceType}) where {MMT<:MetricManifold}
    g = local_metric(M, x)
    copyto!(v.data, g*w.data)
    return v
end

function sharp!(M::MMT, v::FVector{TangentSpaceType}, x, w::FVector{CotangentSpaceType}) where {MMT<:MetricManifold}
    ginv = inverse_local_metric(M, x)
    copyto!(v.data, ginv*w.data)
    return v
end


const μ = 3.986e14 * 60^2
const r = 42164.0e3
const r2 = r^2
const mc = 500.0
const n2 = μ / r^3
const n = sqrt(n2)

const two_n = 2*n
const μ_r2 = μ/r2

const K₁ = [-28.8287 0.1005 -1449.9754 0.0046;
            -0.087 -33.2562 0.00462 -1451.5013]
const K₂ = [-288.0288 0.1312 -9614.9898 0.0;
            -0.1312 -288.0 0.0 -9614.9883]

const K₁mc = K₁/mc
const K₂mc = K₂/mc

function mymul!(v, A, x)
    for ind = 1:length(v)
        v[ind] = zero(x[1])
        for jind = 1:length(x)
            v[ind] += A[ind, jind] * x[jind]
        end
    end
    return nothing
end

# dynamics in the 'approaching' mode
@taylorize function spacecraft_approaching!(t, x, dx)
    x_1 = r + x[1]
    x_12 = x_1^2
    x_22 = x[2]^2
    rc = sqrt(x_12 + x_22)
    rc3 = rc^3
    μ_rc3 = μ / rc3

    uxy = Array{typeof(x[1])}(undef, size(K₁mc,1))
    mymul!(uxy, K₁mc, x[1:4])

    # x' = vx
    dx[1] = x[3]
    # y' = vy
    dx[2] = x[4]
    # vx' = n²x + 2n*vy + μ/(r^2) - μ/(rc^3)*(r+x) + ux/mc
    dx[3] = (n2*x[1] + two_n*x[4]) + ((μ_r2 - μ_rc3*x_1) + uxy[1])
    # vy' = n²y - 2n*vx - μ/(rc^3)y + uy/mc
    dx[4] = (n2*x[2] - two_n*x[3]) - (μ_rc3*x[2] - uxy[2])
    # t' = 1
    dx[5] = one(t)

    return dx
end

# dynamics in the 'rendezvous attempt' mode
@taylorize function spacecraft_rendezvous_attempt!(t, x, dx)
    x_1 = r + x[1]
    x_12 = x_1^2
    x_22 = x[2]^2
    rc = sqrt(x_12 + x_22)
    rc3 = rc^3
    μ_rc3 = μ / rc3

    uxy = Array{typeof(x[1])}(undef, size(K₂mc,1))
    mymul!(uxy, K₂mc, x[1:4])

    # x' = vx
    dx[1] = x[3]
    # y' = vy
    dx[2] = x[4]
    # vx' = n²x + 2n*vy + μ/(r^2) - μ/(rc^3)*(r+x) + ux/mc
    dx[3] = (n2*x[1] + two_n*x[4]) + ((μ_r2 - μ_rc3*x_1) + uxy[1])
    # vy' = n²y - 2n*vx - μ/(rc^3)y + uy/mc
    dx[4] = (n2*x[2] - two_n*x[3]) - (μ_rc3*x[2] - uxy[2])
    # t' = 1
    dx[5] = one(t)

    return dx
end

# dynamics in the 'aborting' mode
@taylorize function spacecraft_aborting!(t, x, dx)
    x_1 = r + x[1]
    x_12 = x_1^2
    x_22 = x[2]^2
    rc = sqrt(x_12 + x_22)
    rc3 = rc^3
    μ_rc3 = μ / rc3

    # x' = vx
    dx[1] = x[3]
    # y' = vy
    dx[2] = x[4]
    # vx' = n²x + 2n*vy + μ/(r^2) - μ/(rc^3)*(r+x)
    dx[3] = (n2*x[1] + two_n*x[4]) + (μ_r2 - μ_rc3*x_1)
    # vy' = n²y - 2n*vx - μ/(rc^3)y
    dx[4] = (n^2*x[2] - 2*n*x[3]) - μ/(rc^3)*x[2]
    dx[4] = (n2*x[2] - two_n*x[3]) - μ_rc3*x[2]

    # t' = 1
    dx[5] = one(t)

    return dx
end


function calculate_ratios(pos::Array{T, 1})::Array{T, 2} where {T <: AbstractFloat}
    n::Int = length(pos)
    ratios::Array{T, 2} = zeros(Float64, n, n)
    @fastmath @inbounds for i in 1:n
        for j in i+1:n
            ratios[i,j] = pos[i] / pos[j]
        end
    end
    return ratios
end
function calculate_abs_diffs(pos::Array{T, 1})::Array{T, 2} where {T <: AbstractFloat}
    n::Int = length(pos)
    ratios::Array{T, 2} = zeros(Float64, n, n)
    @fastmath @inbounds for i in 1:n
        for j in i+1:n
            ratios[i, j] = pos[j] - pos[i]
        end
    end
    return ratios
end

function determine_calibration_constant_through_peak_ratios(fPositionX::Array{<:Real,1}, photon_lines::Array{<:Real,1}; α::Float64=0.01, rtol::Real = 5e-3)
    c::Float64 = 0
    photon_lines = Float64.(photon_lines)
    photon_lines = sort(photon_lines)
    fPositionX = sort(fPositionX)
    theo_ratios = calculate_ratios(photon_lines)
    exp_ratios = calculate_ratios(fPositionX)
    Δtheo = calculate_abs_diffs(photon_lines)
    n_peaks = length(fPositionX)
    n_lines = length(photon_lines)
    pcfs = Float64[] # pre calibration factors
    calibration_constant::Float64 = 0
    Δexp::Float64 = 0
    for i in 1:n_peaks
        for j in i + 1:n_peaks
            for it in 1:n_lines-1
                for jt in it + 1:n_lines
                    if 1 - α <= theo_ratios[it, jt] / exp_ratios[i, j] <= 1 + α
                        calibration_constant = (photon_lines[it] - photon_lines[jt]) / (fPositionX[i] - fPositionX[j])
                        Δexp = calibration_constant * (fPositionX[j] - fPositionX[i])
                        exp_peak_j = fPositionX[j] * calibration_constant
                        exp_peak_i = fPositionX[i] * calibration_constant
                        peak_i_rel_diff = abs(exp_peak_i - photon_lines[it]) / photon_lines[it]
                        peak_j_rel_diff = abs(exp_peak_j - photon_lines[jt]) / photon_lines[jt]
                        if peak_i_rel_diff < rtol && peak_j_rel_diff < rtol
                            push!(pcfs, calibration_constant )
                        end
                    end
                end
            end
        end
    end
    if isempty(pcfs)
        error("No peaks identified. Try different values for keyword parameters: `α`, `rtol`")
    end

    c_abs_min_diffs = zeros(Float64, length(pcfs))

    for (ic, c) in enumerate(pcfs)
        foundlines = c * fPositionX
        for (il, l) in enumerate(photon_lines)
            diffs = (foundlines .- l) ./ l
            abs_diffs = abs.(diffs)
            min_abs_diff, nearest_idx = findmin(abs_diffs)
            c_abs_min_diffs[ic] += min_abs_diff
        end
    end
    c = pcfs[findmin(c_abs_min_diffs)[2]]
    return c
end

function _filter_peaks_from_peakfinder(h::Histogram{<:Real, 1}, peakPositions::Vector{T}, σ::Real) where {T <: Real}
    pfits = []
    for p in peakPositions
        fitrange = (p - 5σ*step(h.edges[1]), p + 5σ*step(h.edges[1]))
        d = fit(NormalPeakUvD, subhist(h, fitrange))[1]
        push!(pfits, d)
    end
    d = map(i -> abs(pfits[i].UvNormal.μ - peakPositions[i]), eachindex(peakPositions));
    σs = map(f -> f.UvNormal.σ, pfits);
    hd = harmmean(d);
    hσ = harmmean(σs);
    accepted_peakPositions = T[]
    for (ipf, pf) in enumerate(pfits)
        if d[ipf] < 4 * hd && abs(pf.UvNormal.σ) < 2hσ
            push!(accepted_peakPositions, pf.UvNormal.μ)
        end
    end
    accepted_peakPositions
end

function determine_calibration_constant_through_peak_ratios(h::Histogram{<:Real, 1, E}, photon_lines::Array{<:Real, 1};
            min_n_peaks::Int = length(photon_lines), max_n_peaks::Int = 5 * length(photon_lines), threshold::Real = 10., α::Real = 0.01, σ::Real = 3.0, rtol::Real = 5e-3) where {E}
    h_deconv, peakPositions = peakfinder(h, threshold=threshold, σ = σ)
    fitted_peak_Positions = _filter_peaks_from_peakfinder(h, peakPositions, σ)
    while length(fitted_peak_Positions) > max_n_peaks
        threshold *= 1.1
        h_deconv, peakPositions = peakfinder(h, threshold=threshold, σ = σ)
        fitted_peak_Positions = _filter_peaks_from_peakfinder(h, peakPositions, σ)
    end
    while length(fitted_peak_Positions) < min_n_peaks
        threshold *= 0.75
        h_deconv, peakPositions = peakfinder(h, threshold=threshold, σ = σ)
        fitted_peak_Positions = _filter_peaks_from_peakfinder(h, peakPositions, σ)
    end
    c = determine_calibration_constant_through_peak_ratios(peakPositions, photon_lines, α = α, rtol = rtol)
    return c, h_deconv, peakPositions, threshold
end

function determine_calibration_constant_through_peak_fitting(h::Histogram{<:Real, 1, E}, photon_lines::Vector{<:Real}, c_pre::Real=1.0) where {E}
    c_pre = Float64(c_pre)
    peak_fits = []
    photon_lines = Float64.(photon_lines)
    processed_photon_lines = Float64[]
    for pl in photon_lines
        line::Float64 = pl
        fitrange = (line - 20, line + 20)
        fitrange = fitrange ./ c_pre

        first_bin, last_bin = StatsBase.binindex(h, fitrange[1]), StatsBase.binindex(h, fitrange[2])
        if last_bin > length(h.weights) || first_bin < 1
            @warn("$pl keV line not sufficiently contained in Histogram, skipping...");
            continue
        end
        d = fit(NormalPeakUvD, subhist(h, fitrange))[1]
        push!(peak_fits, d)
        push!(processed_photon_lines, pl)
    end
    photon_lines = intersect(photon_lines, processed_photon_lines)
    fitted_peak_positions = [ fr.UvNormal.μ for fr in peak_fits ]

    @fastmath function _linear_function_fixed_offset_at_zero(p::Vector{T}) where {T}
        return sqrt( sum(((fitted_peak_positions * p[1]) .- photon_lines).^2) )
    end

    opt_result = Optim.optimize(
        _linear_function_fixed_offset_at_zero,
        Float64[0], Float64[Inf], Float64[c_pre],
        Fminbox(BFGS());
        autodiff=:forward )

    c = opt_result.minimizer[1]

    return c, peak_fits, opt_result
end

"""
    calibrate_spectrum(h_uncal::Histogram, photon_lines::Array{Real, 1}; <keyword arguments>)

Returns the calibrated histogram, the deconvoluted spectrum, the found (uncalibrated) peak positions and the final `threshold` value.

# Keywords
- `σ::Real = 2.0`: The expected sigma of a peak in the spectrum. In units of bins.
- `threshold::Real = 10.0`: Threshold for being identified as a peak in the deconvoluted spectrum. A single bin is identified as a peak when its weight exceeds the `threshold` and the previous bin was not identified as an peak.
- `min_n_peaks::Int = 0`: If the number of found peaks is smaller than `min_n_peaks` the functions lowers the parameter `threshold` until enough peaks are found.
- `max_n_peaks::Int = 50`: Use only the first (strongest) `max_n_peaks` peaks for peak identification.
- `α::Real = 0.005`:  = 0.5%. Acceptance level in the comparison of the peak position ratios in the peak indentification step. When the difference between the ratio of two found peak positions and the ratio of two photon lines (`photon_lines`) is smaller than `α`, the found peaks are identified as the two photon lines.
- `rtol::Real = 5e-3`:  = 5e-3. Acceptance level for tolerance of the absolute difference between true and found line position.

Calibrate the spectrum `h_uncal`. This is done by:
1) finding peaks through devoncolution
2) identifying them through comparison of the ratios of their positions with the ratios of the known `lines`
3) fitting all identified peaks (with a gaussian plus first order polynomial) to get their position more precisely
4) performe a linear fit (offset forced to 0) of these positions vs the true positions (`lines`) to get the calibration constant
"""
function calibrate_spectrum(h_uncal::Histogram{<:Real, 1}, photon_lines::Vector{<:Real};
            σ::Real = 3.0, threshold::Real = 50.0, min_n_peaks::Int = length(photon_lines),
            max_n_peaks::Int = 4 * length(photon_lines), α::Real = 0.01, rtol::Real = 5e-3)
    c_precal, h_deconv, peakPositions, threshold = determine_calibration_constant_through_peak_ratios(h_uncal, photon_lines,
                                min_n_peaks = min_n_peaks, max_n_peaks = max_n_peaks, threshold=threshold, α=α, σ=σ, rtol=rtol)
    c = determine_calibration_constant_through_peak_fitting(h_uncal, photon_lines, c_precal)[1]
    h_cal = Histogram( h_uncal.edges[1] .* c, :left )
    h_cal.weights = h_uncal.weights
    return h_cal, h_deconv, peakPositions, threshold, c, c_precal
end


##
## Gamma Function
##

SpecialFunctions.gamma(z::Union{ArbOrRef,AcbOrRef}) = gamma!(zero(z), z)
SpecialFunctions.gamma(z::Series) = gamma_series!(zero(z), z, length(z))

SpecialFunctions.loggamma(x::Union{ArbOrRef,AcbOrRef}) = lgamma!(zero(x), x)
SpecialFunctions.loggamma(x::Series) = lgamma_series!(zero(x), x, length(x))

#SpecialFunctions.logabsgamma(x)
# Not implemented by Arb

#SpecialFunctions.logfactorial(x)
# Only relevant for Arblib

SpecialFunctions.digamma(x::Union{ArbOrRef,AcbOrRef}) = digamma!(zero(x), x)
SpecialFunctions.digamma(x::Series) = digamma_series!(zero(x), x, length(x))

#SpecialFunctions.invdigamma(x)
# Not implemented by Arb

function SpecialFunctions.trigamma(x::AcbOrRef)
    res = one(x)
    return polygamma!(res, res, x)
end

SpecialFunctions.polygamma(s::AcbOrRef, x::AcbOrRef) = polygamma!(zero(x), s, x)

SpecialFunctions.gamma(a::ArbOrRef, z::ArbOrRef) =
    hypgeom_gamma_upper!(Arb(0, prec = _precision((a, z))), a, z, 0)
SpecialFunctions.gamma(a::AcbOrRef, z::AcbOrRef) =
    hypgeom_gamma_upper!(Acb(0, prec = _precision((a, z))), a, z, 0)
SpecialFunctions.gamma(a::ArbOrRef, z::ArbSeries) =
    hypgeom_gamma_upper_series!(zero(z), a, z, 0, length(z))
SpecialFunctions.gamma(a::AcbOrRef, z::AcbSeries) =
    hypgeom_gamma_upper_series!(zero(z), a, z, 0, length(z))

#loggamma(a,z)
# Not implemented by Arb

function SpecialFunctions.gamma_inc(a::ArbOrRef, x::ArbOrRef)
    Γ = hypgeom_gamma_upper!(Arb(0, prec = _precision((a, x))), a, x, 1)
    # γ = 1 - Γ
    γ = neg!(Arb(0, prec = _precision((a, x))), Γ)
    add!(γ, γ, 1)
    return (γ, Γ)
end
function SpecialFunctions.gamma_inc(a::AcbOrRef, x::AcbOrRef)
    Γ = hypgeom_gamma_upper!(Acb(0, prec = _precision((a, x))), a, x, 1)
    # γ = 1 - Γ
    γ = neg!(Acb(0, prec = _precision((a, x))), Γ)
    add!(γ, γ, 1)
    return (γ, Γ)
end
function SpecialFunctions.gamma_inc(a::ArbOrRef, x::ArbSeries)
    Γ = hypgeom_gamma_upper_series!(zero(x), a, x, 1, length(x))
    γ = 1 - Γ
    return (γ, Γ)
end
function SpecialFunctions.gamma_inc(a::AcbOrRef, x::AcbSeries)
    Γ = hypgeom_gamma_upper_series!(zero(x), a, x, 1, length(x))
    γ = 1 - Γ
    return (γ, Γ)
end

function SpecialFunctions.beta_inc(a::ArbOrRef, b::ArbOrRef, x::ArbOrRef)
    β = hypgeom_beta_lower!(Arb(prec = _precision((a, x))), a, b, x, 1)
    # Β = 1 - β
    Β = neg!(Arb(0, prec = _precision((a, x))), β)
    add!(Β, Β, 1)
    return (β, Β)
end
function SpecialFunctions.beta_inc(a::AcbOrRef, b::AcbOrRef, x::AcbOrRef)
    β = hypgeom_beta_lower!(Acb(prec = _precision((a, x))), a, b, x, 1)
    # Β = 1 - β
    Β = neg!(Acb(0, prec = _precision((a, x))), β)
    add!(Β, Β, 1)
    return (β, Β)
end
function SpecialFunctions.beta_inc(a::ArbOrRef, b::ArbOrRef, x::ArbSeries)
    β = hypgeom_beta_lower_series!(zero(x), a, b, x, 1, length(x))
    Β = 1 - β
    return (β, Β)
end
function SpecialFunctions.beta_inc(a::AcbOrRef, b::AcbOrRef, x::AcbSeries)
    β = hypgeom_beta_lower_series!(zero(x), a, b, x, 1, length(x))
    Β = 1 - β
    return (β, Β)
end

#gamma_inc_inv(a, p, q)
# Not implemented by Arb

#beta(x,y)
# Not implemented directly by Arb, could use beta_inc or gamma to implement it?

#logbeta(x,y)
#Not implemented by Arb

#logabsbeta(x,y)
#Not implemented by Arb

#logabsbinomial(x,y)
#Not implemented by Arb

##
## Exponential and Trigonometric Integrals
##

# The version when ν is not specified could be defined directly, but
# it would likely be better if it was defined in SpecialFunctions
# directly.
SpecialFunctions.expint(ν::ArbOrRef, x::ArbOrRef) =
    hypgeom_expint!(Arb(prec = _precision((ν, x))), ν, x)
SpecialFunctions.expint(ν::AcbOrRef, x::AcbOrRef) =
    hypgeom_expint!(Acb(prec = _precision((ν, x))), ν, x)
SpecialFunctions.expint(ν::ArbOrRef, x::ArbSeries) =
    hypgeom_gamma_upper_series!(zero(x), 1 - ν, x, 2, length(x))
SpecialFunctions.expint(ν::AcbOrRef, x::AcbSeries) =
    hypgeom_gamma_upper_series!(zero(x), 1 - ν, x, 2, length(x))

function SpecialFunctions.expint(x::Union{ArbOrRef,AcbOrRef})
    ν = one(x)
    return hypgeom_expint!(ν, ν, x)
end
SpecialFunctions.expint(x::Series) =
    SpecialFunctions.expint(eltype(x)(1, prec = precision(x)), x)

SpecialFunctions.expinti(x::Union{ArbOrRef,AcbOrRef}) = hypgeom_ei!(zero(x), x)
SpecialFunctions.expinti(x::Series) = hypgeom_ei_series!(zero(x), x, length(x))

#expintx(x)
# Not implemented by arb

SpecialFunctions.sinint(x::Union{ArbOrRef,AcbOrRef}) = hypgeom!(zero(x), x)
SpecialFunctions.sinint(x::Series) = hypgeom_si_series!(zero(x), x, length(x))

SpecialFunctions.cosint(x::Union{ArbOrRef,AcbOrRef}) = hypgeom_ci!(zero(x), x)
SpecialFunctions.cosint(x::Series) = hypgeom_ci_series!(zero(x), x, length(x))

##
## Error Functions, Dawson’s and Fresnel Integrals
##

SpecialFunctions.erf(x::Union{ArbOrRef,AcbOrRef}) = hypgeom_erf!(zero(x), x)
SpecialFunctions.erf(x::Series) = hypgeom_erf_series!(zero(x), x, length(x))

#SpecialFunctions.erf(x,y)
# Not implemented by Arb

SpecialFunctions.erfc(x::Union{ArbOrRef,AcbOrRef}) = hypgeom_erfc!(zero(x), x)
SpecialFunctions.erfc(x::Series) = hypgeom_erfc_series!(zero(x), x, length(x))

#SpecialFunctions.erfcinv(x)
# Not implemented by Arb

#SpecialFunctions.erfcx(x)
# Not implemented by Arb

#SpecialFunctions.logerfc(x)
# Not implemented by Arb

#SpecialFunctions.logerfcx(x)
# Not implemented by Arb

#SpecialFunctions.erfi(x)
SpecialFunctions.erfi(x::Union{ArbOrRef,AcbOrRef}) = hypgeom_erfi!(zero(x), x)
SpecialFunctions.erfi(x::Series) = hypgeom_erfi_series!(zero(x), x, length(x))

#SpecialFunctions.erfinv(x)
# Not implemented by Arb

#SpecialFunctions.dawson(x)
# Not implemented by Arb

##
## Airy and Related Functions
##

function SpecialFunctions.airyai(z::ArbOrRef)
    ai = zero(z)
    ccall(
        @libarb(arb_hypgeom_airy),
        Cvoid,
        (Ref{arb_struct}, Ref{Cvoid}, Ref{Cvoid}, Ref{Cvoid}, Ref{arb_struct}, Int64),
        ai,
        C_NULL,
        C_NULL,
        C_NULL,
        z,
        precision(z),
    )
    return ai
end
function SpecialFunctions.airyai(z::AcbOrRef)
    ai = zero(z)
    ccall(
        @libarb(acb_hypgeom_airy),
        Cvoid,
        (Ref{acb_struct}, Ref{Cvoid}, Ref{Cvoid}, Ref{Cvoid}, Ref{acb_struct}, Int64),
        ai,
        C_NULL,
        C_NULL,
        C_NULL,
        z,
        precision(z),
    )
    return ai
end
function SpecialFunctions.airyai(z::ArbSeries)
    ai = zero(z)
    ccall(
        @libarb(arb_hypgeom_airy_series),
        Cvoid,
        (
            Ref{arb_poly_struct},
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{arb_poly_struct},
            Int64,
            Int64,
        ),
        ai,
        C_NULL,
        C_NULL,
        C_NULL,
        z,
        length(z),
        precision(z),
    )
    return ai
end
function SpecialFunctions.airyai(z::AcbSeries)
    ai = zero(z)
    ccall(
        @libarb(acb_hypgeom_airy_series),
        Cvoid,
        (
            Ref{acb_poly_struct},
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{acb_poly_struct},
            Int64,
            Int64,
        ),
        ai,
        C_NULL,
        C_NULL,
        C_NULL,
        z,
        length(z),
        precision(z),
    )
    return ai
end

function SpecialFunctions.airyaiprime(z::ArbOrRef)
    ai_prime = zero(z)
    ccall(
        @libarb(arb_hypgeom_airy),
        Cvoid,
        (Ref{Cvoid}, Ref{arb_struct}, Ref{Cvoid}, Ref{Cvoid}, Ref{arb_struct}, Int64),
        C_NULL,
        ai_prime,
        C_NULL,
        C_NULL,
        z,
        precision(z),
    )
    return ai_prime
end
function SpecialFunctions.airyaiprime(z::AcbOrRef)
    ai_prime = zero(z)
    ccall(
        @libarb(acb_hypgeom_airy),
        Cvoid,
        (Ref{Cvoid}, Ref{acb_struct}, Ref{Cvoid}, Ref{Cvoid}, Ref{acb_struct}, Int64),
        C_NULL,
        ai_prime,
        C_NULL,
        C_NULL,
        z,
        precision(z),
    )
    return ai_prime
end
function SpecialFunctions.airyaiprime(z::ArbSeries)
    ai_prime = zero(z)
    ccall(
        @libarb(arb_hypgeom_airy_series),
        Cvoid,
        (
            Ref{Cvoid},
            Ref{arb_poly_struct},
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{arb_poly_struct},
            Int64,
            Int64,
        ),
        C_NULL,
        ai_prime,
        C_NULL,
        C_NULL,
        z,
        length(z),
        precision(z),
    )
    return ai_prime
end
function SpecialFunctions.airyaiprime(z::AcbSeries)
    ai_prime = zero(z)
    ccall(
        @libarb(acb_hypgeom_airy_series),
        Cvoid,
        (
            Ref{Cvoid},
            Ref{acb_poly_struct},
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{acb_poly_struct},
            Int64,
            Int64,
        ),
        C_NULL,
        ai_prime,
        C_NULL,
        C_NULL,
        z,
        length(z),
        precision(z),
    )
    return ai_prime
end

function SpecialFunctions.airybi(z::ArbOrRef)
    bi = zero(z)
    ccall(
        @libarb(arb_hypgeom_airy),
        Cvoid,
        (Ref{Cvoid}, Ref{Cvoid}, Ref{arb_struct}, Ref{Cvoid}, Ref{arb_struct}, Int64),
        C_NULL,
        C_NULL,
        bi,
        C_NULL,
        z,
        precision(z),
    )
    return bi
end
function SpecialFunctions.airybi(z::AcbOrRef)
    bi = zero(z)
    ccall(
        @libarb(acb_hypgeom_airy),
        Cvoid,
        (Ref{Cvoid}, Ref{Cvoid}, Ref{acb_struct}, Ref{Cvoid}, Ref{acb_struct}, Int64),
        C_NULL,
        C_NULL,
        bi,
        C_NULL,
        z,
        precision(z),
    )
    return bi
end
function SpecialFunctions.airybi(z::ArbSeries)
    bi = zero(z)
    ccall(
        @libarb(arb_hypgeom_airy_series),
        Cvoid,
        (
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{arb_poly_struct},
            Ref{Cvoid},
            Ref{arb_poly_struct},
            Int64,
            Int64,
        ),
        C_NULL,
        C_NULL,
        bi,
        C_NULL,
        z,
        length(z),
        precision(z),
    )
    return bi
end
function SpecialFunctions.airybi(z::AcbSeries)
    bi = zero(z)
    ccall(
        @libarb(acb_hypgeom_airy_series),
        Cvoid,
        (
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{acb_poly_struct},
            Ref{Cvoid},
            Ref{acb_poly_struct},
            Int64,
            Int64,
        ),
        C_NULL,
        C_NULL,
        bi,
        C_NULL,
        z,
        length(z),
        precision(z),
    )
    return bi
end

function SpecialFunctions.airybiprime(z::ArbOrRef)
    bi_prime = zero(z)
    ccall(
        @libarb(arb_hypgeom_airy),
        Cvoid,
        (Ref{Cvoid}, Ref{Cvoid}, Ref{Cvoid}, Ref{arb_struct}, Ref{arb_struct}, Int64),
        C_NULL,
        C_NULL,
        C_NULL,
        bi_prime,
        z,
        precision(z),
    )
    return bi_prime
end
function SpecialFunctions.airybiprime(z::AcbOrRef)
    bi_prime = zero(z)
    ccall(
        @libarb(acb_hypgeom_airy),
        Cvoid,
        (Ref{Cvoid}, Ref{Cvoid}, Ref{Cvoid}, Ref{acb_struct}, Ref{acb_struct}, Int64),
        C_NULL,
        C_NULL,
        C_NULL,
        bi_prime,
        z,
        precision(z),
    )
    return bi_prime
end
function SpecialFunctions.airybiprime(z::ArbSeries)
    bi_prime = zero(z)
    ccall(
        @libarb(arb_hypgeom_airy_series),
        Cvoid,
        (
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{arb_poly_struct},
            Ref{arb_poly_struct},
            Int64,
            Int64,
        ),
        C_NULL,
        C_NULL,
        C_NULL,
        bi_prime,
        z,
        length(z),
        precision(z),
    )
    return bi_prime
end
function SpecialFunctions.airybiprime(z::AcbSeries)
    bi_prime = zero(z)
    ccall(
        @libarb(acb_hypgeom_airy_series),
        Cvoid,
        (
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{Cvoid},
            Ref{acb_poly_struct},
            Ref{acb_poly_struct},
            Int64,
            Int64,
        ),
        C_NULL,
        C_NULL,
        C_NULL,
        bi_prime,
        z,
        length(z),
        precision(z),
    )
    return bi_prime
end

#SpecialFunctions.airyaix(z)
# Not implemented by Arb

#SpecialFunctions.airyaiprimex(z)
# Not implemented by Arb

#SpecialFunctions.airybix(z)
# Not implemented by Arb

#SpecialFunctions.airybiprimex(z)
# Not implemented by Arb

##
## Bessel Functions
##

SpecialFunctions.besselj(ν::ArbOrRef, z::ArbOrRef) =
    hypgeom_bessel_j!(Arb(0, prec = _precision((ν, z))), ν, z)
SpecialFunctions.besselj(ν::AcbOrRef, z::AcbOrRef) =
    hypgeom_bessel_j!(Acb(0, prec = _precision((ν, z))), ν, z)

function SpecialFunctions.besselj0(z::Union{ArbOrRef,AcbOrRef})
    res = zero(z)
    return hypgeom_bessel_j!(res, res, z)
end

function SpecialFunctions.besselj1(z::Union{ArbOrRef,AcbOrRef})
    res = one(z)
    return hypgeom_bessel_j!(res, res, z)
end

#SpecialFunctions.besseljx(nu,z)
# Not implemented by Arb

#SpecialFunctions.sphericalbesselj(nu,z)
# The general method implemented by SpecialFunctions is not completely
# rigorous since it makes a cutoff for small values.
# TODO: We could check for the special case x = 0
function SpecialFunctions.sphericalbesselj(ν::ArbOrRef, x::ArbOrRef)
    # res = besselj(ν + 1 // 2, x)
    res = Arb(1 // 2, prec = _precision((ν, x)))
    add!(res, res, ν)
    hypgeom_bessel_j!(res, res, x)

    # factor = sqrt(π / 2x)
    factor = Arb(π, prec = _precision((ν, x)))
    div!(factor, factor, x)
    mul_2exp!(factor, factor, -1)
    sqrt!(factor, factor)

    return mul!(res, factor, res)
end

SpecialFunctions.bessely(ν::ArbOrRef, z::ArbOrRef) =
    hypgeom_bessel_y!(Arb(0, prec = _precision((ν, z))), ν, z)
SpecialFunctions.bessely(ν::AcbOrRef, z::AcbOrRef) =
    hypgeom_bessel_y!(Acb(0, prec = _precision((ν, z))), ν, z)

function SpecialFunctions.bessely0(z::Union{ArbOrRef,AcbOrRef})
    res = zero(z)
    return hypgeom_bessel_y!(res, res, z)
end

function SpecialFunctions.bessely1(z::Union{ArbOrRef,AcbOrRef})
    res = one(z)
    return hypgeom_bessel_y!(res, res, z)
end

#SpecialFunctions.besselyx(nu,z)
# Not implemented by Arb

#SpecialFunctions.sphericalbessely(nu,z)
# Aliased to √((float(T))(π)/2x) * bessely(nu + one(nu)/2, x) which works fine

SpecialFunctions.besselh(ν::ArbOrRef, k::Integer, z::ArbOrRef) =
    SpecialFunctions.besselh(Acb(ν), k, Acb(z))
function SpecialFunctions.besselh(ν::AcbOrRef, k::Integer, z::AcbOrRef)
    J, Y = Acb(prec = _precision((ν, z))), Acb(prec = _precision((ν, z)))
    hypgeom_bessel_jy!(J, Y, ν, z)
    mul_onei!(Y, Y)
    if k == 1
        return add!(J, J, Y)
    elseif k == 2
        return sub!(J, J, Y)
    else
        throw(SpecialFunctions.AmosException(1)) # This is what SpecialFunctions throw
    end
end

#SpecialFunctions.besselhx(nu,k,z)
# Not implemented by Arb

#SpecialFunctions.hankelh1(nu,z)
# Aliased to besselh(nu, 1, z)

#SpecialFunctions.hankelh1x(nu,z)
# Aliased to besselhx(nu, 1, z)

#SpecialFunctions.hankelh2(nu,z)
# Aliased to besselh(nu, 2, z)

#SpecialFunctions.hankelh2x(nu,z)
# Aliased to besselhx(nu, 2, z)

SpecialFunctions.besseli(ν::ArbOrRef, z::ArbOrRef) =
    hypgeom_bessel_i!(Arb(prec = _precision((ν, z))), ν, z)
SpecialFunctions.besseli(ν::AcbOrRef, z::AcbOrRef) =
    hypgeom_bessel_i!(Acb(prec = _precision((ν, z))), ν, z)

#SpecialFunctions.besselix(nu,z)
# The scaling used by Arb seems to be different from that used by
# SpecialFunctions.

SpecialFunctions.besselk(ν::ArbOrRef, z::ArbOrRef) =
    hypgeom_bessel_k!(Arb(prec = _precision((ν, z))), ν, z)
SpecialFunctions.besselk(ν::AcbOrRef, z::AcbOrRef) =
    hypgeom_bessel_k!(Acb(prec = _precision((ν, z))), ν, z)

SpecialFunctions.besselkx(ν::ArbOrRef, z::ArbOrRef) =
    hypgeom_bessel_k_scaled!(Arb(prec = _precision((ν, z))), ν, z)
SpecialFunctions.besselkx(ν::AcbOrRef, z::AcbOrRef) =
    hypgeom_bessel_k_scaled!(Acb(prec = _precision((ν, z))), ν, z)

#jinc(x)
# Aliased to 2 * besselj1(π*x) / (π*x) which works fine

##
## Elliptic Integrals
##

SpecialFunctions.ellipk(m::AcbOrRef) = elliptic_k!(zero(m), m)
SpecialFunctions.ellipk(m::AcbSeries) = elliptic_k_series!(zero(m), m, length(m))

SpecialFunctions.ellipe(m::AcbOrRef) = elliptic_e!(zero(m), m)

##
## Zeta and Related Functions
##

SpecialFunctions.eta(x::AcbOrRef) = dirichlet_eta!(zero(x), x)

SpecialFunctions.zeta(s::Union{ArbOrRef,AcbOrRef}) = zeta!(zero(s), s)
SpecialFunctions.zeta(s::Series) =
    SpecialFunctions.zeta(s, eltype(s)(1, prec = precision(s)))
SpecialFunctions.zeta(s::ArbOrRef, z::ArbOrRef) =
    hurwitz_zeta!(Arb(prec = _precision((s, z))), s, z)
SpecialFunctions.zeta(s::AcbOrRef, z::AcbOrRef) =
    hurwitz_zeta!(Acb(prec = _precision((s, z))), s, z)
SpecialFunctions.zeta(s::ArbSeries, z::ArbOrRef) = zeta_series!(zero(s), s, z, 0, length(s))
SpecialFunctions.zeta(s::AcbSeries, z::AcbOrRef) = zeta_series!(zero(s), s, z, 0, length(s))


function compute_mat_reg_loss(X::CuArray{Float32,2},
                              reg_mats::AbstractVector)
    s = 0.0
    for i=1:length(reg_mats)
        s += 0.5 * dot(X[i,:], reg_mats[i]*X[i,:])
    end
    return s
end

function add_mat_reg_grad!(grad_X::CuArray{Float32,2},
                           X::CuArray{Float32,2},
                           reg_mats::AbstractVector)
    for i=1:min(length(reg_mats), size(grad_X,1))
        grad_X[i,:] .+= (reg_mats[i]*X[i,:])
    end
    return nothing
end

function compute_vec_reg_loss(x::CuArray,
                              reg_mat::AbstractMatrix)
    return 0.5 * dot(x, reg_mat*x)
end

precompile(compute_mat_reg_loss, (gpu_mat, Vector{gpu_sparse}))
precompile(compute_vec_reg_loss, (gpu_vec, gpu_sparse))


# PolynomialApproximation.jl - polynomial approximations to FunctionalForm with ApproxFun package

using ApproxFun

# specify range of function. Too short + you end up with millions of polynomial coeffs
OHVssσFun=Fun(r->OHVssσ(r), 0.4..10)

# count number of coeffs
ncoefficients(OHVssσFun)

# plot the risidual
#plot( [x -> OHVssσFun(x) - OHVssσ(x)], 0.1,10 )

# derivative of the polynomial fit
OHVssσFunDeriv=OHVssσFun'

# Shouldn't really be doing computation in the src... Oh well!

println("Testing ApproxFun...")

orig=OOPP # OOPP is an EPL

for rcut in 0.05:0.1:0.5
    origFun=Fun(r->orig(r), rcut..10)
    coeff=ncoefficients(origFun)
    resid=origFun(1.0)-orig(1.0)
    println("rcut: $rcut ncoeff OHVssσFun=Fun: $coeff Residual@1.0: $resid")
    print("ApproxFun: ")
    @time map(origFun,0.1:0.01:10)
    print("Original: ")
    @time map(orig,0.1:0.01:10)
end


divodd(a, b) = let u = a ÷ b
    if u < 3
        3
    elseif iseven(u)
        u + 1
    else
        u
    end
end

function mwmean(mean::Function, xs::Matrix, mw::Int)
    n, m = size(xs)
    result = Matrix{eltype(xs)}(undef, n-mw+1, m)
    for i in 1:size(xs, 1)-mw+1
        result[i,:] = mean(xs[(i-1).+(1:mw), :]; dims=1)
    end
    result
end

mwmean(xs::Matrix, mw::Int) = mwmean(mean, xs, mw)

function bin(frames::Array{Float64, 3}; mw=divodd(size(frames, 3), 10))
    n, m, t = size(frames)
    binned = bin(squish(frames); mw=mw)
    reshape(transpose(binned), n, m, t - mw + 1)
end

function bin(frames::Matrix{Float64}; mw=divodd(size(frames, 1), 10))
    bound = abs.(mwmean(mean, frames, mw)) .+ mwmean(std, frames, mw)
    binned = Int.(bound .< abs.(frames[(1+mw÷2):size(frames, 1)-mw÷2,:])) .+ 1
end


"""
Entropy term -TS, where S is the electronic entropy.
Turns the energy E into the free energy F=E-TS.
This is in particular useful because the free energy,
not the energy, is minimized at self-consistency.
"""
struct Entropy end
(::Entropy)(basis) = TermEntropy(basis)

struct TermEntropy <: Term
    basis::PlaneWaveBasis
end

function ene_ops(term::TermEntropy, ψ, occ; kwargs...)
    basis = term.basis
    T = eltype(basis)
    ops = [NoopOperator(term.basis, kpoint) for kpoint in basis.kpoints]

    smearing = basis.model.smearing
    temperature = basis.model.temperature

    temperature == 0 && return (E=zero(T), ops=ops)
    ψ == nothing && return (E=T(Inf), ops=ops)

    filled_occ = filled_occupation(basis.model)
    eigenvalues = kwargs[:eigenvalues]
    εF = kwargs[:εF]

    E = zero(T)
    for (ik, k) in enumerate(basis.kpoints)
        for iband = 1:size(ψ[1], 2)
            E -= (temperature
                  * basis.kweights[ik]
                  * filled_occ
                  * Smearing.entropy(smearing, (eigenvalues[ik][iband] - εF) / temperature))
        end
    end

    (E=E, ops=ops)
end


#File: maxSharpe.jl
#Author:
#Date: 29-June-2020

#OBJECTIVE FUNCTION: MAXIMISE Sharpe Ratio
function maxSharpe(W,R,rf,VCV)
###########################################################################
#INPUTS:
#R     - returns vector
#rf    - risk-free rate
#VCV   - VCV matrix
###########################################################################
    R = R-rf;
    fval = -W'*R/sqrt(W'*VCV*W);
    return fval
end


using LombScargle
using Measurements
using Base.Test

ntimes = 1001
# Observation times
t = linspace(0.01, 10pi, ntimes)
# Randomize times
t += step(t)*rand(ntimes)
t = collect(t)
# The signal
s = sinpi(t) + cospi(2t) + rand(ntimes)
# Frequency grid
nfreqs = 10000
freqs = linspace(0.01, 3, nfreqs)
# Randomize frequency grid
freqs += step(freqs)*rand(nfreqs)
freqs = collect(freqs)
# Use "freqpower" just to call that function and increase code coverage.
# "autofrequency" function is tested below.
@test_approx_eq_eps freqpower(lombscargle(t, s, frequencies=freqs, fit_mean=false))[2] freqpower(lombscargle(t, s, frequencies=freqs, fit_mean=true))[2] 5e-3

# Simple signal, without any randomness
t = collect(linspace(0.01, 10pi, ntimes))
s = sin(t)
pgram1 = lombscargle(t, s, fit_mean=false)
pgram2 = lombscargle(t, s, fit_mean=true)
@test_approx_eq_eps power(pgram1) power(pgram2) 2e-7
pgram3 = lombscargle(t, s, center_data=false, fit_mean=false)
pgram4 = lombscargle(t, s, center_data=false, fit_mean=true)
@test_approx_eq_eps power(pgram3) power(pgram4) 3e-7

# Test the values in order to prevent wrong results in both algorithms
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, fit_mean=true))  [0.029886871262324886,0.0005456198989410226,1.912507742056023e-5, 4.54258409531214e-6, 1.0238342782430832e-5]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, fit_mean=false)) [0.02988686776042212, 0.0005456197937194695,1.9125076826683257e-5,4.542583863304549e-6,1.0238340733199874e-5]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, center_data=false, fit_mean=true))  [0.029886871262325004,0.0005456198989536703,1.9125077421448458e-5,4.5425840956285145e-6,1.023834278337881e-5]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, center_data=false, fit_mean=false)) [0.029886868328967767,0.0005456198924872134,1.9125084251687147e-5,4.542588504467314e-6,1.0238354525870936e-5]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, normalization="model")) [0.030807614469885718,0.0005459177625354441,1.9125443196143085e-5,4.54260473047638e-6,1.0238447607164715e-5]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, normalization="log")) [0.030342586720560734,0.0005457688036440774,1.9125260307148152e-5,4.542594412890309e-6,1.0238395194654036e-5]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, normalization="psd")) [7.474096700871138,0.1364484040771917,0.004782791641128195,0.0011360075968541799,0.002560400630125523]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, normalization="Scargle")) [0.029886871262324904,0.0005456198989410194,1.912507742056126e-5,4.54258409531238e-6,1.0238342782428552e-5]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, normalization="HorneBaliunas")) [14.943435631162451,0.2728099494705097,0.009562538710280628,0.00227129204765619,0.005119171391214276]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, normalization="Cumming")) [15.372999620472974,0.2806521440709115,0.009837423440787873,0.0023365826071340815,0.005266327088140394]
@test_throws ErrorException lombscargle(t, s, frequencies=0.2:0.2:1, normalization="foo")

# Test signal with uncertainties
err = collect(linspace(0.5, 1.5, ntimes))
@test_approx_eq power(lombscargle(t, s, err, frequencies=0.1:0.1:1, fit_mean=true))  [0.06659683848818687,0.09361438921056377,0.006815919926284516,0.0016347568319229223,0.0005385706045724484,0.00021180745624003642,0.00010539881897690343,7.01610752020905e-5,5.519295593372065e-5,4.339157565349008e-5]
@test_approx_eq power(lombscargle(t, s, err, frequencies=0.1:0.1:1, fit_mean=false)) [0.0692080444168825,0.09360343748833044,0.006634919855866448,0.0015362074096358814,0.000485825178683968,0.000181798596773626,8.543735242380012e-5,5.380000205539795e-5,4.010727072660524e-5,2.97840883747593e-5]
@test_approx_eq power(lombscargle(t, s, err, frequencies=0.1:0.1:1, fit_mean=false, center_data=false)) [0.06920814049261209,0.09360344864985352,0.006634919960009565,0.0015362072871144769,0.0004858250831632676,0.00018179850370583626,8.543727416919218e-5,5.379994730581837e-5,4.0107232867763e-5,2.9784059487535237e-5]
@test power(lombscargle(t, s, err)) ==
    power(lombscargle(t, measurement(s, err)))

# Test fast method
t = linspace(0.01, 10pi, ntimes)
s = sin(t)
pgram5 = lombscargle(t, s, maximum_frequency=30, fast=true)
pgram6 = lombscargle(t, s, maximum_frequency=30, fast=false)
@test_approx_eq_eps power(pgram5) power(pgram6) 2e-6
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, fast=true, fit_mean=true))  [0.029886871262324963,0.0005453105325981758,1.9499330722168046e-5,2.0859593514888897e-6,1.0129019249708592e-5]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, fast=true, fit_mean=false)) [0.029886867760422008,0.0005453104197620392,1.9499329579010375e-5,2.085948496002562e-6,1.0128073536975395e-5]
@test_approx_eq power(lombscargle(t, s, frequencies=0.2:0.2:1, fast=true, center_data=false, fit_mean=false)) [0.029886868328967718,0.0005453105220405559,1.949931928224576e-5,2.0859802347505357e-6,1.0127777365273726e-5]
@test_approx_eq power(lombscargle(t, s, err, frequencies=0.2:0.2:1, fast=true, fit_mean=true))  [0.09230959166317655,0.0015654929813132702,0.00019405185108843607,6.0898671943944786e-5,6.0604505038256276e-5]
@test_approx_eq power(lombscargle(t, s, err, frequencies=0.2:0.2:1, fast=true, fit_mean=false)) [0.09219168665786258,0.0015654342453078724,0.00019403694017215876,6.088944186950046e-5,6.05771360378885e-5]
@test_approx_eq power(lombscargle(t, s, err, frequencies=0.2:0.2:1, fast=true, center_data=false, fit_mean=false)) [0.09360344864985332,0.0015354489715019735,0.0001784388515190763,4.744247354697125e-5,3.240223498703448e-5]
@test power(lombscargle(t, s, err)) ==
    power(lombscargle(t, measurement(s, err)))

##################################################
### Testing utilities

# Test findmaxfreq and findmaxpower
@test_approx_eq findmaxfreq(pgram1)        [31.997145470342]
@test_approx_eq findmaxfreq(pgram1, 0.965) [0.15602150741832602,31.685102455505348,31.997145470342,63.52622641842902,63.838269433265665]
t = linspace(0.01, 10pi, 1001)
s = sinpi(2t) + cospi(4t)
p = lombscargle(t, s, maximum_frequency=4)
@test_approx_eq findmaxfreq(p, [0.9, 1.1]) 1.0029954048320957
@test_approx_eq findmaxfreq(p, [1.9, 2.1]) 2.002806697267899
@test_approx_eq findmaxpower(pgram1) 0.9695017551608017

# Test autofrequency function
@test_approx_eq LombScargle.autofrequency(t)                       0.003184112396292367:0.006368224792584734:79.6824127172165
@test_approx_eq LombScargle.autofrequency(t, minimum_frequency=0)                   0.0:0.006368224792584734:79.6792286048202
@test_approx_eq LombScargle.autofrequency(t, maximum_frequency=10) 0.003184112396292367:0.006368224792584734:9.99492881196174

# Test probabilities and FAP
t = collect(linspace(0.01, 10pi, 101))
s = sin(t)
for norm in ("standard", "Scargle", "HorneBaliunas", "Cumming")
    P = lombscargle(t, s, normalization = norm)
    for z_0 in (0.1, 0.5, 0.9)
        @test_approx_eq prob(P, probinv(P, z_0)) z_0
        @test_approx_eq fap(P,  fapinv(P, z_0))  z_0
    end
end
P = lombscargle(t, s, normalization = "log")
@test_throws ErrorException prob(P, 0.5)
@test_throws ErrorException probinv(P, 0.5)

# Test model function
@test_approx_eq s LombScargle.model(t, s, 1/2pi, center_data=false, fit_mean=false)
s = sinpi(t) + pi*cospi(t) + e
@test_approx_eq s LombScargle.model(t, measurement(s, ones(s)), 0.5)

# Test extirpolation function
x = linspace(0, 10)
y = sin(x)
@test_approx_eq LombScargle.extirpolate(x, y) [0.39537718210649553,3.979484140636793,4.833090108345013,0.506805556164743,-3.828112427525919,-4.748341359084166,-1.3022050566901917,3.3367666084342256,5.070478111668922,1.291245296032218,-0.8264466821981216,0.0,0.0]
@test_approx_eq LombScargle.extirpolate(x, y, 11) LombScargle.extirpolate(x, y)[1:11]

# Test trig_sum
C, S = LombScargle.trig_sum(x, y, 1, 10)
@test_approx_eq S [0.0,0.3753570125888358,0.08163980192703546,-0.10139634351774979,-0.4334223744905633,-2.7843373311769777,0.32405810159838055,0.05729663600471602,-0.13191736591325876,-0.5295781583202946]
@test_approx_eq C [8.708141477890015,-0.5402668064176129,-0.37460815054027985,-0.3793457539084364,-0.5972351546196192,14.612204307982497,-0.5020253140297526,-0.37724493022381034,-0.394096831764578,-0.6828241623474718]


using CSV, DataFrames, Plots

clusfile = "Data/Clustered/Healthy/Healthy-chr1-clusters.tsv"
print(clusfile)
df = CSV.read(clusfile, delim=',', copycols=true)
print(first(df,5))

nclusters = maximum(df[!,"clusterid"])
sort!(df, [:chromosome, :clusterid])

df = df[1:5,:]
M = Array{Int64}(zeros(nrow(df), nrow(df)))
genes = df[!,:gname]
cl = df[:,:clusterid]
for i in 1:length(genes)
	clidsrc = cl[i]
	M[i,i] = cl[i]
	for j in i+1:length(genes)
		# println("$i $j")
		cliddst = cl[j]
		clidsrc == cliddst ? M[i,j] = clidsrc : M[i,j] = 0
	end
end

# print(M)
# myplot = spy(M)
# draw(PNG("Plots/myplot.png", 3inch, 3inch), myplot)
heatmap(M, title="Threshold")
plot(size=(1200,1200))

savefig("Plots/clustering.png")


#==============================================================================#
# OpenSCADCube
# December 2014
#==============================================================================#

# This module uses OpenSCADPolyhedron to build a cube.
# This application is simple enough that it could easily be done by hand, but
# it provides a good illustration of how the OpenSCADPolyhedron module is used.

#-------------------------------------------------------------------------------
module OpenSCADCube
using OpenSCADPolyhedron
export cubePoints, cubeFace, cubeBase, cubeTransforms, cubeSymmetries

#-------------------------------------------------------------------------------
# To start, we need the points and face of one square of a cube.
cubePoints = [
  Point3{Float64}([ 1.0,  1.0, -1.0]),
  Point3{Float64}([-1.0,  1.0, -1.0]),
  Point3{Float64}([-1.0, -1.0, -1.0]),
  Point3{Float64}([ 1.0, -1.0, -1.0]),
]
cubeBase = Polyhedron{Float64}(cubePoints)
cubeFace = Int[1, 2, 3, 4]
append!(cubeBase, cubeFace)

# These six transformations generate the six faces of a cube.
cubeTransforms = Function[
  p::Point3{Float64} -> p,
  p::Point3{Float64} -> Point3{Float64}(Float64[ 0  0 -1;  0  1  0;  1  0  0] * p.x),
  p::Point3{Float64} -> Point3{Float64}(Float64[ 1  0  0;  0  0 -1;  0  1  0] * p.x),
  p::Point3{Float64} -> Point3{Float64}(Float64[ 0  0  1;  0  1  0; -1  0  0] * p.x),
  p::Point3{Float64} -> Point3{Float64}(Float64[ 1  0  0;  0  0  1;  0 -1  0] * p.x),
  p::Point3{Float64} -> Point3{Float64}(Float64[ 1  0  0;  0 -1  0;  0  0 -1] * p.x),
]

# These relations specify which points are duplicated by the above transforms.
cubeSymmetries = Dict{(Int, Int), (Int, Int)}()
cubeSymmetries[(2, 2)] = (1, 1)
cubeSymmetries[(2, 3)] = (1, 4)
cubeSymmetries[(3, 1)] = (2, 1)
cubeSymmetries[(3, 3)] = (1, 2)
cubeSymmetries[(3, 4)] = (1, 1)
cubeSymmetries[(4, 1)] = (1, 2)
cubeSymmetries[(4, 2)] = (3, 2)
cubeSymmetries[(4, 4)] = (1, 3)
cubeSymmetries[(5, 1)] = (1, 4)
cubeSymmetries[(5, 2)] = (1, 3)
cubeSymmetries[(5, 3)] = (4, 3)
cubeSymmetries[(5, 4)] = (2, 4)
cubeSymmetries[(6, 1)] = (2, 4)
cubeSymmetries[(6, 2)] = (4, 3)
cubeSymmetries[(6, 3)] = (3, 2)
cubeSymmetries[(6, 4)] = (2, 1)

#-------------------------------------------------------------------------------
# Now we generate the OpenSCAD polyhedron command for the cube.
cube = generatePolyhedron(cubeBase, cubeTransforms, cubeSymmetries)
println(stringify(cube))

end


using OrdinaryDiffEq,DiffEqProblemLibrary, DiffEqDevTools

using DiffEqProblemLibrary.ODEProblemLibrary: importodeproblems; importodeproblems()
import DiffEqProblemLibrary.ODEProblemLibrary: prob_ode_2Dlinear, prob_ode_linear

prob = prob_ode_2Dlinear
sol =solve(prob,Rosenbrock32(),dt=1/2^4)

sol =solve(prob,ExplicitRK(tableau = constructBogakiShampine3()),dt=1/2^4)
val1 = maximum(abs.(sol.u[end] - sol.u_analytic[end]))

sol2 =solve(prob,ExplicitRK(tableau = constructDormandPrince()),dt=1/2^4)
val2 = maximum(abs.(sol2.u[end] - sol2.u_analytic[end]))

sol3 =solve(prob,ExplicitRK(tableau = constructRKF8(Float64)),dt=1/2^4)
val3 = maximum(abs.(sol3.u[end] - sol3.u_analytic[end]))

@test length(sol.t)>length(sol2.t)>=length(sol3.t)
@test max(val1,val2,val3)<2e-3


#=

=#

# Convolution function
# 2D input (e.g. greyscale image) -> 2D convolved output for each image and patch
# Inputs:
# A - inputs from previous layer - dim x dim x num_obs - e.g. greyscale images
# W - weight patches - dim x dim x num_patches
# Output (updated in-place):
# Z - convolved features - dim x dim x num_patches x num_obs
function conv!{F<:AbstractFloat}(Z::Array{F,4}, W::Array{F,3}, A::Array{F,3})
    @threads for img in indices(Z, 4)
        for patch in indices(Z, 3)
            for z_col in indices(Z, 2)
                for z_row in indices(Z, 1)
                    tmp = F(0)
                    for p_col in indices(W, 2)
                        a_col = z_col + p_col - 1
                        for p_row in indices(W, 1)
                            a_row = z_row + p_row - 1
                            @inbounds @fastmath tmp += A[a_row, a_col, img] * W[p_row, p_col, patch]
                        end
                    end
                    @inbounds Z[z_row, z_col, patch, img] = tmp
                end
            end
        end
    end
end

# Convolution function
# 3D input (e.g. colour image) -> 2D convolved output for each image and patch
# Inputs:
# A - inputs from previous layer - dim x dim x channels x num_obs - e.g. colour images
# W - weight patches - dim x dim x channels x num_patches
# Output (updated in-place):
# Z - convolved features - dim x dim x num_patches x num_obs
function conv!{F<:AbstractFloat}(Z::Array{F,4}, W::Array{F,4}, A::Array{F,4})
    @threads for img in indices(Z, 4)
        for patch in indices(Z, 3)
            for z_col in indices(Z, 2)
                for z_row in indices(Z, 1)
                    tmp = F(0)
                    for chan in indices(W, 3)
                        for p_col in indices(W, 2)
                            a_col = z_col + p_col - 1
                            for p_row in indices(W, 1)
                                a_row = z_row + p_row - 1
                                @inbounds @fastmath tmp += A[a_row, a_col, chan, img] * W[p_row, p_col, chan, patch]
                            end
                        end
                    end
                    @inbounds Z[z_row, z_col, patch, img] = tmp
                end
            end
        end
    end
end

# Gradient of weights used in convolution
# 2D input (e.g. greyscale image) -> 2D convolved output for each image and patch
# Inputs:
# A - inputs from previous layer - dim x dim x num_obs - e.g. greyscale images
# Δ - delta i.e. ∂J/∂z of the convolved features - dim x dim x num_patches x num_obs
# Output (updated in-place):
# ∇W - gradients of the weight patches - dim x dim x num_patches
function convgrad!{F<:AbstractFloat}(∇W::Array{F,3}, Δ::Array{F,4}, A::Array{F,3})
    fill!(∇W, F(0))
    for img in indices(Δ, 4)
        for patch in indices(Δ, 3)
            for g_col in indices(∇W, 2)
                for g_row in indices(∇W, 1)
                    tmp = F(0)
                    for δ_col in indices(Δ, 2)
                        a_col = g_col + δ_col - 1
                        for δ_row in indices(Δ, 1)
                            a_row = g_row + δ_row - 1
                            @inbounds @fastmath tmp += A[a_row, a_col, img] * Δ[δ_row, δ_col, patch, img]
                        end
                    end
                    @inbounds ∇W[g_row, g_col, patch] += tmp
                end
            end
        end
    end
end

# Gradient of weights used in convolution
# 3D input (e.g. colour image) -> 2D convolved output for each image and patch
# Inputs:
# A - inputs from previous layer - dim x dim x channels x num_obs - e.g. colour images
# Δ - delta i.e. ∂J/∂z of the convolved features - dim x dim x num_patches x num_obs
# Output (updated in-place):
# ∇W - gradients of the weight patches - dim x dim x channels x num_patches
function convgrad!{F<:AbstractFloat}(∇W::Array{F,4}, Δ::Array{F,4}, A::Array{F,4})
    fill!(∇W, F(0))
    for img in indices(Δ, 4)
        for patch in indices(Δ, 3)
            for chan in indices(∇W, 3)
                for g_col in indices(∇W, 2)
                    for g_row in indices(∇W, 1)
                        tmp = F(0)
                        for δ_col in indices(Δ, 2)
                            a_col = g_col + δ_col - 1
                            for δ_row in indices(Δ, 1)
                                a_row = g_row + δ_row - 1
                                @inbounds @fastmath tmp += A[a_row, a_col, chan, img] * Δ[δ_row, δ_col, patch, img]
                            end
                        end
                        @inbounds ∇W[g_row, g_col, chan, patch] += tmp
                    end
                end
            end
        end
    end
end

# Broadcasts the bias vector throughout the convolved features
# Input:
# b - bias vector - num_patches
# Output (updated in-place):
# Z - convolved features - dim x dim x num_patches x num_obs
function bcastbias!{F<:AbstractFloat}(Z::AbstractArray{F, 4}, b::AbstractArray{F, 1})
    @threads for img in indices(Z, 4)
        for patch in indices(Z, 3)
            for col in indices(Z, 2)
                for row in indices(Z, 1)
                    @inbounds Z[row, col, patch, img] += b[patch]
                end
            end
        end
    end
end

# Gradient of bias used in convolution
# Input:
# Δ - delta i.e. ∂J/∂z of the convolved features - dim x dim x num_patches x num_obs
# Output (updated in-place):
# ∇b - gradients of the bias features - num_patches
function biasgrad!{F<:AbstractFloat}(∇b::AbstractArray{F, 1}, Δ::AbstractArray{F, 4})
    fill!(∇b, F(0))
    for img in indices(Δ, 4)
        for patch in indices(Δ, 3)
            for col in indices(Δ, 2)
                for row in indices(Δ, 1)
                    @inbounds ∇b[patch] += Δ[row, col, patch, img]
                end
            end
        end
    end
end

################################################################################
#
# Word Embedding
#
################################################################################

# Embedding of features in each word
function embed!{F<:AbstractFloat, T<:Integer}(A::Matrix{F}, W::Matrix{F},
                                              input::Matrix{T})
    num_words = size(input, 1)
    num_feats = size(W, 1)
    for col in indices(input, 2)
        for (r1, r2) in zip(1:num_words, 1:num_feats:(num_feats * (num_words - 1) + 1))
            col_w = input[r1, col]
            for (r3, r4) in zip(r2:(r2 + num_feats - 1), 1:num_feats)
                @inbounds A[r3, col] = W[r4, col_w]
            end
        end
    end
end

# Gradient of parameters in embedding layer
function embed_grad!{F<:AbstractFloat, T<:Integer}(∇W::Matrix{F}, Δ::Matrix{F},
                                                   input::Matrix{T})
    num_feats = size(∇W, 1)
    num_words = div(size(Δ, 1), num_feats)
    fill!(∇W, 0.)
    for m in indices(Δ, 2)
        for w in 1:num_words
            col = input[w, m]
            for (r1, r2) in zip(1:num_feats, ((w - 1) * num_feats + 1):(w * num_feats))
                @inbounds ∇W[r1, col] += Δ[r2, m]
            end
        end
    end
end


"""
Transition rate matrices for G-R-S transcription model
n+1 G states, nr R steps, and nr S sites
returns T,B,TA,TI or M matrices
T P(m) = A P(m) + B P(m-1)
"""

"""
transition_rate_mat(nu::Float64,nhist::Int)
Full master equation transition rate matrix for (G=1) (n=0) transcription model
(i.e. Poisson model)
returns M
"""
function transition_rate_mat(nu::Float64,nhist::Int)
	total = nhist + 2
	M = zeros(total,total)
	for m=1:total,mp=1:total
		M[m,mp] = nu *(m==mp+1) - nu*(m==mp) + (mp-1)*(m==mp-1) - (mp-1)*(m==mp)
	end
	M[end,end] += nu
	return M
end

"""
transition_rate_mat(n,gammap,gamman,nu,mu,nhist)
Transition rate matrices for G transcription model (i.e. classic telegraph model with n+1 states)
mRNA decay rate = mu
returns M
"""
function transition_rate_mat(n::Int,gammap::Vector,gamman::Vector,nu::Float64,mu::Float64,nhist::Int)
	T,B = transition_rate_mat(n,gammap,gamman,nu)
	transition_rate_mat(n,T,B,nu,mu,nhist)
end
"""
transition_rate_mat(n,gammap,gamman,nu,nhist)
Full master equation transition rate matrix for GM transcription model (i.e. classic telegraph model with n+1 states)
with mRNA decay time set to 1
returns M
"""
function transition_rate_mat(n::Int,gammap::Vector,gamman::Vector,nu::Float64,nhist::Int)
	T,B = transition_rate_mat(n,gammap,gamman,nu)
	transition_rate_mat(n,T,B,nu,1.,nhist)
end
"""
transition_rate_mat(n::Int,gammap::Vector,gamman::Vector,nu::Float64)

transition rate matrices for G model
returns T and B
"""
function transition_rate_mat(n::Int,gammap::Vector,gamman::Vector,nu::Float64)
	B = transition_rate_mat(n,nu)
	T = transition_rate_mat(n,gammap,gamman)
	return T, B
end
# function transition_rate_mat(n::Int,gammap::Vector,gamman::Vector,nu::Float64)
#     nT = n+1
# 	T = zeros(nT,nT)
# 	B = zeros(nT,nT)
# 	T[1,1] = -(gamman[1]+gammap[2])
# 	T[1,2] = gamman[2]
# 	for i=2:nT-1
# 		# T[i,ip] =  gammap[i]*(ip==i-1) - (gamman[i]+gammap[i+1])*(ip==i) + gamman[i+1]*(ip==i+1)
# 		T[i,i-1] = gammap[i]
# 		T[i,i] = -(gamman[i]+gammap[i+1])
# 		T[i,i+1] = gamman[i+1]
# 	end
# 	T[nT,nT-1] = gammap[nT]
# 	T[nT,nT] = -(gamman[nT]+gammap[nT+1])
# 	B[nT,nT] = nu
# 	return T, B
# end
"""
transition_rate_mat(n::Int,nu::Float64)
B matrix for G model
returns B
"""
function transition_rate_mat(n::Int,nu::Float64)
    nT = n+1
	B = zeros(nT,nT)
	B[nT,nT] = nu
	return B
end
"""
transition_rate_mat(n::Int,gammap::Vector,gamman::Vector)
G state transition rate matrix for G model (Telegraph model state transitions)
returns T
"""
function transition_rate_mat(n::Int,gammap::Vector,gamman::Vector)
    nT = n+1
	T = zeros(nT,nT)
	T[1,1] = -gammap[2]
	T[1,2] = gamman[2]
	for i=2:nT-1
		# T[i,ip] =  gammap[i]*(ip==i-1) - (gamman[i]+gammap[i+1])*(ip==i) + gamman[i+1]*(ip==i+1)
		T[i,i-1] = gammap[i]
		T[i,i] = -(gamman[i]+gammap[i+1])
		T[i,i+1] = gamman[i+1]
	end
	T[nT,nT-1] = gammap[nT]
	T[nT,nT] = -gamman[nT]
	return T
end
"""
transition_rate_mat(T,n::Int)

transtion matrices TA and TI for n-state telegraph model given matrix T
Treat G state n+1 as the ON state
returns T, TA, and TI
"""
function transition_rate_mat(T::Matrix,n)
    nT = (n+1)
    TI = copy(T)
	TI[nT,nT] = 0.
	TI[nT-1,nT] = 0.
    TA = zeros(nT,nT)
	TA[nT,nT] = T[nT,nT]
    return T,TA,TI
end
"""
transition_rate_mat(n::Int,T::Matrix,B::Matrix,nu::Float64,mu::Float64,nhist::Int)

Construct full GM transition rate matrix using outer product (kron) of T and B matrices
returns M
"""
function transition_rate_mat(n::Int,T::Matrix,B::Matrix,nu::Float64,mu::Float64,nhist::Int)
	nT = n+1
	total = nhist + 2
	S = zeros(total,total)
	Sminus = zeros(total,total)
	Splus = zeros(total,total)
	# Generate matrices for m transitions
	Splus[1,2] = mu
	for m=2:total-1
		S[m,m] = -mu*(m-1)
		Sminus[m,m-1] = 1
		Splus[m,m+1] = mu*m
	end
	S[total,total] = -mu*(total-1)
	Sminus[total,total-1] = 1
	# Construct m truncated Master equation rate matrix
	M = kron(S,Matrix{Float64}(I,nT,nT)) + kron(Matrix{Float64}(I,total,total),T-B) + kron(Sminus,B) + kron(Splus,Matrix{Float64}(I,nT,nT))
	M[end,end]+=nu  # boundary condition to ensure probability is conserved
	return M
end

"""
transition_rate_mat(n::Int,nr::Int,gammap::Vector,gamman::Vector,nu::Vector)
transtion matrix for GR (marginalized over m) master equation
returns T and B
"""
function transition_rate_mat(n::Int,nr::Int,gammap::Vector,gamman::Vector,nu::Vector)
	# standard GR intron reporter model
	nT = (n+1)*2^nr
	# Declare Transition matrices
	T = transition_rate_mat_G(n,nr,gammap,gamman,2)
	B = zeros(nT,nT)
	# Generate T = A + B transition matrix and B transition matrix
	# R states are a Base 2 number, e.g. 0100, means presence of polymerase at step 2, other steps are empty
	for w=1:2^nr, z=1:2^nr, i=1:n+1
		a = i + (n+1)*(z-1)
		b = i + (n+1)*(w-1)
		zdigits = digits(z-1,base=2,pad=nr)
		wdigits = digits(w-1,base=2,pad=nr)
		z1 = zdigits[1]
		w1 = wdigits[1]
		zr = zdigits[nr]
		wr = wdigits[nr]
		zbar1 = zdigits[2:nr]
		wbar1 = wdigits[2:nr]
		zbarr = zdigits[1:nr-1]
		wbarr = wdigits[1:nr-1]
		T[a,b] +=  nu[1]*((i==n+1)*(zbar1==wbar1)*((z1==1)-(z1==0))*(w1==0))
		T[a,b] += nu[nr+1]*((zbarr==wbarr)*((zr==0)-(zr==1))*(wr==1))
		B[a,b] =  nu[nr+1]*((zbarr==wbarr)*(zr==0)*(wr==1))
		for j = 1:nr-1
			zbarj = zdigits[[1:j-1;j+2:nr]]
			wbarj = wdigits[[1:j-1;j+2:nr]]
			zj = zdigits[j]
			zj1 = zdigits[j+1]
			wj = wdigits[j]
			wj1 = wdigits[j+1]
			T[a,b] += nu[j+1]*((zbarj==wbarj)*((zj==0)*(zj1==1)-(zj==1)*(zj1==0))*(wj==1)*(wj1==0))
		end
	end
	return T, B
end
"""
transition_rate_mat(n::Int,nr::Int,gammap::Vector,gamman::Vector,nu::Vector)
transtion matrix for GR (marginalized over m) master equation
returns T and B
"""
function transition_rate_mat_offeject(n::Int,nr::Int,gammap::Vector,gamman::Vector,nu::Vector,eta::Vector)
	# standard GR intron reporter model
	nT = (n+1)*2^nr
	# Declare Transition matrices
	T = transition_rate_mat_G(n,nr,gammap,gamman,2)
	B = zeros(nT,nT)
	# Generate T = A + B transition matrix and B transition matrix
	# R states are a Base 2 number, e.g. 0100, means presence of polymerase at step 2, other steps are empty
	for w=1:2^nr, z=1:2^nr, i=1:n+1
		a = i + (n+1)*(z-1)
		b = i + (n+1)*(w-1)
		zdigits = digits(z-1,base=2,pad=nr)
		wdigits = digits(w-1,base=2,pad=nr)
		z1 = zdigits[1]
		w1 = wdigits[1]
		zr = zdigits[nr]
		wr = wdigits[nr]
		zbar1 = zdigits[2:nr]
		wbar1 = wdigits[2:nr]
		zbarr = zdigits[1:nr-1]
		wbarr = wdigits[1:nr-1]
		T[a,b] +=  nu[1]*(i==n+1)*(zbar1==wbar1)*((z1==1)-(z1==0))*(w1==0)
		T[a,b] += nu[nr+1]*(zbarr==wbarr)*((zr==0)-(zr==1))*(wr==1)
		T[a,b] += eta[nr]*(zbarr==wbarr)*((zr==0)-(zr==1))*(wr==1)
		B[a,b] =  nu[nr+1]*((zbarr==wbarr)*(zr==0)*(wr==1))
		for j = 1:nr-1
			zbarj = zdigits[[1:j-1;j+2:nr]]
			wbarj = wdigits[[1:j-1;j+2:nr]]
			zbark = zdigits[[1:j-1;j+1:nr]]
			wbark = wdigits[[1:j-1;j+1:nr]]
			zj = zdigits[j]
			zj1 = zdigits[j+1]
			wj = wdigits[j]
			wj1 = wdigits[j+1]
			T[a,b] += nu[j+1]*((zbarj==wbarj)*((zj==0)*(zj1==1)-(zj==1)*(zj1==0))*(wj==1)*(wj1==0))+ eta[j]*((zbark==wbark)*((zj==0)-(zj==1))*(wj==1))
		end
	end
	return T, B
end
"""
transition_rate_mat_G(n::Int,nr::Int,gammap::Vector,gamman::Vector,base=3)

transition rate matrix for GR(S) model, base = 2 for GR and base = 3 for GRS
returns T
"""
function transition_rate_mat_G(n::Int,nr::Int,gammap::Vector,gamman::Vector,base::Int=3)
	# G state transition matrix for GR and GRS models
	nA = (n+1)*base^nr
	T = zeros(nA,nA)
	# R states are a base 3 number, e.g. 01200, which means pol2 no intron at step 2, pol2 and intron at step 3, and empty elsehwere
	for a = 0 : n+1 :nA-1
		# a = (n+1)*w
		T[a+1,a+1] = -(gamman[1]+gammap[2])
		T[a+1,a+2] = gamman[2]
		for i = 2:n
			T[a+i,a+i-1] = gammap[i]
			T[a+i,a+i] = -(gamman[i]+gammap[i+1])
			T[a+i,a+i+1] = gamman[i+1]
		end
		T[a+n+1,a+n] = gammap[n+1]
		T[a+n+1,a+n+1] = -(gamman[n+1]+gammap[n+2])
	end
	return T
end

# """
# transition_rate_mat(n::Int,nr::Int,gammap::Vector,gamman::Vector,nu::Vector,eta::Vector)
#
# transtion matrices for GRS Master equation
# (introns are spliced)
# returns T, TA, and TI
# """
# function transition_rate_mat(n::Int,nr::Int,gammap::Vector,gamman::Vector,nu::Vector,eta::Vector)
# 	T = transition_rate_mat_T(n,nr,gammap,gamman,nu,eta)
# 	transition_rate_mat(T,n,nr)
# end
# """
# transition_rate_mat(n::Int,nr::Int,gammap::Vector,gamman::Vector,nu::Vector,eta::Vector)
#
# transtion matrices for GRS Master equation
# (introns are spliced)
# returns T, TA, and TI
# """
# function transition_rate_mat_offeject2(n::Int,nr::Int,gammap::Vector,gamman::Vector,nu::Vector,eta::Vector)
# 	T,B = transition_rate_mat_offeject(n,nr,gammap,gamman,nu,eta)
# 	transition_rate_mat(T,n,nr)
# end
"""
transition_rate_mat(T,n::Int,nr::Int)

transtion matrices TA and TI for GRS Master equation given matrix T
(introns are spliced)
returns T, TA, and TI
"""
function transition_rate_mat(T::Matrix,n,nr,base=3)
    nA = (n+1)*base^nr
    TA = zeros(nA,nA)
    TI = zeros(nA,nA)
    # R states are a base 3 number, e.g. 01200, which means pol2 no intron at step 2, pol2 and intron at step 3, and empty elsehwere
    for w=1:base^nr,ip=1:n+1,z=1:base^nr,i=1:n+1
        a = i + (n+1)*(z-1)
        b = ip + (n+1)*(w-1)
        zdigits = digits(z-1,base=base,pad=nr)
        wdigits = digits(w-1,base=base,pad=nr)
		TA[a,b] = T[a,b]*(any(wdigits.> base-2))
		TI[a,b] = T[a,b]*(~any(wdigits.> base-2))
    end
    return T,TA,TI
end
"""
transition_rate_mat_T(n::Int,nr::Int,gammap::Vector,gamman::Vector,nu::Vector,eta::Vector)

transtion matrix T for GRS Master equation
(introns are spliced)
returns T
"""
function transition_rate_mat_T(n::Int,nr::Int,gammap::Vector,gamman::Vector,nu::Vector,eta::Vector)

	T = transition_rate_mat_G(n,nr,gammap,gamman)
	for w=1:3^nr,z=1:3^nr
		zdigits = digits(z-1,base=3,pad=nr)
		wdigits = digits(w-1,base=3,pad=nr)
		z1 = zdigits[1]
		w1 = wdigits[1]
		zr = zdigits[nr]
		wr = wdigits[nr]
		zbar1 = zdigits[2:nr]
		wbar1 = wdigits[2:nr]
		zbarr = zdigits[1:nr-1]
		wbarr = wdigits[1:nr-1]
		B = nu[nr+1]*((zbarr==wbarr)*(((zr==0)-(zr==1))*(wr==1)+((zr==0)-(zr==2))*(wr==2)))
		C = eta[nr]*((zbarr==wbarr)*((zr==1)-(zr==2))*(wr==2))
		T[(n+1)*z,(n+1)*w] += nu[1]*((zbar1==wbar1)*((z1==2)-(z1==0))*(w1==0))
		for i=1:n+1
			a = i + (n+1)*(z-1)
			b = i + (n+1)*(w-1)
			T[a,b] += B
			T[a,b] += C
			for j = 1:nr-1
				zbarj = zdigits[[1:j-1;j+2:nr]]
				wbarj = wdigits[[1:j-1;j+2:nr]]
				zbark = zdigits[[1:j-1;j+1:nr]]
				wbark = wdigits[[1:j-1;j+1:nr]]
				zj = zdigits[j]
				zj1 = zdigits[j+1]
				wj = wdigits[j]
				wj1 = wdigits[j+1]
				T[a,b] += nu[j+1]*((zbarj==wbarj)*(((zj==0)*(zj1==1)-(zj==1)*(zj1==0))*(wj==1)*(wj1==0)+((zj==0)*(zj1==2)-(zj==2)*(zj1==0))*(wj==2)*(wj1==0))) + eta[j]*((zbark==wbark)*((zj==1)-(zj==2))*(wj==2))
			end
		end
	end
	return T
end


function cfun2(a, A)
    cf = mean(abs.(a-A))
    return cf
end

function  pfun(R)
    Rs = copy(R)
    ts = sample(1:length(R), 2)
    Rs[reverse(ts)] = R[ts]
    return Rs
end

function efun6(a,b,c,b2,R,nlag=50,n2lag=25)
    A = autocor(R, 1:nlag)
    B = autocor(abs.(R), 1:n2lag)
    C = crosscor(R, abs.(R), -nlag:nlag)
    B2 = autocor(R.*R, 1:n2lag)
    Z = cfun2(a,A)+cfun2(b,B)+cfun2(c,C)+cfun2(b2,B2)
    return Z
end


### A Pluto.jl notebook ###
# v0.14.3

using Markdown
using InteractiveUtils

# ╔═╡ c4f5040e-7a5c-4635-9402-9008b0f41a71
using Pkg; Pkg.add("Plots")

# ╔═╡ 96b47390-a131-11eb-0f4c-7782f9731bd1
using Plots

# ╔═╡ 763f61ce-189f-4e6b-b422-0a79defd2ca4
using CSV,DataFrames

# ╔═╡ 02fcc95b-2349-4867-a0f3-716a4904b922
md"""
# Define all the functions
"""

# ╔═╡ bb81b5b9-5c35-41e2-b23d-f76e0a1486a9
function ansatz(x,α)
	return exp(-α*x^2)
end;

# ╔═╡ 9f41b093-123d-44f3-84db-ec987cf62628
function probability_density(x,α)
	return (ansatz(x,α)^2)/(sqrt(pi/α))
end;

# ╔═╡ 4b4252e5-4694-4bdd-ae9a-166bd66f41a1
function E_local(x,α)
	return α + (x^2)*(1/2 - 2*α^2)
end;

# ╔═╡ d8ef5c0c-736b-4746-84f2-f5dde310fec8
md" # Function for running the metropolis algorithm."

# ╔═╡ 281d664f-2d2b-4b98-960d-bbc1af5cf1a9
function metropolis(N,α)
	L = 3/sqrt(2*α)
	x = rand(-L:0.000001:L)
	E = 0
	E2 = 0
	Eln_avg = 0
	ln_avg = 0
	rejection_ratio = 0
	#start the loop
	for i in 1:N
		x_t = x + 0.4*(2*rand()*L - L)
		if probability_density(x_t,α) >= probability_density(x,α)
			x = x_t
		else
			dummy = rand()
			if dummy < probability_density(x_t,α)/probability_density(x,α)
				x = x_t
			else
				rejection_ratio += 1/N
			end
		end

		E += E_local(x,α)/N
		E2 += E_local(x,α)^2/N
		Eln_avg += (-1*E_local(x,α)*(x^2))/N
		ln_avg += (-x^2)/N
	end
	return E,E2,Eln_avg,ln_avg,rejection_ratio
end;

# ╔═╡ f74a56fe-595e-4eed-9025-d9ecbc760f45
md"## Iterations for finding the α which minimizes the energy expectation value."

# ╔═╡ bc50f526-55fe-4059-9b12-8c568aea3009
function iterate_alpha()
	α = 0.1
	alpha_iter = 20
	N = 500
	random_walkers = 200
	γ = 0.9
	energy = Vector{Float64}(undef,alpha_iter)
	alpha = Vector{Float64}(undef,alpha_iter)
	variance = Vector{Float64}(undef,alpha_iter)
	En_analytical = Vector{Float64}(undef,alpha_iter)
	Var_analytical = Vector{Float64}(undef,alpha_iter)
	for i in 1:alpha_iter
		E = 0
		E2 = 0
		dE_dalpha = 0
		Eln = 0
		ln = 0
		rejection_ratio = 0
		for j in 1:random_walkers
			E_met, E2_met, Eln_met, ln_met, rejections_met = metropolis(N,α)
			E += E_met/random_walkers
			E2 += E2_met/random_walkers
			Eln += Eln_met/random_walkers
			ln += ln_met/random_walkers
			rejection_ratio += rejections_met/random_walkers
		end
		E_analytical = α/2 + 1/8/α
		var_analytical = (1-4*α^2)^2/32/α^2

		# Define the next α
		# dE_dalpha = 2*(Eln-E*ln)
		α = α + 0.05

		# Update the arrays for plotting
		energy[i] = E
		alpha[i] = α
		variance[i] = E2 - E^2
		En_analytical[i] = E_analytical
		Var_analytical[i] = var_analytical
	end
	return energy,alpha,variance,En_analytical,Var_analytical
end;

# ╔═╡ 9ec4cea0-bcc3-45f2-97e5-33d116da3fb1
md"# Running the simulation."

# ╔═╡ a4b16f08-0cc9-47fd-8836-188efe891b84
energy,alpha,variance,E_analytical,var_analytical = iterate_alpha();

# ╔═╡ 9b375035-df0d-415b-8975-dbb4fcec0381
md" # Plots"

# ╔═╡ 2276a325-9649-4eee-98c9-138dcb457b8f
begin
	plot(energy,
		label = "calculated expectation of energy",
		title = "Energy Plots",
		xlabel = "timestep",
		ylabel = "<E>",
		linecolor = :blue,
	)
	plot!(E_analytical,label = "Analytical Energy")
end

# ╔═╡ 285e1d2b-3e11-4c45-8646-884ce3d06c77
plot(variance,
	title = "Variance in energy",
	label = false,
	ylabel = "Var(E)",
	xlabel = "timestep",
	linecolor = :blue
)

# ╔═╡ 6ecec340-0769-4881-bee2-eedd9fdc4eb4
begin
	scatter(alpha,energy,
		label = "Calculated energy",
		title = "Energy vs α",
		markercolor = :blue,
		xlabel = "α",
		ylabel = "<E>"
	)

	scatter!(alpha,
		E_analytical,
		label = "Calculated energy",
		title = "Energy vs α",
		markercolor = :red
	)
end

# ╔═╡ Cell order:
# ╠═c4f5040e-7a5c-4635-9402-9008b0f41a71
# ╠═96b47390-a131-11eb-0f4c-7782f9731bd1
# ╠═763f61ce-189f-4e6b-b422-0a79defd2ca4
# ╟─02fcc95b-2349-4867-a0f3-716a4904b922
# ╠═bb81b5b9-5c35-41e2-b23d-f76e0a1486a9
# ╠═9f41b093-123d-44f3-84db-ec987cf62628
# ╠═4b4252e5-4694-4bdd-ae9a-166bd66f41a1
# ╟─d8ef5c0c-736b-4746-84f2-f5dde310fec8
# ╠═281d664f-2d2b-4b98-960d-bbc1af5cf1a9
# ╟─f74a56fe-595e-4eed-9025-d9ecbc760f45
# ╠═bc50f526-55fe-4059-9b12-8c568aea3009
# ╟─9ec4cea0-bcc3-45f2-97e5-33d116da3fb1
# ╠═a4b16f08-0cc9-47fd-8836-188efe891b84
# ╟─9b375035-df0d-415b-8975-dbb4fcec0381
# ╠═2276a325-9649-4eee-98c9-138dcb457b8f
# ╠═285e1d2b-3e11-4c45-8646-884ce3d06c77
# ╠═6ecec340-0769-4881-bee2-eedd9fdc4eb4


using NURBS

include("bezier.jl")
include("knot.jl")
include("basis.jl")
include("bspline.jl")
include("rbspline.jl")
include("beziersurfaces.jl")
include("bsurfaces.jl")
include("rbsurfaces.jl")
include("raise.jl")

include("bezsurftest.jl")


using GAIO
using ForwardDiff
using StaticArrays

# example functions
# domain (-5,5)^2
function example_g1(x)
    fac1 = x[1]*x[1] + x[2] - 11
    fac2 = x[1] + x[2]*x[2] - 7

    return SVector(4*x[1]*fac1 + 2*fac2, 2*fac1 + 4*x[2]*fac2)
end

# domain (-5,5)^2
function example_g2(x)
    return SVector(x[1]*x[1]*x[1] - 3*x[1]*x[2]*x[2] - x[1] + 1/sqrt(2),
                    -x[2]*x[2]*x[2] + 3*x[1]*x[1]*x[2] - x[2])
end

# demanding example, newton could diverge at some point
# domain (-3,3)^2
function example_g3(x)
    para = [
        map(i -> 0.1 + (-10 + i) / 1000, StaticArrays.SUnitRange(1, 20));
        map(i -> 0.9 + (-30 + i) / 1000, StaticArrays.SUnitRange(21, 40))
    ]

    return SVector(sin(4*x[2]) * prod(x[1] .- para), sin(4*x[1]) * prod(x[2] .- para))
end

# domain (-40,40)^n, 3^n roots in domain,
function example_g4(x)
    return 100*x + x .^ 2 - x .^ 3 .- sum(x)
end

# domain (-0.3,0.8)^n
function example_g5(x)
    n = length(x)
    c = cos.(x)

    return StaticArrays.SUnitRange(1, n) .* (1 .- c) - sin.(x) .+ (n - sum(c))
end

dim = 3
g = example_g4
Dg = x -> ForwardDiff.jacobian(g, x)

center, radius = [0.0 for _ in 1:dim], [40.0 for _ in 1:dim]
P = BoxPartition(Box(center, radius))

R = cover_roots(g, Dg, P[:], depth=dim*8)

plot(R)


using StanSample, MCMCChains, CSV

df = filter(row -> row[:age] >= 18,
  CSV.read(joinpath(@__DIR__, "..", "..", "data", "Howell1.csv"), DataFrame))

# Define the Stan language model

m4_3s = "
data {
 int < lower = 1 > N; // Sample size
 vector[N] height; // Predictor
 vector[N] weight; // Outcome
}

parameters {
 real alpha; // Intercept
 real beta; // Slope (regression coefficients)
 real < lower = 0 > sigma; // Error SD
}

model {
 height ~ normal(alpha + weight * beta , sigma);
}

generated quantities {
}
";

# Define the Stanmodel and set the output format to :mcmcchains.

m_4_3s = SampleModel("m4.3s", m4_3s);

# Input data for cmdstan

m4_3_data = Dict("N" => size(df, 1), "height" => df[!, :height],
  "weight" => df[!, :weight]);

# Sample using cmdstan

rc = stan_sample(m_4_3s, data=m4_3_data)

# Describe the draws

if success(rc)
  chn = read_samples(m_4_3s; output_format=:mcmcchains)
  #chn = set_names(chn, Dict("mu" => "μ", "sigma" => "σ"))
  describe(chn)
end


function ACQR_simula(nt::Int, seed::Int)
	#
	# simulated model to test the functions
	#
	# nt: number of time instants
	# seed: seed for the random numbers

	# random seed
	# ------------------------------
	Random.seed!(seed)

	# simulated system
	# ----------------------------
	m = simula01()

	# vibration parameters
	# ----------------------------
	# time interval (s)
	dt = m["dt"]

	# state-space matrices
	# --------------------------------------
	A = m["A"]
	B = m["B"]
	C = m["C"]
	D = m["D"]

	# input definition
	# ---------------------------------------
	u = 100*randn(2,nt) # if 100 is not used, Q and R are too small

	# state equation, no noise
	# ---------------------------------
	x = zeros(4,nt)
	w = B*u
	for t=1:nt-1
		x[:,t+1] = A*x[:,t] + w[:,t]
	end
	Q = cov(w')

	# observation equation
	# -------------------------------------
	v = D*u
	y = C*x + v

	# covariance matrices
	Q = cov(w')
	R = cov(v')
	S = cov(w',v')

	# starting values
	m1 = C\y[:,1:1]
	P1 = 0*A

	m = Dict("dt"=>dt,
	"y"=>y,"u"=>u,
	"A"=>A,"B"=>B,"C"=>C,"D"=>D,
	"Q"=>Q,"R"=>R,"S"=>S,
	"m1"=>m1,"P1"=>P1)

	return m
end