using LinearAlgebra
using OrdinaryDiffEq
using ControlSystems
using PyPlot
using GaussianProcesses, BayesianOptimization, Distributions

const Kt = 1/(60.3e-3)
const r = 45/34*13/3        # Gear and belt ratio
const mp = 0.706
# const mr = 0.56424  # m_resin + 4*m_steel = 0.084 + 4 * 0.12946
const mr = 0.223
const m = mp + mr
const lr = 0.18217
const lp = lr/2
const l = (mp*lp + mr*lr)/m
const g = 9.81
const Ip_com = 0.00249
const I_epoxy = 1100 * 7.919e4 / 1e9 * (0.18/2)^2
const I_steel = 9.484e-4
const I_resin = 7.559e-4
const I_resin_new = 4.64e-4
# const I1 = Ip_com + mp*lp^2 + mr*lr^2
# const I2 = I_resin_new + 4*I_steel
const I2 = I_resin + I_epoxy
const I1_estimate = Ip_com + mp*lp^2 + mr*lr^2
const I1_true = I1_estimate*(1 + 0.22);
const τ_max = 0.2
const γ1 = 2. # 1.5*ceil(m*g*l)
const γ2 = 25. # 3*ceil(I1/I2) * γ1 / (γ1 - m*g*l)
const k = (1.0, 10.0)
I1 = I1_true

function compute_cost(sol::ODESolution, par::AbstractArray)
    C = [1 0 0 0; 0 1 0 0];
    Q = C' * C
    R = 100.0
    ctrl(u) = -dot(par,u)
    return sum( 1/2*dot(sol.u[i], Q*sol.u[i]) + 1/2*R*ctrl(sol.u[i])^2 for i=1:length(sol) )
end

function compute_lqr_gain(I1)
    A = [0 0 1 0; 0 0 0 1; m*g*l/I1 0 0 0; 0 0 0 0]
    B = r*[0; 0; -1/I1; 1/I2]
    C = [1 0 0 0; 0 1 0 0];
    sys = ss(A, B, C, 0)
    Q = C' * C
    # Q = 1.0*I
    R = 100.0*I
    L = lqr(sys, Q, R)
    return L
end

compute_lqr_estimate(u) = -(K_estimate * u)[1]
compute_lqr_true(u) = -(K_true * u)[1]

function compute_ida_pbc(u)
    q1, q2, q1dot, q2dot = u
    qbar = q2 + γ2*q1
    qbar_dot = q2dot + γ2*q1dot
    u_es = γ1*sin(q1) + k[1]*sin(qbar)
    u_di = k[2]*qbar_dot
    return 1/r*( u_es + u_di )
end

function compute_switching_control(u)
    q1, q2, q1dot, q2dot = u
    τ = compute_ida_pbc(u)
    if 1 - cos(q1) + 1 - cos(q2) + abs(q1) + abs(q2) < 0.1
        τ = compute_lqr_estimate(u)
        # τ = compute_lqr_true(u)
    end
    return τ
end

function iwp_eom!(du, u, p, t)
    # τ = clamp(compute_lqr_estimate(u), -τ_max, τ_max)
    # τ = clamp(compute_lqr_true(u), -τ_max, τ_max)
    # τ = clamp(compute_ida_pbc(u), -τ_max, τ_max)
    # τ = clamp(compute_switching_control(u), -τ_max, τ_max)

    τ = clamp(-dot(p,u), -τ_max, τ_max)

    du[1] = u[3]
    du[2] = u[4]
    du[3] = 1/I1 * (m*g*l*sin(u[1]) - r*τ)
    du[4] = 1/I2 * r*τ
end

function compute_control(sol::ODESolution)
    # return hcat( [clamp((-K*sol.u[i])[1], -τ_max, τ_max) for i = 1:length(sol)]... )'
    # return hcat( [clamp(compute_ida_pbc(sol.u[i]), -τ_max, τ_max) for i = 1:length(sol)]... )'
    # return hcat( [clamp(compute_lqr_estimate(sol.u[i]), -τ_max, τ_max) for i = 1:length(sol)]... )'
    return hcat( [clamp(compute_lqr_true(sol.u[i]), -τ_max, τ_max) for i = 1:length(sol)]... )'
end

K_estimate = compute_lqr_gain(I1_estimate)
K_true = compute_lqr_gain(I1_true)
u0 = [1*π/180, 0.5, 0, 0]
tspan = (0.0, 2.0)
p = K_true
prob = ODEProblem(iwp_eom!, u0, tspan, p)
sol = solve(prob, Tsit5(), saveat=0.01, dtmax=0.01)

function f(x)
    prob = ODEProblem(iwp_eom!, u0, tspan, x)
    sol = solve(prob, Tsit5(), saveat=0.01, dtmax=0.01)
    return compute_cost(sol, x)
end

# Choose as a model an elastic GP with input dimensions 2.
# The GP is called elastic, because data can be appended efficiently.
model = ElasticGPE(4,                            # 2 input dimensions
                   mean = MeanConst(0.),
                   kernel = SEArd([0., 0., 0., 0.], 5.),
                   logNoise = 0.,
                   capacity = 3000)              # the initial capacity of the GP is 3000 samples.
set_priors!(model.mean, [Normal(1, 2)])

# Optimize the hyperparameters of the GP using maximum a posteriori (MAP) estimates every 50 steps
modeloptimizer = MAPGPOptimizer(every = 50, noisebounds = [-4, 3],       # bounds of the logNoise
                                kernbounds = [[-1, -1, -1, -1, 0], [4, 4, 4, 4, 10]],  # bounds of the 3 parameters GaussianProcesses.get_param_names(model.kernel)
                                maxeval = 40)

# modeloptimizer = NoModelOptimizer()

opt = BOpt(f,
            model,
            UpperConfidenceBound(),                   # type of acquisition
            modeloptimizer,
            [-10., -10., -10., -10.], [0., 0., 0., 0.],                     # lowerbounds, upperbounds
            repetitions = 5,                          # evaluate the function for each input 5 times
            maxiterations = 1000,                      # evaluate at 100 input positions
            sense = Min,                              # minimize the function
            acquisitionoptions = (method = :LD_LBFGS, # run optimization of acquisition function with NLopts :LD_LBFGS method
                                restarts = 5,       # run the NLopt method from 5 random initial conditions each time.
                                maxtime = 0.1,      # run the NLopt method for at most 0.1 second each time
                                maxeval = 1000),    # run the NLopt methods for at most 1000 iterations (for other options see https://github.com/JuliaOpt/NLopt.jl)
            verbosity = Progress)

# x = Array{Array{Float64, 1}, 1}()
# push!(x, K_estimate[:])
# push!(x, K_true[:])
# y = -f.(x)
# append!(model, hcat(x...), y)

opt = BOpt(f,
           model,
        #    UpperConfidenceBound(),
           ThompsonSamplingSimple(),
           modeloptimizer,
           [-10., -10., -10., -10.], [0., 0., 0., 0.],
           maxiterations = 1000,
           sense = Min,
           initializer_iterations = 0
          )

result = boptimize!(opt)

################################################################################
## Plotting

# fig = figure(1)
# fig.clf()
# ax = fig.add_subplot(2,1,1)
# ax.plot(sol.t, getindex.(sol.u, 1), label=L"$q_1$")
# ax.plot(sol.t, getindex.(sol.u, 3), label=L"$\dot{q}_1$")
# ax.legend(fontsize=15)
# ax = fig.add_subplot(2,1,2)
# ax.plot(sol.t, getindex.(sol.u, 2), label=L"$q_2$")
# ax.plot(sol.t, getindex.(sol.u, 4), label=L"$\dot{q}_2$")
# ax.legend(fontsize=15)
# ax.set_xlabel("time", fontsize=15)

# fig = figure(2)
# fig.clf()
# ax = fig.add_subplot(1,1,1)
# ax.plot(sol.t, compute_control(sol), label=L"$\tau$")
# ax.set_xlabel("time", fontsize=15)
# ax.legend(fontsize=15)


using DataStructures
using Unitful
using MonteCarloMeasurements
using Distributions

t = range(0.0, stop=10.0, length=100)
uniform1(xmin,xmax) = MonteCarloMeasurements.Particles(      100,Distributions.Uniform(xmin,xmax))
uniform2(xmin,xmax) = MonteCarloMeasurements.StaticParticles(100,Distributions.Uniform(xmin,xmax))
particles1 = uniform1(-0.4, 0.4)
particles2 = uniform2(-0.4, 0.4)
result = OrderedDict{String,Any}()

# ∓ are 100 StaticParticles uniform distribution

result["time"] = t*u"s"
result["phi1"] = [sin(t[i]) + particles1*t[i]/10.0 for i in eachindex(t)]*u"rad"
result["phi2"] = [sin(t[i]) + particles2*t[i]/10.0 for i in eachindex(t)]*u"rad"
result["phi3"] = [sin(t[i]) ∓ 0.4*t[i]/10.0        for i in eachindex(t)]*u"rad"

plot(result, ["phi1", "phi2", "phi3"], figure=1,
     heading="Sine(time) with MonteCarloParticles/StaticParticles (plot area)")

plot(result, ["phi1", "phi2", "phi3"], figure=2,
     heading="Sine(time) with MonteCarloParticles/StaticParticles (plot all runs)")

println("\n... result info:")
printResultInfo(result)


using FastLapackInterface
using LinearAlgebra
using Test

include("LinSolveAlgo_test.jl")
include("SchurAlgo_test.jl")
include("QrAlgo_test.jl")


module DivGradCurls

export Gradient
export Divergence
export Curl

using ..Common
using LinearAlgebra
using SparseArrays

struct Gradient{N}
    components::NTuple{N,SparseMatrixCSC{Float64,Int}}
end

Gradient(components::Vararg{T,N}) where {T<:AbstractArray,N} = Gradient{N}(components)

function Gradient(sim::Simulation{1,Common.Symmetric})
    dx = sim.dx
    n = length(sim)
    m = n+1
    ∇ = spdiagm(m,n,0=>vcat(2/dx,fill(1/dx,n-1)),-1=>vcat(fill(-1/dx,n-1),-2/dx))
    return Gradient(∇)
end

function Gradient(sim::Simulation{2,Common.Symmetric})
    dx = sim.dx
    dy = sim.dy
    nx, ny = sim.latticesize
    mx = nx+1; my = ny+1
    ∇ₓ = spdiagm(mx,nx,0=>vcat(2/dx,fill(1/dx,nx-1)),-1=>vcat(fill(-1/dx,nx-1),-2/dx))
    ∇ᵤ = spdiagm(my,ny,0=>vcat(2/dy,fill(1/dy,ny-1)),-1=>vcat(fill(-1/dy,ny-1),-2/dy))
    return Gradient(kron(sparse(I,ny,ny),∇ₓ),kron(∇ᵤ,sparse(I,nx,nx)))
end

struct Divergence{N}
    gradient::Gradient{N}
end

function Divergence(sim::Simulation{1,Common.Symmetric})
    dx = sim.dx
    m = length(sim)
    n = length(sim)+1
    ∇ = spdiagm(m,n,0=>vcat(fill(-1/dx,m)),1=>vcat(fill(1/dx,m)))
    return Divergence(Gradient(∇))
end

function Divergence(sim::Simulation{2,Common.Symmetric})
    dx = sim.dx
    dy = sim.dy
    mx, my = sim.latticesize
    nx = mx+1
    ny = my+1
    ∇ₓ = spdiagm(mx,nx,0=>vcat(fill(-1/dx,mx)),1=>vcat(fill(1/dx,mx)))
    ∇ᵤ = spdiagm(my,ny,0=>vcat(fill(-1/dy,my)),1=>vcat(fill(1/dy,my)))
    return Divergence(Gradient(kron(sparse(I,my,my),∇ₓ),kron(∇ᵤ,sparse(I,mx,mx))))
end

struct Curl{N}

end

function Curl(sim::Simulation{N}) where N

end

################################################################################
# extend mul!

function LinearAlgebra.mul!(Y::NTuple{N,ScalarField{N}}, A::Gradient{N}, B::ScalarField{N}) where N
    for i ∈ 1:N
        mul!(Y[i], A.components[i], B.values)
    end
    return Y
end

function LinearAlgebra.mul!(Y::ScalarField{N}, A::Divergence{N}, B::NTuple{N,ScalarField{N}}) where N
    if N==1
        mul!(Y.values, A.gradient.components[1], B[1].values)
    else
        fill!(Y.values,0)
        for i ∈ 1:N
            mul!(Y.values, A.gradient.components[i], B[i],1,1)
        end
    end
    return Y
end

end # module

using .DivGradCurls


#########################################################
## Randomized singular value and eigen- decompositions ##
########################################################

# This file provides a rudimentary implementation of the randomized singular
# value decomposition and spectral (eigen-) decomposition as described in
# [^Halko2011].

import LinearAlgebra: Eigen, SVD

export rsvd, reigen

"""
    rsvd(A, n, p=0)

Compute partial singular value decomposition of `A` using a randomized
algorithm.

# Arguments

`A`: input matrix.

`n::Int`: number of singular value/vector pairs to find.

`p::Int=0`: number of extra vectors to include in computation.

# Output

`::SVD`: singular value decomposition.

!!! warning "Accuracy"
    This variant of the randomized singular value decomposition is the most
    commonly found implementation but is not recommended for accurate
    computations, as it often has trouble finding the `n` largest singular pairs,
    but rather finds `n` large singular pairs which may not necessarily be the
    largest.

# Implementation note

This function calls `rrange`, which uses naive randomized rangefinding to
compute a basis for a subspace of dimension `n` (Algorithm 4.1 of
[^Halko2011]), followed by `svd_restricted()`, which computes the
exact SVD factorization on the restriction of `A` to this randomly selected
subspace (Algorithm 5.1 of [^Halko2011]).

Alternatively, you can mix and match your own randomized algorithm using
any of the randomized range finding algorithms to find a suitable subspace
and feeding the result to one of the routines that computes the `SVD`
restricted to that subspace.
"""
function rsvd(A, n::Int, p::Int=0)
    Q = rrange(A, n+p)
    svd_restricted(A, Q, n)
end

"""
    rsvdvals(A, n, p=0)

Compute partial singular value decomposition of `A` using a randomized
algorithm.

# Arguments

- `A`: input matrix;
- `n::Int`: number of singular value/vector pairs to find;
- `p::Int=0`: number of extra vectors to include in computation.

# Output

- `::Vector`: estimated singular values of `A`.

!!! warning "Accuracy"
    This variant of the randomized singular value decomposition is the most
    commonly found implementation but is not recommended for accurate
    computations, as it often has trouble finding the `n` largest singular pairs,
    but rather finds `n` large singular pairs which may not necessarily be the
    largest.

# Implementation note

This function calls `rrange`, which uses naive randomized rangefinding to
compute a basis for a subspace of dimension `n` (Algorithm 4.1 of
[^Halko2011]), followed by `svd_restricted()`, which computes the
exact SVD factorization on the restriction of `A` to this randomly selected
subspace (Algorithm 5.1 of [^Halko2011]).

Alternatively, you can mix and match your own randomized algorithm using
any of the randomized range finding algorithms to find a suitable subspace
and feeding the result to one of the routines that computes the `SVD`
restricted to that subspace.
"""
function rsvdvals(A, n::Int, p::Int=0)
    Q = rrange(A, n+p)
    svdvals_restricted(A, Q, n)
end

"""
    rrange(A, r=0)

Compute an orthonormal basis for a subspace of `A` of dimension `r` using
naive randomized rangefinding.

# Arguments

- `A`: Input matrix. Must support `size(A)` and premultiply.
- `r::Int = 0`: The number of basis vectors to compute.

# Output

- `::Matrix`: matrix of dimension `size(A,1) x r` containing the basis
vectors of the computed subspace of `A`.

!!! warning "Warning"
    The Reference explicitly discourages using this algorithm.

# Implementation note

Whereas [^Halko2011] recommends classical Gram-Schmidt with double
reorthogonalization, we instead compute the basis with `qr()`, which
for dense `A` computes the QR factorization using Householder reflectors.
"""
function rrange(A, l::Int=0)
    m, n = size(A)
    if l > m
	    throw(ArgumentError("Cannot find $l linearly independent vectors of $m x $n matrix"))
    end
    Ω = randn(n, l)
    Y = A*Ω
    Q = Matrix(qr!(Y).Q)
    @assert m==size(Q, 1)
    @assert l==size(Q, 2)
    return Q
end

"""
    rrange_adaptive(A, r, ϵ=eps(); maxiter=10)

Compute an orthogonal basis for a subspace of `A` of dimension `l` using adaptive
randomized rangefinding.

Similar to `rrange`, but determines the oversampling parameter adaptively given
a threshold `ϵ`.

# Arguments

- `A`: input matrix. Must support `size(A)` and premultiply.
- `r::Integer`: number of basis vectors to compute.
- `ϵ::Real = eps()`: threshold to determine adaptive fitting.

## Keywords

- `maxiter::Int = 10`: maximum number of iterations to run.

# Output

`::Matrix`: matrix of dimension `size(A,1)` x l containing the basis
vectors of the computed subspace of `A`.

# References

Algorithm 4.2 of [^Halko2011]
"""
function rrange_adaptive(A, r::Integer, ϵ::Real=eps(); maxiter::Int=10)
    m, n = size(A)

    Ω = randn(n,r)
    Y = A*Ω
    Q = zeros(m,0)

    tol = ϵ / (10*√(2/π))
    for j=1:maxiter
        Tol = maximum([norm(Y[:,i]) for i=j:(j+r-1)])
        Tol > tol || break

        y = view(Y,:,j)
        y = Y[:,j] = y - Q*(Q'y)
        q = y/norm(y)
        Q = [Q q]

        ω = randn(n)
        y = A*ω
        y = y - Q*(Q'y)
        Y = [Y y]
        Yb = view(Y, :, (j+1):(j+r-1))
        Yb = Y[:, (j+1):(j+r-1)] = Yb - q * q'Yb

        j==maxiter && warn("Maximum number of iterations reached with norm $Tol > $tol")
    end
    Q
end

basis(V) = Matrix(qr(V).Q)

"""
    rrange_si(A, l; At=A', q=0)

Compute an orthonormal basis for a subspace of `A` of dimension `l` using
randomized rangefinding by subspace iteration.

*Note:* Running with `q=0` is functionally equivalent to `rrange`.

# Arguments

- `A`: input matrix. Must support `size(A)` and premultiply.
- `l::Int`: number of basis vectors to compute.

## Keywords

- `At = A'`: transpose of `A`.
- `q::Int = 0`: number of subspace iterations.
- `p` : oversampling parameter. The number of extra basis vectors to use
  in the computation, which get discarded at the end.

# Output

- `::Matrix`: A dense matrix of dimension `size(A,1) x l` containing the basis
  vectors of the computed subspace of `A`.

# Implementation note

Whereas the Reference recommends classical Gram-Schmidt with double
reorthogonalization, we instead compute the basis with `qr()`, which
for dense A computes the QR factorization using Householder reflectors.

# References

Algorithm 4.4 of [^Halko2011]
"""
function rrange_si(A, l::Int; At=A', q::Int=0)
    n = size(A, 2)
    Ω = randn(n,l+p)
    Y = A*Ω
    Q = basis(Y)
    for iter=1:q #Do some power iterations
        Ỹ=At*Q
        Q̃=basis(Ỹ)
        Y=A*Q̃
        Q=basis(Y)
    end
    Q
end

"""
    rrange_f(A, l)

Compute an orthonormal basis for a subspace of `A` of dimension `l` using
naive randomized rangefinding using stochastic randomized Fourier transforms.

*Note:* similar to `rrange`, but does not use gaussian random matrices.

# Arguments

- `A` : input matrix. Must support `size(A)` and premultiply.
- `l::Int` : number of basis vectors to compute.
- `p::Int = 0` : oversampling parameter. The number of extra basis vectors to use
  in the computation, which get discarded at the end.

# Output

- `::Matrix`: matrix of dimension `size(A,1)` x `l` containing the basis
  vectors of the computed subspace of `A`.

# Implementation note

Whereas the Reference recommends classical Gram-Schmidt with double
reorthogonalization, we instead compute the basis with `qr()`, which
for dense `A` computes the QR factorization using Householder reflectors.

# References

Algorithm 4.5 of [^Halko2011]
"""
function rrange_f(A, l::Int)
    n = size(A, 2)
    Ω = srft(l+p)
    Y = A*Ω
    Q = Matrix(qr!(Y).Q)
end

"""
    svd_restricted(A, Q, n)

Compute the SVD factorization of `A` restricted to the subspace spanned by `Q`
using exact projection.

# Arguments

- `A`: input matrix. Must support postmultiply.
- `Q`: matrix containing basis vectors of the subspace whose restriction to is
  desired.

# Output

`::SVD`: singular value decomposition.

# References

Algorithm 5.1 of [^Halko2011]
"""
function svd_restricted(A, Q, n::Int)
    B=Q'A
    S=svd!(B)
    SVD((Q*S.U)[:, 1:n], S.S[1:n], S.Vt[1:n, :])
end

"""
    svdvals_restricted(A, Q, n)

Compute the singular values of `A` restricted to the subspace spanned by `Q`
using exact projection.

# Arguments

- `A`: input matrix. Must support postmultiply.
- `Q`: matrix containing basis vectors of the subspace whose restriction to is
  desired.

# Output

- `::Vector`: estimated singular values of `A`.

# References

Algorithm 5.1 of [^Halko201]
"""
function svdvals_restricted(A, Q, n::Int)
    B=Q'A
    S=svdvals!(B)[1:n]
end

"""
    svd_re(A, Q)

Compute the SVD factorization of `A` restricted to the subspace spanned by `Q`
using row extraction.

*Note:* Remark 5.2 of [^Halko2011] recommends input of `Q` of the form `Q=A*Ω`
where `Ω` is a sample computed by `randn(n,l)` or even `srft(l)`.

# Arguments

- `A`: input matrix. Must support postmultiply
- `Q`: matrix containing basis vectors of the subspace whose restriction to is
  desired. Need not be orthogonal or normalized.

# Output

- `::SVD`: singular value decomposition.

# See also

A faster but less accurate variant of [`svd_restricted`](@ref) which uses the
interpolative decomposition `id`.

# References

Algorithm 5.2 of [^Halko2011]
"""
function svd_re(A, Q)
    F = id(Q)
    X, J = F[:B], F[:P]
    R′, W′= qr(A[J, :])
    Z = X*R′
    S=svd(Z)
    SVD(S.U, S.S, S.Vt * W′)
end

"""
    eigen_restricted(A, Q)

Compute the spectral (`Eigen`) factorization of `A` restricted to the subspace
spanned by `Q` using row extraction.

# Arguments

- `A::Hermitian`: input matrix. Must be `Hermitian` and support pre- and post-multiply.
- `Q`: orthonormal matrix containing basis vectors of the subspace whose
  restriction to is desired.

# Output

- `::LinearAlgebra.Eigen`: eigen factorization.

# References

Algorithm 5.3 of [^Halko2011]
"""
function eigen_restricted(A::Hermitian, Q)
    B = Q'A*Q
    E = eigen!(B)
    Eigen(E.values, Q*E.vectors)
end

"""
    eigen_re(A, Q)

Compute the spectral (`Eigen`) factorization of `A` restricted to the subspace
spanned by `Q` using row extraction.

*Note:* Remark 5.2 of [^Halko2011] recommends input of `Q` of the form `Q=A*Ω`
where `Ω` is a sample computed by `randn(n,l)` or even `srft(l)`.

# Arguments

- `A::Hermitian`: input matrix. Must be `Hermitian` and support pre- and post-multiply.
- `Q`: matrix containing basis vectors of the subspace whose restriction to is
  desired. Need not be orthogonal or normalized.

# Output

- `::LinearAlgebra.Eigen`: eigen factorization.

# See also

A faster but less accurate variant of `eigen_restricted()` which uses the
interpolative decomposition `id()`.

# References

Algorithm 5.4 of [^Halko2011]
"""
function eigen_re(A::Hermitian, Q)
    X, J = id(Q)
    F = qr!(X)
    V, R = F.Q, F.R
    Z=R*A[J, J]*R'
    E=eigen(Z)
    Eigen(E.values, V*E.vectors)
end

"""
    eigen_nystrom(A, Q)

Compute the spectral (`Eigen`) factorization of `A` restricted to the subspace
spanned by `Q` using the Nyström method.

# Arguments

- `A`: input matrix. Must be positive semidefinite.
- `Q`: orthonormal matrix containing basis vectors of the subspace whose
  restriction to is desired.

# Output

- `::LinearAlgebra.Eigen`: eigen factorization.

# See also

More accurate than [`eigen_restricted`](@ref) but is restricted to matrices
that can be Cholesky decomposed.

# References

Algorithm 5.5 of [^Halko2011]
"""
function eigen_nystrom(A, Q)
    B₁=A*Q
    B₂=Q'*B₁
    C=cholesky!(Hermitian(B₂))
    F=B₁/C
    S=svd!(F)
    Eigen(S.S.^2, S.U)
end

"""
    eigfact_onepass(A, Ω)

Compute the spectral (`Eigen`) factorization of `A` using only one matrix
product involving `A`.

# Arguments

- `A::Hermitian`: input matrix.
- `Ω`: sample matrix for the column space, e.g. `randn(n, l)` or `srft(l)`.
- `Ω̃;`: sample matrix for the row space. Not neeeded for `Hermitian` matrices.
- `At = A'`: computes transpose of input matrix.

# Output

- `::Base.LingAlg.Eigen`: eigen factorization.

# References

Algorithm 5.6 of [^Halko2011]
"""
function eigen_onepass(A::Hermitian, Ω)
    Y=A*Ω; Q = Matrix(qr!(Y).Q)
    B=(Q'Y)\(Q'Ω)
    E=eigen!(B)
    Eigen(E.values, Q*E.vectors)
end
function eigen_onepass(A, Ω, Ω̃; At=A')
    Y=A *Ω; Q = Matrix(qr!(Y).Q)
    Ỹ=At*Ω; Q̃ = Matrix(qr!(Ỹ).Q)
    #Want least-squares solution to (5.14 and 5.15)
    B=(Q'Y)\(Q̃'Ω)
    B̃=(Q̃'Ỹ)\(Q'Ω̃)
    #Here is a very very very hacky way to solve the problem
    B=0.5(B + B̃')
    E=eigen!(B)
    Eigen(E.values, Q*E.vectors)
end

"""
    reigen(A, l)

Compute the spectral (`Eigen`) decomposition of `A` using a randomized
algorithm.

# Arguments

- `A`: input matrix.
- `l::Int`: number of eigenpairs to find.

# Output

- `::LinearAlgebra.Eigen`: eigen decomposition.

# Implementation note

This is a wrapper around `eigen_onepass()` which uses the randomized
samples found using `srft(l)`.
"""
reigen(A::Hermitian, l::Int) = eigen_onepass(A, srft(l))
reigen(A, l::Int) = eigen_onepass(A, srft(l), srft(l))


using Logging

using StructuralIdentifiability

#QWWC
logger = Logging.SimpleLogger(stdout, Logging.Debug)
global_logger(logger)

ode = @ODEmodel(
    x'(t) = a * (y(t) - x(t)) + y(t) * z(t),
    y'(t) = b * (x(t) + y(t)) - x(t) * z(t),
    z'(t) = - c * z(t) - d * w(t) + x(t) * y(t),
    w'(t) = e * z(t) - f * w(t) + x(t) * y(t),
    g(t) = x(t)
)

println(assess_identifiability(ode))


" Allometry. Scaling rules to relate size to mass. "
abstract type AbstractAllometry end

@columns @flattenable struct SqrtAllometry{B0,B1} <: AbstractAllometry
    # Field       | Flatn | Default | Unit | Pror            | Limits         | Log  | Description
    β0::B0        | false | 1e-4*24 | g    | Gamma(2.0, 0.2) | [1e-6, 10.00]  | true | "Intercept"
    β1::B1        | true  | 0.1     | m    | Gamma(2.0, 0.2) | [1e-2, 1.0]    | true | "Scalar"
end

@columns @flattenable struct Allometry{B0,B1,A} <: AbstractAllometry
    β0::B0        | false | 1e-4*24 | g    | Gamma(2.0, 0.2) | [1e-6, 10.00]  | true | "Intercept"
    β1::B1        | true  | 0.1     | m    | Gamma(2.0, 0.2) | [1e-5, 10.00]  | true | "Scalar"
    α::A          | true  | 0.1     | _    | Gamma(2.0, 0.2) | [1e-3, 100.00] | true | "Exponent"
end

@columns struct FixedAllometry{M} <: AbstractAllometry
    height::M             | 1.0     | m    | Gamma(2.0, 0.2) | [1e-3, 100.00] | true | "Fixed height or depth"
end

update_height!(o, u) = update_height!(allometry_pars(o), o, u)
update_height!(f::Nothing, o, u) = nothing
update_height!(f, o, u) = set_height!(o, allometric_height(f, mass(o, u)))

allometric_height(f::FixedAllometry, mass) = f.height
allometric_height(f::SqrtAllometry, mass) = sqrt((mass - f.β0) / unit(f.β0)) * f.β1
allometric_height(f::Allometry, mass) = begin
    f.β1 * ((max(zero(mass), mass - f.β0))/unit(f.β0))^f.α
end


# Pentaquark decay chain: Lb -> Jψ p K
#   in the s-channel scattering kinematics
#   Lb Jψ -> p K
#
# it is check for an example described in arXiv:1805.02113
# https://arxiv.org/pdf/1805.02113.pdf
# at the page 29

using ThreeBodyDecay

mLb = 5.62; mJψ = 3.09; mp=0.938; mK = 0.49367
const tbs = ThreeBodySystem(mJψ,mp,mKmLb; two_jps = ([2,1,0,1],['-','+','-','+']))

# helicity couplings
# Lb Jψ
H1(two_λ0,two_λ1;two_LS=(4,3),two_s=3) = CG_doublearg(1,two_λ0,2,-two_λ1,two_LS[2],two_λ0-two_λ1) *
                                         CG_doublearg(two_LS[1],0,two_LS[2],two_λ0-two_λ1,two_s,two_λ0-two_λ1)
# pK
H2(two_λ2;two_l=4,two_s=3) = CG_doublearg(1,two_λ2,0,0,1,two_λ2) *
                             CG_doublearg(two_l,0,1,two_λ2,two_s,two_λ2)

# amplitude
function A(z,two_λs;two_s=3,two_LS=(4,3),two_l=4)
    return H1(two_λs[4],two_λs[1]; two_LS=two_LS, two_s=two_s) *
        H2(two_λs[2]; two_l=two_l, two_s=two_s) *
        # (two_λs[1]!=0 ? 1.0 : 2.0) * # weight of A(λ_J/ψ = 0) could be different with E/m factor
        wignerd_doublearg(two_s,two_λs[4]-two_λs[1],two_λs[2],z)
end

# amplitude squared
Asq_summed(z;two_s=3,two_LS=(4,3),two_l=4) =
    sum(abs2(A(z,two_λs;two_s=two_s,two_LS=two_LS,two_l=two_l))
        for two_λs in Iterators.product(-tbs.two_js[1]:2:tbs.two_js[1],
                                        -tbs.two_js[2]:2:tbs.two_js[2],
                                        -tbs.two_js[3]:2:tbs.two_js[3],
                                        -tbs.two_js[4]:2:tbs.two_js[4]))
I(σs) = Asq_summed(cosθ23(σs,tbs.msq))

## plotting
using Plots
# S=3/2, D-wave
plot(z->Asq_summed(z),-1,1, lab="", ylim=(0,1))
# P-wave
plot(z->Asq_summed(z;two_s=3,two_LS=(2,3),two_l=4),-1,1, lab="", ylim=(0,1))
# s = 1/2, LS=(0,1/2), l=0, angle dependence is constant again
plot(z->Asq_summed(z;two_s=1,two_LS=(0,1),two_l=0)+1e-5*z,-1,1, lab="", ylim=(0,3))
#


# first example draws data from exponential
# and fits using two statistics
using Turing, Statistics, Distributions, Random, LinearAlgebra, StatsPlots
using AdvancedMH

@views function main()
θ = 2.0
n = 100 # sample size
S = 100 # number of simulation draws
y = rand.(Exponential(θ),n) # the data generating process
# summary statistics for estimation
function moments(y)
    [sqrt(n)*mean(y), sqrt(n)*std(y)]
end

z = moments(y)
zs = zeros(S,2)

@model function abc(z)
    θ ~ LogNormal(-1.,1.) # the prior for the parameters
    # sample from the model, at the trial parameter value, and compute statistics
    @inbounds for i = 1:S
        y .= rand(Exponential(θ), n)
        zs[i,:] .= moments(y) # simulated summary statistics
    end
    # the asymptotic Gaussian distribution of the statistics
    m = mean(zs, dims=1)[:]
    Σ = Symmetric((1. + 1/S)*cov(zs))
    z ~ MvNormal(m, Σ)
end;

# sample chains, 8 chains of length 1000, with 100 burnins dropped
chain = sample(abc(z), MH(:θ => AdvancedMH.RandomWalkProposal(Normal(0, 0.25))), MCMCThreads(), 1100, 8)
display(chain)
p = plot(chain[101:end])
display(p)
end
main()


using ADCME
using PyCall
using LinearAlgebra
using PyPlot
using Random
Random.seed!(233)

function halo_exchange_two_d(u,fill_value,m,n)
    halo_exchange_two_d_ = load_op_and_grad("./build/libHaloExchangeTwoD","halo_exchange_two_d")
    u,fill_value,m,n = convert_to_tensor(Any[u,fill_value,m,n], [Float64,Float64,Int64,Int64])
    halo_exchange_two_d_(u,fill_value,m,n)
end

mpi_init()
U = reshape(1:24, 4, 6)'|>Array

m = 3
n = 2
fill_value = 1.0

M = mpi_rank()÷n+1
N = mpi_rank()%n+1
ulocal = U[(M-1)*2 + 1: M * 2, (N-1)*2+1:N*2]

sess = Session(); init(sess)

# TODO: change your test parameter to `m`
#       in the case of `multiple=true`, you also need to specify which component you are testings
# gradient check -- v
function scalar_function(x)
    x = mpi_bcast(x)
    u = x .* ulocal
    return mpi_sum(sum(halo_exchange_two_d(u,fill_value,m,n)^2))
end

# TODO: change `m_` and `v_` to appropriate values
m_ = constant(rand(2,2))
v_ = rand(2,2)
y_ = scalar_function(m_)
dy_ = gradients(y_, m_)
ms_ = Array{Any}(undef, 5)
ys_ = Array{Any}(undef, 5)
s_ = Array{Any}(undef, 5)
w_ = Array{Any}(undef, 5)
gs_ =  @. 1 / 10^(1:5)

for i = 1:5
    g_ = gs_[i]
    ms_[i] = m_ + g_*v_
    ys_[i] = scalar_function(ms_[i])
    s_[i] = ys_[i] - y_
    w_[i] = s_[i] - g_*sum(v_.*dy_)
end

sess = Session(); init(sess)
sval_ = run(sess, s_)
wval_ = run(sess, w_)

if mpi_rank()==0
    close("all")
    loglog(gs_, abs.(sval_), "*-", label="finite difference")
    loglog(gs_, abs.(wval_), "+-", label="automatic differentiation")
    loglog(gs_, gs_.^2 * 0.5*abs(wval_[1])/gs_[1]^2, "--",label="\$\\mathcal{O}(\\gamma^2)\$")
    loglog(gs_, gs_ * 0.5*abs(sval_[1])/gs_[1], "--",label="\$\\mathcal{O}(\\gamma)\$")

    plt.gca().invert_xaxis()
    legend()
    xlabel("\$\\gamma\$")
    ylabel("Error")
    savefig("gradtest.png")
end

mpi_finalize()


module TestLSS

using QuantEcon
using Base.Test
using FactCheck

rough_kwargs = Dict(:atol => 1e-7, :rtol => 1e-7)

# set up
A = .95
C = .05
G = 1.
mu_0 = [.75;]
Sigma_0 = fill(0.000001, 1, 1)

ss = LSS(A, C, G, mu_0)
ss1 = LSS(A, C, G, mu_0, Sigma_0)

vals = stationary_distributions(ss, max_iter=1000, tol=1e-9)

facts("Testing lss.jl") do
    context("test stationarity") do
        vals = stationary_distributions(ss, max_iter=1000, tol=1e-9)
        ssmux, ssmuy, sssigx, sssigy = vals

        @fact ssmux --> roughly(ssmuy; rough_kwargs...)
        @fact sssigx --> roughly(sssigy; rough_kwargs...)
        @fact ssmux --> roughly([0.0]; rough_kwargs...)
        @fact sssigx --> roughly(ss.C.^2 ./ (1 - ss.A .^2); rough_kwargs...)
    end

    context("test replicate") do
        xval1, yval1 = replicate(ss, 100, 5000)
        xval2, yval2 = replicate(ss; t=100, num_reps=5000)
        xval3, yval3 = replicate(ss1; t=100, num_reps=5000)

        for (x, y)  in [(xval1, yval1), (xval2, yval2), (xval3, yval3)]
            @fact x --> roughly(y; rough_kwargs...)
            @fact abs(mean(x)) <= 0.05 --> true
        end

    end

    context("test convergence error") do
        @fact_throws stationary_distributions(ss; max_iter=1, tol=eps())
    end

    context("test geometric_sums") do
        β = 0.98
        xs = rand(10)
        for x in xs
            gsum_x, gsum_y = QuantEcon.geometric_sums(ss, β, [x])
            @fact gsum_x --> roughly([x/(1-β *ss.A[1])])
            @fact gsum_y --> roughly([ss.G[1]*x/(1-β *ss.A[1])])
        end
    end

    context("test constructors") do
        # kwarg version
        other_ss = LSS(A, C, G; mu_0=[mu_0;])
        for nm in fieldnames(ss)
            @fact getfield(ss, nm) --> getfield(other_ss, nm)
        end
    end

end  # facts
end  # module


# by lazarusA
using GLMakie
using GeometryBasics, LinearAlgebra, StatsBase
using Makie: get_dim, surface_normals
ϵ = 1e-7
# thanks to @jkrumbiegel for the lift
let
    function getMesh(x,y,z)
        positions = vec(map(CartesianIndices(z)) do i
        GeometryBasics.Point{3, Float32}(
            get_dim(x, i, 1, size(z)),
            get_dim(y, i, 2, size(z)),
            z[i])
        end)
        faces = decompose(GLTriangleFace, Rect2D(0f0, 0f0, 1f0, 1f0), size(z))
        normals = surface_normals(x, y, z)
        vertices = GeometryBasics.meta(positions; normals=normals)
        meshObj = GeometryBasics.Mesh(vertices, faces)
        meshObj
    end
    function Π(v, n)
        v - dot(v, n) / norm(n)^2 * n
    end
    function multiply(q₁, q₂)
        z₁ = Complex(q₁[1], q₁[2])
        z₂ = Complex(q₁[3], q₁[4])
        A = [z₁ -conj(z₂); z₂ conj(z₁)]
        w₁ = Complex(q₂[1], q₂[2])
        w₂ = Complex(q₂[3], q₂[4])
        B = [w₁ -conj(w₂); w₂ conj(w₁)]
        C = A * B
        x₁, x₂ = C[1,1], C[2,1]
        [real(x₁); imag(x₁); real(x₂); imag(x₂)]
    end
    d(Φ, p) = begin
        rows = length(p)
        D = Array{Float64,1}(undef, rows)
        E = Array{Float64,2}(I, rows, rows) .* ϵ
        P = repeat(p, 1, rows)
        P′ = P + E
        for i in 1:rows
            D[i] = (Φ(P′[:, i]) - Φ(P[:, i])) ./ ϵ
        end
        D
    end
    directional(Φ, p, x) = begin
        rows = length(x)
        D = Array{Float64,1}(undef, rows)
        E = Array{Float64,2}(I, rows, rows) .* ϵ
        P = repeat(x, 1, rows)
        P′ = P + E
        for i in 1:rows
            D[i] = (Φ(p, P′[:, i]) - Φ(p, P[:, i])) ./ ϵ
        end
        D
    end
    getz₀(longitude, latitude, phase) = ℯ^(im * (longitude + phase)) * √((1 - sin(latitude)) / 2)
    getz₁(longitude, latitude, phase) = ℯ^(im * phase) * √((1 + sin(latitude)) / 2)
    TS³(z₀, z₁) =  begin
        # initialization
        X₀ = Complex(rand() + im * rand())
        X₁ = Complex(rand() + im * rand())
        getloss(z, x) = begin
            z₀ = Complex(z[1] + im * z[2])
            z₁ = Complex(z[3] + im * z[4])
            X₀ = Complex(x[1] + im * x[2])
            X₁ = Complex(x[3] + im * x[4])
            abs(conj(z₀) * X₀ + conj(z₁) * X₁)
        end
        x = [real(X₀); imag(X₀); real(X₁); imag(X₁)]
        z = [real(z₀); imag(z₀); real(z₁); imag(z₁)]
        loss = getloss(z, x)
        threshold = 1e-4
        η = 1e-2
        iterations = 150
        i = 0
        while loss > threshold && i < iterations
            g = directional(getloss, z, x)
            X₀ = Complex(x[1] - η * g[1] + im * (x[2] - η * g[2]))
            X₁ = Complex(x[3] - η * g[3] + im * (x[4] - η * g[4]))
            x = [real(X₀); imag(X₀); real(X₁); imag(X₁)]
            loss = getloss(z, x)
            i += 1
        end
        X₀, X₁
    end
    connection1form(z₀, z₁) = begin
        X₀, X₁ = TS³(z₀, z₁)
        1/2 * (conj(z₀) * X₀ - z₀ * conj(X₀) + conj(z₁) * X₁ - z₁ * conj(X₁))
    end
    tangent_tail(x, f) = [x[1]; x[2]; f(x)]
    tangent_head(x, f, ξ) = begin
        dΦ = d(f, x)
        ξΦ = dot(dΦ, ξ)
        dξΦ = dΦ .* ξ
        [dξΦ[1]; dξΦ[2]; (f([x[1] + dξΦ[1] * ϵ; x[2] + dξΦ[2] * ϵ]) - f(x)) ./ ϵ] .* ξΦ
    end
    f₁(i) = begin
        √(1 - (i[1])^2 - (i[2])^2)
    end
    f₂(i) = -f₁(i)
    getpoints(start, stop, segments1, segments2, point, radius) = begin
        getp(point) = begin
            ψ, ϕ, θ = point
            z₀ = getz₀(ϕ, θ, ψ)
            z₁ = getz₁(ϕ, θ, ψ)
            q₁, q₂ = real(z₀), imag(z₀)
            q₃, q₄ = real(z₁), imag(z₁)
            q = [q₁; q₂; q₃; q₄]
            p = q[1:3] ./ (1 - q₄)
            magnitude = norm(p)
            normalize(p) * tanh(magnitude)
        end
        getlengths(points) = begin
            lengths = []
            segments = length(points)
            for i in 1:segments-1
                p₁ = getp(points[i])
                p₂ = getp(points[i + 1])
                distance = norm(p₁ - p₂)
                push!(lengths, distance)
            end
            lengths
        end
        getcircumference(points) = begin
            circumference = 0
            segments = length(points)
            for i in 1:segments-1
                p₁ = getp(points[i])
                p₂ = getp(points[i + 1])
                distance = norm(p₁ - p₂)
                circumference += distance
            end
            circumference
        end
        getdistances(points, base) = begin
            distances = []
            segments1, segments2 = size(points)
            for i in 1:segments1
                for j in 1:segments2
                    p₁ = getp(points[i, j])
                    p₂ = getp([points[i, j][1]; base])
                    distance = norm(p₁ - p₂)
                    push!(distances, distance)
                end
            end
            distances
        end
        inputs = Array{Array{Float64,1},2}(undef, segments1, segments2)
        #ϕ, θ = point
        ϕ, θ = [point[1], -π/2]
        linearspace1 = collect(range(start, stop = stop, length = segments1))
        linearspace2 = collect(range(0, stop = 2π, length = segments2))
        for i in 1:segments1
            for j in 1:segments2
                ψ = linearspace1[i]
                ϕ = point[1] + cos(linearspace2[j]) * radius
                θ = point[2] + sin(linearspace2[j]) * radius
                inputs[i, j] = [ψ; ϕ; θ]
            end
        end
        getvariance(X) = begin
            N = length(X)
            μ = sum(X) / N
            σ² = sum((X .- μ).^2) / N
            σ²
        end
        getvariance1(X) = begin
            N = length(X)
            μ = sum(X) / N
            σ² = sum(exp.((X .- μ).^2) .- 1) / N
            σ²
        end
        converttovector(points) = begin
            vector = []
            segments1, segments2 = size(points)
            for i in 1:segments1
                for j in 1:segments2
                    vector = [vector; points[i, j]]
                end
            end
            vector
        end
        converttomatrix(vector, shape) = begin
            matrix = Array{Array{Float64,1},2}(undef, shape...)
            index = 1
            for i in 1:shape[1]
                for j in 1:shape[2]
                    matrix[i, j] = vector[index: index + 2]
                    index += 3
                end
            end
            matrix
        end
        getloss(points, base) = begin
            segments1, segments2 = size(points)
            loss = 0
            # get the length of segments along a fiber
            lengths = [getlengths(points[:, i]) for i in 1:segments2]
            loss += sum(getvariance.(lengths)) / segments2
            # get the length of segments along the sectional cut of a fiber
            lengths = [getlengths(points[i, :]) for i in 1:segments1]
            loss += sum(getvariance.(lengths)) / segments1
            # get the circumference of the sectional cut of a fiber
            circumferences = [getcircumference(points[i, :]) for i in 1:segments1]
            loss += getvariance(circumferences) / segments1
            # get the the distance between the central point of a sectional cut of a fiber and the surrounding star-shaped points
            distances = getdistances(points, base)
            loss += sum(getvariance.(distances)) / (segments1 * segments2)
            error1 = sum((points[begin, :][begin] .- start).^2) / segments2
            error2 = sum((points[end, :][begin] .- stop).^2) / segments2
            loss + error1 + error2
        end
        getloss1(v₁, v₂) = getloss(converttomatrix(v₁ + v₂, size(inputs)), point)
        v₂ = converttovector(inputs)
        v₁ = zeros(size(v₂))
        loss = getloss1(v₁, v₂)
        threshold = 1e-3
        η = 1e-3
        iterations = 300
        α = 0.9
        i = 1
        while loss > threshold && i < iterations
            heat = rand(size(v₂)...)
            v₂ = v₂ + (0.001 .* heat)
            g = directional(getloss1, v₁, v₂)
            new = v₂ - (η .* g)
            v₂ = α .* v₂ + (1 - α) .* new
            loss = getloss1(v₁, v₂)
            i += 1
        end
        outputs = converttomatrix(v₂, size(inputs))
        [getp(outputs[i, j]) for i in 1:segments1, j in 1:segments2]
    end

    segments = 30
    segments2 = 30
    radius = 0.05
    # Grids of polar and azimuthal angles
    θ = LinRange(0, π, segments)
    ϕ = LinRange(0, 2π, segments)
    xx = [sin(θ)*sin(ϕ) for θ in θ, ϕ in ϕ]
    yy = [sin(θ)*cos(ϕ) for θ in θ, ϕ in ϕ]
    zz = [cos(θ)        for θ in θ, ϕ in ϕ]

    l = Node(4)
    m = Node(1)

    lon = Node(1.0)
    lat = Node(1.5)
    ang = Node(1.0)
    vector = Node([1.0; 0.0; 0.0])

    #w₀ = @lift(ℯ^(im * $ang) * sqrt((1 + sin($lat)) / 2))
    #w₁ = @lift(ℯ^(im * ($lon + $ang)) * sqrt((1 - sin($lat)) / 2))
    #w = @lift($w₁ / $w₀)
    w = lift(lon, lat) do lon, lat
        a₁ = cos(lat) * cos(lon)
        a₂ = cos(lat) * sin(lon)
        a₃ = sin(lat)
        a₁ / (1 - a₃) + im * a₂ / (1 - a₃)
    end
    fiber = lift(lon, lat, ang) do lon, lat, ang
        #points = [[lon; lat] + [cos(i); sin(i)] .* radius for i in range(0, stop = 2π, length = segments2)]
        start = 0
        stop = ang
        #ψ = range(0, stop = ang, length = segments)
        #actions = [gets1action(start, stop, segments, point) for point in points]
        xyz(i, j) = begin
            point = points[j]
            action = actions[j][i]
            v₀ = getz₀(point[1], point[2], action)
            v₁ = getz₁(point[1], point[2], action)
            q₁, q₂ = real(v₀), imag(v₀)
            q₃, q₄ = real(v₁), imag(v₁)
            q = [q₁; q₂; q₃; q₄]
            p = q[1:3] ./ (1 - q₄)
            magnitude = norm(p)
            normalize(p) * tanh(magnitude)
        end
        #a = [xyz(i, j) for i in 1:segments, j in 1:length(points)]
        point = [lon; lat]
        a = getpoints(start, stop, segments, segments2, point, radius)
        a₁ = map(i -> i[1], a)
        a₂ = map(i -> i[2], a)
        a₃ = map(i -> i[3], a)
        a₁, a₂, a₃
    end
    fiber₁ = @lift($fiber[1])
    fiber₂ = @lift($fiber[2])
    fiber₃ = @lift($fiber[3])

    fiber2 = lift(lon, lat, ang) do lon, lat, ang
        #points = [[lon; lat] + [cos(i); sin(i)] .* radius for i in range(0, stop = 2π, length = segments2)]
        start = ang
        stop = 2π
        #ψ = range(start, stop = stop, length = segments)
        #actions = [gets1action(start, stop, segments, point) for point in points]
        xyz(i, j) = begin
            point = points[j]
            action = actions[j][i]
            v₀ = getz₀(point[1], point[2], action)
            v₁ = getz₁(point[1], point[2], action)
            q₁, q₂ = real(v₀), imag(v₀)
            q₃, q₄ = real(v₁), imag(v₁)
            q = [q₁; q₂; q₃; q₄]
            p = q[1:3] ./ (1 - q₄)
            magnitude = norm(p)
            normalize(p) * tanh(magnitude)
        end
        #a = [xyz(i, j) for i in 1:segments, j in 1:length(points)]
        point = [lon; lat]
        a = getpoints(start, stop, segments, segments2, point, radius)
        a₁ = map(i -> i[1], a)
        a₂ = map(i -> i[2], a)
        a₃ = map(i -> i[3], a)
        a₁, a₂, a₃
    end
    fiber2₁ = @lift($fiber2[1])
    fiber2₂ = @lift($fiber2[2])
    fiber2₃ = @lift($fiber2[3])

    tail = lift(w) do w
        u, v = real(w), imag(w)
        denominator = u^2 + v^2 + 1
        u = 2u / denominator
        v = 2v / denominator
        f = abs(w) ≤ 1 ? f₂ : f₁
        tangent_tail([u; v], f)
    end
    xconst = lift(w, tail) do w, tail
        u, v = real(w), imag(w)
        denominator = u^2 + v^2 + 1
        u = 2u / denominator
        v = 2v / denominator
        f = abs(w) ≤ 1 ? f₂ : f₁
        normalize(tangent_head([u; v], f, [1; 0]))
    end
    yconst = lift(w, tail) do w, tail
        u, v = real(w), imag(w)
        denominator = u^2 + v^2 + 1
        u = 2u / denominator
        v = 2v / denominator
        f = abs(w) ≤ 1 ? f₂ : f₁
        normalize(tangent_head([u; v], f, [0; 1]))
    end
    head = lift(w, ang, tail, xconst, yconst) do w, ang, tail, x, y
        # parallel transport
        #v = to_value(vector)
        #n = normalize(cross(x, y))
        #if -1 < dot(x, y) < 1
        #    projected = Π(v, n)
        #else
        #    projected = v
        #end
        #projected = normalize(projected)
        #vector[] = projected
        #projected
        u, v = real(w), imag(w)
        denominator = u^2 + v^2 + 1
        u = 2u / denominator
        v = 2v / denominator
        f = abs(w) ≤ 1 ? f₂ : f₁
        normalize(tangent_head([u; v], f, [cos(ang); sin(ang)]))

        #p₁ = [lon; lat]
        #p₂ = p₁ + [0.1; 0.1]
        #v₀ = ℯ^(im * (p₁[1] + ψ)) * sqrt((1 - sin(p₁[2])) / 2)
        #v₁ = ℯ^(im * ψ) * sqrt((1 + sin(p₁[2])) / 2)

    end
    connectiontail = lift(lon, lat, ang) do lon, lat, ang
        point = [lon; lat]
        v₀ = getz₀(point[1], point[2], ang)
        v₁ = getz₁(point[1], point[2], ang)
        q₁, q₂ = real(v₀), imag(v₀)
        q₃, q₄ = real(v₁), imag(v₁)
        q = [q₁; q₂; q₃; q₄]
        p = q[1:3] ./ (1 - q₄)
        magnitude = norm(p)
        normalize(p) * tanh(magnitude)
    end
    connectionhead = lift(lon, lat, ang) do lon, lat, ang
        point = [lon; lat]
        v₀ = getz₀(point[1], point[2], ang)
        v₁ = getz₁(point[1], point[2], ang)
        oneform = connection1form(v₀, v₁)
        v₀, v₁ = oneform * v₀, oneform * v₁
        q₁, q₂ = real(v₀), imag(v₀)
        q₃, q₄ = real(v₁), imag(v₁)
        q = [q₁; q₂; q₃; q₄]
        p = q[1:3] ./ (1 - q₄)
        magnitude = norm(p)
        h = normalize(p) * tanh(magnitude)
        t = to_value(connectiontail)
        h - t
    end

    tails₁ = @lift([$tail[1], $tail[1], $tail[1]])
    tails₂ = @lift([$tail[2], $tail[2], $tail[2]])
    tails₃ = @lift([$tail[3], $tail[3], $tail[3]])
    heads₁ = @lift([$head[1], $xconst[1], $yconst[1]])
    heads₂ = @lift([$head[2], $xconst[2], $yconst[2]])
    heads₃ = @lift([$head[3], $xconst[3], $yconst[3]])

    conn_tail₁ = @lift([$connectiontail[1]])
    conn_tail₂ = @lift([$connectiontail[2]])
    conn_tail₃ = @lift([$connectiontail[3]])
    conn_head₁ = @lift([$connectionhead[1]])
    conn_head₂ = @lift([$connectionhead[2]])
    conn_head₃ = @lift([$connectionhead[3]])

    x₁ = @lift(xx .* 0.1 .+ $tail[1])
    y₁ = @lift(yy .* 0.1 .+ $tail[2])
    z₁ = @lift(zz .* 0.1 .+ $tail[3])

    ambient =  Vec3f0(0.75, 0.75, 0.75)
    with_theme(theme_black()) do
        fig = Figure(resolution = (1400, 800))

        ax1 = Axis3(fig, aspect = :data, perspectiveness = 0.5, elevation = π/8, azimuth = 2.225π)
        ax2 = Axis3(fig, aspect = :data, perspectiveness = 0.5, elevation = π/8, azimuth = 2.225π)
        pltobj1 = mesh!(ax1, getMesh(xx, yy, zz), color = :blue, ambient = ambient)
        pltobj3 = mesh!(ax1, @lift(getMesh($x₁, $y₁, $z₁)), color = :gold, ambient = ambient)
        pltobj4 = arrows!(ax1, tails₁, tails₂, tails₃, heads₁, heads₂, heads₃, color = [:red, :silver, :gray], lengthscale = 0.3f0)
        pltobj5 = mesh!(ax2, @lift(getMesh($fiber₁, $fiber₂, $fiber₃)), color = :red, ambient = ambient)
        pltobj6 = mesh!(ax2, @lift(getMesh($fiber2₁, $fiber2₂, $fiber2₃)), color = :blue, ambient = ambient)
        pltobj7 = arrows!(ax2, conn_tail₁, conn_tail₂, conn_tail₃, conn_head₁, conn_head₂, conn_head₃, color = [:gold], lengthscale = 1.0f0)
        fig[1,1] = ax1
        fig[1,2] = ax2
        round3(n) = round(n, sigdigits = 3)
        degree(n) = round3(180(n / π))
        fig[0,1:2] = Label(fig, @lift("Hopf fiber: longitude = $(degree($lon)), latitude = $(degree($lat)), S¹action = $(degree($ang))."), textsize = 20)
        sl3 = Slider(fig[end+1, 1:2], range = range(-π, stop = π, length = 100))
        sl4 = Slider(fig[end+1, 1:2], range = range(-π/2, stop = 0.99 * π/2, length = 100))
        sl5 = Slider(fig[end+1, 1:2], range = range(0, stop = 2π, length = 100))
        set_close_to!(sl3, to_value(lon))
        set_close_to!(sl4, to_value(lat))
        set_close_to!(sl5, to_value(ang))
        connect!(lon, sl3.value)
        connect!(lat, sl4.value)
        connect!(ang, sl5.value)
        display(fig)
        framerate = 30
        totaltime = 30
        timestamps = range(0, totaltime, step=1/framerate)
#=
        record(fig, joinpath("gallery", "connection1form_a.mp4"), timestamps;
            framerate = framerate) do t
            step = t / totaltime
            longitude = π/2
            latitude = π/4
            action = step * 2π
            #set_close_to!(sl3, longitude)
            #set_close_to!(sl4, latitude)
            set_close_to!(sl5, action)
        end =#
    end
end


# hierarchical multi-objetive LP
#

using Gurobi, Test

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

A = [1. 1.]
lb = [0.; 0.]
b = [1.]
f = [1.; 1.]
model = gurobi_model(env, sense = :maximize, A = A, b = b, lb = lb, f = [0., 0.])

Gurobi.set_multiobj_n!(model, 2)
Gurobi.update_model!(model)
Gurobi.set_multiobj!(model, 1, [1., 1.], 10, 1.0)

Gurobi.set_multiobj!(model, 2, [1., 0.], 1, 1.0)
Gurobi.update_model!(model)

optimize(model)
@test get_solution(model) == [1., 0.]

Gurobi.set_multiobj!(model, 2, [0., 1.], 1, 1.0)
Gurobi.update_model!(model)

optimize(model)
@test get_solution(model) == [0., 1.]


import CLBLAS
using OpenCL: cl

const clblas = CLBLAS
clblas.setup()
alpha = cl.cl_float(5)
beta = cl.cl_float(2)

device, context, queue = clblas.get_next_compute_context()
A = clblas.randBuf(8, 12; ctx=context, queue=queue)
B = clblas.randBuf(12, 24; ctx=context, queue=queue)
C = clblas.randBuf(8, 24; ctx=context, queue=queue)

matA = clblas.fetch(A)
matB = clblas.fetch(B)
matC = clblas.fetch(C)

data = (5.0*matA*matB) + (2.0*matC)
future = clblas.clblasSgemm(UInt32(0), UInt32(0), alpha, A, B, beta, C)

result = fetch(future)
println(result)
println("------------")
println(data)
for i in 1:length(data)
  Test.@test_approx_eq_eps result[i] data[i]  0.00001
end

info("success!")
clblas.teardown()


using luteos
using SymPy

# let # limit scope

Ps = [P1(), P2(), P3()]         # Range of polynomial order
Ns = [9, 17]#, 33] # Range of grid size

dim = 3

κ = 1.0
c = fill( 0.0, dim )

# Material
mat = Material( ν=0.33, E=1.0 )

## Set up problem
#   Get functions for exact solution
function ExactSol3D(  )

  dim = 3

  @syms x1 x2 x3

  u = sin.(pi*x1) .* sin.(0.5*pi*x2) .* sin.(pi*x3)

  g = (x1^2 - x1) .* (x2^2 - x2) .* (x3^2 - x3)

  J = integrate( integrate( integrate( u .* g , x1, 0, 1 ), x2, 0, 1 ), x3, 0, 1 )

  ∂u∂x1 = diff( u, x1 )
  ∂u∂x2 = diff( u, x2 )
  ∂u∂x3 = diff( u, x3 )

  F  = diff( -∂u∂x1, x1 ) + diff( -∂u∂x2, x2 ) + diff( -∂u∂x3, x3 )

  ufunc_org = lambdify( u, [x1, x2, x3] )

  Ffunc_org  = lambdify( F, [x1, x2, x3] )

  q1func_org = lambdify( ∂u∂x1, [x1, x2, x3] )
  q2func_org = lambdify( ∂u∂x2, [x1, x2, x3] )
  q3func_org = lambdify( ∂u∂x3, [x1, x2, x3] )

  gfunc_org  = lambdify( g, [x1, x2, x3] )

  # make sure you can call it with matrices
  ufunc = x -> ufunc_org.(x[:,1], x[:,2], x[:,3])

  Ffunc = x -> Ffunc_org.(x[:,1], x[:,2], x[:,3])

  q1func = x -> q1func_org.(x[:,1], x[:,2], x[:,3])
  q2func = x -> q2func_org.(x[:,1], x[:,2], x[:,3])
  q3func = x -> q3func_org.(x[:,1], x[:,2], x[:,3])

  gfunc  = x -> gfunc_org.(x[:,1], x[:,2], x[:,3])

  return( ufunc,
          q1func,q2func,q3func,
          Ffunc, gfunc, J )

end

( ufunc, q1func, q2func, q3func, Ffunc, gfunc, J ) = ExactSol3D(  )

#   Setup boundary conditions
function funcB( p::Array{Float64} )
  return fill( 0.0, size(p,1) )
end
bctype = [1,1,1,1,1,2]

#   Setup source function
source = (p) -> Ffunc(p)

## Compute error for different polynomial orders and grid sizes
#   Initialize arrays
Err_uh  = fill( 0.0, length(Ps), length(Ns) )
Err_qh1 = fill( 0.0, length(Ps), length(Ns) )
Err_qh2 = fill( 0.0, length(Ps), length(Ns) )
Err_qh3 = fill( 0.0, length(Ps), length(Ns) )
Err_J   = fill( 0.0, length(Ps), length(Ns) )

#   Loop over polynomial order and grid size
for ii in 1:length(Ps), jj in 1:length(Ns)

  P = Ps[ii]; N = Ns[jj]

  @printf( " %6i %6i\n", P.p, N )

  mesh   = Mesh3D( "cube", P, N = N)
  master = Master3D( P )

  prob = CDR( @sprintf("Poisson - Reg %i %i", P.p, N), κ, c, source, bctype, false, [funcB, funcB, funcB, funcB, funcB, funcB] )

  (uhath, uh, qh) = hdgSolve( master, mesh, prob )

  #   Initialize arrays
  err_uh = 0.0
  err_qh = fill( 0.0, dim )
  Jcomp  = 0.0

  # preallocate
  jcwd = fill( 0.0, size(master.∇ϕ,2), size(master.∇ϕ,2) )
  ∂ξ∂x = fill( 0.0, size(master.∇ϕ,2), dim^2 )
  ∂x∂ξ = fill( 0.0, size(master.∇ϕ,2), dim^2 )

  for kk in 1:size(mesh.t,1)

    # Compute Jacobians
    luteos.compJacob!( master, mesh.nodes[:,:,kk], ∂ξ∂x, jcwd, ∂x∂ξ )

    # u
    Δuh     = master.ϕ' * ( uh[:,1,kk] - ufunc( mesh.nodes[:,:,kk] ) )
    err_uh += Δuh' * jcwd * Δuh

    # σ
    Δqh1       = master.ϕ' * ( qh[:,1,kk] - q1func( mesh.nodes[:,:,kk] ) )
    Δqh2       = master.ϕ' * ( qh[:,2,kk] - q2func( mesh.nodes[:,:,kk] ) )
    Δqh3       = master.ϕ' * ( qh[:,3,kk] - q3func( mesh.nodes[:,:,kk] ) )

    # output functional
    gquad  = master.ϕ' * gfunc( mesh.nodes[:,:,kk] )
    uquad  = master.ϕ' * uh[:,1,kk]
    Jcomp += gquad' * jcwd * uquad

    err_qh[1] += Δqh1' * jcwd * Δqh1
    err_qh[2] += Δqh2' * jcwd * Δqh2
    err_qh[3] += Δqh3' * jcwd * Δqh3

  end

  Err_uh[ii,jj]  = sqrt(err_uh)

  Err_qh1[ii,jj] = sqrt(err_qh[1])
  Err_qh2[ii,jj] = sqrt(err_qh[2])
  Err_qh3[ii,jj] = sqrt(err_qh[3])

  Err_J[ii,jj]   = abs(Jcomp - J)

end

# Compute convergence rates
h = 1 ./ ( Ns - 1 )
conv_uh  = (log.( Err_uh[:,end-1]) - log.( Err_uh[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));

conv_qh1 = (log.( Err_qh1[:,end-1]) - log.( Err_qh1[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
conv_qh2 = (log.( Err_qh2[:,end-1]) - log.( Err_qh2[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
conv_qh3 = (log.( Err_qh3[:,end-1]) - log.( Err_qh3[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));

conv_J   = (log.( Err_J[:,end-1])   - log.( Err_J[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));

# Output to terminal
@printf("\n")
@printf("   Convergence rates for Poisson 3D Neumann problem\n\n")
@printf("   ------------------------------------------------\n\n")
@printf( "P   ")
for jj in 1:size(Ps,1)
  @printf( " %6i", Ps[jj].p )
end
@printf( "\n" )

#   u
@printf( "u   ")
for jj in 1:size(Ps,1)
  @printf( " %6.4f", conv_uh[jj] )
end
@printf( "\n" )

#   q
@printf( "q₁  ")
for jj in 1:length(Ps)
  @printf( " %6.4f", conv_qh1[jj] )
end
@printf( "\n" )
@printf( "q₂  ")
for jj in 1:length(Ps)
  @printf( " %6.4f", conv_qh2[jj] )
end
@printf( "\n" )
@printf( "q₃  ")
for jj in 1:length(Ps)
  @printf( " %6.4f", conv_qh3[jj] )
end
@printf( "\n" )

# J
@printf( "J   ")
for jj in 1:length(Ps)
  @printf( " %6.4f", conv_J[jj] )
end
@printf( "\n" )

open("errors_Poisson_Neumann3D.dat", "w") do f
  @printf(f, "P \t N \t E_uh \t E_qh1 \t E_qh2 \t E_qh3 \t E_J\n")
  for ii in 1:length(Ps), jj in 1:length(Ns)
    @printf(f, "%i \t %i \t %16.15e \t %16.15e \t %16.15e \t %16.15e \t %16.15e\n",
    Ps[ii].p,Ns[jj],Err_uh[ii,jj],Err_qh1[ii,jj],Err_qh2[ii,jj],Err_qh3[ii,jj],Err_J[ii,jj])
  end
end

# end # limit scope


for f in (:log, :log2, :log10, :log1p, :exp, :exp2, :exp10, :expm1,
          :cos, :sin, :tan, :sec, :csc, :cot, :acos, :asin, :atan,
          :cosh, :sinh, :tanh, :sech, :csch, :coth, :acosh, :asinh, :atanh,
          :eint, :li2, :gamma, :lngamma, :digamma, :zeta, :erf, :erfc,
          :j0, :j1, :y0, :y1, :ai)
    fJ = Symbol(f, :!)
    @eval begin
        export $fJ
        function ($fJ)(rop::BigFloat, op::BigFloat, rnd=ROUNDING_MODE[])
            ccall(($(string("mpfr_", f)), :libmpfr), Int32,
                  (Ref{BigFloat}, Ref{BigFloat}, MPFR.MPFRRoundingMode),
                  rop, op, rnd)
            return rop
        end
    end
end

@doc """
    log!(rop, op, rnd)

Set `rop` to the natural logarithm of `op` rounded in the direction
`rnd`. Set `rop` to +0 if `op` is 1 (in all rounding modes), for
consistency with the ISOC99 and IEEE 754-2008 standards. Set `rop` to
−Inf if `op` is ±0 (i.e., the sign of the zero has no influence on the
result).
""" log!

@doc """
    log2!(rop, op, rnd)

Set `rop` to log₂ of `op` rounded in the direction `rnd`. Set `rop` to
+0 if `op` is 1 (in all rounding modes), for consistency with the
ISOC99 and IEEE 754-2008 standards. Set `rop` to −Inf if `op` is ±0
(i.e., the sign of the zero has no influence on the result).
""" log2!

@doc """
    log10!(rop, op, rnd)

Set `rop` to log\\_{10} of `op` rounded in the direction `rnd`. Set
`rop` to +0 if `op` is 1 (in all rounding modes), for consistency with
the ISOC99 and IEEE 754-2008 standards. Set `rop` to −Inf if `op` is
±0 (i.e., the sign of the zero has no influence on the result).
""" log10!

@doc """
    log1p!(rop, op, rnd)

Set `rop` to the logarithm of one plus `op`, rounded in the direction
`rnd`. Set `rop` to −Inf if `op` is −1.
""" log1p!

@doc """
    exp!(rop, op, rnd)

Set `rop` to the exponential of `op`, rounded in the direction `rnd`.
""" exp!

@doc """
    exp2!(rop, op, rnd)

Set `rop` to `2^op`, rounded in the direction `rnd`.
""" exp2!

@doc """
    exp10!(rop, op, rnd)

Set `rop` to `10^op`, rounded in the direction `rnd`.
""" exp10!

@doc """
    expm1!(rop, op, rnd)

Set `rop` to `e^op−1`, rounded in the direction `rnd`.
"""

@doc """
    cos!(rop, op, rnd)

Set `rop` to the cosine of `op`, rounded in the direction `rnd`.
""" cos!

@doc """
    sin!(rop, op, rnd)

Set `rop` to the sine of `op`, rounded in the direction `rnd`.
""" sin!

@doc """
    tan!(rop, op, rnd)

Set `rop` to the tangent of `op`, rounded in the direction `rnd`.
""" tan!

@doc """
    sec!(rop, op, rnd)

Set `rop` to the secant of `op`, rounded in the direction `rnd`.
""" sec!

@doc """
    csc!(rop, op, rnd)

Set `rop` to the cosecant of `op`, rounded in the direction `rnd`.
""" csc!

@doc """
    cot!(rop, op, rnd)

Set `rop` to the cotangent of `op`, rounded in the direction `rnd`.
""" cot!

@doc """
    acos!(rop, op, rnd)

Set `rop` to the arc-cosine of `op`, rounded in the direction
`rnd`. Note that since `acos(-1)` returns the floating-point number
closest to π according to the given rounding mode, this number might
not be in the output range `0≤rop<π` of the arc-cosine function;
still, the result lies in the image of the output range by the
rounding function.
""" acos!

@doc """
    asin!(rop, op, rnd)

Set `rop` to the arc-sine of `op`, rounded in the direction
`rnd`. Note that since `asin(1)` (resp. `asin(-1)`) returns the
floating-point number closest to π/2 (resp -π/2) according to the
given rounding mode, this number might not be in the output range
`-π/2≤rop<π/2` of the arc-sine function; still, the result lies in the
image of the output range by the rounding function.
""" asin!

@doc """
    atan!(rop, op, rnd)

Set `rop` to the arc-tangent of `op`, rounded in the direction
`rnd`. Note that since `atan(Inf)` (resp. `atan(-Inf)`) returns the
floating-point number closest to π/2 (resp -π/2) according to the
given rounding mode, this number might not be in the output range
`-π/2≤rop<π/2` of the arc-sine function; still, the result lies in the
image of the output range by the rounding function. The same holds for
`atan(op)` with large `op` and small precision of `rop`.
""" atan!

@doc """
    cosh!(rop, op, rnd)

Set `rop` to the hyperbolic cosine of `op`, rounded in the direction
`rnd`.
""" cosh!

@doc """
    sinh!(rop, op, rnd)

Set `rop` to the hyperbolic sine of `op`, rounded in the direction
`rnd`.
""" sinh!

@doc """
    tanh!(rop, op, rnd)

Set `rop` to the hyperbolic tangent of `op`, rounded in the direction
`rnd`.
""" tanh!

@doc """
    sech!(rop, op, rnd)

Set `rop` to the hyperbolic secant of `op`, rounded in the direction
`rnd`.
""" sech!

@doc """
    csch!(rop, op, rnd)

Set `rop` to the hyperbolic cosecant of `op`, rounded in the direction
`rnd`.
""" csch!

@doc """
    coth!(rop, op, rnd)

Set `rop` to the hyperbolic cotangent of `op`, rounded in the direction
`rnd`.
""" coth!

@doc """
    acosh!(rop, op, rnd)

Set `rop` to the inverse hyperbolic cosine of `op`, rounded in the
direction `rnd`.
""" acosh!

@doc """
    asinh!(rop, op, rnd)

Set `rop` to the inverse hyperbolic sine of `op`, rounded in the
direction `rnd`.
""" asinh!

@doc """
    atanh!(rop, op, rnd)

Set `rop` to the inverse hyperbolic tangent of `op`, rounded in the
direction `rnd`.
""" atanh!

@doc """
    eint!(rop, op, rnd)

Set `rop` to the exponential integral of `op`, rounded in the
direction `rnd`. This is the sum of Euler’s constant, of the logarithm
of the absolute value of `op`, and of the sum for k from 1 to infinity
of op^k/(k·k!). For positive `op`, it corresponds to the Ei function
at `op` (see formula 5.1.10 from the Handbook of Mathematical
Functions from Abramowitz and Stegun), and for negative `op`, to the
opposite of the E1 function (sometimes called eint1) at `−op` (formula
5.1.1 from the same reference).
""" eint!

@doc """
    li2!(rop, op, rnd)

Set `rop` to real part of the dilogarithm of `op`, rounded in the
direction `rnd`. MPFR defines the dilogarithm function as
−∫_{t=0}^op log(1−t)/t dt.

""" li2!

@doc """
    gamma!(rop, op, rnd)

Set `rop` to the value of the Gamma function on `op`, rounded in the
direction `rnd`. For gamma!, when `op` is a negative integer, `rop` is
set to NaN.
""" gamma!

@doc """
    lngamma!(rop, op, rnd)

Set `rop` to the value of the logarithm of the Gamma function on `op`,
rounded in the direction `rnd`. When `op` is 1 or 2, set `rop` to +0
(in all rounding modes). When `op` is an infinity or a nonpositive
integer, set `rop` to +Inf, following the general rules on special
values. When `−2k−1<op<−2k`, `k` being a nonnegative integer, set
`rop` to NaN.
""" lngamma!

@doc """
    digamma!(rop, op, rnd)

Set `rop` to the value of the Digamma (sometimes also called Psi)
function on `op`, rounded inthe direction `rnd`. When `op` is a
negative integer, set `rop` to NaN.
""" digamma!

@doc """
    zeta!(rop, op, rnd)

Set `rop` to the value of the Riemann Zeta function on `op`, rounded
in the direction `rnd`.
""" zeta!

@doc """
    erf!(rop, op, rnd)

Set `rop` to the value of the error function on `op`, rounded in the
direction `rnd`.
""" erf!

@doc """
    erfc!(rop, op, rnd)

Set `rop` to the value of the complementary error function on `op`,
rounded in the direction `rnd`.
""" erfc!

@doc """
    j0!(rop, op, rnd)

Set `rop` to the value of the first kind Bessel function of order 0 on
`op`, rounded in the direction `rnd`. When `op` is NaN, `rop` is
always set to NaN. When `op` is ±Inf, `rop` is set to +0.
""" j0!

@doc """
    j1!(rop, op, rnd)

Set `rop` to the value of the first kind Bessel function of order 1 on
`op`, rounded in the direction `rnd`. When `op` is NaN, `rop` is
always set to NaN. When `op` is ±Inf, `rop` is set to +0. When `op` is
zero, `rop` is set to +0 or −0 depending on the sign of `op`.
""" j1!

@doc """
    y0!(rop, op, rnd)

Set `rop` to the value of the second kind Bessel function of order 0
on `op`, rounded in the direction `rnd`. When `op` is NaN or negative,
`rop` is always set to NaN. When `op` is +Inf, `rop` is set to
+0. When `op` is zero, `rop` is set to -Inf.

""" y0!

@doc """
    y1!(rop, op, rnd)

Set `rop` to the value of the second kind Bessel function of order 0
on `op`, rounded in the direction `rnd`. When `op` is NaN or negative,
`rop` is always set to NaN. When `op` is +Inf, `rop` is set to
+0. When `op` is zero, `rop` is set to -Inf.
""" y1!

@doc """
    ai!(rop, x, rnd)

Set `rop` to the value of the Airy function Ai on `x`, rounded in the
direction `rnd`.  When `x` is NaN, `rop` is always set to NaN. When
`x` is +Inf or −Inf, `rop` is +0. The current implementation is not
intended to be used with large arguments. It works with `|x|`
typically smaller than 500.
""" ai!

for f in (:sin_cos, :sinh_cosh)
    fJ = Symbol(f, :!)
    @eval begin
        export $fJ
        function ($fJ)(sop::BigFloat, cop::BigFloat, op::BigFloat, rnd=ROUNDING_MODE[])
            ccall($(string("mpfr_", f), :libmpfr), Int32,
                  (Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, MPFR.MPFRRoundingMode),
                  sop, cop, op, rnd)
            return (sop, cop)
        end
    end
end

@doc """
    sin_cos!(sop, cop, op, rnd)

Set simultaneously `sop` to the sine of `op` and `cop` to the cosine
of `op`, rounded in the direction `rnd` with the corresponding
precisions of `sop` and `cop`, which must be different variables.
""" sin_cos!

@doc """
    sinh_cosh!(sop, cop, op, rnd)

Set simultaneously `sop` to the hyperbolic sine of `op` and `cop` to
the hyperbolic cosine of `op`, rounded in the direction `rnd` with the
corresponding precisions of `sop` and `cop`, which must be different
variables.
""" sinh_cosh!

for f in (:atan2, :gamma_inc, :beta, :agm, :hypot)
    fJ = Symbol(f, :!)
    @eval begin
        export $fJ
        function ($fJ)(rop::BigFloat, op1::BigFloat, op2::BigFloat, rnd=ROUNDING_MODE[])
            ccall(($(string("mpfr_", f)), :libmpfr), Int32,
                  (Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, MPFR.MPFRRoundingMode),
                  rop, op1, op2, rnd)
            return rop
        end
    end
end

@doc """
    atan2!(rop, y, x, rnd)

Set `rop` to the arc-tangent2 of `y` and `x`, rounded in the direction
`rnd`: if x > 0, `atan2(y, x) = atan(y/x)`; if x < 0, `atan2(y, x) =
sign(y)*(π - atan(|y/x|))`, thus a number from `−π` to `π`. As for
`atan`, in case the exact mathematical result is `+π` or `−π`, its
rounded result might be outside the function output range.
""" atan2!

@doc """
    gamma_inc!(rop, op, op2, rnd)

Set `rop` to the incomplete Gamma function on `op1` and `op2`, rounded
in the direction `rnd`. (In the literature, `gamma_inc` is called
upper incomplete Gamma function, or sometimes complementary incomplete
Gamma function.) For `gamma_inc`, when `op2` is zero and `op` is a
negative integer, `rop` is set to NaN.

Note: the current implementation of `gamma_inc` is slow for large
values of `rop` or `op`, in which case some internal overflow might
also occur.
""" gamma_inc!

@doc """
    beta!(rop, op1, op2, rnd)

Set `rop` to the value of the Beta function at arguments `op1` and
`op2`. Note: the current code does not try to avoid internal overflow
or underflow, and might use a huge internal precisionin some cases.
""" beta!

export factorial!
"""
    factorial!(rop, op, rnd)

Set `rop` to the factorial of `op`, rounded in the direction `rnd`.
"""
function factorial!(rop::BigFloat, op::CulongMax, rnd=ROUNDING_MODE[])
    ccall(("mpfr_fac_ui", :libmpfr), Int32, (Ref{BigFloat}, Culong, MPFR.MPFRRoundingMode),
          rop, op, rnd)
    return rop
end

for f in (:jn, :yn)
    fJ = Symbol(f, :!)
    @eval begin
        export $fJ
        function ($fJ)(rop::BigFloat, n::ClongMax, op::BigFloat, rnd=ROUNDING_MODE[])
            ccall(($(string("mpfr_", f)), :libmpfr), Int32,
                  (Ref{BigFloat}, Clong, Ref{BigFloat}, MPFR.MPFRRoundingMode),
                  rop, n, op, rnd)
            return rop
        end
    end
end

@doc """
    jn!(rop, n, op, rnd)

Set `rop` to the value of the first kind Bessel function of order `n`
on `op`, rounded in the direction `rnd`. When `op` is NaN, `rop` is
always set to NaN. When `op` is ±Inf, `rop` is set to +0. When `op` is
zero, and `n` is not zero, `rop` is set to +0 or −0 depending on the
parity and sign of `n`, and the sign of `op`.
""" jn!

@doc """
    yn!(rop, n, op, rnd)

Set `rop` to the value of the second kind Bessel function of order `n`
on `op`, rounded in the direction `rnd`. When `op` is NaN or negative,
`rop` is always set to NaN. When `op` is +Inf, `rop` is set to
+0. When `op` is zero, `rop` is set to +Inf or −Inf depending on the
parity and sign of `n`.
""" yn!

@doc """
    agm!(rop, op1, op2, rnd)

Set `rop` to the arithmetic-geometric mean of `op1` and `op2`, rounded
in the direction `rnd`. The arithmetic-geometric mean is the common
limit of the sequences `u_n` and `v_n`, where `u0=op1`, `v0=op2`,
u_{n+1} is the arithmetic mean of `u_n` and `v_n`, and `v_{n+1}` is
the geometric mean of `u_n` and `v_n`. If any operand is negative and
the other one is not zero, set `rop` to NaN. If any operand is zero
and the other one is finite (resp. infinite), set `rop` to+0
(resp. NaN).
"""

@doc """
    hypot!(rop, op1, op2, rnd)

"""

for f in (:fma, :fms,)
    fJ = Symbol(f, :!)
    @eval begin
        export $fJ
        function ($fJ)(rop::BigFloat, op1::BigFloat, op2::BigFloat, op3::BigFloat,
                       rnd=ROUNDING_MODE[])
            ccall(($(string("mpfr_", f)), :libmpfr), Int32,
                  (Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat},
                   MPFR.MPFRRoundingMode),
                  rop, op1, op2, op3, rnd)
            return rop
        end
    end
end

@doc """
    fma(rop, op1, op2, op3, rnd)

Set `rop` to `(op1×op2)+op3` rounded in the direction
`rnd`. Concerning special values (signed zeros, infinities, NaN),
these functions behave like a multiplication followed by a separate
addition or subtraction. That is, the fused operation matters only for
rounding.
""" fma!

@doc """
    fms(rop, op1, op2, op3, rnd)

Set `rop` to `(op1×op2)-op3` rounded in the direction
`rnd`. Concerning special values (signed zeros, infinities, NaN),
these functions behave like a multiplication followed by a separate
addition or subtraction. That is, the fused operation matters only for
rounding.
""" fms!

for f in (:fmma, :fmms,)
    fJ = Symbol(f, :!)
    @eval begin
        export $fJ
        function ($fJ)(rop::BigFloat, op1::BigFloat, op2::BigFloat, op3::BigFloat,
                       op4::BigFloat, rnd=ROUNDING_MODE[])
            ccall(($(string("mpfr_", f)), :libmpfr), Int32,
                  (Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat}, Ref{BigFloat},
                   Ref{BigFloat}, MPFR.MPFRRoundingMode),
                  rop, op1, op2, op3, op4, rnd)
            return rop
        end
    end
end


# Create example data
x = repeat(LinRange(-2, 2, 20), inner=10)
y = repeat(LinRange(-1, 1, 10), outer=20)
u = x .* (x.^2 .+ y.^2)
v = y .* (x.^2 .+ y.^2)
# Plot arrows
quiver(x, y, u, y, arrowscale=0.1)
# Expand the y-axes to see the whole arrows
ylim(-1.15, 1.15)


"""
secant(f,x0; rtol=1.e-6, atol=1.e-12, maxit=10,
        armmax=10, armfix=false, pdata=nothing,
        printerr=true, keepsolhist=true, stagnationok=false)

C. T. Kelley, 2021

The secant method for scalar equations.

Input:\n
f: function\n
x0: initial iterate

Keyword Arguments (kwargs):\n
rtol, atol: real and absolute error tolerances\n

maxit: upper bound on number of nonlinear iterations\n

If you use secant and your initial iterate is poor, you have made
a mistake. You will get an error message.

armmax: upper bound on stepsize reductions in linesearch

armfix:\n
The default is a parabolic line search (ie false). Set to true and
the stepsize will be fixed at .5. Don't do this unless you are doing
experiments for research.

printerr:\n
I print a helpful message when the solver fails. To supress that
message set printerr to false.

keepsolhist:\n
Set this to true to get the history of the iteration in the output
tuple. This is on by default for scalar equations and off for systems.
Only turn it on if you have use for the data, which can get REALLY LARGE.

stagnationok:\n
Set this to true if you want to disable the line search and either
observe divergence or stagnation. This is only useful for research
or writing a book.

Output:\n
A named tuple (solution, functionval, history, stats, idid,
               errcode, solhist)
where

solution = converged result
functionval = F(solution)
history = the vector of residual norms (||F(x)||) for the iteration
stats = named tuple of the history of (ifun, ijac, iarm), the number
of functions/derivatives/steplength reductions at each iteration.
For the secant method, ijac = 0.

idid=true if the iteration succeeded and false if not.

errcode = 0 if if the iteration succeeded
        = -1 if the initial iterate satisifies the termination criteria
        = 10 if no convergence after maxit iterations
        = 1  if the line search failed

solhist:\n
This is the entire history of the iteration if you've set
keepsolhist=true\n
secant builds solhist with a function from the Tools directory. For
systems, solhist is an N x K array where N is the length of x and K
is the number of iteration + 1. So, for scalar equations (N=1), solhist
is a row vector. Hence the use of solhist' in the example below.

### Example for secant.jl

```jldoctest

julia> secout=secant(atan,1.0;maxit=6,atol=1.e-12,rtol=1.e-12);

julia> secout.history
7-element Array{Float64,1}:
 7.85398e-01
 5.18729e-01
 5.39030e-02
 4.86125e-03
 4.28860e-06
 3.37529e-11
 2.06924e-22
```
"""
function secant(
    f,
    x0;
    rtol = 1.e-6,
    atol = 1.e-12,
    maxit = 10,
    solver = "secant",
    armmax = 5,
    armfix = false,
    dx = 1.e-7,
    pdata = nothing,
    printerr = true,
    keepsolhist = true,
    stagnationok = false,
)
    itc = 0
    idid = true
    errcode = 0
    iline = false
    #=
     The theory does not support convergence of the secant-Armijo iteration
     and you assume a risk when you use it. The same is true for Broyden
     and any other quasi-Newton method.
    =#
    fc = 0.0
    fc = EvalF!(f, fc, x0, pdata)
    fm = fc
    xm = copy(x0)
    xm = x0 * 1.0001
    if xm == 0
        xm = 0.0001
    end
    fm = fc
    fm = EvalF!(f, fm, xm, pdata)
    newfun0 = 1
    derivative_is_old = false
    resnorm = abs(fc)
    jfact = nothing
    stagflag = stagnationok && (armmax == 0)
    (ItRules, x, n) =
        Secantinit(x0, dx, f, solver, armmax, armfix, maxit, printerr, pdata, jfact)
    #
    # Initialize the iteration statistics
    #
    newiarm = -1
    ItData = ItStats(resnorm, 2)
    newfun = 0
    newjac = 0
    newsol = x
    xt = x
    keepsolhist ? (solhist = solhistinit(n, maxit, x)) : (solhist = [])
    #
    # Fix the tolerances for convergence and define the derivative df
    # outside of the main loop for scoping.
    #
    tol = rtol * resnorm + atol
    residratio = 1
    df = 0.0
    armstop = true
    #
    # If the initial iterate satisfies the termination criteria, tell me.
    #
    toosoon = (resnorm <= tol)
    #
    # The main loop stops on convergence, too many iterations, or a
    # line search failure after a derivative evaluation.
    #
    while (resnorm > tol) && (itc < maxit) && (armstop || stagnationok)
        newfun = 0
        #
        # Extra function call at the start.
        #
        newjac = 0
        newfun = 0
        #
        df = (fc - fm) / (x - xm)
        derivative_is_old = (newjac == 0) && (solver == "newton")
        #
        # Compute the Newton direction and call the line search.
        #
        xm = x
        fm = fc
        ft = fc
        d = -fc / df
        AOUT = armijosc(xt, x, ft, fc, d, resnorm, ItRules, derivative_is_old)
        #
        # update solution/function value
        #
        xm = x
        x = AOUT.ax
        fm = fc
        fc = AOUT.afc
        #
        # If the line search fails and the derivative is current,
        # stop the iteration.
        #
        armstop = AOUT.idid || derivative_is_old
        iline = ~armstop && ~stagflag
        newiarm = AOUT.aiarm
        #
        # Keep the books.
        #
        residm = resnorm
        resnorm = AOUT.resnorm
        residratio = resnorm / residm
        updateStats!(ItData, newfun, newjac, AOUT)
        #
        itc += 1
        ~keepsolhist || (@views solhist[:, itc+1] .= x)
    end
    (idid, errcode) = NewtonOK(resnorm, iline, tol, toosoon, itc, ItRules)
    newtonout = CloseIteration(x, fc, ItData, idid, errcode, keepsolhist, solhist)
    return newtonout
end


function quick_sort!(arr::Vector{T}, l::Int = 1, r::Int = length(arr)) where {T}
    if l >= r
        return
    end
    mid = arr[(l+r)>>1]
    l_pos = l
    r_pos = r
    while l_pos <= r_pos
        while arr[l_pos] < mid
            l_pos += 1
        end
        while arr[r_pos] > mid
            r_pos -= 1
        end
        if l_pos <= r_pos
            t = arr[l_pos]
            arr[l_pos] = arr[r_pos]
            arr[r_pos] = t
            l_pos += 1
            r_pos -= 1
        end
    end
    if l < r_pos
        quick_sort!(arr, l, r_pos)
    end
    if l_pos < r
        quick_sort!(arr, l_pos, r)
    end
end


#####
##### Competing Poisson processes
#####

export CompetingPoisson

struct CompetingPoisson{T,D,E}
    total_rate::T
    index_distribution::D
    events::E
end

"""
$(SIGNATURES)

The first arrival from competing Poisson point processes with the given `rates`.

`rand` returns a `NamedTuple{(:duration,:event)}`, where `duration` is the duration until
the first event and `event` is drawn from `events`.

Varios properties are supported, see [`propertynames`](@ref).

# Note

The distribution of `duration` is `Exponential(1/sum(rates))`, so this process can also be
thought of as competing exponentials.
"""
function CompetingPoisson(rates::AbstractVector; events = axis(rates, 1))
    total_rate = sum(rates)
    CompetingPoisson(total_rate, Categorical(rates ./ total_rate), events)
end

function Base.rand(rng::AbstractRNG, sampler::SamplerTrivial{<:CompetingPoisson})
    @unpack total_rate, index_distribution, events = sampler[]
    τ = rand(rng, Exponential(1 / total_rate))
    i = rand(rng, index_distribution)
    (duration = τ, event = events[i])
end

function Base.propertynames(cp::CompetingPoisson, private = false)
    public = (:total_rate, :rates, :probabilities, :events)
    private ? (:index_distribution, public...) : public
end

function Base.getproperty(cp::CompetingPoisson, key::Symbol)
    if key ≡ :probabilities
        probs(getfield(cp, :index_distribution))
    elseif key ≡ :rates
        cp.probabilities .* cp.total_rate
    else
        getfield(cp, key)
    end
end

function Base.show(io::IO, cp::CompetingPoisson)
    print(io, "Competing Poisson with total rate $(round(cp.total_rate; sigdigits = 3));")
    for (e, p) in zip(cp.events, cp.probabilities)
        print(io, " P($(e))=$(round(p; sigdigits = 3))")
    end
end


# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .jl
#       format_name: light
#       format_version: '1.3'
#       jupytext_version: 0.8.6
#   kernelspec:
#     display_name: Julia 1.0.3
#     language: julia
#     name: julia-1.0
# ---

using Random
using DifferentialEquations

# BM model for beta(t): d beta(t) = beta(t) d W(t)
# Function to compute rates of change
spsir_bm = function(du, u, p, t)
    #Susceptible Individuals
    S = u[1]
    #Infected Individuals
    I = u[2]
    #Recovered Individuals
    R = u[3]

    β = max(0., u[4])

    N = S + I + R
    γ = p[1]

    du[1] = -β * S * I / N
    du[2] = β * S * I / N - γ * I
    du[3] = γ * I
    du[4] = 0.
end

# Function to add noise
sigma_spsir = function(du, u, p, t )
    σ = p[2]

    du[1] = 0.
    du[2] = 0.
    du[3] = 0.
    du[4] = σ
end

# +
# BM for logbeta(t), with drift
spsir_logbm_drift = function(du, u, p, t)
    S = u[1]
    I = u[2]
    R = u[3]
    β = exp( u[4] )

    N = S + I + R
    γ = p[1]
    α = p[3]

    du[1] = -β * S * I / N
    du[2] = β * S * I / N - γ * I
    du[3] = γ * I
    du[4] = -α * I
end

# +
# set random seed
Random.seed!( 1111 )

## Simulation of BM model
# starting conditions
u0 = [50.;1.0;0.0;2.0]
tspan = (0.0,10.0)
# parameters gamma sigma
p = [1.; 1.]
spsir_bm_prob = SDEProblem(spsir_bm, sigma_spsir, u0, tspan, p)
spsir_bm_sol = solve(spsir_bm_prob)

# +
## Simulation of log-BM with drift model

# starting condtions
u0 = [50.;1.0;0.0;log(3.) ]
tspan = (0.0,10.0)
# parameters gamma sigma alpha
p = [1.; 1.; 0.1]
spsir_logbm_drift_prob = SDEProblem(spsir_logbm_drift, sigma_spsir, u0, tspan, p)
spsir_logbm_drift_sol = solve(spsir_logbm_drift_prob)

# -

## Plotting for BM model
using Plots
# Plot evolution of number infected
plot( spsir_bm_sol , vars = 2 )

# Plot evolution of transmission rate
plot( spsir_bm_sol , vars = 4 )

# +
## Plotting for BM with drift model
# Plot evolution of number infected
plot( spsir_logbm_drift_sol , vars = 2 )

# -

# Plot evolution of transmission rate
plot( spsir_logbm_drift_sol , vars = 4 )


@testset "potential_ET scalars" begin
    Tair,pressure,Rn = 30.0,100.0,500.0
    ET_pot, LE_pot = potential_ET(PriestleyTaylor(), Tair,pressure,Rn; alpha=1.26)
    # compare to R
    @test ≈(ET_pot, 0.0002035969; rtol = 1e-5)
    @test ≈(LE_pot, 494.7202; rtol = 1e-5)
    #
    VPD,Ga_h = 2.0, 0.1
    Gs_pot = 0.5 # assume known surface conductance
    ET_pot,LE_pot = potential_ET(PenmanMonteith(), Tair,pressure,Rn,VPD,Ga_h; Gs_pot)
    Gs_ms, Gs_mol = surface_conductance(InversePenmanMonteith(), Tair,pressure,VPD,LE_pot,Rn,Ga_h)
    @test Gs_mol ≈ Gs_pot
end

@testset "potential_ET dataframe" begin
    dfo = DataFrame(Tair = 20.0:1.0:30.0,pressure = 100.0, Rn = 500.0)
    df = copy(dfo)
    potential_ET!(df, PriestleyTaylor(); alpha=1.26)
    @test ≈(last(df).ET_pot, 0.0002035969; rtol = 1e-5)
    @test ≈(last(df).LE_pot, 494.7202; rtol = 1e-5)
    #
    df2 = copy(dfo)
    df2[!, :VPD] .= 2.0
    df2[!, :Ga_h] .= 0.1
    potential_ET!(df2, PenmanMonteith(); Gs_pot = 0.4)
    df2b = rename!(copy(df2, copycols=false),SA[:LE_pot => :LE])
    surface_conductance!(df2b, InversePenmanMonteith())
    @test all(df2b.Gs_mol .≈ 0.4)
end

@testset "potential_ET dataframe missings in G" begin
    df = @pipe DataFrame(Tair = 20.0,pressure = 100.0, Rn = 500.0, G = 100.0:1:110) |>
      allowmissing(_, Cols(:G))
    df.G[2] = missing
    df_ET = potential_ET!(copy(df), PriestleyTaylor(); G = df.G)
    @test ncol(df_ET) == ncol(df)+2 # two columns added: ET_pot, LE_pot
    @test nrow(df_ET) == nrow(df)
    @test df_ET.ET_pot[end] < df_ET.ET_pot[1] # smaller: mith higher G less available energy
    @test df_ET.ET_pot[1] < 0.0002035969 # smaller as without G because less energy available
    @test ismissing(df_ET.LE_pot[2])
    #
    df2 = copy(df)
    df2[!, :VPD] .= 2.0
    df2[!, :Ga_h] .= 0.1
    potential_ET!(df2, PenmanMonteith(), S = df.G; Gs_pot = 0.3)
    @test ncol(df2) == ncol(df)+2+2 # two columns added: ET_pot, LE_pot
    @test nrow(df2) == nrow(df)
    @test ismissing(df2.LE_pot[2])
    df2b = rename!(copy(df2, copycols=false),SA[:LE_pot => :LE])
    surface_conductance!(df2b, InversePenmanMonteith(); S = df.G)
    @test ismissing(df2b.Gs_mol[2])
    @test all(df2b.Gs_mol[1:end .!= 2] .≈ 0.3)
end

@testset "equilibrium_imposed_ET scalars" begin
    # regression from R package example
    Tair,pressure,Rn, VPD, Gs = 20.0,100.0,50.0, 0.5, 0.01
    ET_eq, ET_imp, LE_eq, LE_imp = equilibrium_imposed_ET(Tair,pressure,VPD,Gs, Rn)
    @test ≈(ET_eq, 1.399424e-05; rtol = 1e-5)
    @test ≈(ET_imp, 3.695727e-05; rtol = 1e-5)
    @test ≈(LE_eq, 34.33628; rtol = 1e-5)
    @test ≈(LE_imp, 90.67837; rtol = 1e-5)
    #
    df = DataFrame(Tair = 20.0:1.0:22.0, pressure = 100.0, Rn = 50.0,  VPD = 0.5,  Gs = 0.01)
    ncoldf0 = ncol(df)
    # df_ET = @test_logs (:info,r"G is not provided") (:info,r"S is not provided") equilibrium_imposed_ET(df)
    # @test ncol(df) == ncoldf0
    # @test nrow(df_ET) == nrow(df)
    # @test ≈(first(df_ET).ET_eq, ET_eq)
    # @test ≈(first(df_ET).ET_imp, ET_imp)
    #
    dfm = copy(df)
    equilibrium_imposed_ET!(dfm)
    @test ncol(dfm) == ncoldf0 + 4
    @test nrow(dfm) == nrow(df)
    @test ≈(first(dfm).ET_eq, ET_eq)
    @test ≈(first(dfm).ET_imp, ET_imp)
end


#=
  Changepoint detection
  From https://nishanthu.github.io/articles/ChangePointAnalysis.html

  See ~/jags/change_point.R
  The JAGS model
  model {
     for (i in 1:N) {
       mu0[i] = ifelse(i < cp, mu1, mu2)
       x[i] ~ dnorm(mu0[i], 1)
     }
     mu1 ~ dunif(0, mu)
     mu2 ~ dunif(mu, 10*mu)
     cp ~ dunif(0, N)
   }
"""
out$cp %>% as.vector %>% mean %>% round(1)
[1] 13.5
"""

   Compare with
     - changepoint_coal_miners.jl
     - changepoint_text_messages.jl

=#

# using Memoization
using Turing, StatsPlots, DataFrames
using ReverseDiff, Zygote, Tracker
using Printf
# Turing.setadbackend(:reversediff)
# Turing.setadbackend(:zygote)
# Turing.setadbackend(:tracker)

include("jl_utils.jl")

@model function changepoint(ys)
    N = length(ys)
    mu = mean(ys)

    mu1 ~ Uniform(0, mu)
    mu2 ~ Uniform(mu, 10*mu)
    cp ~ DiscreteUniform(1,N)

    for i in 1:N
        ys[i] ~ i < cp ? Normal(mu1,1) : Normal(mu2,1)
    end

end

# Generated by R:
# > c(rnorm(13, mean=0, sd=0.5), rnorm(20, mean=4, sd=0.5))
# ys = [0.90685440,0.38219559,0.13508408,-0.96054590,0.78553871,0.06791336,-0.09750806,-0.85431057,
#      0.33478539,0.76388689,0.52911522,-0.41226421,1.53302740,
#      5.01118838,4.75164473,3.79149212,3.97544567,4.33985561,4.52511631,4.21723600,
#      4.22733898,3.80925885,3.65504091,3.25267865,3.85339052,3.40136529,4.21462117,
#      3.94033196,4.00900359,4.44148942,4.23449516,4.52032000,4.41335270];

# But we use Julia here...
# Note:
#  - cp should be 14
#  - mu1 should be 0
#  - mu2 should be 4
ys = vcat(rand(Normal(0,0.5),13), rand(Normal(4,0.5),20) )

# Larger instance (130 + 200 elements)
# cp: 131
# mu1: 1
# mu2: 14
# ys = vcat(rand(Normal(1,0.5),130), rand(Normal(14,0.5),200) )

model = changepoint(ys)

num_chns = 4

# chns = sample(model, Prior(), MCMCThreads(), 40_000, num_chns)

# chns = sample(model, MH(), MCMCThreads(), 40_000, num_chns)
chns = sample(model, MH(), 100_000)

# chns = sample(model, SMC(), 10_000)
# chns = sample(model, PG(20), 1_000)

display(chns[[:cp,:mu1,:mu2]]) # We are not interested in :ys

show_var_dist_pct(chns, :cp)
show_var_dist_pct(chns, :mu1)
show_var_dist_pct(chns, :mu2)


using LightGraphs
using GraphPlot
using Plots
using GraphRecipes

g = LightGraphs.grid([20, 1], periodic=true)

n_start = copy(nv(g))
for v in 1:n_start
    add_vertex!(g)
    add_edge!(g, n_start + v, v)
end
n_new = copy(nv(g))
for v in (n_start + 1):n_new
    add_edge!(g, v, ((v + 1) % n_start) + n_start)
end

gplot(g, layout = spring_layout)
graphplot(g, dim=3, markersize=2, curvature_scalar=0.01)


using Test
using TestSetExtensions
using Logging

include("../src/RationalFunctionFields.jl")

using .RationalFunctionFields

# Ask me about this
AA = RationalFunctionFields.AbstractAlgebra
Sing = RationalFunctionFields.Singular
Nemo = RationalFunctionFields.Nemo
Primes = RationalFunctionFields.Primes

@info "Testing started"

@testset "All the tests" begin

    @includetests ARGS

end

@info "All tests OK"


# ------------------------------------------------------------------------------------------
# # 绘图
#
# ## 基础
# Julia 有包括调用 `PyPlot` 在内的、多种不同的绘图方式。
#
# 这里将介绍如何使用 `Plots.jl`。
# 如果还没安装 `Plots.jl`，你需要通过包管理器（package manager）来安装它，Julia会在你第一次导入它的时候将它预编译：
# ------------------------------------------------------------------------------------------

# using Pkg
# Pkg.add("Plots")
using Plots

# ------------------------------------------------------------------------------------------
# `Plots.jl` 的优势之一是可以无缝地切换后端（backends）。
# 在这个 notebook 中，我们将尝试 `gr()` 和 `plotlyjs()` 后端。
#
# 以科学调查之名，我们用这个 notebook 调查一下大概1860到2000年的全球温度和海盗数量的关系。
# ------------------------------------------------------------------------------------------

globaltemperatures = [14.4, 14.5, 14.8, 15.2, 15.5, 15.8]
numpirates = [45000, 20000, 15000, 5000, 400, 17];

# ------------------------------------------------------------------------------------------
# Plots 通过同样的接口可以支持多种后端——指的是实际执行绘制的库。
# 刚开始，我们先使用 GR 后端。通过调用 `gr()` 来选择 GR 后端：
# ------------------------------------------------------------------------------------------

gr()

# ------------------------------------------------------------------------------------------
# 现在我们可以调用如 `plot` 和 `scatter` 的函数来绘制图像。
# ------------------------------------------------------------------------------------------

plot(numpirates, globaltemperatures, label="line")
scatter!(numpirates, globaltemperatures, label="points")

# ------------------------------------------------------------------------------------------
# `scatter!` 函数名后面的 `!` 意味着它是一个原地修改传入变量的函数，表示散点图会被添加到已存在的图像上。
#
# 与此对应的，你可以试试换成 `scatter` 看看会发生什么。
#
# 接着，我们通过 `xlabel!`，`ylabel!` 和 `title!` 函数来给图像加上更多信息。
# ------------------------------------------------------------------------------------------

xlabel!("Number of Pirates [Approximate]")
ylabel!("Global Temperature (C)")
title!("Influence of pirate population on global warming")

# ------------------------------------------------------------------------------------------
# 这看上去仍不对劲。自1860以来海盗数量是减少的，而从左往右看其实时间上是倒序的。
# 我们来把 X 轴反过来，可以更清楚地看到时间顺序下海盗人口是如何导致全球温度的变化！
# ------------------------------------------------------------------------------------------

xflip!()

# ------------------------------------------------------------------------------------------
# 绘图就是这样简单！
#
# 注意：这是一个关于人们是如何经常结合相关性和因果性的笑话。
#
# **不需要修改语法，我们可以在 `unicodeplots()` 后端中绘制同样的图像**
# ------------------------------------------------------------------------------------------

using Pkg; Pkg.add("UnicodePlots")
unicodeplots()

plot(numpirates, globaltemperatures, label="line")
scatter!(numpirates, globaltemperatures, label="points")
xlabel!("Number of Pirates [Approximate]")
ylabel!("Global Temperature (C)")
title!("Influence of pirate population on global warming")

# ------------------------------------------------------------------------------------------
# 注意到第二幅图和第一幅的区别！
# 既然 Jupyter notebook 有酷炫的绘图能力，使用文本符号来绘图就显得有些傻了。
# 但其实这在终端中进行快速简陋的可视化是很实用的。
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## 练习Exercises
#
# ### 8.1
# 已有
# ```julia
# x = -10:10
# ```
# 要绘制 y 随 x 变化曲线，其中 $y = x^2$。你可能需要先把后端切换回去。
# ------------------------------------------------------------------------------------------

x = -10:10

# 在此作答

# ------------------------------------------------------------------------------------------
# ### 8.2
# 执行以下代码
# ------------------------------------------------------------------------------------------

p1 = plot(x, x)
p2 = plot(x, x.^2)
p3 = plot(x, x.^3)
p4 = plot(x, x.^4)
plot(p1, p2, p3, p4, layout = (2, 2), legend = false)

# ------------------------------------------------------------------------------------------
# 然后使用 `p1`、`p2`、`p3` 和 `p4` 作为子图创建一个四行一列的图。
#
# 提示：你可以参考 Plots 的帮助文档 [GR - Plots](http://docs.juliaplots.org/latest/examples/gr/#magic-
# grid-argument)
# ------------------------------------------------------------------------------------------

# 在此作答


using OrdinaryDiffEq, DiffEqDevTools, Sundials, ModelingToolkit, ODEInterfaceDiffEq,
      Plots, DASSL, DASKR
using LinearAlgebra

ModelingToolkit.@parameters begin
      k₁=18.7
      k₂=0.58
      k₃=0.09
      k₄=0.42
      kbig=34.4
      kla=3.3
      ks=115.83
      po2=0.9
      hen=737
end

@variables begin
      t
      y₁(t)  = 0.444
      y₂(t)  = 0.00123
      y₃(t)  = 0.0
      y₄(t)  = 0.007
      y₅(t)  = 1.0
      y₆(t)  = 115.83*0.444*0.007 # ks*y₁*y₄
end

D = Differential(t)

r₁  = k₁ * (y₁^4.)*sqrt(abs(y₂))
r₂  = k₂ * y₃ * y₄
r₃  = k₂/kbig * y₁ * y₅
r₄  = k₃*y₁*(y₄^2)
r₅  = k₄*(y₆^2)*sqrt(abs(y₂))
fin = kla*(po2/hen-y₂)

eqs = [
      D(y₁) ~ -2. * r₁ + r₂ - r₃ - r₄
      D(y₂) ~ -0.5 * r₁ - r₄ - 0.5*r₅ + fin
      D(y₃) ~ r₁ - r₂ + r₃
      D(y₄) ~ -r₂ + r₃ - 2. * r₄
      D(y₅) ~ r₂ - r₃ + r₅
      0. ~ ks * y₁ * y₄ - y₆
]

ModelingToolkit.@named sys = ModelingToolkit.ODESystem(eqs)

tspan = (0.0, 180.0)
mmprob = ODEProblem(sys, [], tspan)
sol = solve(mmprob, Rodas4(),abstol=1/10^14,reltol=1/10^14)

odaeprob = ODAEProblem(structural_simplify(sys),[],tspan)
ode_ref_sol = solve(odaeprob,CVODE_BDF(),abstol=1/10^14,reltol=1/10^14);

du = mmprob.f(mmprob.u0,mmprob.p,0.0)
du0 = D.(states(sys)) .=> du
daeprob = DAEProblem(sys,du0,[],tspan)
ref_sol = solve(daeprob,IDA(),abstol=1/10^14,reltol=1/10^14);

probs = [mmprob,daeprob,odaeprob]
refs = [ref_sol,ref_sol,ode_ref_sol];

plot(ode_ref_sol, vars = [y₁,y₂,y₃,y₄,y₅,y₆])

abstols = 1.0 ./ 10.0 .^ (5:8)
reltols = 1.0 ./ 10.0 .^ (1:4);
setups = [Dict(:prob_choice => 1, :alg=>Rosenbrock23()),
          Dict(:prob_choice => 1, :alg=>Rodas4()),
          #Dict(:prob_choice => 1, :alg=>FBDF()),
          Dict(:prob_choice => 1, :alg=>QNDF()),
          Dict(:prob_choice => 1, :alg=>rodas()),
          Dict(:prob_choice => 1, :alg=>radau()),
          Dict(:prob_choice => 1, :alg=>RadauIIA5()),
          Dict(:prob_choice => 2, :alg=>DFBDF()),
          Dict(:prob_choice => 2, :alg=>IDA()),
]

wp = WorkPrecisionSet(probs,abstols,reltols,setups;
                      save_everystep=false,appxsol=refs,maxiters=Int(1e5),numruns=10)
plot(wp)

abstols = 1.0 ./ 10.0 .^ (6:8)
reltols = 1.0 ./ 10.0 .^ (2:4);
setups = [Dict(:prob_choice => 1, :alg=>Rosenbrock23()),
          Dict(:prob_choice => 1, :alg=>Rodas4()),
          Dict(:prob_choice => 2, :alg=>IDA()),
          Dict(:prob_choice => 3, :alg=>Rosenbrock23()),
          Dict(:prob_choice => 3, :alg=>Rodas4()),
          Dict(:prob_choice => 3, :alg=>CVODE_BDF()),
          Dict(:prob_choice => 3, :alg=>TRBDF2()),
          Dict(:prob_choice => 3, :alg=>KenCarp4()),
          ]
wp = WorkPrecisionSet(probs,abstols,reltols,setups;
                      save_everystep=false,appxsol=refs,maxiters=Int(1e5),numruns=10)
plot(wp)

abstols = 1.0 ./ 10.0 .^ (6:8)
reltols = 1.0 ./ 10.0 .^ (3:5);
setups = [Dict(:prob_choice => 3, :alg=>Rosenbrock23()),
          Dict(:prob_choice => 3, :alg=>Rodas4()),
          Dict(:prob_choice => 2, :alg=>IDA()),
          Dict(:prob_choice => 2, :alg=>DASSL.dassl()),
          Dict(:prob_choice => 2, :alg=>DASKR.daskr()),
          ]
wp = WorkPrecisionSet(probs,abstols,reltols,setups;
                      save_everystep=false,appxsol=refs,maxiters=Int(1e5),numruns=10)
plot(wp)

abstols = 1.0 ./ 10.0 .^ (5:8)
reltols = 1.0 ./ 10.0 .^ (1:4);
setups = [Dict(:prob_choice => 1, :alg=>Rosenbrock23()),
          Dict(:prob_choice => 1, :alg=>Rodas4()),
          #Dict(:prob_choice => 1, :alg=>FBDF()), # too slow
          Dict(:prob_choice => 1, :alg=>QNDF()),
          Dict(:prob_choice => 1, :alg=>rodas()),
          Dict(:prob_choice => 1, :alg=>radau()),
          Dict(:prob_choice => 1, :alg=>RadauIIA5()),
          Dict(:prob_choice => 2, :alg=>DFBDF()),
          Dict(:prob_choice => 2, :alg=>IDA()),
          ]
wp = WorkPrecisionSet(probs,abstols,reltols,setups;error_estimate = :l2,
                      save_everystep=false,appxsol=refs,maxiters=Int(1e5),numruns=10)
plot(wp)

abstols = 1.0 ./ 10.0 .^ (6:8)
reltols = 1.0 ./ 10.0 .^ (2:4);
setups = [Dict(:prob_choice => 1, :alg=>Rosenbrock23()),
          Dict(:prob_choice => 1, :alg=>Rodas4()),
          Dict(:prob_choice => 2, :alg=>IDA()),
          Dict(:prob_choice => 3, :alg=>Rosenbrock23()),
          Dict(:prob_choice => 3, :alg=>Rodas4()),
          Dict(:prob_choice => 3, :alg=>CVODE_BDF()),
          Dict(:prob_choice => 3, :alg=>TRBDF2()),
          Dict(:prob_choice => 3, :alg=>KenCarp4()),
          ]
wp = WorkPrecisionSet(probs,abstols,reltols,setups;error_estimate = :l2,
                      save_everystep=false,appxsol=refs,maxiters=Int(1e5),numruns=10)
plot(wp)

abstols = 1.0 ./ 10.0 .^ (7:12)
reltols = 1.0 ./ 10.0 .^ (4:9)

setups = [Dict(:prob_choice => 1, :alg=>Rodas5()),
          Dict(:prob_choice => 3, :alg=>Rodas5()),
          Dict(:prob_choice => 1, :alg=>Rodas4()),
          Dict(:prob_choice => 3, :alg=>Rodas4()),
          #Dict(:prob_choice => 1, :alg=>FBDF()),
          Dict(:prob_choice => 1, :alg=>QNDF()),
          Dict(:prob_choice => 1, :alg=>rodas()),
          Dict(:prob_choice => 1, :alg=>radau()),
          Dict(:prob_choice => 1, :alg=>RadauIIA5()),
          Dict(:prob_choice => 2, :alg=>DFBDF()),
          Dict(:prob_choice => 2, :alg=>IDA()),
          ]

wp = WorkPrecisionSet(probs,abstols,reltols,setups;
                      save_everystep=false,appxsol=refs,maxiters=Int(1e5),numruns=10)
plot(wp)

wp = WorkPrecisionSet(probs,abstols,reltols,setups;error_estimate = :l2,
                      save_everystep=false,appxsol=refs,maxiters=Int(1e5),numruns=10)
plot(wp)

abstols = 1.0 ./ 10.0 .^ (7:9)
reltols = 1.0 ./ 10.0 .^ (4:6)

setups = [Dict(:prob_choice => 1, :alg=>Rodas5()),
          Dict(:prob_choice => 3, :alg=>Rodas5()),
          Dict(:prob_choice => 1, :alg=>Rodas4()),
          Dict(:prob_choice => 3, :alg=>Rodas4()),
          Dict(:prob_choice => 2, :alg=>DFBDF()),
          Dict(:prob_choice => 2, :alg=>IDA()),
          Dict(:prob_choice => 2, :alg=>DASKR.daskr()),
          ]
gr()
wp = WorkPrecisionSet(probs,abstols,reltols,setups;
                      save_everystep=false,appxsol=refs,maxiters=Int(1e5),numruns=10)
plot(wp)

using SciMLBenchmarks
SciMLBenchmarks.bench_footer(WEAVE_ARGS[:folder],WEAVE_ARGS[:file])