@enum(Vexity, VEX_CONST, VEX_AFFINE, VEX_CONVEX, VEX_CONCAVE, VEX_NONDCP, VEX_UNSET)

function -(v::Vexity)
    if v == VEX_CONVEX
        return VEX_CONCAVE
    elseif v == VEX_CONCAVE
        return VEX_CONCAVE
    end
    return v
end

function +(v::Vexity, w::Vexity)
    if (v == VEX_CONST) && (w == VEX_CONST)            # constant times other vexity cases
        return v
    elseif (v == VEX_CONST) && (w == VEX_NONDCP)
        return w
    elseif (v == VEX_NONDCP) && (w == VEX_CONST)
        return v
    elseif (v == VEX_CONST)
        return w
    elseif (w == VEX_CONST)
        return v
    elseif (v == VEX_AFFINE) && (w == VEX_AFFINE)      # affine times other vexity cases
        return v
    elseif (v == VEX_AFFINE) && (w == VEX_CONVEX)
        return w
    elseif (v == VEX_CONVEX) && (w == VEX_AFFINE)
        return v
    elseif (v == VEX_AFFINE) && (w == VEX_CONCAVE)
        return w
    elseif (v == VEX_CONCAVE) && (w == VEX_AFFINE)     # remaining vexity cases
        return v
    elseif (v == VEX_CONVEX) && (w == VEX_CONVEX)
        return v
    elseif (v == VEX_CONCAVE) && (w == VEX_CONCAVE)
        return v
    elseif (v == VEX_CONCAVE) && (w == VEX_CONVEX)
        return VEX_NONDCP
    end
    return VEX_NONDCP                                  # (v == VEX_CONVEX) && (w == VEX_CONCAVE)
end

curvature(f, x::Interval) = error("curvature(f,x) for $f is not implemented.")

function curvature(d2f::Interval{T}, x::Interval{T}) where T
    if zero(T) <= d2f
        return VEX_CONCAVE
    elseif zero(T) >= d2f
        return VEX_CONVEX
    elseif isatomic(x)
        return VEX_CONST
    end
    return VEX_NONDCP
end

for F in (positive, negative, lower_bnd, upper_bnd, bnd, rad2deg, deg2rad)
    @eval curvature(::typeof($F), x::Interval) = VEX_AFFINE
end

curvature(::typeof(+), x::Interval, y::Interval, args...) = VEX_CONST
curvature(::typeof(+), x::Interval) = VEX_CONST

curvature(::typeof(-), x::Interval, y::Interval) = VEX_AFFINE
curvature(::typeof(-), x::Interval) = VEX_AFFINE

for F in (abs, abs2, exp, exp2, exp10, expm1, leaky_relu, xlogx, cosh)
    @eval curvature(::typeof($F), x::Interval) = VEX_CONVEX
end

for F in (log, log2, log10, log1p, acosh, sqrt)
    @eval curvature(::typeof($F), x::Interval) = VEX_CONCAVE
end

curvature(::typeof(sin), x::Interval) = curvature(-sin(x), x)
curvature(::typeof(cos), x::Interval) = curvature(-cos(x), x)
curvature(::typeof(tan), x::Interval) = curvature(2*tan(x)*sec(x)^2, x)

for F in (asin, acot, sinh, csch, coth, atanh, acsch, xabsx, erfinv, erfc)
    @eval function curvature(::typeof($F), x::Interval)
        if x <= zero(T)
            return VEX_CONCAVE
        elseif x >= zero(T)
            return VEX_CONVEX
        end
        return VEX_NONDCP
    end
end

for F in (acos, atan, cbrt, tanh, erf, erfcinv)
    @eval function curvature(::typeof($F), x::Interval)
        if x <= zero(T)
            return VEX_CONVEX
        elseif x >= zero(T)
            return VEX_CONCAVE
        end
        return VEX_NONDCP
    end
end

function curvature(::typeof(xexpax), x::Interval{T}, a::Interval{T}) where T
    if isatomic(a)
        inflect_pnt = -2/a.lo
        if inflect_pnt < x
            return VEX_CONVEX
        elseif inflect_pnt > x
            return VEX_CONCAVE
        end
    end
    return VEX_NONDCP
end

# TODO: HANDLE ARGMAX/ARGMIN IS CONSTANT CASE -> VEXAFFINE CASE... (need z interval bounds?)
curvature(::typeof(max), x::Interval{T}, args...) = VEX_CONVEX
curvature(::typeof(min), x::Interval{T}, args...) = VEX_CONCAVE

function curvature(::typeof(relu), x::Interval{T}) where T
    if x >= zero(T)
        return VEX_CONVEX
    end
    return VEX_CONST
end

# TODO: sech
# TODO: maxtanh, pentanh, sigmoid, bisigmoid, softsign, softplus, elu


using ValueOrientedRiskManagementInsurance
using DataFrames
using Test
using Distributions

println("start NonLife.jl...")

function logpar2statpar(m_s::Vector)
  Real[exp(m_s[1] + m_s[2]^2 / 2),
       √(exp(m_s[2]^2)-1) * exp(m_s[1] + m_s[2]^2 / 2)]
end

function statpar2logpar(μ_σ::Vector)
  vc = μ_σ[2]/μ_σ[1]
  Real[log(μ_σ[1]) - 0.5 * log(1+vc^2), √(log(1+vc^2))]
end

"""
   dr = ( θ(t)-ar ) dt + σ dW
"""
mutable struct HullWhite
  t₀::Real
  T::Real
  δt::Real
  θ::Vector{Real}
  a::Real
  σ::Real
end

function HullWhite(t₀::Real,
                   δt::Real,
                   θ::Vector{Real},
                   a::Real,
                   σ::Real)
  T = t₀ + length(θ) * δt
  HullWhite(t₀, T, δt, θ, a, σ)
end

periods_int(t::Real, hw::HullWhite) = int(div(t, hw.Δt))
periods_frac(t::Real, hw::HullWhite) = t/hw.Δt - period_int(t, hw)

function r(rstart::Real, k::Int, Δk::Int, hw::HullWhite)
  μ =
    exp(-hw.a*Δk*hw.δt)/hw.a *
    (hw.a * rstart  +
     sum([(exp(-hw.a*𝑗*hw.δt) - exp(-hw.a*(𝑗-1)*hw.δt)) *
           hw.θ[k+𝑗-1] for 𝑗 ∈ 1:Δk]))
  std = hw.σ * √( (1-2exp(-2hw.a*Δk*hw.δt)) / (2hw.a) )
  Normal(μ, std)
end

nllobs = [:fire, :liab, :theft]
n_nl = length(nllobs)
nl_names = [ucfirst(string(nllobs[𝑖])) for 𝑖 ∈ 1:n_nl]
df_claims = Vector(n_nl)
claims = Vector(n_nl)
res = Vector(n_nl)
β = Vector(n_nl)

claimpath = "test/NonLife_Input_Claims_"
for 𝑖 ∈ 1:n_nl
  df_claims[𝑖] =
    CSV.read(claimpath * nl_names[𝑖]  * ".csv",
              header = false)
  res[𝑖] = Mack(df_claims[𝑖])
  β[𝑖] = res[𝑖].futureclaims / sum(res[𝑖].futureclaims)
end

#
# mean, sde -> logmean, logsigma
# R[i] = LogNormal(log_mean[i], log_sigma[i])
# v⋅mack[i].β * R[i]
#
# v: stochastic discont
# β
# R[i] = stochastic total undiscounted future paiments,
#

println("...end NonLife.jl")


using Survival: KaplanMeier
using Recombinase: Analysis
function _survival(times, events; axis = discrete_axis(times, npoints = npoints), kwargs...)
    km = fit(KaplanMeier, times, events)
    surv = zeros(length(axis))
    for (i, ax) in enumerate(axis)
        if ax < km.times[1]
            surv[i] = 1
        elseif ax > km.times[end]
            surv[i] = km.survival[end]
        else
            surv[i] = km.survival[searchsortedfirst(km.times, ax)]
        end
    end
    return zip(axis, surv)
end

const survival = Analysis((; discrete = _survival))


# Product between PolyVar and Monomial -> Monomial
function Base.:(*)(x::PolyVar{true}, y::PolyVar{true})
    if x === y
        Monomial{true}([x], [2])
    else
        Monomial{true}(x > y ? [x,y] : [y,x], [1,1])
    end
end
function multiplyvar(v::Vector{PolyVar{true}}, x::PolyVar{true})
    i = findfirst(w->w <= x, v)
    if (i != nothing && i > 0) && v[i] == x
        multiplyexistingvar(v, x, i)
    else
        insertvar(v, x, (i == nothing || i == 0) ? length(v)+1 : i)
    end
end
function Base.:(*)(x::PolyVar{true}, y::Monomial{true})
    w, updatez = multiplyvar(y.vars, x)
    Monomial{true}(w, updatez(y.z))
end
Base.:(*)(y::MonomialVector{true}, x::PolyVar{true}) = x * y
function Base.:(*)(x::PolyVar{true}, y::MonomialVector{true})
    w, updatez = multiplyvar(y.vars, x)
    MonomialVector{true}(w, updatez.(y.Z))
end
function multdivmono!(output_variables::Vector{PolyVar{true}},
                      v::Vector{PolyVar{true}}, x::Monomial{true}, op)
    if v == x.vars
        updatez! = (output, z) -> @. output = op(z, x.z)
    else
        w, maps = mergevars([v, x.vars])
        n = length(v)
        resize!(output_variables, length(w))
        output_variables[maps[1]] = v[1:n]
        updatez! = (output, z) -> begin
            resize!(output, length(w))
            z[maps[1]] = z[1:n]
            I = maps[1]; i = 1; lI = length(I)
            J = maps[2]; j = 1; lJ = length(J)
            while i <= lI || j <= lJ
                if i > lI || (j <= lJ && J[j] < I[i])
                    output[J[j]] = op(0, x.z[j])
                    j += 1
                elseif j > lJ || (i <= lI && I[i] < J[j])
                    output[I[i]] = op(z[I[i]], 0)
                    i += 1
                else
                    @assert I[i] == J[j]
                    output[I[i]] = op(z[I[i]], x.z[j])
                    i += 1
                    j += 1
                end
            end
        end
    end
    return updatez!
end
function multdivmono(v::Vector{PolyVar{true}}, x::Monomial{true}, op)
    if v == x.vars
        w = copy(v)
        updatez = z -> op.(z, x.z)
    else
        w, maps = mergevars([v, x.vars])
        updatez = z -> begin
            newz = zeros(Int, length(w))
            I = maps[1]; i = 1; lI = length(I)
            J = maps[2]; j = 1; lJ = length(J)
            while i <= lI || j <= lJ
                if i > lI || (j <= lJ && J[j] < I[i])
                    newz[J[j]] = op(0, x.z[j])
                    j += 1
                elseif j > lJ || (i <= lI && I[i] < J[j])
                    newz[I[i]] = op(z[i], 0)
                    i += 1
                else
                    @assert I[i] == J[j]
                    newz[I[i]] = op(z[i], x.z[j])
                    i += 1
                    j += 1
                end
            end
            newz
        end
    end
    w, updatez
end
function MP.mapexponents_to!(output::Monomial{true}, f::Function, x::Monomial{true}, y::Monomial{true})
    updatez! = multdivmono!(output.vars, x.vars, y, f)
    updatez!(output.z, x.z)
    return x
end
function MP.mapexponents!(f::Function, x::Monomial{true}, y::Monomial{true})
    updatez! = multdivmono!(x.vars, x.vars, y, f)
    updatez!(x.z, x.z)
    return x
end
function MP.mapexponents(f::Function, x::Monomial{true}, y::Monomial{true})
    w, updatez = multdivmono(x.vars, y, f)
    Monomial{true}(w, updatez(x.z))
end
function Base.:(*)(x::Monomial{true}, y::MonomialVector{true})
    w, updatez = multdivmono(y.vars, x, +)
    MonomialVector{true}(w, updatez.(y.Z))
end
Base.:(*)(y::MonomialVector{true}, x::Monomial{true}) = x * y
Base.:(*)(x::Monomial{true}, y::PolyVar{true}) = y * x


N = 30
ph_constrained = discrete_phs(N, 0, 1);
set_constraint!(ph_constrained, ph_constrained.B[:,1], ph_constrained.D[1:1,1:1]);
eigval, eigvec = eigen(ph_constrained)
num_freq = frequencies(eigval)
exact_freq = pi*collect(-0:1:N-1);
err = exact_freq - num_freq;

@test abs(err[2]) < 1e-13

ph_c_elim = constraint_elimination(ph_constrained)
eigval, eigvec = eigen(ph_c_elim);
num_freq = frequencies(eigval)
exact_freq = pi*collect(0:1:N-1);
err = exact_freq - num_freq;

@test abs(err[2]) < 1e-13


###################################################
# METODO DE PONTOS INTERIORES PRIMAL DUAL AFIM
# ESCALA PARA PL's NA FORMA PADRÃO
#
# min c^t x
# s.a. Ax = b
#       x >= 0
#
# Autor: Leonardo D. Secchin
# Data : 24/06/2021
# leonardosecchin.github.io/topicospo/
#
# Entrada:
#    -- Obrigatórios --
#    A,b,c:   dados do problema (esparsos ou não)
#
#    -- Opcionais --
#    maxit : número máximo de iterações (padrão = 100)
#    eps   : precisao para parada com sucesso (padrão = 1e-8)
#    tau   : ajuste de passos (padrão = 0.99995)
#    epsz  : epsilon para z inicial (padrão = 30)
#    saidas: imprimi saídas na tela? true(padrão) ou false
#
# Saida:
#    x:       solução ou último iterando
#    f:       valor final da FO
#    iter:    número de iterações executadas
#    status:
#       0 = sucesso
#       1 = problema ilimitado ou inviável
#       2 = maxit atingido
#       3 = A tem posto numericamente deficiente
#       4 = erro em Cholesky de ADA' (ADA' numericamente singular?)
#       9 = outro erro
#
# Para testar o método nos problemas pré-selecionados, execute
#   testar()
# ou
#   testar(saidas=true)
# Esta função fará o download dos problemas automaticamente.
#
# Exemplos de uso da função principal:
#   pdafimescala(A,b,c)
#   x, f, iter, status = ptoint(A,b,c, maxit=100, eps=1e-8, tau=0.99995, epsz=30, saidas=true)
###################################################

# pacotes para manipulação matricial
using LinearAlgebra, SparseArrays

# pacotes impressão e gravação de resultados
using Printf, DataFrames

# pacote para download de matrizes de https://sparse.tamu.edu
using MatrixDepot

################################
# MÉTODO PRIMAL DUAL AFIM ESCALA
################################

function pdafimescala(A,b,c; maxit=100, eps=1e-8, tau=0.99995, epsz=30, saidas=true)

    # Lê dimensões
    m,n = size(A)

    # Verifica se as dimensões dos dados são compatíveis
    if (length(b) != m) | (length(c) != n) | (m > n)
        error("Dados com dimensões incompatíveis!")
    end

    # Certifica-se que b e c são matrizes coluna
    if size(b)[1] == 1; b = Vector(b'[:,1]); end
    if size(c)[1] == 1; c = Vector(c'[:,1]); end

    #####################
    # Inicialização
    #####################

    status = 9
    iter   = 0
    viab   = Inf
    KKT    = Inf

    # Realoca A em estrutura esparsa, caso não esteja
    A = sparse(A)

    # c e b são vetores cheios
    c = Vector(c[:,1])
    b = Vector(b[:,1])

    # Guarda normas 1 de b e c para teste de convergência
    normb = norm(b,1)
    normc = norm(c,1)

    # Tenta fatorar A*A' por Cholesky.
    # Caso falhe, é porque A não tem posto completo. Neste caso retorna erro.
    # Caso tenha sucesso, capturamos a permutação p obtida por AMD.
    # "Symmetric" garante que internamente a matriz seja tratada como simétrica perfeita.
    # check=true faz cholesky retornar erro caso G*G' != P'(A*A')P
    C = []

    try
        C  = cholesky(Symmetric(sparse(A*A')), check=true, perm=nothing)
    catch
        println("A tem posto numericamente deficiente. Não se pode continuar!")
        status = 3

        return NaN,NaN,iter,viab,KKT,status
    end

    pAMD = C.p  # permutação de AMD

    # A permutação usada internamente por cholesky é gravada em C.p.
    # Assim, temos
    #   G*G' = (A*A')[C.p,C.p]
    # Dado um vetor v,
    #   v[C.p]
    # retorna P'*v. Para retornar P*v, precisamos da permutação
    # inversa de C.p:
    invpAMD = Vector(1:m)
    invpAMD[pAMD] = Vector(1:m)

    #####################
    # Ponto inicial
    #####################

    # Calcula A'(AA')^-1 b
    xbar = A'*((A*A')\b)

    # x, y, z iniciais
    x = max.( xbar , maximum( [ -minimum(xbar); 100; 0.01*normb/norm(A,1) ] ) )
    y = zeros(m,1)
    z = abs.(c) + epsz * (c .>= 0)
    z[(c .> -epsz) .& (c .< 0)] .= epsz

    # x, y, z, resíduos e direções são vetores cheios ao longo do processo
    x = Vector(x[:,1])
    y = Vector(y[:,1])
    z = Vector(z[:,1])

    # avalia FO primal f(x)=c^t x no ponto inicial
    f = dot(c,x)

    # Limite para critério de inviabilidade
    M = 1e+6 * norm([x;z],1)

    # inicia passos = Inf para correta impressão da 1a iteração
    alphap = Inf
    alphad = Inf

    # LOOP PRINCIPAL
    while iter < maxit

        # Calcula resíduos
        rp = b - A*x
        rd = c - A'*y - z
        rc = -x.*z  # =-XZe=-(x1z1, ... ,xnzn)

        #####################
        # Critério de parada
        #####################

        # Teste de convergência KKT
        KKTp = norm(rp,1)/(1 + normb)  # viabilidade primal relativa
        KKTd = norm(rd,1)/(1 + normc)  # viabilidade dual relativa
        fd   = dot(b,y)
        KKTc = abs(f - fd)/(1 + abs(f) + abs(fd))  # brecha de dualidade relativa

        KKT  = maximum([KKTp KKTd KKTc])  # medida de otimalidade (viab primal + dual + dualidade)

        viab = norm(rp,1)  # viabilidade primal absoluta

        # imprimi dados da iteração na tela
        if saidas
            if mod(iter,10) == 0
                @printf("\nit     \tf         viab      alphamax  KKT")
                @printf("\n===========================================================\n")
            end
            @printf("%d\t%8.2e  %8.2e  %8.2e  %8.2e\n",
                    iter, f, viab, max(alphap,alphad), KKT)
        end

        # Parar com sucesso?
        if KKT < eps
            println("\n--- SOLUÇÃO ENCONTRADA! ---")
            status = 0

            # retorno da função principal
            return x,f,iter,viab,KKT,status
        end

        # Teste de ilimitabilidade ou inviabilidade: parar com insucesso?
        # TEOREMA: se P é inviável ou ilimitado então |x|->Inf ou |z|->Inf
        # Assim, um critério para "suspeitar" que P não tem solução é |(x,z)| >> 1
        if norm([x,z],1) > M
            println("\n--- Problema provavelmente inviável ou ilimitado ---")
            status = 1
            f = NaN

            # retorno da função principal
            return x,f,iter,viab,KKT,status
        end

        # D = Z^{-1} X  (D será um vetor aqui, pois só precisamos armazenar a diagonal)
        D = x./z

        #####################
        # Direção Newtoniana
        #####################

        # Decomposição de Cholesky de ADA'.
        # spdiagm(0 => D) é a matriz diagonal esparsa. "0=>D" indica diagonal principal = D.
        AD = sparse(A*spdiagm(0 => D))

        # Usa a permutação pAMD calculada no inicio do método
        try
            C  = cholesky(Symmetric(sparse(AD*A')), check=true, perm=pAMD)
        catch
            println("Cholesky de ADA' falhou!")
            status = 4

            # retorno da função principal
            return x,f,iter,viab,KKT,status
        end

        # G será o fator de Cholesky triangular INFERIOR
        # Portanto temos GG' = P'(ADA')P.
        G  = sparse(C.L)

        # (ADA') dy = rp + AD(rd - X^-1 rc)
        #        dx = D(A'dy - rd + X^-1 rc)
        #        dz = X^-1 (rc - Zdx)
        # Temos GG' = P'(ADA')P. Resolvemos então
        #  (i) GG'h = P'(ADA')(Ph) = P'(rp + AD(rd - X^-1 rc))
        # (ii)   dy = Ph
        rcx = rc./x                    # rcx = X^-1 rc
        r   = rd - rcx                 # r  = rd - X^-1 rc
        s   = G\(rp + AD*r)[pAMD]      # Gs = P'(rp + AD(rd - X^-1 tc))
        tdy = G'\s                     # G' tdy = s
        dy  = tdy[invpAMD]             # dy = P tdy
        dx  = AD'*dy - D.*r
        dz  = rcx - (dx.*z)./x

        #####################
        # Tamanhos de passo (<= 1)
        #####################
        xdx = x./dx  # =(x1/dx1, ... ,xn/dxn)
        zdz = z./dz  # =(z1/dz1, ... ,zn/dzn)
        alphap = minimum( [ 1 ; -tau * xdx[dx .< 0] ] )  # .< testa < em cada coordenada
        alphad = minimum( [ 1 ; -tau * zdz[dz .< 0] ] )

        #####################
        # Atualização do ponto corrente
        #####################
        x = x + alphap*dx
        y = y + alphad*dy
        z = z + alphad*dz

        # Recalcula FO primal
        f = dot(c,x)

        iter += 1

    end

    # Neste ponto, maxit foi atingido... retorna último x obtido
    println("\n--- Número máximo de iterações atingido ---")
    status = 2

    return x,f,iter,viab,KKT,status
end
################################
################################

# Função para testes com problemas da NETLIB
# http://www.netlib.org/lp/data/index.html
function testar(; saidas=false)

    # PARAMETROS SUGERIDOS
    maxit = 100
    eps   = 1e-8
    tau   = 0.99995
    epsz  = 30

    # Problemas na forma padrão selecionados da NETLIB
    #   Fonte dos arquivos: grupo "LPnetlib" - https://sparse.tamu.edu/LPnetlib
    netlib = [
        "LPnetlib/lp_adlittle",
        "LPnetlib/lp_afiro",
        "LPnetlib/lp_agg",
        "LPnetlib/lp_agg2",
        "LPnetlib/lp_agg3",
        "LPnetlib/lp_bandm",
        "LPnetlib/lp_beaconfd",
        "LPnetlib/lp_blend",
        "LPnetlib/lp_bnl2",
        "LPnetlib/lp_d2q06c",
        "LPnetlib/lp_e226",
        "LPnetlib/lp_fffff800",
        "LPnetlib/lp_osa_60",
        "LPnetlib/lp_israel",
        "LPnetlib/lp_lotfi",
        "LPnetlib/lp_maros_r7",
        "LPnetlib/lp_osa_07",
        "LPnetlib/lp_osa_14",
        "LPnetlib/lp_osa_30",
        "LPnetlib/lp_sc50a",
        "LPnetlib/lp_sc105",
        "LPnetlib/lp_sc205",
        "LPnetlib/lp_sc50b",
        "LPnetlib/lp_scagr25",
        "LPnetlib/lp_scagr7",
        "LPnetlib/lp_scfxm1",
        "LPnetlib/lp_scfxm2",
        "LPnetlib/lp_scfxm3",
        "LPnetlib/lp_scrs8",
        "LPnetlib/lp_scsd1",
        "LPnetlib/lp_scsd6",
        "LPnetlib/lp_scsd8",
        "LPnetlib/lp_sctap1",
        "LPnetlib/lp_sctap2",
        "LPnetlib/lp_sctap3",
        "LPnetlib/lp_share1b",
        "LPnetlib/lp_share2b",
        "LPnetlib/lp_woodw",
        "LPnetlib/lp_stocfor1",
        "LPnetlib/lp_stocfor2",
        "LPnetlib/lp_stocfor3",
        "LPnetlib/lp_truss",
        "LPnetlib/lpi_itest2", #inviável
        "LPnetlib/lpi_itest6", #inviável
        "LPnetlib/lpi_bgindy", #inviável
        "LPnetlib/lpi_bgprtr", #inviável
        "LPnetlib/lpi_klein1", #inviável
        "LPnetlib/lpi_klein2", #inviável
        "LPnetlib/lpi_klein3"  #inviável
    ];

    # cabeçalho tabela com resultados
    resultados = DataFrame(Prob=[],n=[],m=[],st=[],it=[],f=[],viab=[],KKT=[],tempo=[])

    # executa pontos interiores em cada problema
    for p in netlib

        # lê problema (e baixa caso não tenha sido baixado)
        PL = mdopen(p);
        nomeprob = p[10:end]

        # PL é uma estutura com as seguintes propriedades:
        # PL.A, PL.b : sistema Ax=b
        # PL.c       : coeficiente função objetivo
        # PL.n, PL,m : número de variáveis e restrições
        # PL.nnz     : número de não zeros em A

        PLstatus = -1
        PLiter   = -1
        PLviab   = -1
        PLKKT    = -1
        PLf      = NaN
        PLtempo  = -1

        try
            println("\n==================")
            println("Prob: "*p)
            println("n="*string(PL.n)*", m="*string(PL.m))
            println("nnz em A: "*string(PL.nnz))
            println("==================")

            # aplica pontos interiores com reordenamento AMD
            PLtempo = @elapsed ~, PLf, PLiter, PLviab, PLKKT, PLstatus =
                pdafimescala(PL.A, PL.b, PL.c, maxit=maxit, eps=eps, tau=tau, epsz=epsz, saidas=saidas);

            print("Tempo (seg): ")
            println(PLtempo)

            # grava linha em resultados
            push!(resultados, (nomeprob, PL.n, PL.m, PLstatus, PLiter, PLf, PLviab, PLKKT, PLtempo))

        catch
            println("ERRO ao resolver $(nomeprob)!")
        end

    end

    # ordena resultados por nome do problema (coluna 1)
    sort!(resultados, 1)

    # salva resultados em TXT
    txt = open("resultados.txt", "w")
    write(txt, @sprintf("%s", resultados))
    close(txt)

    # Você pode salvar o resultado em planilhas.
    # Veja https://leonardosecchin.github.io/juliaopt_ex11/
end


using Base.Test
using CUDArt
using CUDNN

include("pool.jl")

using CUDNN: PD, cudnnPoolingForward, CUDNN_POOLING_MAX
psize = 2
pd = PD(2, psize, 0, 1, CUDNN_POOLING_MAX)
x = reshape(Float32[1:20;], 5, 4, 1, 1); tx = CudaArray(x); @show x
y = zeros(Float32, cudnnGetPoolingNdForwardOutputDim(pd, tx)); ty = CudaArray(y); @show y

# y = to_host(cudnnPoolingForward(tx, ty; pd=pd)); @show y
y = to_host(cudnnPoolingForward(tx, ty; pd=pd)); @show y
y2 = kudnnPoolingForward(x,y; window=psize)
@test_approx_eq y y2

using CUDNN: cudnnPoolingBackward
dx = zeros(Float32, size(x)); tdx = CudaArray(dx);
dy = rand(Float32, size(y)); tdy = CudaArray(dy); @show dy
dx = to_host(cudnnPoolingBackward(ty, tdy, tx, tdx; pd=pd)); @show dx
dx2 = kudnnPoolingBackward(y, dy, x, dx; window=psize); @show dx2
@test_approx_eq dx dx2

psize = 8
pd = PD(2, psize, 0, 1, CUDNN_POOLING_MAX)
x = rand(Float32,64,64,3,128); tx = CudaArray(x)
y = zeros(Float32, cudnnGetPoolingNdForwardOutputDim(pd, tx)); ty = CudaArray(y);
y = to_host(cudnnPoolingForward(tx, ty; pd=pd));
y2 = kudnnPoolingForward(x,y; window=psize)
@test_approx_eq y y2

dx = zeros(Float32, size(x)); tdx = CudaArray(dx);
dy = rand(Float32, size(y)); tdy = CudaArray(dy);
dx = to_host(cudnnPoolingBackward(ty, tdy, tx, tdx; pd=pd));
dx2 = kudnnPoolingBackward(y, dy, x, dx; window=psize);
@test_approx_eq dx dx2


function EM_E_z_obs(gate_expert_ll_comp, gate_expert_ll)
    tmp = exp.(gate_expert_ll_comp .- gate_expert_ll)
    tmp[abs.(tmp) .< 1e-15] .= 0.0
    nan2num(tmp, 1/size(gate_expert_ll_comp)[2])
    return tmp
end

function EM_E_z_lat(gate_expert_tn_bar_comp, gate_expert_tn_bar)
    tmp = exp.(gate_expert_tn_bar_comp .- gate_expert_tn_bar)
    tmp[abs.(tmp) .< 1e-15] .= 0.0
    nan2num(tmp, 1/size(gate_expert_tn_bar_comp)[2])
    return tmp
end

# function EM_E_k(gate_expert_tn)
#     return expm1.( - gate_expert_tn )
# end

# function EM_E_k(gate_expert_tn_bar_k)
#     # return exp.( gate_expert_tn_bar_k )
#     return expm1.( - log1mexp.(gate_expert_tn_bar_k) )
# end

function EM_E_k_update(gate_expert_tn)
    return expm1(-gate_expert_tn) > 1e-15 ? expm1(-gate_expert_tn) : 0.0
end

function EM_E_k(gate_expert_tn)
    return EM_E_k_update.( - gate_expert_tn )
end

function EM_E_z_zero_obs_update(lower, prob, ll_vec)
    return lower == 0.0 ? prob/(prob + (1-prob)*exp(ll_vec)) : 0.0
end

function EM_E_z_zero_lat_update(lower, prob, ll_vec)
    return lower > 0.0 ? prob/(prob + (1-prob)*exp(ll_vec)) : 0.0
end

function EM_E_z_zero_obs(yl, p_old, gate_expert_ll_pos_comp)
    return EM_E_z_zero_obs_update.(yl, p_old, gate_expert_ll_pos_comp)
end

function EM_E_z_zero_lat(tl, p_old, gate_expert_tn_bar_pos_comp)
    return EM_E_z_zero_lat_update.(tl, p_old, gate_expert_tn_bar_pos_comp)
end

# EM

function EM_M_dQdα(X, comp_zkz_j, comp_zkz_marg, pp_j)

    return vec(sum(X .* (comp_zkz_j - comp_zkz_marg .* pp_j), dims = 1))

end

function EM_M_dQ2dα2(X, comp_zkz_marg, pp_j, qq_j)
    return -X' * (comp_zkz_marg .* pp_j .* qq_j .* X)
end

function EM_M_α(X, α, z_e_obs, z_e_lat, k_e;
                α_iter_max = 5, penalty = true, pen_α = 5)
    let comp_zkz, comp_zkz_marg, α_new, α_old, iter
        # X, α_old, α_new, z_e_obs, z_e_lat, k_e, α_iter_max, penalty, pen_α, comp_zkz, comp_zkz_marg, iter

        comp_zkz = z_e_obs .+ (k_e .* z_e_lat)
        comp_zkz_marg = vec(sum(comp_zkz, dims = 2))

        α_new = copy(α)
        α_old = copy(α_new) .- Inf

        iter = fill(0, size(α_new)[1])

        for j in 1:(size(α)[1]-1)
            while (iter[j] < α_iter_max) & (sum((α_new[j,:] - α_old[j,:]).^2) > 1e-08)

                α_old[j,:] = α_new[j,:]
                gate_body = X * α_new'
                pp = exp.(LogitGating(α_new, X))
                qqj = exp.(rowlogsumexp(gate_body[:, Not(j)]) - rowlogsumexp(gate_body))

                dQ = EM_M_dQdα(X, comp_zkz[:,j], comp_zkz_marg, pp[:,j]) .- (penalty ? vec(α_new[j,:] ./ pen_α^2) : 0.0)
                dQ2 = EM_M_dQ2dα2(X, comp_zkz_marg, pp[:,j], qqj) - (penalty ? (1.0 ./ pen_α^2) * I(size(α_new)[2]) : (1e-07) * I(size(α_new)[2]))

                α_new[j,:] = α_new[j,:] .- inv(dQ2) * dQ
                iter[j] = iter[j] + 1

            end
        end

        return α_new
    end

end

function EM_M_zero(z_zero_e_obs, z_pos_e_obs, z_zero_e_lat, z_pos_e_lat, k_e)
    num = sum(z_zero_e_obs .+ (z_zero_e_lat .* k_e))
    denom = num + sum(z_pos_e_obs .+ (z_pos_e_lat .* k_e))
    return num/denom
end


module TestGeodesy

using Test
using LinearAlgebra
using Flight.Geodesy, Flight.Attitude

export test_geodesy

function test_geodesy()
    @testset verbose = true "Geodesy" begin
        @testset verbose = true "NVector" begin test_NVector() end
        @testset verbose = true "LatLon" begin test_LatLon() end
        @testset verbose = true "Altitude" begin test_Altitude() end
        @testset verbose = true "Geographic" begin test_Geographic() end
        @testset verbose = true "CartECEF" begin test_CartECEF() end
    end
end

function test_NVector()

    @test NVector() isa NVector #no-argument constructor

    n_e = NVector([1,2,3]) #normalize by default
    @test norm(n_e[:]) ≈ 1
    @test !(norm(NVector([1,2,3], normalization = false)[:]) ≈ 1) #bypass normalization

    #test basic operators
    @test n_e == n_e
    @test n_e ≈ n_e
    @test n_e != -n_e
    @test !(n_e ≈ -n_e)

    q_el = RQuat([1,2,3,4])
    m_el = RMatrix(q_el)
    a_el = RAxAng(q_el)

    #construction from ECEF to LTF rotation
    @test NVector(q_el) ≈ NVector(m_el) ≈ NVector(a_el)

    #LTF constructors and LTF conversion
    r_en = ltf(n_e, 0)
    @test r_en == ltf(n_e)
    @test get_ψ_nl(r_en) ≈ 0

    ψ_nl = π/3
    r_nl = Rz(ψ_nl)
    r_el = r_en ∘ r_nl
    @test get_ψ_nl(r_el) ≈ ψ_nl
    @test get_ψ_nl(RMatrix(r_el)) ≈ ψ_nl
    @test get_ψ_nl(RAxAng(r_el)) ≈ ψ_nl

    #r_en and r_el should return the same NVector
    @test NVector(r_el) ≈ n_e
    @test NVector(RMatrix(r_el)) ≈ n_e
    @test NVector(RAxAng(r_el)) ≈ n_e

    #radii
    @test radii(n_e) isa NamedTuple

end

function test_LatLon()

    @test LatLon() isa LatLon
    @test LatLon(ϕ = π/4, λ = -π/3) isa LatLon
    @test_throws ArgumentError LatLon(ϕ = 3π/2)
    @test_throws ArgumentError LatLon(λ = -5π/2)

    #test basic operators
    latlon = LatLon(ϕ = π/3, λ = -π/6)
    @test latlon ≈ latlon
    @test !(latlon ≈ -latlon)

    #ltf
    @test ltf(latlon) ≈ ltf(NVector(latlon))
    ψ_nl = π/3
    r_el = ltf(latlon, 0) ∘ Rz(ψ_nl)
    @test get_ψ_nl(r_el) ≈ ψ_nl

    #conversion torture test: from LatLon to NVector, to r_en, then r_el, then
    #NVector, back to LatLon
    ϕ_range = range(-π/2, π/2, length = 10)
    λ_range = range(-π, π, length = 10)
    ψ_nl_range = range(-π, π, length = 10)

    ll_array = [LatLon(ϕ, λ) for (ϕ, λ, _) in Iterators.product(ϕ_range, λ_range, ψ_nl_range)]
    ψ_array = [ψ_nl for (_, _, ψ_nl) in Iterators.product(ϕ_range, λ_range, ψ_nl_range)]
    ll_array_test = [ll |> NVector |> ltf |> (r -> r ∘ Rz(ψ_nl)) |> NVector |> LatLon for (ll, ψ_nl) in zip(ll_array, ψ_array)]

    @test all(ll_array .≈ ll_array_test)

end

function test_Altitude()

    h_ellip = Altitude{Ellipsoidal}(1500)
    h_orth = Altitude{Orthometric}(1500)
    Δh = 300
    l2d = NVector([3,1,-5])

    #cannot convert from one datum to another without a 2D location
    @test_throws MethodError Altitude{Ellipsoidal}(h_orth)
    @test_throws MethodError Altitude{Orthometric}(h_ellip)

    #however, with a 2D location we can convert back and forth
    @test Altitude{Orthometric}(Altitude{Ellipsoidal}(h_orth, l2d), l2d) == h_orth
    @test Altitude{Orthometric}(h_ellip, l2d) isa Altitude{Orthometric}
    @test Altitude{Ellipsoidal}(h_orth, l2d) isa Altitude{Ellipsoidal}
    @test Altitude{Geopotential}(h_ellip, l2d) isa Altitude{Geopotential}
    @test Altitude{Geopotential}(h_orth) isa Altitude{Geopotential}

    #operations and conversions
    @test (h_ellip + Δh) isa AltEllip
    @test (h_orth + Δh) isa AltOrth
    @test Float64(h_ellip + Δh) == Float64(h_ellip) + Δh
    @test Float64(h_orth + Δh) == Float64(h_orth) + Δh

    @test h_ellip - h_ellip/2 isa Float64
    @test h_ellip - h_ellip/2 == h_ellip/2
    @test h_ellip + Float64(h_ellip/2) isa AltEllip
    @test 0.7h_ellip isa AltEllip
    @test h_ellip/2 isa AltEllip

    @test h_ellip + Float64(h_ellip/2) == 3h_ellip/2
    @test h_ellip + Float64(h_ellip/2) ≈ 3h_ellip/2
    @test h_ellip >= h_ellip
    @test h_ellip <= h_ellip
    @test !(h_ellip < h_ellip)
    @test !(h_ellip > h_ellip)

    @test h_orth == 1500
    @test h_orth ≈ 1500
    @test h_orth >= 1500
    @test h_orth <= 1500
    @test !(h_orth < 1500)
    @test !(h_orth > 1500)

end

function test_Geographic()

    p_nve = Geographic(alt = AltOrth(1500))
    p_llo = Geographic{LatLon,Orthometric}(p_nve)
    @test p_nve isa Geographic{NVector,Orthometric}
    @test Geographic(l2d = LatLon(), alt = AltOrth()) isa Geographic{LatLon,Orthometric}

    #conversion
    @test Geographic{NVector,Ellipsoidal}(p_llo) isa Geographic{NVector,Ellipsoidal}

    @test_throws ArgumentError p_llo == p_llo
    @test p_nve == p_nve #strict equality only defined for Geographic{NVector}
    @test p_llo ≈ p_llo
    @test p_llo ≈ p_nve
    @test -(-p_llo) ≈ p_llo #requires CartECEF
    @test -(-p_nve) ≈ p_nve #requires CartECEF

    @test ltf(p_nve, π/3) == ltf(p_nve.l2d, π/3)
    @test radii(p_nve) == radii(p_nve.l2d)
    @test gravity(p_llo) ≈ gravity(p_nve)
    @test g_n(p_llo) ≈ g_n(p_nve)
    @test G_n(p_llo) ≈ G_n(p_nve)

end

function test_CartECEF()

    #construction from Geographic
    p_nvo = Geographic(NVector([3,1,-3]), AltOrth(10000))
    p_lle = Geographic{LatLon,Ellipsoidal}(p_nvo)
    r = CartECEF(p_nvo)
    @test r ≈ CartECEF(p_lle)

    @test r == r
    @test -(-r) == r
    @test !(-r == r)
    @test r ≈ r

    #conversion torture test
    ftest(p) = p |> CartECEF |> Geographic{NVector,Ellipsoidal} |> CartECEF |>
                Geographic{NVector,Orthometric} |> CartECEF |>
                Geographic{LatLon, Ellipsoidal} |> Geographic{LatLon, Geopotential} |>
                Geographic{LatLon, Ellipsoidal}

    ϕ_range = range(-π/2, π/2, length = 10)
    λ_range = range(-π, π, length = 10)
    h_range = range(Geodesy.h_min + 200, 10000, length = 10)

    p_array = [Geographic(LatLon(ϕ, λ), AltOrth(h)) for (ϕ, λ, h) in Iterators.product(ϕ_range, λ_range, h_range)]
    p_array_test = [ftest(p) for p in p_array]
    @test all(p_array .≈ p_array_test)

end

end #module


using Distributions
using LinearAlgebra
using NeuralArithmetic
using Flux
using Random

Random.seed!(0)

# define the learning task R²->R⁴
function task(x::Vector)
    a, b = x[1], x[2]
    [a+b, a*b, a/b, sqrt(b)]
end
task(x::Matrix) = mapslices(task, x, dims=1)

function generate_data(batchsize::Int)
    a = Uniform(-2,2)
    b = Uniform(0.01,2) # prevent zero/negative inputs to div/sqrt
    p = Product([a,b])
    x = Float32.(rand(p,batchsize))
    y = task(x)
    (x,y)
end

isize = 2     # input size
hsize = 6     # hidden size
osize = 4     # output size
β     = 1e-4  # L₁ regularization

batchsize = 100
nrsteps   = 50000
lr        = 1e-3

model = Chain(NPU(isize,hsize), NAU(hsize,osize))
ps    = Flux.params(model)
data  = [generate_data(batchsize) for _ in 1:nrsteps]
opt   = ADAM(lr)
mse(x,y) = Flux.mse(model(x),y)
loss(x,y) = mse(x,y) + β*norm(ps,1)

cb = Flux.throttle(()->(@info "training..." loss(data[1]...) mse(data[1]...)),1)
Flux.train!(loss, ps, data, opt, cb=cb)

@info "NPU" model[1].Re model[1].Im model[1].g
@info "NAU" model[2].W

using Plots
pyplot()
p1 = heatmap(model[1].Re, yflip=true, aspectratio=1,
             ylabel="NPU Re", clim=(-1,1), c=:bluesreds, colorbar=false)
p2 = heatmap(model[1].Im, yflip=true, aspectratio=1, title="Learned Solution",
             ylabel="NPU Im", clim=(-1,1), c=:bluesreds, colorbar=false)
p4 = heatmap(model[2].W, yflip=true, aspectratio=1,
             ylabel="NAU W", clim=(-1,1), c=:bluesreds)

Re = [1 -1; 1 1; 0 0.5; 1 0; 0 0; 0 1]
Im = zeros(6,2)
g  = ones(2)
W  = [0 0 0 1 0 1;
      0 1 0 0 0 0;
      1 0 0 0 0 0;
      0 0 1 0 0 0]
p5 = heatmap(Re, yflip=true, aspectratio=1,
             ylabel="NPU Re", clim=(-1,1), c=:bluesreds, colorbar=false)
p6 = heatmap(Im, yflip=true, aspectratio=1, title="Perfect Solution",
             ylabel="NPU Im", clim=(-1,1), c=:bluesreds, colorbar=false)
p8 = heatmap(W, yflip=true, aspectratio=1,
             ylabel="NAU W", clim=(-1,1), c=:bluesreds)
plt1 = plot(p1,p2,p4,p5,p6,p8,layout=(2,3), size=(800,370))


df(sem_fit) = df(sem_fit.model)

df(model::AbstractSemSingle) = n_dp(model) - n_par(model)

df(model::SemEnsemble) = n_dp(model) - n_par(model)

function n_dp(model::AbstractSemSingle)
    nman = n_man(model)
    ndp = 0.5(nman^2 + nman)
    if !isnothing(model.imply.μ)
        ndp += n_man(model)
    end
    return ndp
end

n_dp(model::SemEnsemble) = sum(n_dp.(model.sems))


module Sand_01
using LinearAlgebra, Random
#using StaticArrays
"""
    Sand 01: simple uniform advection of a scalar
"""

mutable struct ring
    N::Integer              # number of cells
    Δt::Float64             # time step
    X_::Array{Float64,1}    # positions (nodes)
    ΔX_::Array{Float64,1}   # cell sizes (cell)
    U_::Array{Float64,1}    # velocity (on nodes)
    Σ_::Array{Float64,1}    # surface density
    S_::Array{Float64,1}    # integral of the above
    t::Float64              # time
end
function ring(N=100; U = 1.0, xi=0.0, xf=10.0)
    xtemp_ = collect(range(xi,xf,length=N+1))
    xtemp_[2:end-1] += (Random.rand(N-1).-0.5) * (xf-xi)/N * 0.9 # 0.8 is somewhat arbitrary
    @assert prod(diff(xtemp_).>0)  # checks to make sure xtemp_ monotonic
    X_ = xtemp_#[2:end]
    ΔX_ = diff(xtemp_)
    U_ = U * ones(N+1)
    """ U_[3*N÷4:end] .*= 0.5 # <--- this does NOT work properly; next step is to fix this. """
    # init w/top-hat density:
    Σ_ = ones(N); Σ_[1] = 0.0;
    Σ_[1:(2*N÷5)] .= 0.0; Σ_[(3*N÷5):end] .= 0.0
    S_ = Σ_ .* ΔX_
    #
    Δt = 0.5 / max(maximum(U_[1:end-1]./ΔX_),-minimum(U_[2:end]./ΔX_))
    #
    return ring(N,Δt,X_,ΔX_,U_,Σ_,S_,0.0)
end

function step!(r::ring; Δt=r.Δt)
    Σ_=r.Σ_; N = r.N; ΔX_ = r.ΔX_; U_ = r.U_; S_ = r.S_
    #
    ΔΣ_ = cat(Σ_[1]-Σ_[end], diff(Σ_), Σ_[1]-Σ_[end], dims=1)
    S_ = Σ_ .* ΔX_
    U⁺_ = U_[1:end-1] .* (U_[1:end-1].>0) # right-traveling
    U⁻_ = U_[2:end  ] .* (U_[2:end  ].<0) # left-traveling
    S_ -= Δt * U⁺_ .* ΔΣ_[1:end-1]
    S_ -= Δt * U⁻_ .* ΔΣ_[2:end]
    #
    r.S_ = S_
    r.Σ_ = S_ ./ ΔX_
    r.t += Δt
#    print(sum(S_),"\n")
    return sum(S_)
end

function tstep!(r::ring,Δt)
    M = floor(Δt / r.Δt)
    Δt_last = Δt - M*r.Δt
    @assert Δt_last ≥ 0
    for i in 1:M
        step!(r)
    end
    step!(r, Δt = Δt_last)
end

end#module


using Gadfly, Colors
using KernelDensityEstimate, TransformUtils, IncrementalInference

# linear product is going to use Euler angles, until improve to a manifold sampler

STD1 = Float64[]
STD2 = Float64[]
STD3 = Float64[]

SPC = [pi/4.0]
SPC = (0:62)./128.0*pi
for t in SPC
  so3sigma = 0.001
  N = 1000
  OUT = Array{Float64,2}(N,4)
  for i in 1:N
    Rp = convert(SO3, so3(so3sigma*randn(3)))
    Rx = convert(SO3, AngleAxis(t,[0;1;0]))
    aa = convert(Euler, Rx*Rp)
    # OUT[i,1] = aa.theta
    # OUT[i,2:4] = aa.ax[:]
    OUT[i,1:3] = [aa.R;aa.P;aa.Y]
  end
  if length(SPC) == 1
    plot(x=OUT[:,1], Geom.histogram)
    plot(x=OUT[:,2], Geom.histogram)
    plot(x=OUT[:,3], Geom.histogram)
  end

  s = std(OUT,1);
  # s[1] > 2*so3sigma || s[3] > 2*so3sigma ? warn("Gimbal lock becoming significant") : nothing
  push!(STD1,s[1])
  push!(STD2,s[2])
  push!(STD3,s[3])

end

plot(
layer(x=SPC*180/pi,y=STD1,Geom.line,Theme(default_color=colorant"red")),
layer(x=SPC*180/pi,y=STD2,Geom.line,Theme(default_color=colorant"black")),
layer(x=SPC*180/pi,y=STD3,Geom.line,Theme(default_color=colorant"deepskyblue")),
Coord.Cartesian(xmax=90,ymax=5*so3sigma)
)

# eval product

covOdo = 0.01*ones(6,1)
covOdo[4:6] = 0.001

mu1 = zeros(6,1)
mu1[:] = veeEuler(SE3(0))

p1 = resample(kde!(mu1, vec(covOdo)),100)

p2 = resample(kde!(mu1, vec(covOdo)),100)

p12 = p1*p2
plotKDE(marginal(p12,[6]))

#


struct Linear
    W
    b
end

foo(x) = x + 2

function linear_cost(m::Linear, x, y)
    sum((m.W * x .+ m.b .- y) .^ 2)
end

function linear_cost_flat(W, b, x, y)
    sum((W * x .+ b .- y) .^ 2)
end

let
    W = randn(500, 784)
    b = randn(500)
    m = Linear(W, b)
    x = randn(784, 100)
    y = randn(500)
    inputs = [:m => m, :x => x, :y => y]
    flat_inputs = [:W => W, :b => b, :x => x, :y => y]

    # first check that df with flat model is correct
    test_compare(linear_cost_flat; flat_inputs...)

    # now check that results from flat and structured models are the same
    df_flat = xdiff(linear_cost_flat; flat_inputs...)
    results_flat = df_flat(W, b, x, y)
    df = xdiff(linear_cost; inputs...)
    results_flat = df(W, b, x, y)
    results_struct = df(m, x, y)
    @test results_flat == results_struct
end


function calc_q_star(reactor, R_0, B_0)
  K_q = (
    5 * reactor.epsilon ^ 2 * reactor.kappa ^ 1.27
  )

  K_q * R_0 * B_0 / calc_I_M(reactor, R_0, B_0)
end


using GeometricBase.Config
using GeometricBase.Utils
using GeometricIntegrators.Integrators
using GeometricIntegrators.Tableaus
using GeometricProblems.LotkaVolterra2d
using SimpleSolvers
using Test

SimpleSolvers.set_config(:nls_atol, 8eps())
SimpleSolvers.set_config(:nls_rtol, 2eps())

const Δt = 0.01
const nt = 10
const q₀ = [1.0, 1.0]
const parameters = (a₁=1.0, a₂=1.0, b₁=-1.0, b₂=-2.0)

ode  = lotka_volterra_2d_ode(q₀; params=parameters)
iode = lotka_volterra_2d_iode(q₀; params=parameters)
lode = lotka_volterra_2d_lode(q₀; params=parameters)
int  = IntegratorFIRK(ode, TableauGauss(8), Δt)
sol  = integrate(ode, int, nt)
refx = sol.q[end]

@testset "$(rpad("Runge-Kutta integrators for implicit equations",80))" begin

    int = IntegratorFIRKimplicit(iode, TableauGauss(1), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 2E-5

    int = IntegratorFIRKimplicit(iode, TableauGauss(2), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 4E-7

    int = IntegratorFIRKimplicit(iode, TableauGauss(3), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 2E-10

    int = IntegratorFIRKimplicit(iode, TableauGauss(4), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 8E-14

    int = IntegratorSRKimplicit(iode, TableauGauss(1), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 2E-5

    int = IntegratorSRKimplicit(iode, TableauGauss(2), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 4E-7

    int = IntegratorSRKimplicit(iode, TableauGauss(3), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 2E-10

    int = IntegratorSRKimplicit(iode, TableauGauss(4), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 8E-14

    int = IntegratorPRKimplicit(iode, PartitionedTableauGauss(1), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 2E-6

    int = IntegratorPRKimplicit(iode, PartitionedTableauGauss(2), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 8E-7

    int = IntegratorPRKimplicit(iode, PartitionedTableauGauss(3), Δt)
    sol = integrate(iode, int, nt)
    @test relative_maximum_error(sol.q, refx) < 4E-12

    # int = IntegratorPRKimplicit(iode, PartitionedTableauGauss(4), Δt)
    # sol = integrate(iode, int, nt)
    # @test relative_maximum_error(sol.q, refx) < 2E-16

end

@testset "$(rpad("Special integrators",80))" begin

    flint = IntegratorFLRK(lode, TableauGauss(2), Δt)
    flsol = integrate(lode, flint, nt)
    @test relative_maximum_error(flsol.q, refx) < 4E-12

    flint = IntegratorFLRK(lode, TableauGauss(3), Δt)
    flsol = integrate(lode, flint, nt)
    @test relative_maximum_error(flsol.q, refx) < 4E-16

    flint = IntegratorFLRK(lode, TableauGauss(4), Δt)
    flsol = integrate(lode, flint, nt)
    @test relative_maximum_error(flsol.q, refx) < 8E-16

    pgint = IntegratorPGLRK(ode, CoefficientsPGLRK(2), Δt)
    pgsol = integrate(ode, pgint, nt)
    @test relative_maximum_error(pgsol.q, refx) < 4E-12

    pgint = IntegratorPGLRK(ode, CoefficientsPGLRK(3), Δt)
    pgsol = integrate(ode, pgint, nt)
    @test relative_maximum_error(pgsol.q, refx) < 4E-16

    pgint = IntegratorPGLRK(ode, CoefficientsPGLRK(4), Δt)
    pgsol = integrate(ode, pgint, nt)
    @test relative_maximum_error(pgsol.q, refx) < 4E-16

end


@everywhere using Evolution
using Base.Test
using ValueHistories
using UnicodePlots

@everywhere function rosenbrock(x::Vector)
    return (1.0 - x[1])^2 + 100.0 * (x[2] - x[1]^2)^2
end

history = VectorUnivalueHistory(Float64)

population = [MinimizerPhenotype(rand(2) * 30, rosenbrock) for i = 1:1000]
for i = 1:30
    map!(fitness!, population)
    mutate!(population, Elitism(RandomNormal(), 0.1))
    crossover!(population, Elitism(Random(), 0.2))

    push!(history, i, fitness(best(population)))
end

solution = best(population)
println(solution)
print(lineplot(get(history)..., height = 10, title = "Best Fitness over Generations"))


f=ones(10000000)
primes=[]
for u in 2:10000000 # this is actually pretty fast
    if f[u]==1
        append!(primes,u)
        for w in u*u:u:10000000
            f[w]=0
        end
    end
end
print(primes[10001])


@safetestset "ChebyshevImplementation" begin include("test_ChebyshevImplementation.jl") end

@safetestset "SecondPoincareInvariant OOP" begin
    using PoincareInvariants

    @testset "$N Points on Square in $(D)D" for D in [2, 10], N in [10, 123, 4321]
        Ω(z, t, p) = CanonicalSymplecticMatrix(D)
        pinv = SecondPoincareInvariant{Float64}(Ω, D, N, Val(false))

        phasepoints = getpoints(pinv) do x, y
            ntuple(D) do i
                i == 1 && return x
                i == D ÷ 2 + 1 && return y
                return 0
            end
        end

        @test abs(1 - compute!(pinv, phasepoints, 0, nothing)) / eps() < 10
    end
end

@safetestset "Free Particle" begin
    using PoincareInvariants

    function free_particle!(state, δt)
        mid = length(state) ÷ 2
        state[1:mid] += state[mid+1:end] .* δt
    end

    @testset "$N Points on Square in $(D)D" for D in [2, 6], N in [500, 10_000]
        Ω(z, t, p) = CanonicalSymplecticMatrix(D)
        pinv = SecondPoincareInvariant{Float64}(Ω, D, N, Val(false))

        parampoints = getpoints(pinv)

        phasepoints = getpoints(pinv) do x, y
            ntuple(D) do i
                i == 1 && return x
                i == D ÷ 2 + 1 && return y
                return 0
            end
        end

        @test abs(1 - compute!(pinv, phasepoints, 0, nothing)) / eps() < 10

        free_particle!.(eachrow(phasepoints), 10)
        @test abs(1 - compute!(pinv, phasepoints, 0, nothing)) / eps() < 20

        free_particle!.(eachrow(phasepoints), 100)
        @test abs(1 - compute!(pinv, phasepoints, 0, nothing)) / eps() < 100

        free_particle!.(eachrow(phasepoints), 1000)
        @test abs(1 - compute!(pinv, phasepoints, 0, nothing)) / eps() < 1_000
    end

    @testset "$N Points on Quarter Circle in $(D)D " for D in [4, 12], N in [234, 5678]
        Ω(z, t, p) = CanonicalSymplecticMatrix(D)
        pinv = SecondPoincareInvariant{Float64}(Ω, D, N, Val(false))

        parampoints = getpoints(pinv)

        phasepoints = getpoints(pinv) do r, θ
            ntuple(D) do i
                i == 1 && return r * cos(θ * π/2)
                i == D ÷ 2 + 1 && return r * sin(θ * π/2)
                return 0
            end
        end

        @test abs(π/4 - compute!(pinv, phasepoints, 0, nothing)) / eps() < 10

        free_particle!.(eachrow(phasepoints), 10)
        @test abs(π/4 - compute!(pinv, phasepoints, 0, nothing)) / eps() < 20

        free_particle!.(eachrow(phasepoints), 100)
        @test abs(π/4 - compute!(pinv, phasepoints, 0, nothing)) / eps() < 200

        free_particle!.(eachrow(phasepoints), 1000)
        @test abs(π/4 - compute!(pinv, phasepoints, 0, nothing)) / eps() < 2_000
    end
end


using Plots
using UnPack
using LinearAlgebra

using StartUpDG
using StartUpDG.ExplicitTimestepUtils
using EntropyStableEuler.Fluxes1D
import EntropyStableEuler: γ
using FluxDiffUtils

N = 3
K1D = 16
CFL = .5
T = .50

rd = init_reference_interval(N)
VX,EToV = uniform_1D_mesh(K1D)
md = init_DG_mesh(VX,EToV,rd)

@unpack M,Dr,Vq,Pq,Vf,wf,nrJ = rd
Qr = Pq'*M*Dr*Pq
Ef = Vf*Pq
Br = diagm(wf.*nrJ)
Qrh = .5*[Qr-Qr' Ef'*Br;
        -Br*Ef  Br]
Vh = [Vq;Vf]
Ph = M\transpose(Vh)
VhP = Vh*Pq

# make skew symmetric versions of the operators"
Qrhskew = .5*(Qrh-transpose(Qrh))

LX = 2
build_periodic_boundary_maps!(md,rd,LX)

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

@unpack x = md
rho = @. 2 + .5*exp(-25*(x^2))
u = @. 0*x
p = @. rho^γ

Q = primitive_to_conservative(rho,u,p)
SBP_ops = (Matrix(Qrhskew'),Vh,Ph,VhP)

# interpolate geofacs to both vol/surf nodes, pack back into md
@unpack Vq,Vf = rd
@unpack rxJ = md
rxJ = [Vq;Vf]*rxJ # interp to hybridized points
@pack! md = rxJ

# convert to rho,u,v,beta vars
function cons_to_prim_withlogs(rho,rhou,E)
    rho,u,beta = conservative_to_primitive_beta(rho,rhou,E)
    return (rho,u,beta),(log.(rho),log.(beta))
end

mxm_accum!(X,x,e) = X[:,e] .+= 2*Ph*x

function rhs(Q, SBP_ops, rd::RefElemData, md::MeshData)

    QrTr,Vh,Ph,VhP = SBP_ops
    @unpack LIFT,Vf,Vq = rd
    @unpack rxJ,J,K = md
    @unpack nxJ,sJ = md
    @unpack mapP,mapB = md

    Nh,Nq = size(VhP)

    # entropy var projection
    VU = (x->VhP*x).(v_ufun((x->Vq*x).(Q)...))
    Uh = u_vfun(VU...)
    Qh,logQh = cons_to_prim_withlogs(Uh...)
    Qh_log = (Qh...,logQh...)

    # compute face values
    QM = (x->x[Nq+1:Nh,:]).(Qh_log)
    QP = (x->x[mapP]).(QM)

    # simple lax friedrichs dissipation
    Uf =  (x->x[Nq+1:Nh,:]).(Uh)
    (rhoM,rhouM,EM) = Uf
    rhoUM_n = @. rhouM*nxJ/sJ
    lam = abs.(wavespeed_1D(rhoM,rhoUM_n,EM))
    LFc = .25*max.(lam,lam[mapP]).*sJ

    fSx = euler_fluxes(QM...,QP...)
    normal_flux(fx,u) = fx.*nxJ - LFc.*(u[mapP]-u)
    flux = map(normal_flux,fSx,Uf)
    rhsQ = (x->LIFT*x).(flux)

    # compute volume contributions using flux differencing
    rhse = zero.(getindex.(Qh,:,1))
    for e = 1:K
        QxTr = rxJ[1,e]*QrTr

        # computes sum(Ax.*Fx + Ay.*Fy,dims=2)
        # wrap QxTr, euler_fluxes in tuple to make it look "multidimensional"
        hadamard_sum!(rhse,(QxTr,),tuple∘euler_fluxes,getindex.(Qh_log,:,e);
                      skip_index=(i,j)->(i>Nq)&(j>Nq))
        mxm_accum!.(rhsQ,rhse,e)
    end

    rhstest = zero(eltype(first(rhsQ)))
    compute_rhstest = false
    if compute_rhstest
        for fld in eachindex(rhsQ)
            VUq = VU[fld][1:Nq,:]
            rhstest += sum(md.wJq.*VUq.*(Vq*rhsQ[fld]))
        end
    end

    return map(x -> -x./J,rhsQ),rhstest
end

# "Time integration coefficients"
rk4a,rk4b,rk4c = ck45()
CN = (N+1)^2/2  # estimated trace constant
dt = CFL * 2 / (CN*K1D)
Nsteps = convert(Int,ceil(T/dt))
dt = T/Nsteps

# "Perform time-stepping"
resQ = zero.(Q)
for i = 1:Nsteps
    for INTRK = 1:5
        rhsQ,rhstest = rhs(Q,SBP_ops,rd,md)
        bcopy!.(resQ, @. rk4a[INTRK]*resQ + dt*rhsQ)
        bcopy!.(Q,@. Q + rk4b[INTRK]*resQ)
    end

    if i%10==0 || i==Nsteps
        println("Time step $i out of $Nsteps")
    end
end

# plotting nodes
gr(aspect_ratio=1,legend=false)
scatter((x->rd.Vp*x).((x,Q[1])),zcolor=rd.Vp*Q[1],msw=0,ms=2,cam=(0,90))


import .SpecialFunctions
import .SpecialFunctions: erf, erfc, besselj0, besselj1, bessely0, bessely1,
    besselj, bessely, gamma, lgamma

erf(x::Float128) =
    Float128(@ccall(libquadmath.erfq(x::Cfloat128)::Cfloat128))
erfc(x::Float128) =
    Float128(@ccall(libquadmath.erfcq(x::Cfloat128)::Cfloat128))

besselj0(x::Float128) =
    Float128(@ccall(libquadmath.j0q(x::Cfloat128)::Cfloat128))
besselj1(x::Float128) =
    Float128(@ccall(libquadmath.j1q(x::Cfloat128)::Cfloat128))

bessely0(x::Float128) =
    Float128(@ccall(libquadmath.y0q(x::Cfloat128)::Cfloat128))
bessely1(x::Float128) =
    Float128(@ccall(libquadmath.y1q(x::Cfloat128)::Cfloat128))

besselj(n::Integer, x::Float128) =
    Float128(@ccall(libquadmath.jnq(n::Cint, x::Cfloat128)::Cfloat128))
bessely(n::Integer, x::Float128) =
    Float128(@ccall(libquadmath.ynq(n::Cint, x::Cfloat128)::Cfloat128))

gamma(x::Float128) =
    Float128(@ccall(libquadmath.tgammaq(x::Cfloat128)::Cfloat128))
lgamma(x::Float128) =
    Float128(@ccall(libquadmath.lgammaq(x::Cfloat128)::Cfloat128))


using BlockBandedMatrices

@testset "Adj/Trans" begin
    A = BandedBlockBandedMatrix(randn(ComplexF64,10,14), 1:4,2:5, (1,2), (2,1))

    @test A'[Block(1,1)] == A[Block(1,1)]'
    @test A'[Block(2,3)] == A[Block(3,2)]'
    @test transpose(A)[Block(1,1)] == transpose(A[Block(1,1)])
    @test transpose(A)[Block(2,3)] == transpose(A[Block(3,2)])

    @test blockbandwidths(A') == blockbandwidths(transpose(A)) == (2,1)
    @test subblockbandwidths(A') == subblockbandwidths(transpose(A)) == (1,2)

    @test BandedBlockBandedMatrix(A') == A'
    @test BandedBlockBandedMatrix(transpose(A)) == transpose(A)
end


using PyPlot, Revise, transD_GP, Random, Statistics
include("skytem_response.jl")
##  modeling parameters
ntimesperdecade = 10
nfreqsperdecade = 5
freqlow = 1e-3
## LM operator
Flm = transD_GP.AEM_VMD_HMD.HFieldDHT(
                      ntimesperdecade = ntimesperdecade,
                      nfreqsperdecade = nfreqsperdecade,
                      lowpassfcs = lowpassfcs,
                      times  = LM_times,
                      ramp   = LM_ramp,
                      zTx    = zTx,
                      rRx    = rRx,
                      rTx    = rTx,
                      zRx    = zRxLM,
                      freqlow = freqlow
                      )
## HM operator
Fhm = transD_GP.AEM_VMD_HMD.HFieldDHT(
                      ntimesperdecade = ntimesperdecade,
                      nfreqsperdecade = nfreqsperdecade,
                      lowpassfcs = lowpassfcs,
                      times  = HM_times,
                      ramp   = HM_ramp,
                      zTx    = zTx,
                      rRx    = rRx,
                      rTx    = rTx,
                      zRx    = zRxHM,
                      freqlow = freqlow
                      )
## get the fields in place
@time transD_GP.AEM_VMD_HMD.getfieldTD!(Flm, zfixed, rho)
@time transD_GP.AEM_VMD_HMD.getfieldTD!(Fhm, zfixed, rho)
## plot
figure()
s1 = subplot(211)
loglog(Flm.times,4*pi*1e-7*abs.(Flm.dBzdt), label="lm_julia")
loglog(Fhm.times,4*pi*1e-7*abs.(Fhm.dBzdt), label="hm_julia")
loglog(Flm.times,dLM, ".", label="lm_skytem")
loglog(Fhm.times,dHM, ".", label="hm_skytem")
legend()
grid(which="both")
ylabel("dBzdt V/Am^4")
s2 = subplot(212, sharex=s1)
semilogx(Flm.times,(4*pi*1e-7*Flm.dBzdt-dLM)./dLM*100, label="lm")
semilogx(Fhm.times,(4*pi*1e-7*Fhm.dBzdt-dHM)./dHM*100, label="hm")
ylabel("% difference")
xlabel("time s")
legend()
grid(which="both")


using StaticArrays
using BenchmarkTools

const  ⊗ = kron

function cost(𝛉::AbstractArray, 𝒞::Tuple{AbstractArray, Vararg{AbstractArray}}, 𝒟::Tuple{AbstractArray, Vararg{AbstractArray}})
    ℳ, ℳʹ = collect(𝒟)
    Λ₁, Λ₂ = collect(𝒞)
    Jₐₘₗ = 0.0
    N = length(𝒟[1])
    𝚲ₙ = @MMatrix zeros(4,4)
    # 𝐞₁ = @SVector [1.0, 0.0, 0.0]
    # 𝐞₂ = @SVector [0.0, 1.0, 0.0]
    𝐞₁ = @SMatrix [1.0; 0.0; 0.0]
    𝐞₂ = @SMatrix [0.0; 1.0; 0.0]
    @inbounds for n = 1:N
        index = SVector(1, 2)
        𝚲ₙ[1:2,1:2] .= Λ₁[n][index, index]
        𝚲ₙ[3:4,3:4] .= Λ₂[n][index, index]
        𝐦 = ℳ[n]
        𝐦ʹ= ℳʹ[n]
        𝐔ₙ = (𝐦 ⊗ 𝐦ʹ)
        ∂ₓ𝐮ₙ =  [(𝐞₁ ⊗ 𝐦ʹ) (𝐞₂ ⊗ 𝐦ʹ) (𝐦 ⊗ 𝐞₁) (𝐦 ⊗ 𝐞₂)]
        𝐁ₙ =  ∂ₓ𝐮ₙ * 𝚲ₙ * ∂ₓ𝐮ₙ'
        𝚺ₙ = 𝛉' * 𝐁ₙ * 𝛉
        Jₐₘₗ += 𝛉' * 𝐔ₙ * (𝚺ₙ \ 𝐔ₙ') * 𝛉
    end
    Jₐₘₗ
end

# Some sample data
N = 3376821
ℳ = [@MMatrix(rand(3,1)) for i = 1:N]
ℳʹ = [@MMatrix(rand(3,1)) for i = 1:N]
Λ₁ =  [SMatrix{3,3}(diagm([1.0,1.0,0.0])) for i = 1:length(ℳ)]
Λ₂ =  [SMatrix{3,3}(diagm([1.0,1.0,0.0])) for i = 1:length(ℳ)]
t = @MVector rand(9)
𝒞 = (Λ₁,Λ₂)
𝒟 = (ℳ, ℳʹ)

cost(t,𝒞 , 𝒟)
@time cost(t,𝒞 , 𝒟)
@btime cost($t,$𝒞 , $𝒟)


@testset "Elastic net ($T)" for T in [Float32, Float64, ComplexF32, ComplexF64]

    using ProximalOperators
    using ProximalAlgorithms
    using LinearAlgebra
    using Random

    Random.seed!(0)

    # TODO: generate problem with known solution instead

    m, n = 50, 200

    A = randn(T, m, n)
    b = randn(T, m)

    R = real(T)

    reg = ElasticNet(R(1), R(1))
    reg1 = NormL1(R(1))
    reg2 = SqrNormL2(R(1))
    loss = Translate(SqrNormL2(R(1)), -b)

    L = opnorm(A)^2

    x0 = zeros(T, n)
    solver = ProximalAlgorithms.PANOC{R}(tol=eps(R))
    x_panoc, it = solver(x0, f=loss, A=A, g=reg)

    @testset "DavisYin" begin

        x0 = zeros(T, n)
        solver = ProximalAlgorithms.DavisYin{R}(tol=R(1e-6))
        xf_dys, xg_dys, it_dys = solver(
            x0, f=reg1, g=reg2, h=loss, A=A, L=L
        )
        @test eltype(xf_dys) == T
        @test eltype(xg_dys) == T
        @test norm(xf_dys - x_panoc, Inf) <= 1e-3
        @test norm(xg_dys - x_panoc, Inf) <= 1e-3
        @test it_dys <= 1900

        x0 = randn(T, n)
        solver = ProximalAlgorithms.DavisYin{R}(tol=R(1e-6))
        xf_dys, xg_dys, it_dys = solver(
            x0, f=reg1, g=reg2, h=loss, A=A, L=L
        )
        @test eltype(xf_dys) == T
        @test eltype(xg_dys) == T
        @test norm(xf_dys - x_panoc, Inf) <= 1e-3
        @test norm(xg_dys - x_panoc, Inf) <= 1e-3
        @test it_dys <= 2600

    end

    afba_test_params = [
        (R(2), R(0), 230),
        (R(1), R(1), 230),
        (R(0), R(1), 470),
        (R(0), R(0), 500),
        (R(1), R(0), 230)
    ]

    @testset "AFBA" for (theta, mu, maxit) in afba_test_params

        x0 = randn(T, n)
        y0 = randn(T, m)

        solver = ProximalAlgorithms.AFBA{R}(theta=theta, mu=mu, tol=R(1e-6))
        x_afba, y_afba, it_afba = solver(
            x0, y0, f=reg2, g=reg1, h=loss, L=A, betaQ=R(1),
        )
        @test eltype(x_afba) == T
        @test eltype(y_afba) == T
        @test norm(x_afba - x_panoc, Inf) <= 1e-4
        @test it_afba <= maxit

    end
end


module ElasticStabilityConditions

using LinearAlgebra: diag

using Crystallography
using StaticArrays: FieldVector, FieldMatrix, FieldArray, SVector, SHermitianCompact

export TensorStress,
    TensorStrain,
    TensorStiffness,
    TensorCompliance,
    EngineeringStress,
    EngineeringStrain,
    EngineeringCompliance,
    EngineeringStiffness
export stability_conditions, isstable

struct TensorStress{T} <: FieldMatrix{3,3,T}
    xx::T
    yx::T
    zx::T
    xy::T
    yy::T
    zy::T
    xz::T
    yz::T
    zz::T
end

struct TensorStrain{T} <: FieldMatrix{3,3,T}
    xx::T
    yx::T
    zx::T
    xy::T
    yy::T
    zy::T
    xz::T
    yz::T
    zz::T
end

struct TensorStiffness{T} <: FieldArray{Tuple{3,3,3,3},T,4}
    xxxx::T
    yxxx::T
    zxxx::T
    xyxx::T
    yyxx::T
    zyxx::T
    xzxx::T
    yzxx::T
    zzxx::T
    xxyx::T
    yxyx::T
    zxyx::T
    xyyx::T
    yyyx::T
    zyyx::T
    xzyx::T
    yzyx::T
    zzyx::T
    xxzx::T
    yxzx::T
    zxzx::T
    xyzx::T
    yyzx::T
    zyzx::T
    xzzx::T
    yzzx::T
    zzzx::T
    xxxy::T
    yxxy::T
    zxxy::T
    xyxy::T
    yyxy::T
    zyxy::T
    xzxy::T
    yzxy::T
    zzxy::T
    xxyy::T
    yxyy::T
    zxyy::T
    xyyy::T
    yyyy::T
    zyyy::T
    xzyy::T
    yzyy::T
    zzyy::T
    xxzy::T
    yxzy::T
    zxzy::T
    xyzy::T
    yyzy::T
    zyzy::T
    xzzy::T
    yzzy::T
    zzzy::T
    xxxz::T
    yxxz::T
    zxxz::T
    xyxz::T
    yyxz::T
    zyxz::T
    xzxz::T
    yzxz::T
    zzxz::T
    xxyz::T
    yxyz::T
    zxyz::T
    xyyz::T
    yyyz::T
    zyyz::T
    xzyz::T
    yzyz::T
    zzyz::T
    xxzz::T
    yxzz::T
    zxzz::T
    xyzz::T
    yyzz::T
    zyzz::T
    xzzz::T
    yzzz::T
    zzzz::T
end

struct TensorCompliance{T} <: FieldArray{Tuple{3,3,3,3},T,4}
    xxxx::T
    yxxx::T
    zxxx::T
    xyxx::T
    yyxx::T
    zyxx::T
    xzxx::T
    yzxx::T
    zzxx::T
    xxyx::T
    yxyx::T
    zxyx::T
    xyyx::T
    yyyx::T
    zyyx::T
    xzyx::T
    yzyx::T
    zzyx::T
    xxzx::T
    yxzx::T
    zxzx::T
    xyzx::T
    yyzx::T
    zyzx::T
    xzzx::T
    yzzx::T
    zzzx::T
    xxxy::T
    yxxy::T
    zxxy::T
    xyxy::T
    yyxy::T
    zyxy::T
    xzxy::T
    yzxy::T
    zzxy::T
    xxyy::T
    yxyy::T
    zxyy::T
    xyyy::T
    yyyy::T
    zyyy::T
    xzyy::T
    yzyy::T
    zzyy::T
    xxzy::T
    yxzy::T
    zxzy::T
    xyzy::T
    yyzy::T
    zyzy::T
    xzzy::T
    yzzy::T
    zzzy::T
    xxxz::T
    yxxz::T
    zxxz::T
    xyxz::T
    yyxz::T
    zyxz::T
    xzxz::T
    yzxz::T
    zzxz::T
    xxyz::T
    yxyz::T
    zxyz::T
    xyyz::T
    yyyz::T
    zyyz::T
    xzyz::T
    yzyz::T
    zzyz::T
    xxzz::T
    yxzz::T
    zxzz::T
    xyzz::T
    yyzz::T
    zyzz::T
    xzzz::T
    yzzz::T
    zzzz::T
end

struct EngineeringStress{T} <: FieldVector{6,T}
    xx::T
    yy::T
    zz::T
    yz::T
    xz::T
    xy::T
end

struct EngineeringStrain{T} <: FieldVector{6,T}
    xx::T
    yy::T
    zz::T
    yz::T
    xz::T
    xy::T
end

struct EngineeringCompliance{T} <: FieldMatrix{6,6,T}
    xxxx::T
    yyxx::T
    zzxx::T
    yzxx::T
    xzxx::T
    xyxx::T
    xxyy::T
    yyyy::T
    zzyy::T
    yzyy::T
    xzyy::T
    xyyy::T
    xxzz::T
    yyzz::T
    zzzz::T
    yzzz::T
    xzzz::T
    xyzz::T
    xxyz::T
    yyyz::T
    zzyz::T
    yzyz::T
    xzyz::T
    xyyz::T
    xxxz::T
    yyxz::T
    zzxz::T
    yzxz::T
    xzxz::T
    xyxz::T
    xxxy::T
    yyxy::T
    zzxy::T
    yzxy::T
    xzxy::T
    xyxy::T
end

struct EngineeringStiffness{T} <: FieldMatrix{6,6,T}
    xxxx::T
    yyxx::T
    zzxx::T
    yzxx::T
    xzxx::T
    xyxx::T
    xxyy::T
    yyyy::T
    zzyy::T
    yzyy::T
    xzyy::T
    xyyy::T
    xxzz::T
    yyzz::T
    zzzz::T
    yzzz::T
    xzzz::T
    xyzz::T
    xxyz::T
    yyyz::T
    zzyz::T
    yzyz::T
    xzyz::T
    xyyz::T
    xxxz::T
    yyxz::T
    zzxz::T
    yzxz::T
    xzxz::T
    xyxz::T
    xxxy::T
    yyxy::T
    zzxy::T
    yzxy::T
    xzxy::T
    xyxy::T
end

stability_conditions(::Cubic) =
    ["C_{11} > | C_{12} |", "C_{11} + 2 C_{12} > 0", "C_{44} > 0"]
stability_conditions(::Hexagonal) = [
    "C_{11} > | C_{12} |",
    "2 C_{13}^2 < C_{33} (C_{11} + C_{12})",
    "C_{44} > 0",
    "C_{66} > 0",
]

function isstable(::Cubic, c::EngineeringStiffness)
    c11, c12, c44 = c[1, 1], c[1, 2], c[1, 4]
    return all([  # Must satisfy all criteria!
        c11 > abs(c12),
        c11 + 2 * c12 > 0,
        c44 > 0,
    ])
end # function isstable
function isstable(::Hexagonal, c::EngineeringStiffness)
    c11, c12, c13, c33, c44, c66 = c[1, 1], c[1, 2], c[1, 3], c[3, 3], c[4, 4], c[6, 6]
    return all([  # Must satisfy all criteria!
        c66 == (c11 - c12) / 2,
        c11 > abs(c12),
        2 * c13^2 < c33 * (c11 + c12),
        c44 > 0,
        c66 > 0,
    ])
end # function isstable
function isstable(::Tetragonal, c::EngineeringStiffness)
    c11, c12, c13, c16, c33, c44, c66 =
        c[1, 1], c[1, 2], c[1, 3], c[1, 6], c[3, 3], c[4, 4], c[6, 6]
    if c16 == 0  # Tetragonal (I) class
        return all([c11 > abs(c12), 2 * c13^2 < c33 * (c11 + c12), c44 > 0, c66 > 0])
    end
    return [  # Tetragonal (II) class
        c11 > abs(c12),
        2 * c13^2 < c33 * (c11 + c12),
        c44 > 0,
        2 * c16^2 < c66 * (c11 - c12),
    ]
end # function isstable
function isstable(::Trigonal, c::EngineeringStiffness)
    c11, c12, c13, c14, c15, c33, c44, c66 =
        c[1, 1], c[1, 2], c[1, 3], c[1, 4], c[1, 5], c[3, 3], c[4, 4], c[6, 6]
    if c15 == 0  # Rhombohedral (I) class
        return all([
            c11 > abs(c12),
            c44 > 0,
            c13^2 < 0.5 * c33 * (c11 + c12),
            c14^2 < 0.5 * c44 * (c11 - c12),
            0.5 * c44 * (c11 - c12) == c44 * c66,
        ])
    end
    return all([  # Rhombohedral (II) class
        c11 > abs(c12),
        c44 > 0,
        c13^2 < 0.5 * c33 * (c11 + c12),
        c14^2 + c15^2 < 0.5 * c44 * (c11 - c12),
        0.5 * c44 * (c11 - c12) == c44 * c66,
    ])
end # function isstable
function isstable(::Orthorhombic, c::EngineeringStiffness)
    c11, c22, c33, c44, c55, c66 = diag(c)
    c12, c13, c23 = c[1, 2], c[1, 3], c[2, 3]
    return all([
        c11 > 0,
        c11 * c22 > c12^2,
        c11 * c22 * c33 + 2 * c12 * c13 * c23 > c11 * c23^2 + c22 * c13^2 + c33 * c12^2,
        c44 > 0,
        c55 > 0,
        c66 > 0,
    ])
end # function isstable
function isstable(::Monoclinic, c::EngineeringStiffness)
    c11, c22, c33, c44, c55, c66 = diag(c)
    c12, c13, c15, c23, c25, c35, c46 =
        c[1, 2], c[1, 3], c[1, 5], c[2, 3], c[2, 5], c[3, 5], c[4, 6]
    g = c11 * c22 * c33 - c11 * c23 * c23 - c22 * c13 * c13 - c33 * c12 * c12 +
        2 * c12 * c13 * c23
    return all([
        all([c11, c22, c33, c44, c55, c66] .> 0),
        c11 + c22 + c33 + 2 * (c12 + c13 + c23) > 0,
        c33 * c55 - c35^2 > 0,
        c44 * c66 - c46^2 > 0,
        c22 + c33 - 2 * c23 > 0,
        c22 * (c33 * c55 - c35^2) + 2 * c23 * c25 * c35 - c23^2 * c55 - c25^2 * c33 > 0,
        2 * (
            c15 * c25 * (c33 * c12 - c13 * c23) +
            c15 * c35 * (c22 * c13 - c12 * c23) +
            c25 * c35 * (c11 * c23 - c12 * c13)
        ) - (
            c15 * c15 * (c22 * c33 - c23^2) +
            c25 * c25 * (c11 * c33 - c13^2) +
            c35 * c35 * (c11 * c22 - c12^2)
        ) + c55 * g > 0,
    ])
end # function isstable
isstable(C::CrystalSystem, s::EngineeringCompliance) = isstable(C, inv(s))

function Base.convert(::Type{<:TensorStress}, s::EngineeringStress)
    return TensorStress(SHermitianCompact(SVector(s.xx, s.xy, s.xz, s.yy, s.yz, s.zz)))
end # function Base.convert
function Base.convert(::Type{<:EngineeringStress}, s::TensorStress)
    return EngineeringStress(s.xx, s.yy, s.zz, s.yz, s.xz, s.xy)
end # function Base.convert
function Base.convert(::Type{<:TensorStrain}, e::EngineeringStrain)
    return TensorStrain(SHermitianCompact(SVector(
        e.xx,
        e.xy / 2,
        e.xz / 2,
        e.yy,
        e.yz / 2,
        e.zz,
    )))
end # function Base.convert
function Base.convert(::Type{<:EngineeringStrain}, e::TensorStrain)
    return EngineeringStrain(e.xx, e.yy, e.zz, 2 * e.yz, 2 * e.xz, 2 * e.xy)
end # function Base.convert

Base.inv(c::EngineeringStiffness) = EngineeringCompliance(inv(collect(c)))
Base.inv(s::EngineeringCompliance) = EngineeringStiffness(inv(collect(s)))

end


using LinearAlgebra
using SparseArrays
using CUDA
using Printf

include("bicgstab.jl")

function square_preconditioned(n :: Int=10)
        A   = ones(n, n) + (n-1) * Matrix(I, n, n)
        b   = 10.0 * [1:n;]
        M⁻¹ = 1/n * Matrix(I, n, n)
        return A, b, M⁻¹
end

A, b, N = square_preconditioned(100)
x = similar(b); r = similar(b)
spA = sparse(A)
spN = sparse(N)
y, iter, status = bicgstab(spA, b, spN, x)
@show y
@show iter
@show status

cuspA = CUDA.CUSPARSE.CuSparseMatrixCSR(spA)
cuspN = CUDA.CUSPARSE.CuSparseMatrixCSR(spN)
cux = CuVector(x)
cub = CuVector(b)
cuy, iter, status = bicgstab(cuspA, cub, cuspN, cux)
@show cuy
@show iter
@show status


# Replicate the Biogeme Swissmetro example
# https://biogeme.epfl.ch/examples/swissmetro/01logit.html

using Distributed
procs = addprocs(4)
Distributed.@everywhere begin
    using Pkg; Pkg.activate(joinpath(@__DIR__, "..", ".."))
end

@everywhere using Dagger, DiscreteChoiceModels

@testset "Biogeme swissmetro distributed" begin
    using Dagger
    using DiscreteChoiceModels
    using Test
    using StatsBase
    using CSV

    @test length(workers()) == 4

    data = DTable(
        CSV.File(joinpath(dirname(Base.source_path()), "../data/biogeme_swissmetro.dat"), delim='\t'),
        2683
    )

    @test length(data.chunks) == 4

    # filter to wanted cases
    data = filter(data) do row
        ((row.PURPOSE == 1) | (row.PURPOSE == 3)) && row.CHOICE != 0
    end

    @test length(data) == 6768

    data = map(data) do r
        merge((
            avtr=(r.TRAIN_AV == 1) && (r.SP != 0),
            avsm=r.SM_AV == 1,
            avcar=(r.CAR_AV == 1) && (r.SP != 0)
        ), pairs(r))
    end

    model = multinomial_logit(
            @utility(begin
                1 ~ αtrain + βtravel_time * TRAIN_TT / 100 + βcost * (TRAIN_CO * (GA == 0)) / 100
                2 ~ αswissmetro + βtravel_time * SM_TT / 100 + βcost * SM_CO * (GA == 0) / 100
                3 ~ αcar + βtravel_time * CAR_TT / 100 + βcost * CAR_CO / 100

                αswissmetro = 0, fixed  # fix swissmetro ASC to zero
            end),
            :CHOICE,
            data,
            availability=[
                1 => :avtr,
                2 => :avsm,
                3 => :avcar,
            ]
        )

    coefs = Dict(zip(coefnames(model), round.(coef(model), sigdigits=3)))
    ses = Dict(zip(coefnames(model), round.(stderror(model), sigdigits=3)))

    @test coefs[:αcar] ≈ -0.155 # biogeme says 0.155 which would round to 0.16, but it's actually 0.1546
    @test coefs[:αswissmetro] == 0  # fixed
    @test coefs[:αtrain] ≈ -0.701
    @test coefs[:βcost] ≈ -1.08
    @test coefs[:βtravel_time] ≈ -1.28

    @test ses[:αcar] ≈ 0.0432
    @test isnan(ses[:αswissmetro])  # fixed
    @test ses[:αtrain] ≈ 0.0549
    @test ses[:βcost] ≈ 0.0518
    @test ses[:βtravel_time] ≈ 0.0569

    idx = Dict(zip(coefnames(model), 1:length(coefs)))
    vcovmat = round.(vcov(model), sigdigits=3)
    @test vcovmat[idx[:αtrain], idx[:αcar]] ≈ 0.00138
    @test vcovmat[idx[:βcost], idx[:αcar]] ≈ 0.000485
    @test vcovmat[idx[:βcost], idx[:αtrain]] ≈ 8.22e-6
    @test vcovmat[idx[:βtravel_time], idx[:αcar]] ≈ -0.00144
    @test vcovmat[idx[:βtravel_time], idx[:αtrain]] ≈ -0.00225
    @test vcovmat[idx[:βtravel_time], idx[:βcost]] ≈ 0.000550

    @test all(isnan.(vcovmat[idx[:αswissmetro], :]))
    @test all(isnan.(vcovmat[:, idx[:αswissmetro]]))

    @test round(loglikelihood(model), digits=3) ≈ -5331.252
    @test round(model.init_ll, digits=3) ≈ -6964.663

    sleep(2)  # let dagger shut down
end

# clean up
rmprocs(procs)


### Computing fields and other physical quantities ###
using LinearAlgebra
import LinearAlgebra: transpose!
import CartesianGrids: convective_derivative!

"""
    vorticity(u,sys::NavierStokes,t)

Return the vorticity field associated with state vector `u` on the grid in `sys`,
at time `t`.
"""
@inline vorticity(w::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY},t::Real) where {NX,NY} =
            vorticity!(sys.Wn,w,sys,t)

function vorticity!(w::Nodes{Dual,NX,NY},win::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY},t::Real) where {NX,NY}
  w .= 0.0
  _vorticity!(w,win,sys,t)
  _vorticity_sheet!(w,sys,t)
  return w
end

@inline _vorticity!(w::Nodes{Dual,NX,NY},win::Nodes{Dual,NX,NY},
                    sys::NavierStokes{NX,NY},t::Real) where {NX,NY} = w .+= win/cellsize(sys)

_vorticity_sheet!(w::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY,0},t::Real) where {NX,NY} = w

_vorticity_sheet!(w::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY,N,MT,FS,
                                              ExternalInternalFlow},t::Real) where {NX,NY,N,MT,FS} = w

function _vorticity_sheet!(w::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY,N,MT,FS,SD},t::Real) where {NX,NY,N,MT,FS,SD}
  if isnothing(sys.Rn)
    regop = _regularization(sys)
    Rn = RegularizationMatrix(regop,sys.Sb,sys.Sn)
  else
    Rn = sys.Rn
  end
  surface_velocity_jump!(sys.Δus,sys,t)
  cross!(sys.Sb,sys.Δus,normals(sys))
  w .+= Rn*sys.Sb
  return w
end

function velocity!(u::Edges{Primal,NX,NY},w::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY,N},t::Real) where {NX,NY,N}
    u .= 0.0
    _velocity_vorticity!(u,w,sys)
    _velocity_single_layer!(u,sys,t)
    _velocity_freestream!(u,sys,t)
    return u
end

function _velocity_vorticity!(u::Edges{Primal,NX,NY},w::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY,N}) where {NX,NY,N}
    sys.Wn .= 0.0
    _unscaled_streamfunction_vorticity!(sys.Wn,w,sys)
    sys.Vf .= 0.0
    curl!(sys.Vf,sys.Wn)
    u .+= sys.Vf
    return u
end

@inline _velocity_single_layer!(u::Edges{Primal,NX,NY},sys::NavierStokes{NX,NY,N,MT,FS,
                                      ExternalInternalFlow},t::Real) where {NX,NY,N,MT,FS} = u

@inline _velocity_single_layer!(u::Edges{Primal,NX,NY},sys::NavierStokes{NX,NY,0},t::Real) where {NX,NY} = u

function _velocity_single_layer!(u::Edges{Primal,NX,NY},sys::NavierStokes{NX,NY,N,MT,FS,SD},t::Real) where {NX,NY,N,MT,FS,SD}
    surface_velocity_jump!(sys.Δus,sys,t)
    pointwise_dot!(sys.Sb,sys.Δus,normals(sys))
    sys.Sb .*= cellsize(sys)
    sys.slc(sys.Sc,sys.Sb)
    _unscaled_scalarpotential!(sys.Sc,sys.Sc,sys)
    sys.Vf .= 0.0
    grad!(sys.Vf,sys.Sc)
    u .+= sys.Vf
    return u
end

function _velocity_freestream!(u::Edges{Primal,NX,NY},sys::NavierStokes{NX,NY,N,MT,FS,SD},t::Real) where {NX,NY,N,MT,FS,SD}
  U∞, V∞ = freestream(t,sys)
  u.u .+= U∞
  u.v .+= V∞
  return u
end

"""
    velocity(u,sys::NavierStokes,t)

Return the velocity field associated with state vector `u` on the grid in `sys`,
at time `t`.
"""
velocity(w::Nodes{Dual,NX,NY},a...) where {NX,NY} = velocity!(Edges(Primal,(NX,NY)),w,a...)

function streamfunction!(ψ::Nodes{Dual,NX,NY},w::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY},t::Real) where {NX,NY}
  ψ .= 0.0
  _unscaled_streamfunction_vorticity!(ψ,w,sys)
  ψ .*= cellsize(sys)
  _streamfunction_freestream!(ψ,sys,t)
  return ψ
end

function _unscaled_streamfunction_vorticity!(ψ::Nodes{Dual,NX,NY},w::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY}) where {NX,NY}
  ldiv!(sys.Sn,sys.L,w)
  ψ .-= sys.Sn
  return ψ
end

function _streamfunction_freestream!(ψ::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY},t::Real) where {NX,NY}
  xg, yg = coordinates(ψ,sys.grid)
  U∞, V∞ = freestream(t,sys)
  ψ .+= U∞*yg' .- V∞*xg
  return ψ
end

"""
    streamfunction(u,sys::NavierStokes,t)

Return the streamfunction field associated with state vector `u` on the grid in `sys`,
at time `t`.
"""
streamfunction(w::Nodes{Dual,NX,NY},sys::NavierStokes{NX,NY},t::Real) where {NX,NY} = streamfunction!(Nodes(Dual,(NX,NY)),w,sys,t)

function scalarpotential!(ϕ::Nodes{Primal,NX,NY},dil::Nodes{Primal,NX,NY},sys::NavierStokes{NX,NY},t::Real) where {NX,NY}
  _unscaled_scalarpotential!(ϕ,dil,sys)
  # This assumes that dil is the physical dilation times the grid spacing
  ϕ .*= cellsize(sys)
  return ϕ
end

function _unscaled_scalarpotential!(ϕ::Nodes{Primal,NX,NY},dil::Nodes{Primal,NX,NY},sys::NavierStokes{NX,NY}) where {NX,NY}
  ldiv!(ϕ,sys.L,dil)
  return ϕ
end

"""
    scalarpotential(u,sys::NavierStokes,t)

Return the scalar potential field associated with state vector `u` on the grid in `sys`,
at time `t`.
"""
scalarpotential(dil::Nodes{Primal,NX,NY},sys::NavierStokes{NX,NY},t::Real) where {NX,NY} = scalarpotential!(Nodes(Primal,(NX,NY)),dil,sys,t)

function convective_derivative!(out::Edges{Primal,NX,NY},u::Edges{Primal,NX,NY},
                        sys::NavierStokes{NX,NY},t::Real) where {NX,NY}
  out .= u
  _unscaled_convective_derivative!(out,sys)
  Δx⁻¹ = 1/cellsize(sys)
  out .*= Δx⁻¹
  return out
end

"""
    convective_derivative(u,sys::NavierStokes,t)

Return the convective derivative associated with state vector `u` on the grid in `sys`,
at time `t`.
"""
convective_derivative(u::Edges{Primal,NX,NY},sys::NavierStokes{NX,NY},t::Real) where {NX,NY} =
      convective_derivative!(Edges(Primal,(NX,NY)),u,sys,t)

"""
    vorticity(integrator)

Return the vorticity field associated with `integrator` at its current state.
"""
function vorticity end

"""
    velocity(integrator)

Return the velocity field associated with `integrator` at its current state.
"""
function velocity end

"""
    streamfunction(integrator)

Return the streamfunction field associated with `integrator` at its current state.
"""
function streamfunction end

"""
    scalarpotential(integrator)

Return the scalar potential field associated with `integrator` at its current state.
"""
function scalarpotential end

"""
    convective_derivative(integrator)

Return the convective derivative associated with `integrator` at its current state.
"""
function convective_derivative end

for fcn in (:vorticity,:velocity,:streamfunction,:scalarpotential,:convective_derivative)
  @eval $fcn(s::ConstrainedSystems.ArrayPartition,sys::NavierStokes,t) = $fcn(state(s),sys,t)

  @eval $fcn(integ::ConstrainedSystems.OrdinaryDiffEq.ODEIntegrator) = $fcn(integ.u,integ.p,integ.t)
end

function pressure(u::ConstrainedSystems.ArrayPartition,sys::NavierStokes{NX,NY},t) where {NX,NY}
    w, τ = state(u), constraint(u)
    fill!(sys.Vn,0.0)
    _vel_ns_rhs_convectivederivative!(sys.Vn,w,sys,t)
    _vel_ns_rhs_double_layer!(sys.Vn,sys,t)
    fill!(sys.Vf,0.0)
    _vel_ns_op_constraint_force!(sys.Vf,τ,sys)
    sys.Vn .-= sys.Vf
    fill!(sys.Sc,0.0)
    divergence!(sys.Sc,sys.Vn)
    press = zero(sys.Sc)
    ldiv!(press,sys.L,sys.Sc)
    press .*= cellsize(sys)
    return press
end
pressure(integ::ConstrainedSystems.OrdinaryDiffEq.ODEIntegrator) = pressure(integ.u,integ.p,integ.t)

# Surface field quantities

for fcn in (:traction,:pressurejump)
  @eval $fcn(s::ConstrainedSystems.ArrayPartition,a...;kwargs...) = $fcn(constraint(s),a...;kwargs...)
  @eval $fcn(integ::ConstrainedSystems.OrdinaryDiffEq.ODEIntegrator) = $fcn(integ.u,integ.p,integ.t)
end

traction(τ,sys::NavierStokes{NX,NY,0,MT,FS,SD},t) where {NX,NY,MT,FS,SD} = τ

traction(τ::VectorData{N},sys::NavierStokes{NX,NY,N,MT,FS,ExternalInternalFlow},t) where {NX,NY,N,MT,FS} = τ

function traction(τ::VectorData{N},sys::NavierStokes{NX,NY,N,MT,FS,SD},t) where {NX,NY,N,MT,FS,SD}
    nrm = normals(sys.bodies)
    relative_surface_velocity!(sys.Δus,sys,t)
    pointwise_dot!(sys.Sb,nrm,sys.Δus)
    surface_velocity_jump!(sys.Δus,sys,t)
    product!(sys.Vb,sys.Δus,sys.Sb)
    return τ + sys.Vb
end

function pressurejump(τ::VectorData{N},sys::NavierStokes{NX,NY,N},t) where {NX,NY,N}

    #_hasfilter(sys) ? (τf = similar(τ); τf .= sys.Cf*force(τ,sys)) : τf = deepcopy(force(τ,sys))
    #τf = deepcopy(τ)
    #return nrm.u∘τf.u + nrm.v∘τf.v # This might need some modification

    press = zero(sys.Sb)
    nrm = normals(sys.bodies)
    pointwise_dot!(press,nrm,traction(τ,sys,t))
    return -press
end

## Total quantities

force(τ,sys::NavierStokes{NX,NY,0},t,bodyi::Int) where {NX,NY} = Vector{Float64}(), Vector{Float64}()

function force(τ::VectorData{N},sys::NavierStokes,t,bodyi::Int) where {N}
    product!(sys.τ,traction(τ,sys,t),areas(sys.bodies))
    fxvec = sum(sys.τ.u,sys.bodies,bodyi)
    fyvec = sum(sys.τ.v,sys.bodies,bodyi)
    return fxvec, fyvec
end

force(s::ConstrainedSystems.ArrayPartition,sys,t,bodyi) = force(constraint(s),sys,t,bodyi)

"""
    force(integ,bodyindex)

Given the state of the system in integrator `integ`, return the current force
on the body with index `bodyindex`.
"""
force(integ::ConstrainedSystems.OrdinaryDiffEq.ODEIntegrator,bodyi) = force(integ.u,integ.p,integ.t,bodyi)

"""
    force(sol,sys::NavierStokes,bodyindex) -> Tuple{Vector,Vector}

Given the solution history vector `sol` and the system `sys`, return the force
history on the body with index `bodyindex` as a tuple of vectors, one for
each component.
"""
function force(sol::ConstrainedSystems.OrdinaryDiffEq.ODESolution,sys::NavierStokes,bodyi::Int)
    fx = map((u,t) -> force(u,sys,t,bodyi)[1],sol.u,sol.t)
    fy = map((u,t) -> force(u,sys,t,bodyi)[2],sol.u,sol.t)
    fx, fy
end

function moment(τ::VectorData{N},bodies::BodyList,sys::NavierStokes,t,bodyi::Int;center=(0.0,0.0)) where {N}
    xc, yc = center
    x, y = collect(bodies)
    dX = VectorData(x .- xc,  y .- yc)
    product!(sys.τ,traction(τ,sys,t),areas(bodies))
    return sum(cross(dX,sys.τ),bodies,bodyi)
end

function moment(s::ConstrainedSystems.ArrayPartition,sys::NavierStokes{NX,NY,N,MovingPoints},t,bodyi;center=(0.0,0.0)) where {NX,NY,N}
    bodies = deepcopy(sys.bodies)
    tl! = RigidTransformList(aux_state(s))
    tl!(bodies)
    moment(constraint(s),bodies,sys,t,bodyi;center=center)
end

moment(s::ConstrainedSystems.ArrayPartition,sys::NavierStokes{NX,NY,N,StaticPoints},t,bodyi;center=(0.0,0.0)) where {NX,NY,N} =
        moment(constraint(s),sys.bodies,sys,t,bodyi;center=center)

"""
    moment(integ,bodyindex[,center=(0,0))

Given the state of the system in integrator `integ`, return the current moment
on the body with index `bodyindex`.  The center of the
moment can be passed as an optional tuple argument.
"""
moment(integ::ConstrainedSystems.OrdinaryDiffEq.ODEIntegrator,bodyi;center=(0.0,0.0)) =
      moment(integ.u,integ.p,integ.t,bodyi,center=center)

"""
    moment(sol,sys::NavierStokes,bodyindex[,center=(0,0)]) -> Vector

Given the solution history vector `sol` and the system `sys`, return the moment
history on the body with index `bodyindex` as a vector. The center of the
moment can be passed as an optional tuple argument.
"""
function moment(sol::ConstrainedSystems.OrdinaryDiffEq.ODESolution,sys::NavierStokes,bodyi::Int;center=(0.0,0.0))
    mom = map((u,t) -> moment(u,sys,t,bodyi,center=center),sol.u,sol.t)
end


export FactorizationMachines

"""
    FactorizationMachines(
        data::DataAccessor,
        k::Int
    )

Recommendation based on second-order factorization machines (FMs). Number of factors is configured by `k`.

Learning FM requires a set of parameters ``\\Theta = \\{w_0, \\mathbf{w}, V\\}`` and a loss function ``\\ell(\\hat{y}(\\mathbf{x} \\mid \\Theta), y)``. Ultimately, the parameters can be optimized by stochastic gradient descent (SGD).
"""
struct FactorizationMachines <: Recommender
    data::DataAccessor
    p::Int
    k::Int
    w0::Base.RefValue{Float64} # making mutable
    w::AbstractVector
    V::AbstractMatrix

    function FactorizationMachines(data::DataAccessor, k::Int)
        n_user, n_item = size(data.R)

        uv = []
        user_vectors = collect(values(data.user_attributes))
        if !isempty(user_vectors)
            uv = user_vectors[1]
        end
        iv = []
        item_vectors = collect(values(data.item_attributes))
        if !isempty(item_vectors)
            iv = item_vectors[1]
        end
        p = n_user + n_item + size(uv, 1) + size(iv, 1)

        w0 = Ref(0.)
        w = vector(p, type=Float64, initializer=undef)
        V = matrix(p, k, type=Float64, initializer=undef)

        new(data, p, k, w0, w, V)
    end
end

FactorizationMachines(data::DataAccessor) = FactorizationMachines(data, 20)

isbuilt(recommender::FactorizationMachines) = isfilled(recommender.V)

function build!(recommender::FactorizationMachines;
               reg_w0::Float64=1e-3,
               reg_w::Float64=1e-3,
               reg_V::Float64=1e-3,
               learning_rate::Float64=1e-3,
               eps::Float64=1e-3, max_iter::Int=100,
               random_init::Bool=false)
    n_user, n_item = size(recommender.data.R)

    if random_init
        w0 = rand()
        w = rand(Float64, size(recommender.w))
        V = rand(Float64, size(recommender.V))
    else
        w0 = 0.
        w = zeros(size(recommender.w))
        # initialize with small constants
        V = ones(size(recommender.V)) * 0.1
    end

    pairs = vec([(u, i) for u in 1:n_user, i in 1:n_item])
    for it in 1:max_iter
        converged = true

        shuffled_pairs = shuffle(pairs)
        for (u, i) in shuffled_pairs
            r = recommender.data.R[u, i]
            if isnan(r); continue; end

            u_onehot = zeros(n_user)
            u_onehot[u] = 1.

            i_onehot = zeros(n_item)
            i_onehot[i] = 1.

            x = vcat(u_onehot, i_onehot,
                     get_user_attribute(recommender.data, u),
                     get_item_attribute(recommender.data, i))

            interaction = sum((V' * x).^2 - (V'.^2 * x.^2)) / 2.
            pred = w0 + dot(w, x) + interaction

            err = r - pred
            if abs(err) >= eps; converged = false; end

            w0 = w0 + 2. * learning_rate * (err * 1. - reg_w0 * w0)

            prev_w = copy(w)
            prev_V = copy(V)

            prod = (x' * prev_V)'

            for j in 1:recommender.p
                if x[j] == 0.; continue; end

                w[j] = prev_w[j] + 2. * learning_rate * (err * x[j] - reg_w * prev_w[j])

                g = err * x[j] * (prod - x[j] * prev_V[j, :])
                V[j, :] = prev_V[j, :] + 2. * learning_rate * (g - reg_V * prev_V[j, :])
            end
        end

        if converged; break; end;
    end

    recommender.w0[] = w0
    recommender.w[:] = w[:]
    recommender.V[:] = V[:]
end

function predict(recommender::FactorizationMachines, u::Int, i::Int)
    check_build_status(recommender)
    n_user, n_item = size(recommender.data.R)

    u_onehot = zeros(n_user)
    u_onehot[u] = 1.

    i_onehot = zeros(n_item)
    i_onehot[i] = 1.

    x = vcat(u_onehot, i_onehot,
             get_user_attribute(recommender.data, u),
             get_item_attribute(recommender.data, i))

    interaction = sum((recommender.V' * x).^2 - (recommender.V'.^2 * x.^2)) / 2.
    recommender.w0[] + dot(recommender.w, x) + interaction
end


struct BruteForce <: HamiltonianSolver end

min_path(T, stops, ::BruteForce) = _min_length_path_exhaustive(T, stops)

function _min_length_path_exhaustive(T::TravelTimes, stops::Vector{<:AbstractStop})
    _min_length_path_exhaustive(T.travel_Times, stops)
end

# sdlve the minimum length hamiltonian path by brute-force search
# computes the minimum path of the given stops, starting from the first stop given
# in `stops` and finishing in any other other stops
function _min_length_path_exhaustive(T::AbstractMatrix, stops::Vector{<:AbstractStop})
    p0 = stops[1]

    # FIXME refactor
    stop_names = [name(s) for s in stops]
    M = view(T, stop_names, stop_names)

    perm = permutations(stops[2:end])

    num_stops = length(stops)

    minvalue = Inf
    local result
    for p in perm
        value = M[name(p0), name(p[1])]
        for i in 1:num_stops-2
            value += M[name(p[i]), name(p[i+1])]
        end

        if value < minvalue
            minvalue = value
            result = vcat(p0, p)
        end
    end

    return (minvalue, result)
end


module EmpiricalCovarianceOperatorsTests
    using EmpiricalCovarianceOperators
    using Test
    using Random
    using Statistics

    @testset "CovarianceOperator Tests" begin
        x = randn(500)
        X = randn(20,500)
        C = cov(X,X)
        A = CovarianceOperator(X)
        y = C*x
        Y = randn(20,300)
        A2 = CrossCovarianceOperator(X,Y)
        C2 = cov(X,Y)
        x2 = randn(300)
        y2 = C2*x2
        y1p5 = C2'*x
        @test isapprox(C, convert(Array{Float64}, A))
        @test isapprox(y, A*x)
        @test isapprox(convert(Array{Float64}, A), convert(Array{Float64}, A'))
        @test_broken isapprox(C2, convert(Array{Float64}, A2))
        @test isapprox(y2, A2*x2)
        @test isapprox(y1p5, A2'*x)
        @test_broken isapprox(x'*B*x2, x2'*B'*x)
    end
end


# global constant e0 and mass2 is expected
function interactionDynamic(config, qd, τIn, τOut)
    para = config.para

    dτ = abs(τOut - τIn)

    kDiQ = sqrt(dot(qd, qd))
    vd = 4π * e0^2 / (kDiQ^2 + mass2)
    if kDiQ <= para.qgrid.grid[1]
        q = para.qgrid.grid[1] + 1.0e-6
        wd = vd * Grid.linear2D(para.dW0, para.qgrid, para.τgrid, q, dτ)
        # the current interpolation vanishes at q=0, which needs to be corrected!
    else
        wd = vd * Grid.linear2D(para.dW0, para.qgrid, para.τgrid, kDiQ, dτ) # dynamic interaction, don't forget the singular factor vq
    end

    return vd / β, wd
end

function vertexDynamic(config, qd, qe, τIn, τOut)
    vd, wd = interactionDynamic(config, qd, τIn, τOut)
    ve, we = interactionDynamic(config, qe, τIn, τOut)

    return -vd, -wd, ve, we
end


@defcomp emissions begin
    CCA     = Variable(index=[time])    # Cumulative indiustrial emissions
    E       = Variable(index=[time])    # Total CO2 emissions (GtC per year)
    EIND    = Variable(index=[time])    # Industrial emissions (GtC per year)

    etree   = Parameter(index=[time])   # Emissions from deforestation
    MIU     = Parameter(index=[time])   # Emission control rate GHGs
    sigma   = Parameter(index=[time])   # CO2-equivalent-emissions output ratio
    YGROSS  = Parameter(index=[time])   # Gross world product GROSS of abatement and damages (trillions 2005 USD per year)

    function run_timestep(p, v, d, t)
        # Define function for EIND
        v.EIND[t] = p.sigma[t] * p.YGROSS[t] * (1 - p.MIU[t])

        # Define function for E
        v.E[t] = v.EIND[t] + p.etree[t]

        # Define function for CCA
        if is_first(t)
            v.CCA[t] = v.EIND[t] * 10
        else
            v.CCA[t] = v.CCA[t - 1] + v.EIND[t] * 10
        end
    end
end


export doubledescription
# Naive implementation of the double description Algorithm
# See JuliaPolyhedra/ConvexHull.jl for an efficient implementation

polytypefor(::Type{T}) where {T <: Integer} = Rational{BigInt}
polytypefor(::Type{Float32}) = Float64
polytypefor(::Type{T}) where {T} = T

polyvectortype(a) = a
# TODO sparse halfspaces does not mean sparse points
polyvectortype(::Type{<:SparseVector{T}}) where T = Vector{T}

dualtype(RepT::Type{<:Representation}) = dualtype(RepT, polyvectortype(vectortype(RepT)))
function dualfullspace(rep::Representation, d::FullDim, ::Type{T}) where T
    dualfullspace(rep, d, T, polyvectortype(similar_type(vectortype(rep), d, T)))
end
function dualfullspace(rep::Representation{N, T}) where {N, T}
    dualfullspace(rep, FullDim{N}(), polytypefor(T))
end

"""
    doubledescription(h::HRepresentation)

Computes the V-representation of the polyhedron represented by `h` using the Double-Description algorithm [1, 2].

    doubledescription(V::VRepresentation)

Computes the H-representation of the polyhedron represented by `v` using the Double-Description algorithm [1, 2].

[1] Motzkin, T. S., Raiffa, H., Thompson, G. L. and Thrall, R. M.
The double description method
*Contribution to the Theory of Games*, *Princeton University Press*, **1953**

[2] Fukuda, K. and Prodon, A.
Double description method revisited
*Combinatorics and computer science*, *Springer*, **1996**, 91-111
"""
function doubledescription end

function doubledescription(h::HRepresentation)
    # The redundancy of V-elements are measured using
    # the number of hyperplane they are in. If there are
    # redundant hyperplanes, it does not matter since no point
    # will be inside them but if there are duplicates it is a problem

    # FIXME Note that removevredundancy, uses `h` which contains all hyperplanes and halfspaces
    # already taken into account but also all the other ones. We should check that this
    # is the right idea.
    h = removeduplicates(h)
    v = dualfullspace(h)
    checkvconsistency(v)
    for hp in hyperplanes(h)
        #v = removeduplicates(v ∩ hel)
        # removeduplicates is cheaper than removevredundancy since the latter
        # needs to go through all the hrep element
        v = removevredundancy(removeduplicates(v ∩ hp), h)
        #v = removevredundancy(v ∩ hel, h)
    end
    for hs in halfspaces(h)
        #v = removeduplicates(v ∩ hel)
        # removeduplicates is cheaper than removevredundancy since the latter
        # needs to go through all the hrep element
        v = removevredundancy(removeduplicates(v ∩ hs), h)
        #v = removevredundancy(v ∩ hel, h)
    end
    v
end

function doubledescription(v::VRepresentation{N, T}) where {N, T}
    checkvconsistency(v)
    lv = LiftedVRepresentation{N, T, Matrix{T}}(v)
    R = -lv.R
    vl = doubledescription(MixedMatHRep{N+1, T}(R, zeros(T, size(R, 1)), lv.linset))
    LiftedHRepresentation{N, T}(vl.R, vl.Rlinset)
end