using Integrals
const P = Integrals
using Test
using StaticArrays
using LinearAlgebra
using Random
using Setfield
using Cuba: vegas
using HCubature

struct Constant{T} <: Function
    value::T
end

(f::Constant)(_) = f.value

@test integral === ∫

function isconsistent(truth, est; nstd=6, kw_approx...)
    val = est.value
    Δ = nstd * est.std
    if ≈(val, truth; kw_approx...)
        true
    elseif (truth - Δ <= val <= truth + Δ)
        true
    else
        @show truth
        @show est
        @show kw_approx
        false
    end
end

ALGS = [Vegas(), MCVanilla(), CubaAlg(vegas), HCubatureAlg()]

@testset "RNG Reproducibility" begin
    for alg in [Vegas(), MCVanilla()]
        @set! alg.rng = MersenneTwister(1)
        res1 = ∫(cos, (0,1), alg)
        @set! alg.rng = MersenneTwister(1)
        res2 = ∫(cos, (0,1), alg)
        @test res1 === res2
        res3 = ∫(cos, (0,1), alg)
        @test !(res3 === res2)
    end
end

@testset "Domain" begin
    dom = Domain((1f0, 2f0))
    @test P.pointtype(typeof(dom)) === SVector{1, Float32}
    @test P.volume(dom) === 1f0
end

@testset "zero function" begin
    f = Constant(0.)
    for alg in ALGS
        for dim in 1:4
            lower = @SVector randn(dim)
            upper = lower + @SVector rand(dim)
            dom = Domain(lower, upper)
            est = ∫(f, dom, alg)
            @test est.value ≈ 0 atol=100eps(Float64)
        end
    end
end

@testset "exotic types $(typeof(alg))" for alg in [
        MCVanilla(10),
        Vegas(100),
        CubaAlg(vegas),
       ]
    est = ∫(identity, (0f0, 1f0), alg)
    @test typeof(est.value) === Float32
    @test typeof(est.std) === Float32

    est = ∫(_ -> true, (0f0, 1f0), alg)
    @test typeof(est.value) === Float64
    @test typeof(est.std) === Float64

    est = ∫(identity, (0, 1), alg)
    @test typeof(est.value) === Float64
    @test typeof(est.std) === Float64

    est = ∫(identity, (big"0", big"1"), alg)
    @test typeof(est.value) === BigFloat
    @test typeof(est.std) === BigFloat

    v = @SVector[1.,2.]
    est = ∫(_ -> v, (0,1), alg)
    @test typeof(est.value) === typeof(v)
    @test typeof(est.std)   === typeof(est.value)
end

@testset "Inferred $(typeof(alg))" for alg in [MCVanilla(), Vegas(),
                                               CubaAlg(vegas), HCubatureAlg()]
    f_scalar = x -> x[1]^2
    f_vec    = x -> @SVector[1.,2,3]
    dom = Domain((-pi, 1))

    @inferred ∫(f_scalar, dom, alg)
    # @inferred ∫(f_vec, dom, alg)
end

@testset "constant $(typeof(alg))" for alg in ALGS
    for dim in 1:4
        for _ in 1:3
            lower = @SVector randn(dim)
            upper = lower + @SVector rand(dim)
            val = randn()
            f = Constant(val)
            dom = Domain(lower, upper)
            est = ∫(f, dom, alg)
            @test !isnan(est.value)
            vol = prod(upper .- lower)
            truth = val * vol
            @test isconsistent(truth, est)
            @test est.std < 1e-3
        end
    end
end

@testset "∫x^2" begin
    est_vegas = ∫(x->x^2, (0,1), Vegas(10000))
    @test isconsistent(1/3, est_vegas)
    @test_broken est_vegas.std < 1e-3

    est_vanilla = ∫(x->x^2, (0,1), MCVanilla(1000))
    @test isconsistent(1/3, est_vanilla)
    @test est_vanilla.std < 1e-2
end

@testset "∫(f, (a,b), $(typeof(alg)))" for alg in [
        MCVanilla(10^4),
        Vegas(10^4),
       ]

    funs = [
        (f = x -> x^2, F = x -> x^3/3),
        (f = cos, F = sin),
        let
            a = randn()
            (f = _ -> a, F = x -> a*x)
        end
    ]

    for (f,F) in funs
        for _ in 1:10
            a = 10randn()
            b = a + 10rand()
            est = ∫(f, (a,b), alg)
            truth = F(b) - F(a)
            @test isconsistent(truth, est)
        end
    end
end

@testset "Fubini $(typeof(alg))" for alg in [
    MCVanilla(10^4),
    Vegas(10^4)]
    for _ in 1:10
        a1,a2 = randn(2)
        b1 = a1 + 10rand()
        b2 = a2 + 10rand()
        dom1 = Domain((a1, b1))
        dom2 = Domain((a2, b2))
        dom = Domain(((a1, b1), (a2, b2)))
        f1 = x -> cos(x[1])
        F1 = x -> sin(x[1])
        f2 = x -> x[1]^2
        F2 = x -> x[1]^3/3
        f  = v -> f1(v[1]) * f2(v[2])
        est1 = ∫(f1, dom1, alg)
        est2 = ∫(f2, dom2, alg)
        est  = ∫(f , dom , alg)
        @test isconsistent(est1.value*est2.value, est)
    end
end

@testset "Ball $(typeof(alg))" for alg in [
    MCVanilla(10^4),
    Vegas(10^4, rng=MersenneTwister(1))]

    f(x) = norm(x) < 1 ? 1. : 0.
    est = ∫(f, (-2,2), alg)
    @test isconsistent(2,est)

    est = ∫(f, ((-1,1),(-1,1)), alg)
    @test isconsistent(π, est)

    est = ∫(f, ((-1,1),(-1,1),(-1,1)), alg)
    @test isconsistent(4π/3, est)
end

@testset "std bernoulli" begin
    for _ in 1:10
        p = 0.1 + 0.8*rand()
        f = x -> 0 <= x <= p

        neval = 1000
        true_value = p
        true_std = sqrt(p*(1-p)/neval)

        est = ∫(f, (0,1), MCVanilla(neval))
        @test est.std ≈ true_std rtol=0.2
        @test isconsistent(true_value, est)

        est = ∫(f, (0,1), Vegas(neval))
        if est.std < true_std
            @test est.std < true_std
        else
            @test_broken est.std < true_std
        end
        @test isconsistent(true_value, est)
    end
end


######################################################
# EXEMPLO SIMPLES DE USO DO PACOTE FLUX
# para treinamento de uma rede neural pequena para
# o problema de classificação de caracteres numéricos
# da MNIST.
#
# (adaptado do exemplo do "model-zoo" https://github.com/FluxML/model-zoo/blob/master/vision/mlp_mnist/mlp_mnist.jl)
#
# Autor: Leonardo D. Secchin
# Data : 31/08/2021
# leonardosecchin.github.io/topicospo/
#
# Execução:
#   treinar()
# ou
#   treinar(ninter = 30, eta = 1e-2, batchsize = 256, nepochs = 15)
######################################################

using Plots, Statistics

# carrega Flux
using Flux

# exporta estruturas internas do Flux para ambiente do usuário
# (assim não precisa escrever Flux.ESTRUTURA)
using Flux.Data: DataLoader
using Flux: onehotbatch, onecold, @epochs
using Flux.Losses: mse

# carrega pacote contendo base de dados MNIST
using MLDatasets

##########################
# CARREGA DATASET MNIST
##########################
function carrega_dados(batchsize)

    # carrega/baixa MNIST
    #   (xtrain,ytrain): pares entrada-saída para treino da rede
    #   (xtest,ytest)  : pares entrada-saída para teste da rede treinada
    xtrain, ytrain = MLDatasets.MNIST.traindata(Float32)
    xtest,  ytest  = MLDatasets.MNIST.testdata(Float32)

    # Reorganiza imagens para estrutura Flux
    xtrain = Flux.flatten(xtrain)
    xtest = Flux.flatten(xtest)

    # Codifica as saídas (labels '0' a '9') em vetores de 0's e 1's de tamanho 10 (one-hot-encode)
    #   Para visualizar o resultado, execute por exemplo  onehotbatch([1 2 3], 0:9)
    ytrain, ytest = onehotbatch(ytrain, 0:9), onehotbatch(ytest, 0:9)

    # Divide os dados de treinamento em mini-lotes de tamanho batchsize
    #   shuffle=true diz que os dados são embaralhados antes de cada época
    #   (veja pgs 8 e 13 de https://leonardosecchin.github.io/files/topicospo/ML_4-sg.pdf)
    #   DataLoader é uma estrutura própria do pacote Flux.jl. Execute ?DataLoader para detalhes.
    train_loader = DataLoader((xtrain, ytrain), batchsize=batchsize, shuffle=true)

    # Divide os dados de teste em mini-lotes
    test_loader = DataLoader((xtest, ytest), batchsize=batchsize)

    # retorna os dados de treino e teste
    return train_loader, test_loader
end

##########################
# MONTA A REDE NEURAL
##########################
function constroi_rede(ninter)
    # Chain e Dense são funções do pacote Flux para criação da rede neural
    #
    #   Dense(n[l-1],n[l], ativação) cria uma camada tradicional (como vista em aula)
    #   com n[l-1] neurônios entrando, n[l] saindo, e onde o fluxo é guiado por
    #   a[l] = a.(W[l] * a[l-1] .+ bias[l])
    #   Pode-se indicar diferentes funções de ativação: sigmoid, relu ...
    #   Veja ?Dense e a ajuda do Flux para mais detalhes
    #
    #   Chain agrega as camadas em um único objeto

    # Abaixo, uma rede com a seguinte estrutura é criada:
    #   camada de entrada com 784=28*28 neurônios
    #   ninter neurônios na segunda camada, todos com ativação sigmoid
    #   camada de saída com 10 neurônios, todos com ativação sigmoid
    #
    # Pesos e vieses são iniciados randomicamente
    return Chain(
 	        Dense(784, ninter, sigmoid),
            Dense(ninter, 10, sigmoid)
            )
end

##########################
# AVALIA RISCO EMPÍRICO (feedforward) (sobre todos os dados de treinamento)
#
# Algumas funções de perda implementadas no Flux:
#   mse (+ usada aula) : (ŷ .- y) .^ 2
#   msle               : log.(ŷ .+ ϵ) .- log.(y .+ ϵ)) .^ 2
#   crossentropy       : -sum(y .* log.(ŷ .+ ϵ))
#   binarycrossentropy : -y * log(ŷ + ϵ) - (1 - y) * log(1 - ŷ + ϵ)
#
# Legenda
#   y = a[L] é o rótulo do dado de entrada
#   ŷ é a resposta da rede
#
# Muitas outras funções estão disponíveis. Veja
# https://fluxml.ai/Flux.jl/stable/models/losses
#
# Obs: é possível implementar sua própria função de perda.
# Veja exemplos do model-zoo.
##########################
function loss(data_loader, rede)
    acc = 0
    ls = 0.0f0
    num = 0

    # percorre todos os dados em data_loader
    for (x, y) in data_loader
        # pares entrada-saída do treinamento
        x, y = cpu(x), cpu(y)

        # respostas da rede (feedforward)
        yy = rede(x)

        # soma perda dos dados calculados
        ls  += mse(yy, y, agg=sum)

        # calcula taxa de acerto (número de respostas corretas)
        # Aqui, "onecold" decodifica o dado do vetor de 0's e 1's para rótulo de '0' a '9'
        acc += sum(onecold(cpu(yy)) .== onecold(cpu(y)))

        # conta dados de treinamento calculados
        num += size(x, 2)
    end

    # retorna média das perdas (risco empírico) e percentual de acertos
    return ls/num, acc/num
end

##########################
# FUNÇÃO PRINCIPAL
##########################
function treinar(; ninter = 32, eta = 4e-1, batchsize = 256, nepochs = 20)

    # PARÂMETROS
    # ninter    : número de neurônios na camada intermediária
    # eta       : taxa de aprendizagem
    # batchsize : tamanho do mini-lote
    # nepochs   : número de épocas

    # carrega base de dados MNIST e cria mini-lotes
    train_loader, test_loader = carrega_dados(batchsize)

    # constrói rede
    # Obs: "cpu" refere-se à execução em CPU normal.
    # Flux suporta processamento por GPU, veja documentação/exemplos.
    rede = constroi_rede(ninter) |> cpu

    # variáveis da rede a serem otimizadas
    wb = Flux.params(rede)

    ##############################################
    ######### Algoritmo para treinamento #########
    ##############################################
    # Descent(η = 0.1)              : gradiente estocástico básico (SGD)
    # Momentum(η = 0.01, ρ = 0.9)   : SGD com momento
    # RMSProp(η = 0.001, ρ = 0.9)
    # ADAM(η = 0.001, β::Tuple = (0.9, 0.999))
    # ADAGrad(η = 0.1)
    # ADADelta(ρ = 0.9)
    #
    # *** Não citados em aula: ***
    # Nesterov(η = 0.001, ρ = 0.9)  : método acelerado de Nesterov
    # RADAM(η = 0.001, β::Tuple = (0.9, 0.999)): Rectified ADAM
    # AdaMax(η = 0.001, β::Tuple = (0.9, 0.999))
    # AMSGrad(η = 0.001, β::Tuple = (0.9, 0.999))
    # NADAM(η = 0.001, β::Tuple = (0.9, 0.999))
    # ADAMW(η = 0.001, β::Tuple = (0.9, 0.999), decay = 0)
    # OADAM(η = 0.0001, β::Tuple = (0.5, 0.9))
    # AdaBelief(η = 0.001, β::Tuple = (0.9, 0.999))
    #
    # Legenda
    #   η: taxa de aprendizagem fixa (SGD) ou "de referência" (veja Adagrad, pg 16 de https://leonardosecchin.github.io/files/topicospo/ML_4-sg.pdf)
    #   ρ: balanço do momento (β da pg 20 de https://leonardosecchin.github.io/files/topicospo/ML_4-sg.pdf)
    #   β: parâmetros de momento (não visto com detalhes em aula)
    # consulte https://fluxml.ai/Flux.jl/stable/training/optimisers/ para detalhes

    opt = Descent(eta)
#     opt = ADAM(eta)

    # medidas para montagem de gráficos
    train_loss_hist = []
    train_acc_hist  = []
    test_loss_hist  = []
    test_acc_hist   = []

    ###############################
    ######### Treinamento #########
    ###############################

    println("Treinando a rede neural...")

    # loop épocas
    for epoch in 1:nepochs

        # percorre os mini-lotes de treinamento (época)
        for (x, y) in train_loader

            # dados de treinamento do mini-lote
            x, y = cpu(x), cpu(y)

            # computa gradiente no ponto wb corrente
            # (backpropagation nos dados do mini-lote atual)
            g = gradient(() -> mse(rede(x), y), wb)

            # realiza uma iteração do método, atualizando pesos e vieses da rede
            Flux.Optimise.update!(opt, wb, g)

        end

        # calcula risco empírico sobre todos os dados de treinamento
        train_loss, train_acc = loss(train_loader, rede)

        # Calcula risco empírico nos dados de teste
        # (somente para impressão na tela).
        # Poderia ser usado para parar prematuramente (dados de validação).
        test_loss,  test_acc  = loss(test_loader, rede)

        # imprimi dados da época
        println()
        println("Época: $epoch")
        println("  Dados treinamento:  risco empir = $train_loss ,  % acertos = $train_acc")
        println("  Dados teste      :  risco empir = $test_loss ,  % acertos = $test_acc")

        # guarda medidas para montagem de gráficos
        append!(train_loss_hist, train_loss)
        append!(train_acc_hist, train_acc)
        append!(test_loss_hist, test_loss)
        append!(test_acc_hist, test_acc)
    end

    # Plota gráficos
    fig_loss = plot(title="Perda", xlabel="épocas", ylabel="", fontfamily="Computer Modern")
    fig_loss = plot!(1:length(train_loss_hist), train_loss_hist, lw=1, label="Dados treinamento (risco empírico)")
    fig_loss = plot!(1:length(test_loss_hist), test_loss_hist, lw=1, label="Dados teste")
    savefig(fig_loss, "fig_loss.pdf")

    fig_acc = plot(title="% acerto", xlabel="épocas", ylabel="% acerto", fontfamily="Computer Modern", legend=:bottomright)
    fig_acc = plot!(1:length(train_acc_hist), train_acc_hist, lw=1, label="Dados treinamento")
    fig_acc = plot!(1:length(test_acc_hist), test_acc_hist, lw=1, label="Dados teste")
    savefig(fig_acc, "fig_acc.pdf")
end


"""

## fsparm!

Function fsparm assembles fin_el matrices into a Julia sparse
global stiffness matrix.

### Method
```julia
fsparm!(gsm, g, km)
```

### Arguments
```julia
* gsm::SparseArrays{Float64, Float64}   : Sparse stiffnes matrix (Updated)
* g::Vector{Int}                      : Global coordinate vector.
* km::Matrix{Float64}                   : Stiffness matrix.
```
"""
function fsparm!(gsm, g, km)
  #println("g = $g")
  ndof = size(g, 1)
  #println("ndof = $ndof")
  for i in 1:ndof
    k = g[i]
    #println("g[$i] = k = $k")
    if k !== 0
      for j in 1:ndof
        #println("i = $i, j = $j, g[$j] = $(g[j])")
        if g[j] !== 0
          iw = k - g[j]
          #println("iw = $iw")
          if iw >= 0
            if g[i] == g[j]
              gsm[g[i], g[j]] += km[i, j]
            else
              gsm[g[i], g[j]] += km[i, j]
              gsm[g[j], g[i]] += km[i, j]
            end
            #println("gsm[$(g[i]), $(g[j])] += $(km[i,j])")
            #full(gsm) |> display
            #println()
          end
        end
      end
    end
  end
end


### A Pluto.jl notebook ###
# v0.14.7

using Markdown
using InteractiveUtils

# ╔═╡ a11a5250-c5a3-11eb-2cb2-65a0eb1d220d
md"""
### Test Code for Module LogReg
"""

# ╔═╡ cff452a8-1123-4228-bdba-a6613e02c5bd
module script include("script.jl") end

# ╔═╡ a54d8027-2db7-4b0e-8794-f614e6b5ad50
LogReg = script.LogReg

# ╔═╡ 65be2db3-70fb-4cc3-ae2d-d53d9c94740f
import Plots

# ╔═╡ bfefa9b3-ecfb-496c-9176-9882672541d8
import Random

# ╔═╡ 394a6ebc-7544-499b-ad97-7b4452b1555e
begin
# generate linearly separated random data
X_data = Random.rand(0.0:0.5:100.0, (800, 2)) # first column X-axis, second column Y-axis
Y_data = Array{Int64}(undef, size(X_data)[1])
linear_func(x) = 2 * x - 3
for i = 1:size(X_data)[1]
    if X_data[i, 2] > linear_func(X_data[i, 1]) + randn()*20
        Y_data[i] = 0
    else
        Y_data[i] = 1
    end
end
size(X_data), size(Y_data)
end

# ╔═╡ 389887b7-2f1e-4dda-90c0-40f42f1eb5e7
# scale data
X_data_scaled = LogReg.scale(X_data);

# ╔═╡ 649f29fe-9f96-4be3-bff7-dfbbf0c1700f
let
# plot the data
plot_X1 = [X_data[i, 1] for i in 1:(size(X_data)[1]) if Y_data[i] == 0]
plot_Y1 = [X_data[i, 2] for i in 1:(size(X_data)[1]) if Y_data[i] == 0]
plot_X2 = [X_data[i, 1] for i in 1:(size(X_data)[1]) if Y_data[i] == 1]
plot_Y2 = [X_data[i, 2] for i in 1:(size(X_data)[1]) if Y_data[i] == 1]
plot_X3 = [i for i in 0:100]
plot_Y3 = [linear_func(m) for m in plot_X3]
Plots.gr()
Plots.scatter(plot_X1, plot_Y1, leg=false, c="red", background_color=Plots.RGB(0.2, 0.2, 0.2))
Plots.scatter!(plot_X2, plot_Y2, leg=false, c="blue")
Plots.plot!(plot_X3, plot_Y3, leg=false, c="orange", linewidth=5)
Plots.xlims!((0.0, 100.0))
Plots.ylims!((0.0, 100.0))
end

# ╔═╡ c7df3c2c-54d8-4177-ba02-4d0098087cf6
let
# run logistic regression
weights = LogReg.train(X_data_scaled, Y_data, learning_rate=0.1, max_iter=10, early_stop=false)
predictions = LogReg.predict(X_data_scaled, weights)
string("Accuracy: ", LogReg.accuracy(predictions, Y_data))
end

# ╔═╡ c4cdf49e-3929-4393-a9fc-5db66cd2a812
let
# increase iteractions
weights = LogReg.train(X_data_scaled, Y_data, learning_rate=0.1, max_iter=100, early_stop=false)
predictions = LogReg.predict(X_data_scaled, weights)
string("Accuracy: ", LogReg.accuracy(predictions, Y_data))
end

# ╔═╡ 5e698890-39fb-47dc-a975-53d6c8e716a6
let
# increase iteractions
weights = LogReg.train(X_data_scaled, Y_data, learning_rate=0.1, max_iter=1000, early_stop=false)
predictions = LogReg.predict(X_data_scaled, weights)
string("Accuracy: ", LogReg.accuracy(predictions, Y_data))
end

# ╔═╡ 6bb2b4a7-2f56-4d42-a5ac-29333a69abba
let
# increase iteractions
weights = LogReg.train(X_data_scaled, Y_data, learning_rate=0.1, max_iter=10000, early_stop=false)
predictions = LogReg.predict(X_data_scaled, weights)
string("Accuracy: ", LogReg.accuracy(predictions, Y_data))
end

# ╔═╡ 50f6b449-6e24-48c1-8d33-dc13c1fb5b9c
let
# plot comparison graph
plot_X1 = [X_data[i, 1] for i in 1:(size(X_data)[1]) if Y_data[i] == 0]
plot_Y1 = [X_data[i, 2] for i in 1:(size(X_data)[1]) if Y_data[i] == 0]
plot_X2 = [X_data[i, 1] for i in 1:(size(X_data)[1]) if Y_data[i] == 1]
plot_Y2 = [X_data[i, 2] for i in 1:(size(X_data)[1]) if Y_data[i] == 1]
plot_X3 = [i for i in 0:100]
plot_Y3 = [linear_func(m) for m in plot_X3]
Plots.gr()
Plots.scatter(plot_X1, plot_Y1, c="red", background_color=Plots.RGB(0.2, 0.2, 0.2), label="")
Plots.scatter!(plot_X2, plot_Y2, c="blue", label="")
Plots.plot!(plot_X3, plot_Y3, c="orange", linewidth=5, label="Actual")
accuracy = Array{Float64}(undef, 4)
for (i, iternum) in enumerate([10, 100, 1000, 10000])
    weights = LogReg.train(X_data_scaled, Y_data, learning_rate=0.1, max_iter=iternum)
    accuracy[i] = LogReg.accuracy(LogReg.predict(X_data_scaled, weights), Y_data)
    @assert size(weights)[1] == 3
    plot_Y3_ = -(weights[3] .+ (weights[1] .* plot_X3)) ./ weights[2]
    Plots.plot!(plot_X3, plot_Y3_, palette=:lightrainbow, linewidth=3, label="iter=$iternum", alpha=0.5)
end
Plots.xlims!((0.0, 100.0))
Plots.ylims!((0.0, 100.0))
end

# ╔═╡ Cell order:
# ╟─a11a5250-c5a3-11eb-2cb2-65a0eb1d220d
# ╠═cff452a8-1123-4228-bdba-a6613e02c5bd
# ╠═a54d8027-2db7-4b0e-8794-f614e6b5ad50
# ╠═65be2db3-70fb-4cc3-ae2d-d53d9c94740f
# ╠═bfefa9b3-ecfb-496c-9176-9882672541d8
# ╠═394a6ebc-7544-499b-ad97-7b4452b1555e
# ╠═389887b7-2f1e-4dda-90c0-40f42f1eb5e7
# ╠═649f29fe-9f96-4be3-bff7-dfbbf0c1700f
# ╠═c7df3c2c-54d8-4177-ba02-4d0098087cf6
# ╠═c4cdf49e-3929-4393-a9fc-5db66cd2a812
# ╠═5e698890-39fb-47dc-a975-53d6c8e716a6
# ╠═6bb2b4a7-2f56-4d42-a5ac-29333a69abba
# ╠═50f6b449-6e24-48c1-8d33-dc13c1fb5b9c


"""
Beta Uniform Mixture (BUM) Model

# Arguments

- π0 : Contributing fraction of the uniform distribution to the full model
- α, β : Parameters of the Beta distribution, Float64, default: 0.5, 3.0

# Return values

`MixtureModel`, as defined in the `Distributions` package, composed of

- a uniform distribution in the interval [0, 1], with weight/prior π₀
- a Beta distribution with parameters α and β, with weight/prior 1-π₀

# Examples

```jldoctest
julia> bum = BetaUniformMixtureModel(0.2, 0.5, 1.0);

julia> using Distributions

julia> pdf.(bum, 0.2:0.2:1.0)
5-element Array{Float64,1}:
 1.09443
 0.832456
 0.716398
 0.647214
 0.6

```

# References

Pounds, S., and Morris, S.W. (2003). Estimating the occurrence of false
positives and false negatives in microarray studies by approximating and
partitioning the empirical distribution of p-values. Bioinformatics 19,
1236–1242.

"""
function BetaUniformMixtureModel(π0::AbstractFloat, α::AbstractFloat = 0.5, β::AbstractFloat = 1.0)
    isin(π0, 0, 1) || throw(DomainError("π0 must be in [0, 1]"))
    bum = MixtureModel([Beta(α, β), Uniform()], [1-π0, π0])
    return bum
end


using JuMP, VariationalInequality
using Test

# https://cdr.lib.unc.edu/indexablecontent/uuid:778ca632-74ca-4858-8c3c-6dcfc7e6e703
# Example 3.9. I created this simple two dimenional example for testing.
m = VIPModel()

@variable(m, x[1:2])

@constraint(m, x[1]^2 + x[2]^2 <= 1)

@mapping(m, F1, 3x[1] + x[2] - 5)
@mapping(m, F2, 2x[1] + 5x[2] - 3)

F = [F1, F2]
@innerproduct(m, F, x)

sol1, Fval1, gap1 = solveVIP(m, algorithm=:extra_gradient, max_iter=1000, step_size=0.1)

@show sol1
@show Fval1
@show gap1

# (0.9890, 0.1480)


using DataFrames
using CSV
using Lathe.preprocess:TrainTestSplit
using GLM
using StatsBase
using StatsModels
using ROC
using Plots

data = DataFrame(CSV.File("tacke1000a.csv"))

dataTrain, dataTest = TrainTestSplit(data,.80)

fm = @formula(boja ~ x + y)

logisticRegeressor = glm(fm,dataTrain, Binomial(), ProbitLink())

dataPredictBoja = predict(logisticRegeressor, dataTest)

dataPredictTestClass = zeros(length(dataPredictBoja))

for i in 1:length(dataPredictBoja)
    if dataPredictBoja[i] < 0.5
        dataPredictTestClass[i] = 0;
    else
        dataPredictTestClass[i] = 1;
    end
end

FPtest = 0
FNtest = 0
TPtest = 0
TNtest = 0

for i in 1:length(dataPredictTestClass)
    if dataTest.boja[i] == 0 && dataPredictTestClass[i] == 0
        global TNtest +=1
    elseif dataTest.boja[i] == 1 && dataPredictTestClass[i] == 1
        global TPtest +=1
    elseif dataTest.boja[i] == 1 && dataPredictTestClass[i] == 0
        global FNtest +=1
    elseif dataTest.boja[i] == 0 && dataPredictTestClass[i] == 1
        global FPtest +=1
    end
end

accuracyTest = (TPtest + TNtest) / (TPtest + TNtest + FPtest + FNtest)

sensitivity = TPtest / (TPtest + FNtest)

specificity = TNtest / (TNtest + FPtest)

println("TP = $TPtest, FP = $FPtest, TN = $TNtest, FN = $FNtest")
println("accuracy = $accuracyTest\n sensitivity = $sensitivity\n specificity = $specificity\n")

rocTest = ROC.roc(dataPredictBoja, dataTest.boja, true)

aucTest = AUC(rocTest)

if aucTest > 0.9
    println("Klasifikator je jako dobar!")
elseif aucTest > 0.8
    println("Klasifikator je veoma dobar!")
elseif  aucTest > 0.7
    println("Klasifikator je dosta dobar!")
elseif aucTest > 0.5
    println("Klasifikator je relativno dobar!")
else
    println("Klasifikator je los!")
end

#plot(rocTest, label="ROC")


using Roots, DataFrames, DataFramesMeta, Query

function compute_marginal_CI(α, Λ_of_t, wms_of_t; mass = 0.95)
    qinfCI, qupCI = get_quantiles_from_mass(mass)
    K = length(α)
    times = Λ_of_t |> keys |> collect |> sort
    Iterators.product(times, 1:K) |>
        collect |>
        x -> map(pr -> DataFrame(time = pr[1],
            margin = pr[2],
            infCI = compute_quantile_beta_mixture(α, Λ_of_t[pr[1]], wms_of_t[pr[1]], pr[2], qinfCI),
            supCI = compute_quantile_beta_mixture(α, Λ_of_t[pr[1]], wms_of_t[pr[1]], pr[2], qupCI)), x) |>
        dflist -> vcat(dflist...)

end

function compute_quantile_beta_mixture(α, Λ, wms, margin, q::Float64)
    function Beta_mixture_cdf(x::Real)
        return sum(wms.*Float64[cdf(Beta(m[margin] + α[margin], sum(m .+ α) - m[margin] - α[margin]),x) for m in Λ])
    end
    return fzero(x -> Beta_mixture_cdf(x)-q, 0, 10^9)
end

function marginal_densities(α, Λ_of_t, wms_of_t)
    K = α |> length
    map(margin -> for_one_marginal(α, margin, Λ_of_t, wms_of_t), 1:K) |>
    dfs -> vcat(dfs...)
end

function for_one_marginal(α, margin, Λ_of_t, wms_of_t)
    times = Λ_of_t |> keys |> collect |> sort

    marginal_psi_t = [create_marginal_beta_mixture(α, Λ_of_t[t], wms_of_t[t], margin) for t in times]
    [DataFrame(x = range(0, stop = 1, length = 100)) |> df -> @transform(df, dens = f.(:x)) for f in marginal_psi_t] |>
dfs -> map((dfi, ti) -> dfi |> @mutate(time = ti, variable = "x$margin") |> DataFrame, dfs, times) |>
    dfs -> vcat(dfs...)
end

function create_marginal_beta_mixture(α, Λ, wms, margin)
    function Beta_mixture(x::Real)
        return sum(wms.*Float64[pdf(Beta(m[margin] + α[margin], sum(m .+ α) - m[margin] - α[margin]),x) for m in Λ])
    end
    return Beta_mixture
end


"""
    fr = metagauss(f)

Generate a metagaussian field multiplying the spectralfield `f` with random phases and performing an inverse FFT transform to real space.
"""
	function metagauss(f);

        (ns,ns,nt)=size(f);
#	phases as fft of a gaussian noise random field
#        ph=zeros(ns,ns,nt,'single');

        ph=zeros(ns,ns,nt);
        for i=1:nt; ph[:,:,i]=(randn(ns,ns)); end;
	ph=fft(ph);
	ph=ph./abs.(ph); ph[1,1,1]=0;
	ph=f.*ph;
	ph=real(ifft(ph));
end


module IntervalLinearEquations
using ValidatedNumerics;

include("Types.jl")
include("sti.jl")
include("F.jl")
include("G.jl")

function solve{T}(name :: AbstractString, A :: Array{Interval{T}, 2}, B :: Array{Interval{T}, 1}, precision :: AbstractFloat, max_iteration :: Int, scale :: AbstractFloat)
    @assert precision > 0.0
    @assert scale <= 1.0 && scale > 0

    case_module = get_module(name)
    solver = initialize(case_module, A, B)
    condition = Types.StopConditions(precision, max_iteration)

    is_initial = true
    while is_initial || is_stop_iteration(solver, condition)
        solver.previous = solver.current
        calculatedSubDifferential = case_module.subDifferential(solver)
        equationValue = case_module.equation(solver)
        if det(calculatedSubDifferential) == 0
            break
        end
        solver.current = iterate(solver.previous, scale, calculatedSubDifferential, equationValue)
        solver.roots = vcat(solver.roots, [solver.current])
        solver.iteration += 1
        is_initial = false
    end
    solver.current.intervals
end

function initialize{T}(case_module, A :: Array{Interval{T}, 2}, B :: Array{Interval{T}, 1})
    @assert size(A, 1) == size(A, 2)
    @assert size(A, 1) == size(B, 1)

    system = Types.Configuration{T}(
        A, Types.IntervalVector(B, IntervalLinearEquations.sti.STI(B)),
        size(A, 1), size(A, 1)*2
    )
    initialInNumbers = case_module.initialConditions(system)
    initial = Types.IntervalVector(sti.reverseSTI(initialInNumbers), initialInNumbers)
    solver = Types.Solver{T}(
        initial,
        initial,
        initial,
        system,
        [initial],
        1
    )
    solver
end

function is_stop_iteration{T}(solver :: Types.Solver{T}, condition :: Types.StopConditions)
    get_iter_diff(solver) > condition.precision && solver.iteration > condition.max_iterations
end

function get_iter_diff{T}(solver :: Types.Solver{T})
    diff_for_dimensions = map(interval_diff, solver.current.intervals, solver.previous.intervals)
    diff = sqrt(sum(diff_for_dimensions))
    diff
end

function interval_diff{T}(current :: Interval{T}, previous :: Interval{T})
    hi_diff = current.hi - previous.hi
    lo_diff = current.lo - previous.lo
    sqrt(hi_diff^2 + lo_diff^2)
end

function iterate(previous, scale, subdiff, equation_value)
    new_sti = previous.sti - scale * inv(subdiff) * equation_value
    Types.IntervalVector(IntervalLinearEquations.sti.reverseSTI(new_sti), new_sti)
end

function get_module(name :: AbstractString)
    @assert name == "F" || name == "G"

    if name == "F"
        F
    else
        G
    end
end

end


#=
Popis
  Funkcia vypocita hodnotu eccentricity centrality pre kazdy vrchol.

Parametre
  g : datova struktura grafu :: GenericGraph
  dist_key : nazov atributu, v ktorom je ulozene ohodnotenie hrany :: String

Navratova hodnota
  eccentricity : slovnik s mapovanim (kluc=vrchol_grafu => hodnota=eccentricita) :: Dict{Int64, Float64}

Poznamky
  Implementacia tejto funkcie vyuziva schopnost poli, ktora sa nazyva list comprehension.
  Vdaka tejto vlastnosti je mozne vykonat zlozitejsie operacie, funkcie, podmienky priamo pri tvorbe pola.
=#

function centrality_eccentricity_list_comprehension(g::GenericGraph; dist_key::AbstractString="weight")

  return eccentricity = [ v.index => (z = 1.0/maximum(filter(x -> x != Inf, dijkstra_shortest_paths(g, AttributeEdgePropertyInspector{Float64}(dist_key), [v]).dists))) == Inf ? 0.0 : z  for v = vertices(g) ]

end


"Implicit Euler"
function getTableauImplicitEuler()
    a = ones(Float64, 1, 1)
    b = [1.0]
    c = [1.0]
    o = 1

    TableauFIRK(:implicit_euler, o, a, b, c)
end

"Implicit Midpoint"
function getTableauImplicitMidpoint()
    a = 0.5*ones(Float64, 1, 1)
    b = [1.0]
    c = [0.5]
    o = 2

    TableauFIRK(:implicit_midpoint, o, a, b, c)
end

function getTableauGLRK(s::Int)
    TableauFIRK(getCoefficientsGLRK(s))
end

"Gauss-Lobatto-IIIA Runge-Kutta, s=2"
function getTableauLobIIIA2()
    TableauFIRK(getCoefficientsLobIIIA2())
end

"Gauss-Lobatto-IIIA Runge-Kutta, s=3"
function getTableauLobIIIA3()
    TableauFIRK(getCoefficientsLobIIIA3())
end

"Gauss-Lobatto-IIIA Runge-Kutta, s=4"
function getTableauLobIIIA4()
    TableauFIRK(getCoefficientsLobIIIA4())
end

"Gauss-Lobatto-IIIB Runge-Kutta, s=2"
function getTableauLobIIIB2()
    TableauFIRK(getCoefficientsLobIIIB2())
end

"Gauss-Lobatto-IIIB Runge-Kutta, s=3"
function getTableauLobIIIB3()
    TableauFIRK(getCoefficientsLobIIIB3())
end

"Gauss-Lobatto-IIIB Runge-Kutta, s=4"
function getTableauLobIIIB4()
    TableauFIRK(getCoefficientsLobIIIB4())
end

"Gauss-Lobatto-IIIC Runge-Kutta, s=2"
function getTableauLobIIIC2()
    TableauFIRK(getCoefficientsLobIIIC2())
end

"Gauss-Lobatto-IIIC Runge-Kutta, s=3"
function getTableauLobIIIC3()
    TableauFIRK(getCoefficientsLobIIIC3())
end

"Gauss-Lobatto-IIIC Runge-Kutta, s=4"
function getTableauLobIIIC4()
    TableauFIRK(getCoefficientsLobIIIC4())
end

"Gauss-Lobatto-IIID Runge-Kutta, s=2"
function getTableauLobIIID2()
    TableauFIRK(getCoefficientsLobIIID2())
end

"Gauss-Lobatto-IIID Runge-Kutta, s=3"
function getTableauLobIIID3()
    TableauFIRK(getCoefficientsLobIIID3())
end

"Gauss-Lobatto-IIID Runge-Kutta, s=4"
function getTableauLobIIID4()
    TableauFIRK(getCoefficientsLobIIID4())
end

"Gauss-Lobatto-IIIE Runge-Kutta, s=2"
function getTableauLobIIIE2()
    TableauFIRK(getCoefficientsLobIIIE2())
end

"Gauss-Lobatto-IIIE Runge-Kutta, s=3"
function getTableauLobIIIE3()
    TableauFIRK(getCoefficientsLobIIIE3())
end

"Gauss-Lobatto-IIIE Runge-Kutta, s=4"
function getTableauLobIIIE4()
    TableauFIRK(getCoefficientsLobIIIE4())
end

"Gauss-Lobatto-IIIF Runge-Kutta, s=2"
function getTableauLobIIIF2()
    TableauFIRK(getCoefficientsLobIIIF2())
end

"Gauss-Lobatto-IIIF Runge-Kutta, s=3"
function getTableauLobIIIF3()
    TableauFIRK(getCoefficientsLobIIIF3())
end

"Gauss-Lobatto-IIIF Runge-Kutta, s=4"
function getTableauLobIIIF4()
    TableauFIRK(getCoefficientsLobIIIF4())
end

"Gauss-Lobatto-IIIG Runge-Kutta, s=2"
function getTableauLobIIIG2()
    TableauFIRK(getCoefficientsLobIIIG2())
end

"Gauss-Lobatto-IIIG Runge-Kutta, s=3"
function getTableauLobIIIG3()
    TableauFIRK(getCoefficientsLobIIIG3())
end

"Gauss-Lobatto-IIIG Runge-Kutta, s=4"
function getTableauLobIIIG4()
    TableauFIRK(getCoefficientsLobIIIG4())
end

"Gauss-Radau-IIA Runge-Kutta, s=2"
function getTableauRadIIA2()
    TableauFIRK(getCoefficientsRadIIA2())
end

"Gauss-Radau-IIA Runge-Kutta, s=3"
function getTableauRadIIA3()
    TableauFIRK(getCoefficientsRadIIA3())
end

"Gauss-Legendre Runge-Kutta, s=3"
function getTableauSRK3()
    TableauFIRK(getCoefficientsSRK3())
end


const Instruction = Tuple{Char,Int}

function vfold(M::Matrix{Bool}, x::Int) :: Matrix{Bool}
  sY = size(M)[2]
  for i in 1:min(x-1,sY-x)
    M[:,x-i] = M[:,x-i] .| M[:,x+i]
  end
  return deepcopy(M[:,1:x-1])
end

function hfold(M::Matrix{Bool}, y::Int) :: Matrix{Bool}
  sX = size(M)[1]
  for i in 1:min(y-1,sX-y)
    M[y-i,:] = M[y-i,:] .| M[y+i,:]
  end
  return deepcopy(M[1:y-1,:])
end

fold(M::Matrix{Bool}, I::Instruction) = (I[1]=='x') ? vfold(M,I[2]) : hfold(M,I[2])

parseDot(line::String) = CartesianIndex(map(s->parse(Int,s),split(line,','))...)

function parseInstruction(line::String) :: Instruction
  s = split(line,' ')[3]
  return (first(s), parse(Int,s[3:end]))
end

function parseLine(line::String)
  if isempty(line)
    return
  elseif first(line) == 'f'
    parseInstruction(line)
  else
    parseDot(line)
  end
end

function matrix(dots::Vector{CartesianIndex{2}}) :: Matrix{Bool}
  sX = maximum(map(x->x[1],dots))+1
  sY = maximum(map(x->x[2],dots))+1
  M :: Matrix{Bool} = zeros(sX,sY)
  map(x->M[x] = true, dots)
  return deepcopy(M)
end

function readInput()
  f = open("./inputs/day13.txt")

  data = map( line -> parseLine(line), eachline(f))
  dots::Vector{CartesianIndex{2}} = filter(x->typeof(x) == CartesianIndex{2}, data)
  instructions::Vector{Instruction} = filter(x->typeof(x) == Instruction, data)
  return dots, instructions
end

function test()
  M :: Matrix{Bool} = zeros(5,5)
  M[1,5] = true
  M[2,4] = true
  M[3,2] = true
  M[4,1] = true
  M[5,1] = true
  M[5,4] = true
  M[5,5] = true
  display(M)
  M2 = hfold(M,2)
  display(M2)
  vfold(M2,3)
end

test()

dots, instructions = readInput()
dots = map(p -> p + CartesianIndex(1,1), dots)
instructions = map(x -> (x[1],x[2]+1), instructions)
M = matrix(dots)

M1 = deepcopy(M)

MA = [M1]
for f in instructions
  push!(MA,fold(deepcopy(MA[end]),f))
end

display(MA[end])


### Interferometers ###
"""
Supertype to any concrete interferomter type such as [`UserDefinedInterferometer`](@ref),
[`RandHaar`](@ref), [`Fourier`](@ref),...
"""
abstract type Interferometer end

"""
    UserDefinedInterferometer(U::Matrix)

Creates an instance of [`Interferometer`](@ref) from a given unitary matrix `U`.

    Fields:
        - m::Int
        - U::Matrix
"""
struct UserDefinedInterferometer <: Interferometer

    #actively checks unitarity, inefficient if outputing many matrices that are known to be unitary
    m::Int
    U::Matrix
    UserDefinedInterferometer(U) = is_unitary(U) ? new(size(U,1), U) : error("input matrix is non unitary")
end

"""
    RandHaar(m::Int)

Creates an instance of [`Interferometer`](@ref) from a Haar distributed unitary matrix of dimension `m`.

    Fields:
        - m::Int
        - U::Matrix
"""
struct RandHaar <: Interferometer
    m::Int
    U::Matrix{ComplexF64}
    RandHaar(m) = new(m,rand_haar(m))
end

"""
    Fourier(m::Int)

Creates a Fourier [`Interferometer`](@ref) of dimension `m`.

    Fields:
        - m::Int
        - U::Matrix
"""
struct Fourier <: Interferometer
    m::Int
    U::Matrix{ComplexF64}
    Fourier(m::Int) = new(m,fourier_matrix(m))
end

"""
    Hadamard(m::Int)

Creates a Hadamard [`Interferometer`](@ref) of dimension `m`.

    Fields:
        - m::Int
        - U::Matrix
"""
struct Hadamard <: Interferometer
    m::Int
    U::Matrix
    Hadamard(m::Int) = new(m,hadamard_matrix(m))
end

"""
    BeamSplitter(transmission_amplitude::Float64)

Creates a beam-splitter with tunable transmissivity.

    Fields:
        - transmission_amplitude::Float64
        - U::Matrix{ComplexF64}
        - m::Int
"""
struct BeamSplitter <: Interferometer
    transmission_amplitude::Float64
    U::Matrix
    m::Int
    BeamSplitter(transmission_amplitude) = new(transmission_amplitude, beam_splitter(transmission_amplitude),2)
end

"""
    Rotation(angle::Float64)

Creates a Rotation matrix with tunable angle.

    Fields:
        - angle::Float64
        - U::Matrix{ComplexF64}
        - m::Int
"""
struct Rotation <: Interferometer
    angle::Float64
    U::Matrix
    m::Int
    Rotation(angle) = new(angle, rotation_matrix(angle),2)
end

"""
    PhaseShift(shifted_modes::Array, param_::Array)

Creates a phase-shifter that is applied on the modes precised by shifted_modes with phase shifts given in param_.

    Fields:
        - shifted_modes::Array
        - param_::Array
        - m::Int
        - U::Matrix{ComplexF64}
"""
struct PhaseShift <: Interferometer
    shifted_modes::Array
    param_::Array
    m::Int
    U::Matrix
    PhaseShift(shifted_modes, param_) = new(shifted_modes, param_, length(shifted_modes), phase_shift(shifted_modes, param_))
end

# struct Circuit <: Interferometer
#
#     circuit_elements::Vector{Vector{Interferometer}}
#     layers::Vector
#     U::Matrix{ComplexF64}
#
#     Circuit(circuit_elements::Vector{Vector{Interferometer}}, layers::Vector) = new(circuit_elements::Vector{Vector{Interferometer}}, layers::Vector, build_circuit(circuit_elements,layers))
# end

Base.show(io::IO, interf::Interferometer) = print(io, "Interferometer :\n\n", "Type : ", typeof(interf), "\n", "m : ", interf.m)

Base.show(io::IO, interf::UserDefinedInterferometer) = begin
     print(io, "Interferometer :\n\n", "Type : ", typeof(interf), "\n", "m : ", interf.m, "\nUnitary : \n")
     pretty_table(io, interf.U)
end


@testset "INT algorithm" begin
    prob, tspan = exponential_1d()
    sol = solve(prob, tspan=tspan, INT(δ=0.01))
    @test isa(sol.alg, INT)
    @test dim(sol) == 1
    @test length(sol) == 100
    @test setrep(sol) <: Interval
    @test setrep(sol) == Interval{Float64,IntervalArithmetic.Interval{Float64}}

    prob, tspan = exponential_1d(invariant=HalfSpace([-1.0], -0.3)) # x >= 0.3
    sol_inv = solve(prob, tspan=tspan, INT(δ=0.01))
    @test length(sol_inv) == 52
    @test [0.3] ∈ sol_inv[end]
    # check that the following reach-set escapes the invariant
    @test [0.3] ∈ sol[52] && [0.3] ∉ sol[53]

    # check NSTEPS option
    sol = solve(prob, NSTEPS=10, INT(δ=0.01))
    @test length(sol) == 10

    # doesn't work for higher dimensional systems
    prob, tspan = linear5D_homog()
    @test_throws ArgumentError solve(prob, tspan=tspan, INT(δ=0.01))
end

# Issue #378
@testset "Scalar affine ODE" begin
    prob = @ivp(x' = x + [1.0], x(0) ∈ Singleton([1.0]))
    sol = solve(prob, T=4.0)
end


"""
    foil_points_local(;l = CHORD_LENGTH_DEFAULT,
                                 t = THICKNESS_DEFAULT,
                                 c = CAMBER_DEFAULT)
    -> Vector{Points}

2d foil coordinates centered on 0.3·chord length from leading edge.
Default values produce output in units of length.
"""
function foil_points_local(;l = CHORD_LENGTH_DEFAULT,
                                 t = THICKNESS_DEFAULT,
                                 c = CAMBER_DEFAULT)
    # Positions along chord, one way
    px = l * (-CHORD_ZERO .+ FOIL_CHORD_POS)
    # Offset centre line
    Δy_0 = c * FOIL_CAMBER
    # Half thickness
    halft = t * FOIL_HALF_T
    # Suction side, starting at the trailing edge (useful for making a spline)
    px_s = reverse(px)
    py_s = reverse( Δy_0 + halft)
    # Pressure side, starting at the leading edge
    # (useful for making a spline which is sharp at the trailing edge)
    px_p = px
    py_p = Δy_0 - halft
    px_both = vcat(px_s, px_p)
    py_both = vcat(py_s, py_p)
    map(px_both .|> upreferred, py_both .|> upreferred) do x, y
        Point(x, y)
    end
end
"""
    foil_spline_local(;l = CHORD_LENGTH_DEFAULT,
                                 t = THICKNESS_DEFAULT,
                                 c = CAMBER_DEFAULT)
    -> 872-length Vector{Points}

2d foil spline centered on 0.3·chord length from leading edge.
Default values produce output in units of length.
"""
function foil_spline_local(;l = CHORD_LENGTH_DEFAULT,
                                 t = THICKNESS_DEFAULT,
                                 c = CAMBER_DEFAULT)
    polyfit(foil_points_local(;l = l, t = t, c = c))
end

"""
    foil_draw(p::Point ;
        α = 0°,
        rel_scale = 1,
        l = CHORD_LENGTH_DEFAULT,
        t = THICKNESS_DEFAULT,
        c = CAMBER_DEFAULT,
        backgroundcolor = colorant"white")
    -> a large array of points.
Draw an airfoil with the given parameters.

p is the position of the foil, cooresponding to the rough pressure centre and
centre of rotaion, at 0.3·chord from the leading edge.
"""
function foil_draw(p::Point ;
    α = 0°,
    rel_scale = 1,
    l = CHORD_LENGTH_DEFAULT,
    t = THICKNESS_DEFAULT,
    c = CAMBER_DEFAULT,
    backgroundcolor = colorant"white")
    f = foil_spline_local(l = rel_scale * l, t = rel_scale * t, c = rel_scale * c)
    polyrotate!(f, α)
    polymove!(f,  O, p)
    poly(f, :fill)
    luminback = lumin(backgroundcolor)
    luminfront = get_current_lumin()
    avglumin = (luminback + luminfront) / 2
    contrastcol = color_with_lumin(get_current_RGB(), avglumin)
    gsave()
    sethue(contrastcol)
    poly(f, :stroke)
    grestore()
    f
end


using LightGraphs

export
    max_cut,
    tsp_qubo,
    hamiltonian_cycle_qubo

#max cut
function max_cut(g::SimpleGraph, vars::Variables)
    @assert vars.indexcapacities == [nv(g), nv(g)]
    hobo = HOBO(vars)
    for edge = edges(g)
        v = edge.src
        w = edge.dst
        hobo -= (vars[v]-vars[w])^2
    end
    hobo
end

function max_cut(g::SimpleGraph; name::String="Max-Cut")
        max_cut(g, Variables(name, nv(g), "vertices"))
end

# hamiltonian cycle
function hamiltonian_cycle_qubo(n::Int, vars::Variables)
    @assert vars.indexcapacities == [n, n]
    hobo = HOBO(vars)

    for t=1:n
        hobo_tmp = HOBO(vars)
        for j=1:n
            hobo_tmp += vars[t, j]
        end
        hobo += (1. - hobo_tmp)^2
    end

    for j=1:n
        hobo_tmp = HOBO(vars)
        for t=1:n
            hobo_tmp += vars[t, j]
        end
        hobo += (1. - hobo_tmp)^2
    end

    #remove staying
    #for v=1:n, t=1:n
    #    hobo += vars[v, t]*vars[v, (t % n) + 1]
    #end
    hobo
end

function hamiltonian_cycle_qubo(n::Int, name::String="Hamiltonian Cycle Comlete Graph")
    hamiltonian_cycle_qubo(n, Variables(name, [n, n], ["timestep", "vertex"]))
end

function hamiltonian_cycle_qubo(g::SimpleGraph, vars::Variables)
    n = nv(g)
    hobo = hamiltonian_cycle_qubo(n, vars)

    #remove according to graph
    for edge = edges(LightGraphs.complement(g))
        u = edge.src
        v = edge.dst
        hobo += vars[t, u]*vars[t % n + 1, v]
        hobo += vars[t, v]*vars[t % n + 1, u]
    end

    hobo
end

function hamiltonian_cycle_qubo(g::SimpleGraph; name::String="Hamiltonian Cycle")
        hamiltonian_cycle_qubo(g, Variables(name, [nv(g),nv(g)], ["timestep", "vertex"]))
end

# tsp
function tsp_qubo(w::Matrix{T}, vars::Variables; B::Real=1., A::Real=2*B*maximum(w)) where T<:Real
    n = size(w, 1)
    @assert size(w, 1) == size(w, 2)
    @assert vars.indexcapacities == [n, n]
    @assert A >= 0 && B >= 0

    hobo_hamiltonian = hamiltonian_cycle_qubo(n, vars)
    hobo_tsp = HOBO(vars)
    #remove according to graph
    for u = 1:n, v = 1:n, t = 1:n
        if u != v
            hobo_tsp += w[u, v] * vars[t, u] * vars[t % n + 1, v]
        end
    end
    A*hobo_hamiltonian + B*hobo_tsp
end

function tsp_qubo(w::Matrix; name::String="TSP", B::Real=1., A::Real=2*B*maximum(w))
    tsp_qubo(w, Variables(name, collect(size(w)), ["timestep", "city"]), B=B, A=A)
end


"""
Formulate and solve the Beasley and Christofides benchmarks using JuMP
(solved using Gurobi in this case, which seems faster than CPLEX)
"""

using LightGraphs
using JuMP
using Gurobi
using BenchmarkTools # For timing

import Base.parse

"""
Parse a Beasley-Christofides benchmark file and returns
- g - SimpleDiGraph with problem
- min_res - Vector of float with lower bounds
- max_res - Vector of float with upper bounds
- costs - Vector of float with upper bounds
"""
function parse_benchmark(
  filename::String
)::Tuple{
  SimpleDiGraph,Vector{Float64},Vector{Float64},Vector{Float64},AbstractMatrix{Float64}
}
  f = open(filename)
  arc_count = Int(1)
  number_nodes = Int(0)
  number_edges = Int(0)
  number_resources = Int(0)

  for line in eachline(f)
    line = split(line, " ")
    number_nodes, number_edges, number_resources = parse(Int, line[2]),
    parse(Int, line[3]),
    parse(Int, line[4])
    break
  end

  g = SimpleDiGraph(number_nodes)

  min_res = []
  for line in eachline(f)
    line = split(line, " ")
    for r in 1:number_resources
      r = parse(Float64, line[1 + r])
      push!(min_res, r)
    end
    break
  end
  max_res = []
  for line in eachline(f)
    line = split(line, " ")
    for r in 1:number_resources
      r = parse(Float64, line[1 + r])
      push!(max_res, r)
    end
    break
  end
  count = 1
  for line in eachline(f)
    if count == number_nodes
      break
    end
    count += 1
  end
  # Parse edges
  res_costs = zeros(Float64, number_edges, number_resources)
  costs = []
  count = 1
  for line in eachline(f)
    line = split(line, " ")
    i, j, cost = parse(Int, line[2]), parse(Int, line[3]), parse(Float64, line[4])
    if j != 1 || i != number_nodes
      add_edge!(g, i, j)
    end
    push!(costs, cost)
    for r in 1:number_resources
      res = parse(Float64, line[4 + r])
      res_costs[count, r] = res
    end
    count += 1
  end
  close(f)
  return g, min_res, max_res, costs, res_costs
end

function get_result(folder::String, instance_number::Int)::Float64
  f = open("$folder/results.txt")
  count = 1
  result = 0.0
  for line in eachline(f)
    line = split(line, " ")
    if count == instance_number + 1
      result = parse(Int, line[2])
      break
    end
    count += 1
  end
  close(f)
  return result
end

function formulate(
  g::SimpleDiGraph,
  min_res::Vector{Float64},
  max_res::Vector{Float64},
  costs::Vector{Float64},
  res_costs::AbstractMatrix{Float64},
)::Float64
  N = nv(g)
  M = ne(g)
  R = length(max_res)

  model = Model(Gurobi.Optimizer)
  set_silent(model)
  @variable(model, x[e=edges(g)], Bin)
  @constraint(
    model,
    [i = 2:(N - 1)],
    sum(x[Edge(i, j)] for j in outneighbors(g, i)) -
    sum(x[Edge(j, i)] for j in inneighbors(g, i)) == 0
  )
  @constraint(model, sum(x[Edge(1, j)] for j in outneighbors(g, 1)) == 1)
  @constraint(model, -sum(x[Edge(i, N)] for i in inneighbors(g, N)) == -1)
  @constraint(
    model,
    [r = 1:R],
    min_res[r] <=
    sum(res_costs[e_idx, r] * x[e] for (e_idx, e) in enumerate(edges(g))) <=
    max_res[r]
  )

  @objective(model, Min, sum(costs[e_idx] * x[e] for (e_idx, e) in enumerate(edges(g))))
  t = @elapsed optimize!(model)
  @info "$(t*1000)"
  if termination_status(model) == MOI.OPTIMAL
    return objective_value(model)
  end
  return 0.0
end

for i in 1:24
  @info "$i"
  g, min_res, max_res, costs, res_costs = parse_benchmark(
    "../beasley_christofides_1989/rcsp$i.txt"
  )
  obj = formulate(g, min_res, max_res, costs, res_costs)
  opt = get_result("../beasley_christofides_1989/", i)
  if obj != opt
    @error "Failed for instance $i"
  end
end


using PyPlot

function traffic(uinit="step", method="godunov", m=400; T=2, kmul=0.8)
    # Discretization
    h = 4 / m
    x = ((collect(1:m).-0.5) ./(m/4)).-2.0

    k = kmul*h
    N = ceil(Int, T/k)
    a = findmin(map(x->abs(x+(h/2)),x))[2]

    # Initial condition, double step function
    if uinit == "step"
        r = @. Float64(x<0.0)*0.8
	r = zeros(size(x)[1])
        r[1] = 0.5
        "u = zeros(size(x)[1])"
        "u = @. 2 * Float64(abs(x-0.5)<0.25) - 0.5"
    else
        error("Unknown initial condition type")
    end

    clf(); axis([-2, 2, -1, 1.5]); grid(true); ph, = plot(x,r)  # Setup plotting
    #N=5
    for it = 1:N
        rr = r
        rl =  r[[end; 1:end-1]] # Periodic boundary conditions

        F = zeros(m)
        if method == "godunov"
           for i = 1:m
                #if i<=4
                #   println("i=",i," rl[i]=",rl[i]," rr[i]=",rr[i])
                #end
		if rl[i] > rr[i]
		   F[i] = max(rl[i]*(1-rl[i]), rr[i]*(1-rr[i]))
		elseif rl[i] < rr[i]
		   F[i] = min(rl[i]*(1-rl[i]), rr[i]*(1-rr[i]))
		else
		 F[i] = 0
		end

           end
        elseif method == "upwind"
           for i = 1:m
               if (ur[i] + ul[i])/2 > 0
                    F[i] = ul[i]^2/2
               else
                    F[i] = ur[i]^2/2
               end
           end
        elseif method == "roe"
           for i = 1:m
              F[i] = 0.5*((rl[i]*(1-rl[i])) + (rr[i]*(1-rr[i]))) - 0.5*(abs(1.01-rl[i]-rr[i])*(rr[i] - rl[i]))
           end
        else
           error("Unknown method type")
        end

        if div(it,125)%2 == 0
            F[a] = 0
        end
	if ceil(Int,div(it,237.5))%2 == 0
            F[216] = 0
        end
	println(F[a])
        @. r -= k/h * (F[[2:end; 1]] - F)
        #println("it=",it," k=",k," h=",h,"  r=",r[1:4]," F=",F[1:4])

        r[1] = 0.5
        ph.set_data(x,r); pause(1e-3) # Update plot
    end
    return r
end


function wilson_estimate(c::MolecularProperty, p, T)
    return wilson_estimate(p, T, c.ω, c.p_c, c.T_c)
end

"""
    wilson_estimate(p, T, ω, p_c, T_c)

Estimate K-values for a given acentric factor ω and pressure and temperature at current and critical conditions.

Reference [Vapor-Liquid Equilibrium. XI. A New Expression for the Excess Free Energy of Mixing by GM Wilson](https://doi.org/10.1021/ja01056a002)
"""
function wilson_estimate(p::R, T::R, ω::R, p_c::R, T_c::R) where R<:Real
    K = exp(5.37*(1.0 + ω)*(1.0 - T_c/T))*(p_c/p)
    return K::R
end

"""
    wilson_estimate!(K, properties, p, T)

Update a vector K in-place with K-values from `wilson_estimate`.
"""
function wilson_estimate!(K::AbstractVector{R}, props, p::R, T::R) where R<:Real
    @inbounds for i in eachindex(K)
        K[i] = wilson_estimate(props[i], p, T)::R
    end
end

"""
    wilson_estimate(properties, p, T)

Create vector of K-values that holds the `wilson_estimate` for each species.
"""
function wilson_estimate(mixture::MultiComponentMixture, p, T)
    K = zeros(number_of_components(mixture))
    wilson_estimate!(K, mixture.properties, p, T)
    return K
end

"""
    initial_guess_K(eos, cond)

Produce a plausible initial guess for K values for `eos` under current `cond`.
"""
function initial_guess_K(eos, cond)
    K = zeros(number_of_components(eos))
    initial_guess_K!(K, eos, cond)
    return K
end

"""
    initial_guess_K(eos, cond)

In-place version of `initial_guess_K`.
"""
initial_guess_K!(K, eos, cond) = wilson_estimate!(K, eos.mixture.properties, cond.p, cond.T)


using LinearAlgebra
n = 6;
A = rand([1.,10.],n,n)


@testset "utils" begin

    # Test permutation matrix
    p = [5, 3, 4, 1, 2, 6]
    x = randn(length(p))
    @test x[p] == Chordal.build_perm_matrix(p)*x

    # Test random SDP
    n = 100
    c, F, G, xstar, D = Chordal.generate_random_sdp(n)
    Fx = sum(F[i]*xstar[i] for i in 1:n) + G
    @test (
        all(eigvals(Matrix(Fx)) .>= -sqrt(eps())) &&
        all(eigvals(D) .>= -sqrt(eps())) &&
        all([0 ≈ c[i] - tr(F[i]*D) for i in 1:n]) &&
        tr(Fx*D) ≈ 0
    )
end


struct Edge{T} <: AbstractLinePrimitive{T}
    a::CartesianPoint{T}
    b::CartesianPoint{T}
end

direction(e::Edge) = e.b - e.a

Line(e::Edge{T}) where {T} = Line{T}(e.a, direction(e))

function distance(pt::CartesianPoint{T}, e::Edge{T}) where {T}
    return if (pt - e.b) ⋅ direction(e) >= 0
        norm(pt - e.b)
    elseif (pt - e.a) ⋅ direction(e) <= 0
        norm(pt - e.a)
    else
        distance(pt, Line(e))
    end
end

function sample(e::Edge{T}; n = 2)::Vector{CartesianPoint{T}} where {T}
    xs = range(zero(T), stop = one(T), length = n)
    pts = Vector{CartesianPoint{T}}(undef, n)
    dir = direction(e)
    for i in eachindex(pts)
        pts[i] = e.a + xs[i] .* dir
    end
    pts
end
function sample(es::Vector{Edge{T}}; n = 2)::Vector{CartesianPoint{T}} where {T}
    pts = Vector{CartesianPoint{T}}(undef, length(es) * n)
    for i in eachindex(es)
        pts[(i-1)*n+1:i*n] = sample(es[i], n = n)
    end
    pts
end


"""
GeneralizedGamma(a,b,k)

The *Generalized gamma distribution* with shape parameters `a`, scale `b`, and power `k` has probability density
function

```math
f(x; a, b) = \\frac{(k/b^a) x^{a-1} e^{-(x/b)^k}{\\Gamma(a/k)},
\\quad x > 0
```

```julia
GeneralizedGamma(a, b, k)      # GeneralizedGamma distribution with shape a, scale b, power k

params(d)        # Get the parameters, i.e. (a, b, k)
shape(d)         # Get the shape parameter, i.e. a
scale(b)         # Get the scale parameter, i.e. b
```

External links

* [GeneralizedGamma distribution on Wikipedia](https://en.wikipedia.org/wiki/Generalized_gamma_distribution)

"""

struct GeneralizedGamma <: ContinuousUnivariateDistribution
    a
    b
    k
end

#### Parameters
params(d::GeneralizedGamma) = (d.a, d.b, d.k)
shape(d::GeneralizedGamma) = d.a
scale(d::GeneralizedGamma) = d.b
power(d::GeneralizedGamma) = d.k

#### Evaluation
function gamma_cdf(a, x)
    return gamma_inc(a,x,0)[1]
end

function gamma_ccdf(a, x)
    return gamma_inc(a,x,0)[2]
end

function cdf(d::Gamma, x::Real)
    if x < zero(x)
        return zero(x)
    else
        shp, scl = params(d)
        return gamma_cdf(shp, x/scl)
    end
end

function ccdf(d::Gamma, x::Real)
    if x < zero(x)
        return one(x)
    else
        shp, scl = params(d)
        return gamma_ccdf(shp, x/scl)
    end
end

function cdf(d::GeneralizedGamma, x::Real)
    if x < zero(x)
        return zero(x)
    else
        shp, scl, pwr = params(d)
        return gamma_cdf(shp/pwr, (x/scl)^pwr)
    end
end

function ccdf(d::GeneralizedGamma, x::Real)
    if x < zero(x)
        return one(x)
    else
        shp, scl, pwr = params(d)
        return gamma_ccdf(shp/pwr, (x/scl)^pwr)
    end
end

function pdf(d::GeneralizedGamma, x::Real)
    if x < zero(x)
        return zero(x)
    else
        shp, scl, pwr = params(d)
        return (pwr/(scl^shp))*x^(shp-1)*exp(-(x/scl)^pwr)/gamma(shp/pwr)
    end
end

function logpdf(d::GeneralizedGamma, x::Real)
    if x < zero(x)
        return zero(x)
    else
        shp, scl, pwr = params(d)
        return log(pwr)-shp*log(scl) + (shp-1)*log(x) - (x/scl)^pwr -loggamma(shp/pwr)
    end
end

function eqcdf(d::GeneralizedGamma, x::Real)
    out = zero(x)
    if x >= zero(x)
        shp, scl, pwr = params(d)
        out = (one(x) + x*gamma(shp/pwr)*gamma_ccdf(shp/pwr, (x/scl)^pwr)/(scl*gamma((shp+1)/pwr)) - gamma_ccdf((shp+1)/pwr, (x/scl)^pwr))
        return out
    end
end

function quantile(d::GeneralizedGamma, p)
    shp, scl, pwr = params(d)
    r = gamma_inc_inv(shp/pwr, p, 1-p)
    return scl*(r^inv(pwr))
end

function rand(rng::AbstractRNG, d::GeneralizedGamma)
    p = rand(rng)
    return quantile(d, p)
end

#### Statistics

function mean(d::GeneralizedGamma)
    shp, scl, pwr = params(d)
    return scl*gamma((shp+1)/pwr)/gamma(shp/pwr)
end

#var(d::Gamma) = d.α * d.θ^2

#skewness(d::Gamma) = 2 / sqrt(d.α)

#kurtosis(d::Gamma) = 6 / d.α

# function mode(d::Gamma)
#     (α, θ) = params(d)
#     α >= 1 ? θ * (α - 1) : error("Gamma has no mode when shape < 1")
# end

# function entropy(d::Gamma)
#     (α, θ) = params(d)
#     α + loggamma(α) + (1 - α) * digamma(α) + log(θ)
# end

# mgf(d::Gamma, t::Real) = (1 - t * d.θ)^(-d.α)

# cf(d::Gamma, t::Real) = (1 - im * t * d.θ)^(-d.α)

# function kldivergence(p::Gamma, q::Gamma)
#     # We use the parametrization with the scale θ
#     αp, θp = params(p)
#     αq, θq = params(q)
#     θp_over_θq = θp / θq
#     return (αp - αq) * digamma(αp) - loggamma(αp) + loggamma(αq) -
#         αq * log(θp_over_θq) + αp * (θp_over_θq - 1)
# end

#### Evaluation & Sampling

# @_delegate_statsfuns Gamma gamma α θ

# gradlogpdf(d::Gamma{T}, x::Real) where {T<:Real} =
#     insupport(Gamma, x) ? (d.α - 1) / x - 1 / d.θ : zero(T)

# function rand(rng::AbstractRNG, d::Gamma)
#     if shape(d) < 1.0
#         # TODO: shape(d) = 0.5 : use scaled chisq
#         return rand(rng, GammaIPSampler(d))
#     elseif shape(d) == 1.0
#         return rand(rng, Exponential(d.θ))
#     else
#         return rand(rng, GammaGDSampler(d))
#     end
# end

# function sampler(d::Gamma)
#     if shape(d) < 1.0
#         # TODO: shape(d) = 0.5 : use scaled chisq
#         return GammaIPSampler(d)
#     elseif shape(d) == 1.0
#         return sampler(Exponential(d.θ))
#     else
#         return GammaGDSampler(d)
#     end
# end

#### Fit model

# struct GammaStats <: SufficientStats
#     sx::Float64      # (weighted) sum of x
#     slogx::Float64   # (weighted) sum of log(x)
#     tw::Float64      # total sample weight

#     GammaStats(sx::Real, slogx::Real, tw::Real) = new(sx, slogx, tw)
# end

# function suffstats(::Type{<:Gamma}, x::AbstractArray{T}) where T<:Real
#     sx = zero(T)
#     slogx = zero(T)
#     for xi = x
#         sx += xi
#         slogx += log(xi)
#     end
#     GammaStats(sx, slogx, length(x))
# end

# function suffstats(::Type{<:Gamma}, x::AbstractArray{T}, w::AbstractArray{Float64}) where T<:Real
#     n = length(x)
#     if length(w) != n
#         throw(DimensionMismatch("Inconsistent argument dimensions."))
#     end

#     sx = zero(T)
#     slogx = zero(T)
#     tw = zero(T)
#     for i = 1:n
#         @inbounds xi = x[i]
#         @inbounds wi = w[i]
#         sx += wi * xi
#         slogx += wi * log(xi)
#         tw += wi
#     end
#     GammaStats(sx, slogx, tw)
# end

# function gamma_mle_update(logmx::Float64, mlogx::Float64, a::Float64)
#     ia = 1 / a
#     z = ia + (mlogx - logmx + log(a) - digamma(a)) / (abs2(a) * (ia - trigamma(a)))
#     1 / z
# end

# function fit_mle(::Type{<:Gamma}, ss::GammaStats;
#     alpha0::Float64=NaN, maxiter::Int=1000, tol::Float64=1e-16)

#     mx = ss.sx / ss.tw
#     logmx = log(mx)
#     mlogx = ss.slogx / ss.tw

#     a::Float64 = isnan(alpha0) ? (logmx - mlogx)/2 : alpha0
#     converged = false

#     t = 0
#     while !converged && t < maxiter
#         t += 1
#         a_old = a
#         a = gamma_mle_update(logmx, mlogx, a)
#         converged = abs(a - a_old) <= tol
#     end

#     Gamma(a, mx / a)
# end


## Test models ##
module Test_model

using MultipleTesting
using Test
using Distributions

@testset "Models" begin

    @testset "BUM model" begin

        bum = BetaUniformMixtureModel(0.7)
        bum0 = BetaUniformMixtureModel(1.0) ## uniform, all H₀
        bum1 = BetaUniformMixtureModel(0.0) ## beta, all H₁

        x = collect(0:0.01:1)

        @test cdf.(Ref(bum0), x) ≈ cdf.(Ref(Uniform()), x)
        @test cdf.(Ref(bum1), x) ≈ cdf.(Ref(Beta(0.5, 1.0)), x)

        @test pdf.(Ref(bum0), x) ≈ pdf.(Ref(Uniform()), x)
        @test pdf.(Ref(bum1), x) ≈ pdf.(Ref(Beta(0.5, 1.0)), x)

        @test isin(rand(bum, 100), 0.0, 1.0)
        @test isin(rand(bum0, 100), 0.0, 1.0)
        @test isin(rand(bum1, 100), 0.0, 1.0)

        @test_throws MethodError BetaUniformMixtureModel()
        @test_throws DomainError BetaUniformMixtureModel(-0.2)
        @test_throws DomainError BetaUniformMixtureModel(1.2, 0.2, 2.0)

    end

end

end


## Wetted-area method
#======================================================#

# Raymer form factor for wing, pylon, nacelle, fuselage? (Eq. 12.30)
form_factor_wing(x_c, t_c, Λ_m, M) = (1 + 0.6t_c / x_c + 100t_c^4) * (1.34M^0.18 * cos(Λ_m)^0.28)

function form_factor(wing :: HalfWing, M)
    # (x/c)_max, (t/c)_max, (t/c)_max sweep angles
    xcs, tcs, sweeps = max_thickness_to_chord_ratio_sweeps(wing, 60)
    Kf  = form_factor_wing.(xcs, tcs, sweeps, M)
end

form_factor(wing :: Wing, M) = [ reverse(form_factor(wing.left, M)); form_factor(wing.right, M) ]

# Schlichting averaged skin-friction coefficients
cf_lam(Re_c, k_lam = 1.) = 1.328 / √(Re_c * k_lam)
cf_turb(Re_c, M) = 0.455 / log10(Re_c)^2.58 / (1 + 0.144M^2)^0.65
cf_schlichting(Re, Re_xtr, M) = max(cf_lam(Re), cf_turb(Re, M) - (Re_xtr / 320 - 39) / Re)

function skin_friction_coefficients(mean_chords, x_tr, V, ρ, M, μ)
    # Reynolds numbers for chord lengths
    Re_c = reynolds_number.(ρ, V, mean_chords, μ)

    # Reynolds numbers at transition locations
    Re_xtr = reynolds_number.(ρ, V, mean_chords .* x_tr, μ)

    # Skin-friction coefficients
    cfs = cf_schlichting.(Re_c, Re_xtr, M)
end

function wetted_area_drag(mean_chords, S_wets, K_fs, x_tr, V, ρ, M, μ)
    cfs = skin_friction_coefficients(mean_chords, x_tr, V, ρ, M, μ)

    # Profile drag
    Dp_by_q = sum(@. cfs * S_wets * K_fs)

    return Dp_by_q
end

function wetted_area_drag(wing :: AbstractWing, x_tr, V, ρ, a_ref, μ)
    # Chord processing
    mean_chords = (forward_sum ∘ chords)(wing) / 2

    # Wetted areas
    S_wets  = mean_chords .* spans(wing) ./ cos.(dihedrals(wing))

    # Form factors
    M       = V / a_ref
    K_fs    = form_factor(wing, M)

    wetted_area_drag(mean_chords, S_wets, K_fs, x_tr, V, ρ, M, μ)
end

wetted_area_drag(wing :: WingMesh, x_tr, V, ρ, a_ref, μ) = wetted_area_drag(wing.surface, x_tr, V, ρ, a_ref, μ)

# Sato's local-friction and local-dissipation based on power balance method from Mark Drela, Flight Vehicle Aerodynamics, eq. 4.115.
function local_dissipation_drag(wing :: AbstractWing, wetted_areas, ρ_es, u_es, x_tr, V, ρ, M, μ)
    # Chord processing
    mean_chords = (forward_sum ∘ chords)(wing) / 2

    # Compute weighted wetted areas based on inviscid edge velocity distribution.
    weighted_S_wets = sum(@. ρ_es * norm(u_es)^3 * wetted_areas; dims = 1) ./ (ρ * V^3)

    wetted_area_drag(mean_chords, weighted_S_wets, 1., x_tr, V, ρ, M, μ)
end

function local_dissipation_drag(wing :: WingMesh, ρ_es, u_es, x_tr, V, ρ, M, μ)
    # Chord processing
    mean_chords = (forward_sum ∘ chords)(wing.surface) / 2

    # Compute wetted area of camber (for now).
    S_wets = panel_area.(camber_panels(wing))

    # Calculate weighted wetted areas based on inviscid edge velocity distribution.
    weighted_S_wets = sum(@. ρ_es * u_es^3 * S_wets; dims = 1) ./ (ρ * V^3)

    wetted_area_drag(mean_chords, weighted_S_wets, 1., x_tr, V, ρ, M, μ)
end

abstract type AbstractProfileDrag end

struct FormFactor <: AbstractProfileDrag end
struct Dissipation <: AbstractProfileDrag end

profile_drag_coefficient(wing :: AbstractWing, x_tr, V, rho_ref, a_ref, area_ref, μ) = wetted_area_drag(wing, x_tr, V, rho_ref, a_ref, μ) / area_ref

"""
    profile_drag_coefficient(wing :: AbstractWing, x_tr, refs :: References)

Estimate the profile drag coefficient of a ``Wing`` using the **wetted-area method** based on Schlichting's skin-friction coefficient formula with specified transition ratio(s) ``xₜᵣ`` and reference values.
"""
profile_drag_coefficient(wing :: AbstractWing, x_tr, refs :: References) = profile_drag_coefficient(wing, x_tr, refs.speed, refs.density, refs.sound_speed, refs.area, refs.viscosity)

"""
    profile_drag_coefficient(wing :: AbstractWing, x_tr, edge_speeds, panels, refs :: References)

Estimate the profile drag coefficient of a `Wing` using the **local-friction and local-dissipation method** based on Schlichting's skin-friction coefficient formula.
"""
profile_drag_coefficient(wing :: AbstractWing, x_tr, edge_speeds, panels, refs :: References) = local_dissipation_drag(wing, panel_area.(panels), refs.density, edge_speeds, x_tr, refs.speed, refs.density, mach_number(refs), refs.viscosity) / refs.area

"""
    profile_drag_coefficient(wing :: WingMesh, x_tr, edge_speeds, refs :: References)

Estimate the profile drag coefficient of a `Wing` using the **local-friction and local-dissipation method** based on Schlichting's skin-friction coefficient formula.
"""
profile_drag_coefficient(wing :: WingMesh, x_tr, edge_speeds, refs :: References) = profile_drag_coefficient(wing.surface, x_tr, edge_speeds, camber_panels(wing), refs)

function wave_drag(M, Λ, t_by_c, Cl, κ_A)
    M_drag_divergence = κ_A / cos(Λ) - t_by_c / cos(Λ)^2 - Cl / (10 * cos(Λ)^3) # Drag divergence Mach number
    M_crit  = M_drag_divergence - (0.1 / 80)^(1/3) # Critical Mach number
    CD_wave = ifelse(M > M_crit, 20 * (Mach - M_crit)^4, 0.)
end


using JTetra
using Base.Test

println("==== Test De Rham Complex ====")

mesh = getTetraMesh("points.txt", "tetras.txt")

G = getNodalGradientMatrix(mesh)
C = getCurlMatrix(mesh)
D = getDivergenceMatrix(mesh)

# @printf("||G||     = %.3e\n", norm(G,1))
# @printf("||C||     = %.3e\n", norm(C,1))
# @printf("||D||     = %.3e\n", norm(D,1))
@printf("||C * G|| = %.3e\n", norm(C*G,1))
@printf("||D * C|| = %.3e\n", norm(D*C,1))

@test norm(C*G,1) < 1e-12
@test norm(D*C,1) < 1e-12

print("Passed!\n")


using BlockArrays
using ControlSystems
using LinearAlgebra
using PredictiveControl

const MPC = PredictiveControl

# Some utilities for testing
include( "../../src/utilities.jl" )
include( "../testUtils.jl" )

# Create a sample system
A = [1.0 0.0 0.0 0.0;
     0.0 2.0 0.0 0.0;
     0.0 0.0 3.0 0.0;
     0.0 0.0 0.0 4.0];
B = [1.0 0.0;
     0.0 0.0;
     0.0 1.0;
     0.0 0.0];
C = to_matrix( Float64, I, 4 )

sys = StateSpace( A, B, C, 0, 0.1 )

N  = 5
nx = ControlSystems.nstates( sys )
nu = ControlSystems.ninputs( sys )

###################################################################
# Test the propagation of the input through the system
###################################################################

# Horizon must be greater than 1
@test_throws DomainError MPC.prediction( sys, -1 )
@test_throws DomainError MPC.prediction( sys,  0 )

# Form a prediction matrix to test
mat = MPC.prediction( sys, N )

@test blocksize( mat ) == (N, N)
@test size( mat ) == (nx*N, nu*N)

# Make sure the blocks have the proper values for the system
for i = 1:N, j = i:N
    b = view( mat, Block( j, i ) )

    @test size( b ) == (nx, nu)
    @test b == A^(j-i) * B
end

###################################################################
# Test the propagation of the initial state through the controlled
# system to form the new input sequence
###################################################################
K = [1.0 0.0 1.0 0.0;
     0.0 1.0 0.0 1.0]

csys = StateSpace( A - B*K, B, C, 0, 0.1 )

# Horizon must be greater than 1
@test_throws DomainError MPC.inputprediction( csys, K, -1 )
@test_throws DomainError MPC.inputprediction( csys, K,  0 )

# Form a prediction matrix to test
mat = MPC.inputprediction( csys, K, N )

@test blocksize( mat ) == (N, N)
@test size( mat ) == (nu*N, nu*N)

# Make sure the blocks have the proper values for the system
for i = 1:N, j = i:N
    b = view( mat, Block( j, i ) )

    @test size( b ) == (nu, nu)

    if i == j
        @test b == 1.0*I(nu)
    else
        @test b == -K * csys.A^((j-1)-i) * csys.B
    end
end


@doc raw"""
    PiecewisePolynomialKernel(; degree::Int=0, dim::Int)
    PiecewisePolynomialKernel{degree}(dim::Int)

Piecewise polynomial kernel of degree `degree` for inputs of dimension `dim` with support in
the unit ball.

# Definition

For inputs ``x, x' \in \mathbb{R}^d`` of dimension ``d``, the piecewise polynomial kernel
of degree ``v \in \{0,1,2,3\}`` is defined as
```math
k(x, x'; v) = \max(1 - \|x - x'\|, 0)^{\alpha(v,d)} f_{v,d}(\|x - x'\|),
```
where ``\alpha(v, d) = \lfloor \frac{d}{2}\rfloor + 2v + 1`` and ``f_{v,d}`` are
polynomials of degree ``v`` given by
```math
\begin{aligned}
f_{0,d}(r) &= 1, \\
f_{1,d}(r) &= 1 + (j + 1) r, \\
f_{2,d}(r) &= 1 + (j + 2) r + \big((j^2 + 4j + 3) / 3\big) r^2, \\
f_{3,d}(r) &= 1 + (j + 3) r + \big((6 j^2 + 36j + 45) / 15\big) r^2 + \big((j^3 + 9 j^2 + 23j + 15) / 15\big) r^3,
\end{aligned}
```
where ``j = \lfloor \frac{d}{2}\rfloor + v + 1``.

The kernel is ``2v`` times continuously differentiable and the corresponding Gaussian
process is hence ``v`` times mean-square differentiable.
"""
struct PiecewisePolynomialKernel{D,C<:Tuple} <: SimpleKernel
    alpha::Int
    coeffs::C

    function PiecewisePolynomialKernel{D}(dim::Int) where {D}
        dim > 0 || error("number of dimensions has to be positive")
        j = div(dim, 2) + D + 1
        alpha = j + D
        coeffs = piecewise_polynomial_coefficients(Val(D), j)
        return new{D,typeof(coeffs)}(alpha, coeffs)
    end
end

# TODO: remove `maha` keyword argument in next breaking release
function PiecewisePolynomialKernel(; v::Int=-1, degree::Int=0, maha=nothing, dim::Int=-1)
    if v != -1
        Base.depwarn(
            "keyword argument `v` is deprecated, use `degree` instead",
            :PiecewisePolynomialKernel,
        )
        degree = v
    end

    if maha !== nothing
        Base.depwarn(
            "keyword argument `maha` is deprecated, use a `LinearTransform` instead",
            :PiecewisePolynomialKernel,
        )
        dim = size(maha, 1)
        return transform(
            PiecewisePolynomialKernel{degree}(dim), LinearTransform(cholesky(maha).U)
        )
    else
        return PiecewisePolynomialKernel{degree}(dim)
    end
end

piecewise_polynomial_coefficients(::Val{0}, ::Int) = (1,)
piecewise_polynomial_coefficients(::Val{1}, j::Int) = (1, j + 1)
piecewise_polynomial_coefficients(::Val{2}, j::Int) = (1, j + 2, (j^2 + 4 * j)//3 + 1)
function piecewise_polynomial_coefficients(::Val{3}, j::Int)
    return (1, j + 3, (2 * j^2 + 12 * j)//5 + 3, (j^3 + 9 * j^2 + 23 * j)//15 + 1)
end
function piecewise_polynomial_coefficients(::Val{D}, ::Int) where {D}
    return error("invalid degree $D, only 0, 1, 2, or 3 are supported")
end

kappa(κ::PiecewisePolynomialKernel, r) = max(1 - r, 0)^κ.alpha * evalpoly(r, κ.coeffs)

metric(::PiecewisePolynomialKernel) = Euclidean()

function Base.show(io::IO, κ::PiecewisePolynomialKernel{D}) where {D}
    return print(
        io,
        "Piecewise Polynomial Kernel (degree = ",
        D,
        ", ⌊dim/2⌋ = ",
        κ.alpha - 1 - 2 * D,
        ")",
    )
end


function pred_accuracy(preds, obsv)
    return sum(preds .== obsv) / size(preds, 1)
end

function accuracy_measures(obsv::Array{T,1}, preds::Array{T,1}) where {T}
    cm = confusion_matrix(obsv, preds)
    n = sum(cm.matrix)
    if isone(length(cm.classes))
        @warn "Only one class in confusion matrix"
        TN, FN, FP, TP = 0.0, 0.0, 0.0, 1.0
    else
        TN, FN, FP, TP = cm.matrix
    end
    sensitivity = TP / (TP + FN)
    FN_rate = 1 - sensitivity
    specificity = TN / (TN + FP)
    FP_rate = 1 - specificity
    accuracy = (TP + TN) / n
    positive_predictive_power = TP / (TP + FP)
    odds_ratio = (TP * TN) / (FP * FN)
    kappa =
        ((TP + TN) - (((TP + FN) * (TP + FP) + (FP + TN) * (FN + TN)) / n)) /
        (n - (((TP + FN) * (TP + FP) + (FP + TN) * (FN + TN)) / n))
    TSS = 1 - (sensitivity + specificity)
    acc_mes = (
        cm=cm.matrix,
        TN=TN,
        FN=FN,
        FP=FP,
        TP=TP,
        sensitivity=sensitivity,
        FN_rate=FN_rate,
        specificity=specificity,
        FP_rate=FP_rate,
        accuracy=accuracy,
        positive_predictive_power=positive_predictive_power,
        odds_ratio=odds_ratio,
        kappa=kappa,
        TSS=TSS,
    )
    return acc_mes
end

function auc(
    model::Ensemble{S,T}, vld_features::Array{S}, vld_labels::Array{T,1}; plot=true, kw...
) where {S,T}
    presence_proba = apply_forest_proba(model, vld_features, [0, 1])[:, 2]
    thresholds = collect(0.0:0.01:1.0)
    thrsh_preds = [
        Int64.(replace(prob -> prob .>= thrsh ? 1 : 0, presence_proba)) for
        thrsh in thresholds
    ]
    acc_mes = [accuracy_measures(vld_labels, pred) for pred in thrsh_preds]
    FP_rates = map(x -> x.FP_rate, acc_mes)
    sensitivities = map(x -> x.sensitivity, acc_mes)
    score = auc_score(FP_rates, sensitivities)
    if plot
        p = auc_plot(FP_rates, sensitivities, score; kw...)
        return (score=score, FP_rates=FP_rates, sensitivities=sensitivities, plot=p)
    else
        return (score=score, FP_rates=FP_rates, sensitivities=sensitivities)
    end
end
function auc_score(FP_rates, sensitivities)
    FPrev, sensrev = FP_rates[end:-1:1], sensitivities[end:-1:1]
    deltas = FPrev[2:end] .- FPrev[1:(end - 1)]
    areas = deltas .* sensrev[2:end]
    score = sum(areas)
    return score
end
function auc_plot(FP_rates, sensitivities, score; kw...)
    p = plot(
        [0, 1],
        [0, 1];
        xlim=(0, 1),
        ylim=(0, 1),
        title="Receiver operating curve",
        xlabel="False-positive rate (1 - Specificity)",
        ylabel="True-positive rate (Sensitivity)",
        legend=:none,
        aspect_ratio=1,
    )
    plot!(
        p,
        FP_rates,
        sensitivities,
        ann=(0.8, 0.1, "AUC = $(round(score, digits = 3))"),
        kw...,
    )
    return p
end

# From https://github.com/marubontan/MLUtils.jl/blob/master/src/utils.jl
#=
function auc2(yTruth::Array{Int}, yScore::Array{Float64})
    dIndex = findall(1 .== yTruth)
    dnIndex = findall(0 .== yTruth)
    score = 0.0
    for ydn in dnIndex
        for yd in dIndex
            if yScore[yd] > yScore[ydn]
                score += 1
            elseif yScore[ydn] == yScore[yd]
                score += 0.5
            end
        end
    end
    return score / (length(dIndex) * length(dnIndex))
end
yTruth = vld_labels[:, nsp]
yScore = apply_forest_proba(model, vld_features, [0,1])[:,2]
auc2(yTruth, yScore)
=#


#___  ____       ___ _____   _   _       _ _
#|  \/  | |     |_  /  __ \ | | | |     (_| |
#| .  . | |       | | /  \/ | | | |_ __  _| |_ ___
#| |\/| | |       | | |     | | | | '_ \| | __/ _ \
#| |  | | |___/\__/ | \__/\ | |_| | | | | | || (_) |
#_______\_____\____/ \____/  _____|_|___|_____\_____ _____ _____
#| ___ \        (_)         | | \ \ / / / __  |  _  / __  |  _  |
#| |_/ _ __ ___  _  ___  ___| |_ \ V /  `' / /| |/' `' / /| |/' |
#|  __| '__/ _ \| |/ _ \/ __| __|/   \    / / |  /| | / / |  /| |
#| |  | | | (_) | |  __| (__| |_/ /^\ \ ./ /__\ |_/ ./ /__\ |_/ /
#\_|  |_|  \___/| |\___|\___|\__\/   \/ \_____/\___/\_____/\___/
#              _/ |
#             |__/
#
# This code is part of the proposal of the team "MLJC UniTo" - University of Turin
# for "ProjectX 2020" Climate Change for AI.
# The code is licensed under MIT 3.0
# Please read readme or comments for credits and further information.

# Compiler: Julia 1.5

# update NeuralPDE#quadrature_training
using NeuralPDE
using Quadrature, Cubature, Cuba
using Flux, ModelingToolkit, GalacticOptim, Optim, DiffEqFlux
using Plots
using PyPlot
using CUDA
using CUDAdrv

print("Precompiling Done")

# the example is taken from this article https://arxiv.org/abs/1910.10503
@parameters x θ
@variables p(..)
@derivatives Dx'~x
@derivatives Dxx''~x

#2D PDE
α = 0.3
β = 0.5
_σ = 0.5
# Discretization
dx = 0.05
# here we use normalization condition: dx*p(x,θ) ~ 1 in order to get a non-zero solution.
eq  = [(α - 3*β*x^2)*p(x,θ) + (α*x - β*x^3)*Dx(p(x,θ)) ~ (_σ^2/2)*Dxx(p(x,θ)),
       dx*p(x,θ) ~ 1.]

# Initial and boundary conditions
bcs = [p(-2.2,θ) ~ 0. ,p(2.2,θ) ~ 0. , p(-2.2,θ) ~ p(2.2,θ)]

# Space and time domains
domains = [x ∈ IntervalDomain(-2.2,2.2)]

# Neural network
chain = Chain(Dense(1,12,Flux.σ),Dense(12,12,Flux.σ),Dense(12,1)) |>gpu

discretization = NeuralPDE.PhysicsInformedNN(dx,
                                             chain,
                                             strategy= NeuralPDE.GridTraining()) |>gpu

pde_system = PDESystem(eq,bcs,domains,[x],[p])
prob = NeuralPDE.discretize(pde_system,discretization)

cb = function (p,l)
    println("Current loss is: $l")
    return false
end
CUDA.allowscalar(false)
res = GalacticOptim.solve(prob, ADAM(0.1); cb = cb, maxiters=8000)
phi = discretization.phi

analytic_sol_func(x) = 28.022*exp((1/(2*_σ^2))*(2*α*x^2 - β*x^4))

xs = [domain.domain.lower:dx:domain.domain.upper for domain in domains][1]
u_real  = [analytic_sol_func(x) for x in xs]
u_predict  = [first(phi(x,res.minimizer)) for x in xs]

plot(xs ,u_real, label = "analytic")
plot!(xs ,u_predict, label = "predict")


# extracted from Meshes.jl

"""
Evaluation method used to obtain a point along
a Bézier curve from a parametric expression.
"""
abstract type BezierEvalMethod end

"""
Approximate evaluation using Horner's method.
Recommended for a large number of control points,
if you can afford a precision loss.
See https://en.wikipedia.org/wiki/Horner%27s_method.
"""
struct Horner <: BezierEvalMethod end

struct BezierCurve{M<:BezierEvalMethod} <: Curve
  method::M
end

BezierCurve() = BezierCurve(Horner())

degree(p) = length(p) - 1

"""
Apply Horner's method on the monomial representation of the
Bézier curve B = ∑ᵢ aᵢtⁱ with i ∈ [0, n], n the degree of the
curve, aᵢ = binomial(n, i) * pᵢ * t̄ⁿ⁻ⁱ and t̄ = (1 - t).
Horner's rule recursively reconstructs B from a sequence bᵢ
with bₙ = aₙ and bᵢ₋₁ = aᵢ₋₁ + bᵢ * t until b₀ = B.
"""
function (curve::BezierCurve{Horner})(t, points)
  if t < 0 || t > 1
    throw(DomainError(t, "b(t) is not defined for t outside [0, 1]."))
  end
  d = length(points)
  T = eltype(eltype(points))
  t̄ = one(T) - t
  n = degree(points)
  pₙ = last(points)
  aₙ = pₙ

  # initialization with i = n + 1, so bᵢ₋₁ = bₙ = aₙ
  bᵢ₋₁ = aₙ
  cᵢ₋₁ = one(T)
  t̄ⁿ⁻ⁱ = one(T)
  for i in n:-1:1
    cᵢ₋₁ *= i / (n - i + one(T))
    pᵢ₋₁ = points[i]
    t̄ⁿ⁻ⁱ *= t̄
    aᵢ₋₁ = cᵢ₋₁ * pᵢ₋₁ * t̄ⁿ⁻ⁱ
    bᵢ₋₁ = aᵢ₋₁ + bᵢ₋₁ * t
  end

  b₀ = bᵢ₋₁
  b₀
end


"""

```
eulermat(beta, gamma, theta, deg::String)
```

This routine was added because it defines
the rotation in the "human" way, an is thus used
to set the position of the fixed molecules. `deg` can only be `"degree"`, in which
case the angles with be considered in degrees. If no `deg` argument
is provided, radians are used.

That means: `beta` is a counterclockwise rotation around `x` axis.
            `gamma` is a counterclockwise rotation around `y` axis.
            `theta` is a counterclockwise rotation around `z` axis.

"""
function eulermat(beta, gamma, theta, deg::String)

   if deg != "degree"
     error("ERROR: to use radians just omit the last parameter")
   end

   beta = beta*π/180
   gamma = gamma*π/180
   theta = theta*π/180

   return eulermat( beta, gamma, theta )
end

function eulermat(beta::T, gamma::T, theta::T) where T
  c1 = cos(beta)
  s1 = sin(beta)
  c2 = cos(gamma)
  s2 = sin(gamma)
  c3 = cos(theta)
  s3 = sin(theta)
  @SMatrix [   c2*c3                -c2*s3                s2
              (c1*s3 + c3*s1*s2)    (c1*c3 - s1*s2*s3)   -c2*s1
              (s1*s3 - c1*c3*s2)    (c1*s2*s3 + c3*s1)    c1*c2  ]
end


################################################################################
#                          Direction Cosine Matrices
################################################################################

export create_rotation_matrix
export ddcm, dcm2angle, dcm2quat

################################################################################
#                                  Functions
################################################################################

"""
    function create_rotation_matrix(angle::Number, axis::Symbol = :X)

Compute a rotation matrix that rotates a coordinate frame about the axis `axis`
by the angle `angle`. The `axis` must be one of the following symbols: `:X`,
`:Y`, or `:Z`.

# Example

```jldocstest
julia> create_rotation_matrix(pi/2, :X)
3×3 StaticArrays.SArray{Tuple{3,3},Float64,2,9}:
 1.0   0.0          0.0
 0.0   6.12323e-17  1.0
 0.0  -1.0          6.12323e-17
```

"""
function create_rotation_matrix(angle::Number, axis::Symbol = :X)
    cos_angle = cos(angle)
    sin_angle = sin(angle)

    if axis == :X
        dcm = DCM(1,      0,          0,
                  0, +cos_angle, +sin_angle,
                  0, -sin_angle, +cos_angle)'
        return dcm
    elseif axis == :Y
        dcm = DCM(+cos_angle, 0, -sin_angle,
                       0,     1,      0,
                  +sin_angle, 0, +cos_angle)'
        return dcm
    elseif axis == :Z
        dcm = DCM(+cos_angle, +sin_angle, 0,
                  -sin_angle, +cos_angle, 0,
                       0,          0,     1)'
        return dcm
    else
        error("axis must be :X, :Y, or :Z");
    end
end

################################################################################
#                                 Conversions
################################################################################

# Euler Angles
# ==============================================================================

"""
    function dcm2angle(dcm::DCM, rot_seq::Symbol=:ZYX)

Convert the DCM `dcm` to Euler Angles (see `EulerAngles`) given a rotation
sequence `rot_seq`.

The rotation sequence is defined by a `:Symbol`. The possible values are:
`:XYX`, `XYZ`, `:XZX`, `:XZY`, `:YXY`, `:YXZ`, `:YZX`, `:YZY`, `:ZXY`, `:ZXZ`,
`:ZYX`, and `:ZYZ`. If no value is specified, then it defaults to `:ZYX`.

# Example

```julia-repl
julia> D = DCM([1. 0. 0.; 0. 0. -1; 0. -1 0.]);

julia> dcm2angle(D,:XYZ)
ReferenceFrameRotations.EulerAngles{Float64}(1.5707963267948966, 0.0, -0.0, :XYZ)
```

"""
function dcm2angle(dcm::DCM, rot_seq::Symbol=:ZYX)
    if rot_seq == :ZYX

        EulerAngles(atan(+dcm[1,2],+dcm[1,1]),
                    asin(-dcm[1,3]),
                    atan(+dcm[2,3],+dcm[3,3]),
                    rot_seq)

    elseif rot_seq == :XYX

        EulerAngles(atan(+dcm[1,2],-dcm[1,3]),
                    acos(+dcm[1,1]),
                    atan(+dcm[2,1],+dcm[3,1]),
                    rot_seq)

    elseif rot_seq == :XYZ

        EulerAngles(atan(-dcm[3,2],+dcm[3,3]),
                    asin(+dcm[3,1]),
                    atan(-dcm[2,1],+dcm[1,1]),
                    rot_seq)

    elseif rot_seq == :XZX

        EulerAngles(atan(+dcm[1,3],+dcm[1,2]),
                    acos(+dcm[1,1]),
                    atan(+dcm[3,1],-dcm[2,1]),
                    rot_seq)

    elseif rot_seq == :XZY

        EulerAngles(atan(+dcm[2,3],+dcm[2,2]),
                    asin(-dcm[2,1]),
                    atan(+dcm[3,1],+dcm[1,1]),
                    rot_seq)

    elseif rot_seq == :YXY

        EulerAngles(atan(+dcm[2,1],+dcm[2,3]),
                    acos(+dcm[2,2]),
                    atan(+dcm[1,2],-dcm[3,2]),
                    rot_seq)

    elseif rot_seq == :YXZ

        EulerAngles(atan(+dcm[3,1],+dcm[3,3]),
                    asin(-dcm[3,2]),
                    atan(+dcm[1,2],+dcm[2,2]),
                    rot_seq)

    elseif rot_seq == :YZX

        EulerAngles(atan(-dcm[1,3],+dcm[1,1]),
                    asin(+dcm[1,2]),
                    atan(-dcm[3,2],+dcm[2,2]),
                    rot_seq)

    elseif rot_seq == :YZY

        EulerAngles(atan(+dcm[2,3],-dcm[2,1]),
                    acos(+dcm[2,2]),
                    atan(+dcm[3,2],+dcm[1,2]),
                    rot_seq)

    elseif rot_seq == :ZXY

        EulerAngles(atan(-dcm[2,1],+dcm[2,2]),
                    asin(+dcm[2,3]),
                    atan(-dcm[1,3],+dcm[3,3]),
                    rot_seq)

    elseif rot_seq == :ZXZ

        EulerAngles(atan(+dcm[3,1],-dcm[3,2]),
                    acos(+dcm[3,3]),
                    atan(+dcm[1,3],+dcm[2,3]),
                    rot_seq)

    elseif rot_seq == :ZYZ

        EulerAngles(atan(+dcm[3,2],+dcm[3,1]),
                    acos(+dcm[3,3]),
                    atan(+dcm[2,3],-dcm[1,3]),
                    rot_seq)

    else
        throw(ArgumentError("The rotation sequence :$rot_seq is not valid."))
    end
end

# Quaternions
# ==============================================================================

"""
    function dcm2quat(dcm::DCM)

Convert the DCM `dcm` to a quaternion. The type of the quaternion will be
automatically selected by the constructor `Quaternion` to avoid `InexactError`.

# Remarks

By convention, the real part of the quaternion will always be positive.
Moreover, the function does not check if `dcm` is a valid direction cosine
matrix. This must be handle by the user.

This algorithm was obtained from:

    http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/

# Example

```julia-repl
julia> dcm = angle2dcm(pi/2,0.0,0.0,:XYZ);

julia> q   = dcm2quat(dcm)
Quaternion{Float64}:
  + 0.7071067811865476 + 0.7071067811865475.i + 0.0.j + 0.0.k
```

"""
function dcm2quat(dcm::DCM)
    if  tr(dcm) > 0
        # f = 4*q0
        f = 2sqrt(tr(dcm)+1)

        return Quaternion(f/4,
                          (dcm[2,3]-dcm[3,2])/f,
                          (dcm[3,1]-dcm[1,3])/f,
                          (dcm[1,2]-dcm[2,1])/f)

    elseif (dcm[1,1] > dcm[2,2]) && (dcm[1,1] > dcm[3,3])
        # f = 4*q1
        f = 2sqrt(1 + dcm[1,1] - dcm[2,2] - dcm[3,3])

        # Real part.
        q0 = (dcm[2,3]-dcm[3,2])/f

        # Make sure that the real part is always positive.
        s = (q0 > 0) ? +1 : -1

        return Quaternion(s*q0,
                          s*f/4,
                          s*(dcm[1,2]+dcm[2,1])/f,
                          s*(dcm[3,1]+dcm[1,3])/f)

    elseif (dcm[2,2] > dcm[3,3])
        # f = 4*q2
        f = 2sqrt(1 + dcm[2,2] - dcm[1,1] - dcm[3,3])

        # Real part.
        q0 = (dcm[3,1]-dcm[1,3])/f

        # Make sure that the real part is always positive.
        s = (q0 > 0) ? +1 : -1

        return Quaternion(s*q0,
                          s*(dcm[1,2]+dcm[2,1])/f,
                          s*f/4,
                          s*(dcm[3,2]+dcm[2,3])/f)

    else
        # f = 4*q3
        f = 2sqrt(1 + dcm[3,3] - dcm[1,1] - dcm[2,2])

        # Real part.
        q0 = (dcm[1,2]-dcm[2,1])/f

        # Make sure that the real part is always positive.
        s = (q0 > 0) ? +1 : -1

        return Quaternion(s*q0,
                          s*(dcm[1,3]+dcm[3,1])/f,
                          s*(dcm[2,3]+dcm[3,2])/f,
                          s*f/4)
    end
end

################################################################################
#                                  Kinematics
################################################################################

"""
    function ddcm(Dba::DCM, wba_b::AbstractArray)

Compute the time-derivative of the DCM `dcm` that rotates a reference frame `a`
into alignment to the reference frame `b` in which the angular velocity of `b`
with respect to `a`, and represented in `b`, is `wba_b`.

# Returns

The time-derivative of the DCM `Dba` (3x3 matrix of type `SMatrix{3,3}`).

# Example

```julia-repl
julia> D = DCM(Matrix{Float64}(I,3,3));

julia> ddcm(D,[1;0;0])
3×3 StaticArrays.SArray{Tuple{3,3},Float64,2,9}:
 0.0   0.0  0.0
 0.0   0.0  1.0
 0.0  -1.0  0.0
```

"""
function ddcm(Dba::DCM, wba_b::AbstractArray)
    # Auxiliary variable.
    w = wba_b

    # Check the dimensions.
    if length(wba_b) != 3
        throw(ArgumentError("The angular velocity vector must have three components."))
    end

    wx = SMatrix{3,3}(  0  , -w[3], +w[2],
                      +w[3],   0  , -w[1],
                      -w[2], +w[1],   0  )'

    # Return the time-derivative.
    -wx*Dba
end


########## Parameters for scripts ##########

###### bspline-MLE ######

#### cross-timescale ####

cscl = Int(Npd*sfct)  # central scale
wscl = 10  # half scale window width, e.g. <=10
minscl, maxscl = max(1, cscl-wscl), cscl+wscl
# maxscl = Npd - 0*Npd÷4
# # minscl = maxscl ÷ 2
# minscl = maxscl - Npd÷2

wfct = 40  # factor of moving window

parms = (
    estimator = "bspline_MLE_timescale",
    method = :optim,  # method of optimizer

    # Parameters for B-Spline wavelet transform
    scalefactor = sfct,
    sclrng = 2*(minscl:maxscl),  # index range of inertial scales
    # sclrng = 2*[minscl,maxscl],  # index of working scale
    vm = 1,  # vanishing moment of the wavelet
    fmode = :causal,  # mode of filtration
    # ρ = 0,  # threshold for truncation of small eigen values in the covariance matrix
    partial = true,  # true for whittle estimator

    dfct = cscl,  # down-sampling factor
    ssize = wfct,  # size of sub window, set ssize=wsize/dfct to disable sub window
    dlen = 1,  # length of decorrelation of sub window

    windowfactor = wfct,
    wsize = cscl*wfct,  # size of moving window
    pov = Npd÷1,  # step of moving window
    boundary = :hard,  # boundary condition
)

base_estimator = x -> FracFin.bspline_MLE_estim(x, parms.sclrng, parms.vm, parms.ssize, parms.dfct, parms.dlen; partial=parms.partial)


export Pade21

"""
    Pade21()

This uses a Padé-approximation of type (2,1) for prediction.
"""
struct Pade21 <: AbstractPredictor end
struct Pade21Cache{T, AV<:AbstractVector{T}} <: AbstractPredictorCache
    x²::Vector{T}
    x³::Vector{T}
    x_h::AV

    u::Vector{T}
    u₁::Vector{T}
    u₂::Vector{T}

    h₂::Float64
    h₃::Float64
end

function cache(::Pade21, H, x, ẋ, t)
    x², x³, x_h = copy(ẋ), copy(ẋ), copy(x)
    u = evaluate(H, x, t)
    u₁, u₂ = copy(u), copy(u)
    h₂ = nthroot(eps(), 2+2) # x² is the second order derivative and has a 2nd order approximation
    h₃ = nthroot(eps(), 3+2) # x³ is the third order derivative and has a 2nd order approximation
    Pade21Cache(x², x³, x_h, u, u₁, u₂, h₂, h₃)
end
@inline function g!(u, H, x, ẋ, t, h, x_h)
    @inbounds for i in eachindex(x)
        x_h[i] = x[i] + h * ẋ[i]
    end
    evaluate!(u, H, x_h, t + h)
end

@inline function g!(u, H, x, ẋ, ẍ, t, h, x_h)
    h² = 0.5 * h * h
    @inbounds for i in eachindex(x)
        x_h[i] = x[i] + h * ẋ[i] + h² * ẍ[i]
    end
    evaluate!(u, H, x_h, t + h)
end

function update!(cache::Pade21Cache, H, x, ẋ, t, Jac::Jacobian)
    # unpack stuff to make the rest easier to read
    u, u₁, u₂ = cache.u, cache.u₁, cache.u₂
    x_h, h₂, h₃ = cache.x_h, cache.h₂, cache.h₃
    x², x³ = cache.x², cache.x³

    h₂ *= max(abs(t), 1.0)
    # compute the second derivative at (x,t)
    g!(u₁, H, x, ẋ, t, h₂, x_h)
    g!(u₂, H, x, ẋ, t, -h₂, x_h)

    h² = h₂^2
    @inbounds for i in eachindex(u₁)
        u[i] = -(u₁[i] + u₂[i]) / h²
    end
    solve!(x², Jac, u)

    h₃ *= max(abs(t), 1.0)
    g!(u₁, H, x, ẋ, x², t, h₃, x_h)
    g!(u₂, H, x, ẋ, x², t, -h₃, x_h)

    h³ = h₃^3
    @inbounds for i in eachindex(u₁)
        u[i] = -3(u₁[i] - u₂[i]) / h³
    end
    solve!(x³, Jac, u)

    cache
end

function predict!(xnext, ::Pade21, cache::Pade21Cache, H::HomotopyWithCache, x, t, Δt, ẋ, Jac::Jacobian)
    x², x³ = cache.x², cache.x³
    @inbounds for i in eachindex(x)
        δ = 1 - Δt * x³[i] / (3 * x²[i])
        if isnan(δ) || iszero(δ)
            xnext[i] = x[i] + Δt * ẋ[i] + 0.5 * Δt^2 * x²[i]
        else
            xnext[i] = x[i] + Δt * ẋ[i] + (0.5 * Δt^2 * x²[i]) / δ
        end
    end
    nothing
end

order(::Pade21) = 4


function eye(middlesizes::Matrix{Int64})

    cores = Vector{Array{Float64,4}}(undef,size(middlesizes,2));
    for i = 1:size(middlesizes,2)
        core = Matrix(I,(middlesizes[1,i],middlesizes[2,i]));
        core = reshape(core, (1, middlesizes[1,i],middlesizes[2,i], 1) );
        cores[i] = convert(Array{Float64}, core);
    end

    MPT(cores)
end


export LogHomotopy

"""
    LogHomotopy(H::AbstractHomotopy)

Reparametrizes the homotopy ``H(x,t)`` as ``Ĥ(x,s) := H(x,e^{-s})``.
"""
struct LogHomotopy{H<:AbstractHomotopy} <: AbstractHomotopy
    homotopy::H
end

Base.size(H::LogHomotopy) = size(H.homotopy)
basehomotopy(H::LogHomotopy) = basehomotopy(H.homotopy)

struct LogHomotopyCache{HC} <: AbstractHomotopyCache
    cache::HC
end

function cache(H::LogHomotopy, x, s)
    t = exp(-s)
    c = cache(H.homotopy, x, t)
    LogHomotopyCache(c)
end

@propagate_inbounds function evaluate!(u, H::LogHomotopy, x, s, c::LogHomotopyCache)
    evaluate!(u, H.homotopy, x, exp(-s), c.cache)
end
@propagate_inbounds function jacobian!(U, H::LogHomotopy, x, s, c::LogHomotopyCache)
    jacobian!(U, H.homotopy, x, exp(-s), c.cache)
end
@propagate_inbounds function dt!(u, H::LogHomotopy, x, s, c::LogHomotopyCache)
    t = exp(-s)
    dt!(u, H.homotopy, x, t, c.cache)
    LinearAlgebra.rmul!(u, -t)
    u
end

@propagate_inbounds function evaluate_and_jacobian!(
    u,
    U,
    H::LogHomotopy,
    x,
    s,
    c::LogHomotopyCache,
)
    evaluate_and_jacobian!(u, U, H.homotopy, x, exp(-s), c.cache)
end

@propagate_inbounds function jacobian_and_dt!(
    U,
    u,
    H::LogHomotopy,
    x,
    s,
    c::LogHomotopyCache,
)
    t = exp(-s)
    jacobian_and_dt!(U, u, H.homotopy, x, t, c.cache)
    LinearAlgebra.rmul!(u, -t)

    nothing
end

function evaluate(H::LogHomotopy, x, s, c::LogHomotopyCache)
    evaluate(H.homotopy, x, exp(-s), c.cache)
end
function jacobian(H::LogHomotopy, x, s, c::LogHomotopyCache)
    jacobian(H.homotopy, x, exp(-s), c.cache)
end
function dt(H::LogHomotopy, x, s, c::LogHomotopyCache)
    t = exp(-s)
    u = dt(H.homotopy, x, t, c.cache)
    LinearAlgebra.rmul!(u, -t)
    u
end


using MarketTechnicals, MarketData, FactCheck

facts("Momentum") do

    context("rsi") do
        @fact rsi(cl, 14).values[end]              --> roughly(55.84921708898833) # TTR value is 55.84922
        @fact rsi(cl, 14, wilder=true).values[end] --> roughly(55.95931797240996) # TTR value is 55.95932
        @fact rsi(cl, 14).timestamp[end]           --> Date(2001,12,31)
    end

    context("macd") do
        @fact macd(cl).values[end, 1] --> roughly(0.42117515229271874) # TTR value with percent=FALSE is 0.421175152
        @fact macd(cl).values[end, 2] --> roughly(0.44142752613313185) # TTR value with percent=FALSE is 0.4414275
        @fact macd(cl).timestamp[end] --> Date(2001,12,31)
    end

#     context("cci") do
#         @fact cci(ohlc).values[1]      --> roughly(-38.931614)      # TTR::CCI value is -38.931614
#         @fact cci(ohlc).values[end]    --> roughly(46.3511339)      # TTR::CCI value is 46.3511339
#         @fact cci(ohlc).timestamp[end] --> Date(2001,12,31)
#     end
end