module EngEconomics

using Dates

export simpleInterest, findFractionalPeriod

# Simple Interest (F = P(1 + i x n))
# F = Future amount
# P = Principal amount
# i = simple interest rate
# n = period of time
function simpleInterest(P::Number, i::Number, n::Number)::Number
	F = P * (1 + i * n)
end

# Find Fraction Period (360 days a year, 12 months, 60 days)
function findFractionalPeriod(date1::Date, date2::Date)::Number
	monthDays = 30 * (Dates.month(date2) - Dates.month(date1))
	d = Dates.day(date2) - Dates.day(date1) + 1
	return (monthDays + d) / 360
end

export learningCurveModel, learningCurveModelCoeff, learningCurvePercentage
function learningCurveModel(T_init, N, b)
	return T_init * N^b
end

function learningCurveModelCoeff(T1, N1, T2, N2)
	b = log(T1 / T2) / log(N1 / N2)
end

function learningCurvePercentage(b)
	return exp(b * log(2))
end

export compoundAmountFactor, presentWorthFactor
# Compound Amount Factor: (F/P, i, N)
# - gives future amount, F, that is equivalent to a present amount, P, when the
#   interest rate is i, and the number of periods is N
# - How much money will be in the bank after 15 years if we invest 100 dollars
#   now at 8% interest compounded semi-annually
function compoundAmountFactor(i, N)
	(1 + i)^N
end

# Present Worth Factor: (P/F, i, N)
# - gives the present amount, P, that is equivalent to the future amount, F,
#   when the interest rate is i, and the number of periods is N
# - How much should be invested now so that in 4 years we will have 1360.5
#   dollars in the bank if the interest rate is 8%
function presentWorthFactor(i, N)
	1 / ((1 + i)^N)
end

export sinkingFundFactor, uniformSeriesCompoundAmoundFactor
# Sinking Fund Factor: (A/F, i, N)
# - gives the size of A of a repeated receipt or disbursement that is equivalent
#   to a future amount F if the interest rate is i and the number of periods is
#   N
# - If you want to accumulate 50,000 in 5 years, how much should we invest
#   yearly if the interest rate is 10%? F * (A/F,10%, 5)
function sinkingFundFactor(i, N)
	i / ((1 + i)^N - 1)
end

# Uniform Series Compound Amount Factor: (F/A, i, N)
# - gives the future value F that is equivalent to a series of equal size
#   receipts or disbursements of A when the interest rate is i and the number of
#   periods is N
# - If you deposit $200 yearly for 45 years if the interest rate is 12% how much
#   will be in your savings account? 200 (A/F, 12%, 45)
function uniformSeriesCompoundAmoundFactor(i, N)
	((1+i)^N - 1) / i
end

export capitalRecoveryFactor, seriesPresentAmountFactor
# Capital Recovery Factor: (A/P, i, N)
# - gives the value of A of the equal periodic payments or receipts that are
#   equivalent to the present amount P when the interest is i and the number of
#   period is N
# - how much should I pay per month to be equal to just paying $100 dollars now
#   for the thing?
function capitalRecoveryFactor(i, N)
	(i * (1 + i)^N) / ((i+1)^N - 1)
end

# Series Present Worth Factor: (P/A,i,N)
# - gives the present amount, P, that is equivalent to an annuity with
#   disbursements or receipts in the amount of A, where the interest rate is i
#   and the number of periods is N
# - how much should I pay now to get out of paying $200 a month for 40 years if
#   the interest is 2%
function seriesPresentAmountFactor(i, N)
	((1 + i)^N - 1) / (i * (1 + i)^N)
end

export singlePaymentPresentWorthArithmetic, sinkingFundPaymentArithmetic
# Single Payment Present Worth for Arithmetic Gradient Series:
# - gives the present worth of paying a gradient cost for N periods at an
#   interest rate of i
# - (P/G, i, N)
function singlePaymentPresentWorthArithmetic(i, N)
	((1+i)^N - i * N - 1) / (i^2 * (1+i)^N)
end

# Sinking Fund Payment for Arithmetic Gradient Series
# - (A/G, i, N)
function sinkingFundPaymentArithmetic(i, N)
	(1 / i - N / ((1+i)^N - 1))
end

export presentWorthConversionFactorGeometric
# Conversion Factor for Geometric Gradient Series (P/A, g, i, N)
# - gives the present worth P that is equivalent to a geometric gradient series
#   where the base receipt or disbursement is A, the growth rate is g, and the
#   interest rate is i, and the number of periods is N
function presentWorthConversionFactorGeometric(g, i, N)
	i0 = (1 + i) / (1 + g) - 1
	(((1 + i0)^N - 1) / (i0 * (1+i0)^N)) * 1 / (1 + g)
end

export paybackPeriod
function paybackPeriod(firstCost, annualSavings)
	return firstCost / annualSavings
end

end


# ------------------------------------------------------------------------------------------
# # Factorizaciones y otras diversiones
# Autor: Andreas Noack Jensen (MIT) (http://www.econ.ku.dk/phdstudent/noack/)
# (con edición de Jane Herriman=
#
# ## Sinopsis
#  - Factorizaciones
#  - Estructuras de matrices especiales
#  - Álgebra lineal genérica
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Empezamos con un sistema lineal de la forma
#
# `Ax = b`
# ------------------------------------------------------------------------------------------

A = randn(3,3)

x = fill(1.0, (3))
b = A*x

# ------------------------------------------------------------------------------------------
# ### Factorización
# La función `\` esconde cómo regularmente se resuelve el problema.
#
# Dependiendo de las dimensiones de `A`, distintos métodos son elegidos para resolver el
# problema.
#
# ```
# Ax = b
# ```
#
# Un paso intermedio en la solución es el cálculo de la factorización de la matriz `A`.
#
# Básicamente, una factorización de `A` es una manera de expresar `A` como el producto de
# matrices triangulares, unitarias, y de permutación.
#
# Julia guarda estas factorizaciones usando un tipo abstracto llamado `Factorization` y
# varios subtipos.
#
# Un objeto `Factorization` entonces debería ser pensado como una represenatación de la
# matriz `A`.
#
# #### LU
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Cuando `A` es cuadrada, el sistema linear es resuelto factorizando `A` vía
#
# ```
# PA=LU
# ```
#
# donde `P` es una matriz de permutación, `L` es un triangular inferior unitaria y `U` es
# triangular superior.
#
# Julia permita calcular la facorización LU y define un tipo de factorización compuesta para
# guardarlo.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Podemos hacer una factorización LU sobre `A` vía `lu(A)` ó `lufact(A)`.
#
# La función `lu` regresa matrices `l` y `u` y un vector de permutación `p`.
# ------------------------------------------------------------------------------------------

l,u,p = lu(A)

# ------------------------------------------------------------------------------------------
# El pivoteo está prendido por default, o sea que no podemos assumir que A == LU.
# Vamos a comprobar esto viendo la norma de `LU - A`:
# ------------------------------------------------------------------------------------------

norm(l*u - A)

# ------------------------------------------------------------------------------------------
# Esto nos muestra que queremos tomar en cuenta el pivoteo!
#
# Podemos pensar en `A[p,:]` como la sintaxis para `PA`, o como el producto de una matriz de
# permutación con `A`
# ------------------------------------------------------------------------------------------

norm(l*u - A[p,:])

# ------------------------------------------------------------------------------------------
# Por otro lado, podemos apagar el pivoteo usando `Val{false}` en Julia 0.6 ó `Val(false)`
# en versiones más modernas.
# ------------------------------------------------------------------------------------------

l,u,p = lu(A, Val{false})

# ------------------------------------------------------------------------------------------
# Cuando apagamos el pivoteo, `LU = A`
# ------------------------------------------------------------------------------------------

norm(l*u - A)

# ------------------------------------------------------------------------------------------
# Una segunda manera de hacer la factorización es con `lufact`.
# ------------------------------------------------------------------------------------------

Alu = lufact(A)

# ------------------------------------------------------------------------------------------
# Distintas partes de la factorización las puedes accesar con índices especiales
# ------------------------------------------------------------------------------------------

Alu[:P]

Alu[:L]

Alu[:U]

# ------------------------------------------------------------------------------------------
# Podemos calcular la solución de $Ax=b$ del objecto de factorización
# ------------------------------------------------------------------------------------------

# PA = LU
# A = P'LU
# P'LUx = b
# LUx = Pb
# Ux = L\Pb
# x = U\L\Pb
Alu[:U]\(Alu[:L]\(Alu[:P]b))

# ------------------------------------------------------------------------------------------
# *Más importantemente,* podemos despachar sobre el tipo de `LU` y simplemente resolver el
# problema por medio de
# ------------------------------------------------------------------------------------------

Alu\b

# ------------------------------------------------------------------------------------------
# Esto puede ser útil si el mismo lado izquierdo es usado para lados derechos.
#
# La factorización también se puede usar para calcular el determinante pues
#
# $\det(A)=\det(PLU)=\det(P)\det(U)=\pm \prod u_{ii}$
#
# porque $U$ es triangular y el signo está definido por $\det(P)$.
# ------------------------------------------------------------------------------------------

det(A)

det(Alu)

# ------------------------------------------------------------------------------------------
# #### QR
# Cuando `A` es alta,
# ------------------------------------------------------------------------------------------

Atall = randn(3, 2)

# ------------------------------------------------------------------------------------------
# Julia calcula la solución de mínimos cuadrados $\hat{x}$ que minimiza $\|Ax-b\|_2$.
#
# Esto se puede hacer factorizando
#
# ```
# A=QR
# ```
#
# donde $Q$ es unitaria/ortogonal y
#
# $R=\left(\begin{smallmatrix}R_0\\0\end{smallmatrix}\right)$ y  $R_0$ es triangular
# superior.
#
# con la factorización QR la norma mínima se puede expresar como
#
# \begin{equation*}
# \|Ax-b\|=\|QRx-b\|=\|Q(Rx-Q'b)\|=\|Rx-Q'b\|=\left\|\begin{pmatrix}R_0x-Q_0'b\\Q_1'b\end{pm
# atrix}\right\|=\|R_0x-Q_0'b\|+\|Q_1'b\|
# \end{equation*}
#
# Y entonces el problema se puede reducir a resolver el problema cuadrado $R_0x=Q_0'b$ para
# $x$.
#
# Podemos factorizar QR sobre `Atall` vía
# ------------------------------------------------------------------------------------------

Aqr = qrfact(Atall)

# ------------------------------------------------------------------------------------------
# Otra característica de la factorización QR es que los tipos `Q` para guardar las matrices
# unitarias $Q$. Se pueden extraer de tipos `QR` con los índices
# ------------------------------------------------------------------------------------------

Aqr[:Q]

# ------------------------------------------------------------------------------------------
# Similarmente, la matriz superior triangular $R$ se puede extraer con el índice
# ------------------------------------------------------------------------------------------

Aqr[:R]

# ------------------------------------------------------------------------------------------
# En este caso se guarda R como una matriz 2x2 en vez de 3x2 porque el último renglón de R
# está lleno de 0's.

#
#
# Aunque la matriz `Aqr[:Q]` se imprime como $3\times 3$ en el objeto de factorización, en
# la práctica puede representar la versión delgada también. Así
# ------------------------------------------------------------------------------------------

Aqr[:Q]*ones(2)

# ------------------------------------------------------------------------------------------
# funciona y representa a $3 x 2$ matrix por un vector de 2-elementos.
#
# Similarmente,
# ------------------------------------------------------------------------------------------

Aqr[:Q]*ones(3)

# ------------------------------------------------------------------------------------------
# funciona representando la matriz $3x3$ y un vector de 3 elementos.
#
# Sin embargo, esto no significa que podemos multiplicar a `Q` por vectores de longitued
# arbitraria.
# ------------------------------------------------------------------------------------------

Aqr[:Q]*ones(4)

# ------------------------------------------------------------------------------------------
# La matriz tiene representación interna compacta, entonces indexar sólo hace sentido si uno
# sabe cómo la factorización guarda datos.
# ------------------------------------------------------------------------------------------

Aqr[:Q][1]

# ------------------------------------------------------------------------------------------
# El objeto QRCompactWY `\` tiene un método para QR y entonces el problema de los mínimos
# cuadrados es resuelto con
# ------------------------------------------------------------------------------------------

Aqr\b

# ------------------------------------------------------------------------------------------
# Y si en vez escribimos
# ------------------------------------------------------------------------------------------

Atall\b

# ------------------------------------------------------------------------------------------
# En vez de factorizar con QR a `Atall` primero, Julia va a defaultear factorizar *con*
# pivoteo.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Este default a pivotear la factorización QR le permite a Julia resolver problemas
# deficientes de rango.
#
# Podemos explícitamente escoger pivotear durante la factorización QR (de una matriz
# singular, por ejemplo) con `Val{true}`.
# ------------------------------------------------------------------------------------------

v = randn(3)
# Tomar el producto exterio de un vector consigo mismo nos da una matriz singular
singmatriz = v * v'

Aqrp = qrfact(singmatriz,Val{true})

# ------------------------------------------------------------------------------------------
# Notamos que el tipo que resulta del objecto de Factorization es distinto que antes.
#
# `\` también tiene un método de `QRPivoted` y el problema con rango deficiente es entonces
# calculado como
# ------------------------------------------------------------------------------------------

Aqrp\b

# ------------------------------------------------------------------------------------------
# #### Eigendescompisición y los SVDs (Valores de descomposición Singular)
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Los resultados de eigendescomposición y de la descomposición singular de valores se
# guardan en los tipos`Factorization`. Esto también incluye la factorización de Hessenberg y
# de Schur
#
# La eigendescomposición puede ser calculada como
# ------------------------------------------------------------------------------------------

Asym = A + A'
AsymEig = eigfact(Asym)

# ------------------------------------------------------------------------------------------
# Los valores y vectores se pueden recoger del tipo Eigen con un índice especial
# ------------------------------------------------------------------------------------------

AsymEig[:values]

AsymEig[:vectors]

# ------------------------------------------------------------------------------------------
# Una vez más, como la descomposición se guarda en un tipo, podemos despatchar sobre esos
# tipos y explotar un método especializado para cada factorización, e.g. que
# $A^{-1}=(V\Lambda V^{-1})^{-1}=V\Lambda^{-1}V^{-1}$.
# ------------------------------------------------------------------------------------------

inv(AsymEig)*Asym

# ------------------------------------------------------------------------------------------
# Julia también tiene una función `eig` que regresa una tupla con los valores y vectores
# ------------------------------------------------------------------------------------------

eig(Asym)

# ------------------------------------------------------------------------------------------
# No recomendamos esta versión.
#
# La función `svdfact` calcula la descomposición singular de valores
# ------------------------------------------------------------------------------------------

Asvd = svdfact(A[:,1:2])

# ------------------------------------------------------------------------------------------
# y de nuevo `\` tiene un método para el tipo que permite los mínimos cuadrados por SVD
# ------------------------------------------------------------------------------------------

Asvd\b

# ------------------------------------------------------------------------------------------
# Existen funciones especiales para proporcionar los valores sólamente: `eigvals` and
# `svdvals`.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ### Estructuras especiales de matrices
#
# La estructura de matrices es muy importante en el álgebra linear. Ésta estructura se le
# puede hacer explícita a Julia por medio de los tipos compuestos. Ejemplos: `Diagonal`,
# `Triangular`, `Symmetric`, `Hermitian`, `Tridiagonal` y `SymTridiagonal`. Se han escrito
# métodos especializados para cada tipo de matriz especial para aprovechar su estructura.
# Siguen algunos ejemplos:
# ------------------------------------------------------------------------------------------

A

# ------------------------------------------------------------------------------------------
# Creando una matriz Diagonal
# ------------------------------------------------------------------------------------------

Diagonal(diag(A))

Diagonal(A)

# ------------------------------------------------------------------------------------------
# Creando una matriz triangular inferior
# ------------------------------------------------------------------------------------------

LowerTriangular(tril(A))

LowerTriangular(A)

# ------------------------------------------------------------------------------------------
# Creando una matriz simétrica
# ------------------------------------------------------------------------------------------

Symmetric(Asym)

SymTridiagonal(diag(Asym),diag(Asym,1))

# ------------------------------------------------------------------------------------------
# Cuando se sabe que una matriz es e.g. triangular o simétrica Julia puede que resuelva el
# problema más rápido convirtiendo a una matriz especial.
#
# Para algunos procedimientos, Julia checa si el input de matriz es triangular o simétrica y
# lo convierte a tal estructura si lo detecta.
#
# Debería notarse que `Symmetric`, `Hermitian` y `Triangular` no copian la matriz original.
#
# #### Eigenproblema simétrico
# La capacidad de Julia para poder detectar si una matriz es simétrica/Hermitian puede
# influenciar muchísimo sobre qué tan rápido se puede resolver un problema de eigenvalor.
# ------------------------------------------------------------------------------------------

n = 1000;
A = randn(n,n);

# ------------------------------------------------------------------------------------------
# Usamos `A` para genera una matriz simétrica `Asym`
# ------------------------------------------------------------------------------------------

Asym = A + A';

# ------------------------------------------------------------------------------------------
# Ahora creemos una matriz Asym para simular una matriz simétrica con errores de punto
# flotante
# ------------------------------------------------------------------------------------------

Asym_noisy = copy(Asym); Asym_noisy[1,2] += 5eps();

# ------------------------------------------------------------------------------------------
# ¿Puede Julia determinar que ambas `Asym` y `Asym_noisy` son matrices simétricas?
# ------------------------------------------------------------------------------------------

println("Is Asym symmetric? ", issymmetric(Asym))
println("Is Asym_noisy symmetric? ", issymmetric(Asym_noisy))

# ------------------------------------------------------------------------------------------
# Ahora veamos como el ruido de `Asym_noisy` impacta el tiempo en llevar a cabo una
# eigendescomposición
# ------------------------------------------------------------------------------------------

@time eigvals(Asym);

@time eigvals(Asym_noisy);

# ------------------------------------------------------------------------------------------
# Por suerte, le podeemos proveer información explícita sobre la estructura de la matriz a
# Julia
# En este ejemplo, usamos la palabra clave `Symmetric`
# ------------------------------------------------------------------------------------------

@time eigvals(Symmetric(Asym_noisy));

# ------------------------------------------------------------------------------------------
# Y así nuestros cálculos son mucho más eficientes :)
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ### Un gran problema
# Usar matrices tridiagonales permite trabajar con problemas potencialmente muy grandes. El
# siguiente problema no seria posible resolverlo en una laptop si la matriz se tuviera que
# guardar como tipo `Matrix`.
# ------------------------------------------------------------------------------------------

n = 1_000_000;
A = SymTridiagonal(randn(n), randn(n-1));
@time eigmax(A)

# ------------------------------------------------------------------------------------------
# ### Álgebra lineal numérica
# La manera usual de agregar soporte para álgebra lineal numérica es haciendo un wrapper
# para subrutinas de BLAS y LAPACK. Para matrices con elementos `Float32`, `Float64`,
# `Complex{Float32}` ó `Complex{Float64}` esto es lo que hace Julia. Desde hace rato, Julia
# ha tnido soport para la multiplicación genérica de tipos. Así, cuando uno multiplica
# matrices de enteros, obtiene una matriz de enteros.
# ------------------------------------------------------------------------------------------

rand(1:10,3,3)*rand(1:10,3,3)

# ------------------------------------------------------------------------------------------
# Recientemente, más métodos de álgebra lineal se han añadido a Julia y ahora soporta
# factorizaciones generales de tipo `LU` y `QR`. Métodos generales para eigenvalores y SVD
# han sido escritos más recientemente en paquetería externa.
#
# En general, la factorización `LU` puede ser calculada cuando los elementos de la matriz se
# cierraon sobre los operadores `+`, `-`, `*` y `\`. Por supuesto, la matriz también deben
# tener rango completo. El método general de factorización `LU` en Julia aplica pivoteo y
# por lo tanto debe poder soportar `<` y `abs`. Por lo tanto es posible resolver sistemas de
# ecuaciones de e.g. números racionales como en los ejemplos que siguen.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Para usar números racionales, usamos un doble //:
# ------------------------------------------------------------------------------------------

1//2

# ------------------------------------------------------------------------------------------
# #### Ejemplo 1: Sistemas racionales lineales de ecuaciones
# Julia cuenta con números racionales ya instalados. El siguiente ejemplo consta de un
# sistema lineal de ecuaciones resulto sin promover a tipos de punto flotanto. Puede haber
# un error de overflow fácilmente al trabajar con racionales, así que usamos `BigInt`s.
# ------------------------------------------------------------------------------------------

Ar = convert(Matrix{Rational{BigInt}}, rand(1:10,3,3))/10

x = fill(1, (3))
b = Ar*x

Ar\b

lufact(Ar)

# ------------------------------------------------------------------------------------------
# #### Ejemplo 2: Matriz racional de eigenestructura
#
# El siguiente ejemplo muestra como la artimética de matriz racional puede ser usada para
# calcular una matriz dados los eigenvalores y eigenvectores racionales. Yo he encontrado
# ésto útil para mostrar ejemplos de sistemas dinámicos lineales.
# ------------------------------------------------------------------------------------------

λ1,λ2,λ3 = 1//1,1//2,1//4
v1,v2,v3 = [1,0,0],[1,1,0],[1,1,1]
V,Λ = [v1 v2 v3], Diagonal([λ1,λ2,λ3])
A = V*Λ/V

# ------------------------------------------------------------------------------------------
# ### Exercises
#
# 11.1 ¿Cuáles son los eigenvalores de la Matriz A
#
# ```
# A =
# [
#  140   97   74  168  131
#   97  106   89  131   36
#   74   89  152  144   71
#  168  131  144   54  142
#  131   36   71  142   36
# ]
# ```
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# 11.2 Crea una matriz diagonal de los eigenvalores de A
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# 11.3 Realiza un factorización de Hessenberg sobre la matriz A. Verifica que `A = QHQ'`.
# ------------------------------------------------------------------------------------------

.nbexports/introductory-tutorials/intro-to-julia-ES/11.Factorizaciones y otras diversiones.jl


"""
Comparison of means: plot posterior densities and compute odds against difference of 0.

Returns:
        diff_mean, draws_m1, draws_m2, qs, tst       ## out-of-date: plt,diff_mean,qs,tst

        plt = plot of densities
        diff_mean = MC sample from posterior of difference in means
        qs = HPD quantile intervals from diff_mean
        tst = posterior odds against zero difference and area in tail to one side of zero.

Enter either:
        compare_mean(x,y)
        x, y are two samples to compare_means

        compare_means(m1, m2, s1, s2, n1, n2)
        m1, m2, are sample means, s1, s2, are sample SDs, n1, n2 are sample number of obs.

Optional arguments:
        M = MC sample size (larger for improvement numerical accuracy)
        lbl = ["mu 1 label" "mu 1 label"],  use lbl = ["" ""] to eliminate labels
        lgd = :topright (choose legend position, :topleft to move to top left of figure, etc.)
"""
function compare_means(m1, m2, s1, s2, n1, n2; M = 1000000,  lbl=["mu 1" "mu 2"],lgd = :topright)
    draws_m1 = m1 .+ (s1/sqrt(n1)).*rand(TDist(n1-1),M)
    draws_m2 = m2 .+ (s2/sqrt(n2)).*rand(TDist(n2-1),M)
    diff_mean = draws_m1 - draws_m2
    plot(draws_m1,st=:histogram,normalize=true,bins=100)
    l = @layout([a b])
    plt1 = plot(draws_m1,st=:density,fill=(0,0.4,:red),alpha=0.4,label=lbl[1],legend=lgd,title="Posteriors for means")
    plot!(draws_m2,st=:density,fill=(0,0.4,:blue),alpha=0.4,label=lbl[2])
    plt2 = plot(diff_mean,st=:density,fill=(0,0.4,:green),alpha=0.4,label="",title="Posterior difference")
    vline!([0.0],color=:black,label="")
    plt3 = plot(plt1, plt2, layout=l)
    qs = quantile(diff_mean,[0.005 0.025 0.05 0.5 0.95 0.975 0.995])
    @show(qs)
    tst = [mcodds(diff_mean) bayespval(diff_mean)]
    @show(tst)
    return diff_mean, draws_m1, draws_m2, qs, tst, plt3
end

# function overload for using two samples directly
function compare_means(x,y; M = 1000000, lbl=["mu 1" "mu 2"],lgd = :topright)
    m1 = mean(x); m2 = mean(y)
    s1 = std(x); s2 = std(y)
    n1 = length(x); n2 = length(y)
    draws_m1 = m1 .+ (s1/sqrt(n1)).*rand(TDist(n1-1),M)
    draws_m2 = m2 .+ (s2/sqrt(n2)).*rand(TDist(n2-1),M)
    diff_mean = draws_m1 - draws_m2
    plot(draws_m1,st=:histogram,normalize=true,bins=100)
    l = @layout([a b])
    plt1 = plot(draws_m1,st=:density,fill=(0,0.4,:red),alpha=0.4,label=lbl[1],legend=lgd,title="Posteriors for means")
    plot!(draws_m2,st=:density,fill=(0,0.4,:blue),alpha=0.4,label=lbl[2])
    plt2 = plot(diff_mean,st=:density,fill=(0,0.4,:green),alpha=0.4,label="",title="Posterior difference")
    vline!([0.0],color=:black,label="")
    plt3 = plot(plt1, plt2, layout=l)
    qs = quantile(diff_mean,[0.005 0.025 0.05 0.5 0.95 0.975 0.995])
    @show(qs)
    tst = [mcodds(diff_mean) bayespval(diff_mean)]
    @show(tst)
    return diff_mean, draws_m1, draws_m2, qs, tst, plt3
end

"""
 2x2 contigency table (categorical data) comparison of proportions
 """
function compare_proportions(s1, s2, n1, n2; a =1, b = 1,M = 1000000, lbl=["mu 1" "mu 2"],lgd = :topright)
    draws_m1 = rand(Beta((s1+a),(n1-s1+b)),M)
    draws_m2 = rand(Beta((s2+a),(n2-s2+b)),M)
    diff_mean = draws_m1 - draws_m2
    plot(draws_m1,st=:histogram,normalize=true,bins=100)
    l = @layout([a b])
    plt1 = plot(draws_m1,st=:density,fill=(0,0.4,:red),alpha=0.4,label=lbl[1],legend=lgd,title="Posteriors for proportions",ylab="Density",xlab="Proportion")
    plot!(draws_m2,st=:density,fill=(0,0.4,:blue),alpha=0.4,label=lbl[2])
    plt2 = plot(diff_mean,st=:density,fill=(0,0.4,:green),alpha=0.4,label="",title="Posterior difference",xlab="Difference in proportion")
    vline!([0.0],color=:black,label="",linewidth=2)
    plt3 = plot(plt1, plt2, layout=l)
    qs = quantile(diff_mean,[0.005 0.025 0.05 0.5 0.95 0.975 0.995])
    @show(qs)
    tst = [mcodds(diff_mean) bayespval(diff_mean)]
    @show(tst)
    return diff_mean, draws_m1, draws_m2, qs, tst, plt3
end

function compare_proportions(x, y; a =1, b = 1,M = 1000000, lbl=["mu 1" "mu 2"],lgd = :topright)
    n1 = length(x); s1 = sum(x)
    n2 = length(y); s2 = sum(y)
    draws_m1 = rand(Beta((s1+a),(n1-s1+b)),M)
    draws_m2 = rand(Beta((s2+a),(n2-s2+b)),M)
    diff_mean = draws_m1 - draws_m2
    plot(draws_m1,st=:histogram,normalize=true,bins=100)
    l = @layout([a b])
    plt1 = plot(draws_m1,st=:density,fill=(0,0.4,:blue),alpha=0.4,label=lbl[1],legend=lgd,title="Posteriors for proportions")
    plot!(draws_m2,st=:density,fill=(0,0.4,:red),alpha=0.4,label=lbl[2])
    plt2 = plot(diff_mean,st=:density,fill=(0,0.4,:green),alpha=0.4,label="",title="Posterior difference")
    vline!([0.0],color=:black,label="")
    plt3 = plot(plt1, plt2, layout=l)
    qs = quantile(diff_mean,[0.005 0.025 0.05 0.5 0.95 0.975 0.995])
    @show(qs)
    tst = [mcodds(diff_mean) bayespval(diff_mean)]
    @show(tst)
    return diff_mean, draws_m1, draws_m2, qs, tst, plt3
end

"""
Function to draw from Beta(s+a, n-s+b) posterior

     Default prior is uniform: a = b = 1
"""
function beta_posterior(s,n; a=1, b=1, M=100000)
    theta_draws = rand(Beta((s+a),(n-s+b)),M)
    return theta_draws
end

"""
Function to update Beta(s+a, n-s+b) posterior given two samples

     Default prior is uniform: a = b = 1 for each sample distribution
"""
function beta_update(s1,n1,s2,n2; a1=1,b1=1,a2=1,b2=1,M=100000)
    theta_draws = rand(Beta((s1+s2+a1+a2),(n1+n2-s1-s2+b1+b2)),M)
    return theta_draws
end

"""
equiv_test = Equivalence test from MC sample of difference in ATE

    mc_sample = posterior simulation sample of difference in treatments

    interval = 1/2 tolerance interval  = distance from 0.0 to one side of interval

    see TADS_equiv.jl for example of use.
"""
function equiv_test(mc_sample,interval;h0=0.0)
  p1 = p2 = p3 = p4 = p5 = 0.0
  for th1 in mc_sample
      if th1 < (h0-3*interval)
          p1 += 1
      elseif th1 < (h0-interval)
          p2 += 1
      elseif th1 < (h0+interval)
          p3 += 1
      elseif th1 < (h0+3*interval)
          p4 += 1
      else
          p5 += 1
      end
  end
  ps = [p1; p2; p3; p4; p5]
  normps = ps./sum(p1 + p2 + p3 + p4 + p5)
  x = [-4*interval; -2*interval; 0.0; 2*interval; 4*interval]
#  plt = plot(mc_sample,st=:density,label="",ylab="",alpha=0.4,fill=(0,0.4,:green),ylim=(0.0,0.4))
#  plot!(twinx(),x,normps, st=:bar, color=:grey,alpha=0.4,bar_width=2*interval,label="",ylim=(0.0,1.0),ylab="Probability",title = "Equivalence interval probabilities") #,axis=:right
  plt = plot(mc_sample,st=:density,label="",ylab="",alpha=0.2,fill=(0,0.2,:blue)) #,ylim=(0.0,0.4))
  plot!(twinx(),x,normps, st=:bar, color=:green,alpha=0.4,bar_width=2*interval,label="",ylim=(0.0,1.0),ylab="",title = "Equivalence interval probabilities") #,axis=:right
  oddsf35 = normps[3]/normps[5]
  oddsf34 = normps[3]/normps[4]
  oddsf32 = normps[3]/normps[2]
  oddsf31 = normps[3]/normps[1]
  eqodds = [oddsf31 oddsf32 oddsf34  oddsf35]
#  @show(eqodds)
#  @show(normps)
  return eqodds, normps, plt, x
end

"""
Equivalance test with 7 intervals (equiv_test has 5)
"""
function equiv_test7(mc_sample,interval;h0=0.0)
    p1 = p2 = p3 = p4 = p5 = p6 = p7 = 0.0
    for th1 in mc_sample
        if th1 < (h0-5*interval)
            p1 += 1
        elseif th1 < (h0-3*interval)
            p2 += 1
        elseif th1 < (h0-interval)
            p3 += 1
        elseif th1 < (h0+interval)
            p4 += 1
        elseif th1 < (h0+3*interval)
            p5 += 1
        elseif th1 < (h0+5*interval)
            p6 += 1
        else
            p7 += 1
        end
    end
    ps = [p1; p2; p3; p4; p5; p6; p7]
    normps = ps./sum(p1 + p2 + p3 + p4 + p5 + p6 + p7)
    x = [-6*interval; -4*interval; -2*interval; 0.0; 2*interval; 4*interval; 6*interval;]
    #  plt = plot(mc_sample,st=:density,label="",ylab="",alpha=0.4,fill=(0,0.4,:green),ylim=(0.0,0.4))
    #  plot!(twinx(),x,normps, st=:bar, color=:grey,alpha=0.4,bar_width=2*interval,label="",ylim=(0.0,1.0),ylab="Probability",title = "Equivalence interval probabilities") #,axis=:right
    plt = plot(mc_sample,st=:density,label="",ylab="",alpha=0.2,fill=(0,0.2,:blue)) #,ylim=(0.0,0.4))
    plot!(twinx(),x,normps, st=:bar, color=:green,alpha=0.4,bar_width=2*interval,label="",ylim=(0.0,1.0),ylab="",title = "Equivalence interval probabilities") #,axis=:right
    oddsf47 = normps[4]/normps[7]
    oddsf46 = normps[4]/normps[6]
    oddsf45 = normps[4]/normps[5]
    oddsf43 = normps[4]/normps[3]
    oddsf42 = normps[4]/normps[2]
    oddsf41 = normps[4]/normps[1]
    eqodds = [oddsf41 oddsf42 oddsf43 oddsf45 oddsf46 oddsf47]
    #  @show(eqodds)
    #  @show(normps)
    return eqodds, normps, plt, x
end


#
# Univariate Calculus
#

macro test_simplified_isequal(lhs, rhs)
    quote
        @test isequal(simplify($(esc(lhs))), $(esc(rhs)))
    end
end

@test isequal(differentiate(:(2), :x), 0)
@test isequal(differentiate(:(x), :x), 1)
@test_simplified_isequal differentiate(:(x + x), :x) 2
@test_simplified_isequal differentiate(:(x - x), :x) 0
@test_simplified_isequal differentiate(:(2 * x), :x) 2
@test_simplified_isequal differentiate(:(2 / x), :x) :(-2 / x^2)
@test_simplified_isequal differentiate(:(x / 2), :x) 0.5
@test_simplified_isequal differentiate(:(sin(x) / x), :x) :((cos(x) * x - sin(x)) / x^2)
@test_simplified_isequal differentiate(:(x * 2), :x) 2
@test_simplified_isequal differentiate(:(a * x), :x) :a
@test_simplified_isequal differentiate(:(x * a), :x) :a
@test_simplified_isequal differentiate(:(x ^ 2), :x) :(2 * x)
@test_simplified_isequal differentiate(:(a * x ^ 2), :x) :(a * (2 * x))
@test_simplified_isequal differentiate(:(2 ^ x), :x) :(*(0.6931471805599453, ^(2, x)))
@test_simplified_isequal differentiate(:(sin(x)), :x) :(cos(x))
@test_simplified_isequal differentiate(:(cos(x)), :x) :(-sin(x))
@test_simplified_isequal differentiate(:(tan(x)), :x) :(1 + tan(x)^2)
@test_simplified_isequal differentiate(:(exp(x)), :x) :(exp(x))
@test_simplified_isequal differentiate(:(log(x)), :x) :(1 / x)
@test_simplified_isequal differentiate(:(sin(x) + sin(x)), :x) :(cos(x) + cos(x))
@test_simplified_isequal differentiate(:(sin(x) - cos(x)), :x) :(cos(x) + sin(x))
@test_simplified_isequal differentiate(:(x * sin(x)), :x) :(sin(x) + x * cos(x))
@test_simplified_isequal differentiate(:(x / sin(x)), :x) :((sin(x) - x * cos(x)) / (sin(x)^2))
@test_simplified_isequal differentiate(:(sin(sin(x))), :x) :(*(cos(x),cos(sin(x))))
@test_simplified_isequal differentiate(:(sin(cos(x) + sin(x))), :x) :(*(+(-sin(x),cos(x)),cos(+(cos(x),sin(x)))))
@test_simplified_isequal differentiate(:(exp(-x)), :x) :(-exp(-x))
@test_simplified_isequal differentiate(:(log(x^2)), :x) :((2x) * (1 / x^2))
@test_simplified_isequal differentiate(:(abs2(x)), :x) :(2x)
@test_simplified_isequal differentiate(:(inv(x)), :x) :(-abs2(inv(x)))
@test_simplified_isequal differentiate(:(x^n), :x) :(*(n, ^(x, -(n, 1))))
@test_simplified_isequal differentiate(:(n^x), :x) :(*(^(n, x), log(n)))
@test_simplified_isequal differentiate(:(n^n), :x) 0

#
# Multivariate Calculus
#

@test isequal(simplify.(differentiate(:(sin(x) + sin(y)), [:x, :y])), [:(cos(x)), :(cos(y))])
@test isequal(simplify.(differentiate(:(x^2), [:x, :y])), Any[:(2*x), 0])

# TODO: Get the generalized power rule right.
# @test isequal(differentiate(:(sin(x)^2), :x), :(2 * sin(x) * cos(x)))

#
# Strings instead of symbols
#

# @test isequal(differentiate("sin(x) + cos(x)^2"), :(+(cos(x),*(2,cos(x)))))
@test_simplified_isequal differentiate("x + exp(-x) + sin(exp(x))", :x) :(+(1,-exp(-x),*(exp(x),cos(exp(x)))))

# TODO: Make these work
# differentiate(:(sin(x)), :x)(0.0)
# differentiate(:(sin(x)), :x)(1.0)
# differentiate(:(sin(x)), :x)(pi)

#
# SymbolicVariable use
#

x = BasicVariable(:x)
y = BasicVariable(:y)

@test isequal(@sexpr(x + y), :($x + $y))
@test_simplified_isequal differentiate(@sexpr(3 * x), x) 3
@test_simplified_isequal differentiate(:(sin(sin(x))), :x) :(*(cos(x),cos(sin(x))))
@test_simplified_isequal differentiate(@sexpr(sin(sin(x))), x) :(*(cos($x),cos(sin($x))))

function testfun(x)
    z = BasicVariable(:z)
    simplify(differentiate(@sexpr(3*x + x^2*z), z))
end

@test isequal(testfun(x), :(^($(x),2)))
@test isequal(testfun(3), 9)
@test isequal(testfun(@sexpr(x+y)), :(^(+($x,$y),2)))

#
# Simplify tests
#

@test isequal(simplify(:(x-0)), :x)
@test isequal(simplify(:(0-x)), :(-x))

@test isequal(simplify(:(x+y)), :(+(x,y)))
@test isequal(simplify(:(x+3)), :(+(3,x)))
@test isequal(simplify(:(x+3+4)), :(+(7,x)))
@test isequal(simplify(:(2+y+x+3)), :(+(5,y,x)))

@test isequal(simplify(:(x*y)), :(*(x,y)))
@test isequal(simplify(:(x*3)), :(*(3,x)))
@test isequal(simplify(:(x*3*4)), :(*(12,x)))
@test isequal(simplify(:(2*y*x*3)), :(*(6,y,x)))

@test isequal(simplify(:(sin((1*(sin(0/0)))))), NaN)

#
# Tests with ifelse
#
@test_simplified_isequal differentiate(:(ifelse(x < 1, exp(x^2), 1/x)), :x) :(ifelse(x < 1,2x * exp(x^2), -1/x^2))
@test_simplified_isequal differentiate(:(ifelse(x <= 0, 0, ifelse(x > 1, 1, x))), :x) :(ifelse(x <= 0, 0, ifelse(x > 1, 0, 1)))


# 2D diffusion problem

# Packages and inclusions
using ModelingToolkit,DiffEqOperators,LinearAlgebra,Test,OrdinaryDiffEq, DomainSets
using ModelingToolkit: Differential

# Tests
@testset "Test 00: Dt(u(t,x,y)) ~ Dxx(u(t,x,y)) + Dyy(u(t,x,y))" begin

    # Variables, parameters, and derivatives
    @parameters t x y
    @variables u(..)
    Dxx = Differential(x)^2
    Dyy = Differential(y)^2
    Dt = Differential(t)
    t_min= 0.
    t_max = 2.0
    x_min = 0.
    x_max = 2.
    y_min = 0.
    y_max = 2.

    # Analytic solution
    analytic_sol_func(t,x,y) = exp(x+y)*cos(x+y+4t)

    # Equation
    eq  = Dt(u(t,x,y)) ~ Dxx(u(t,x,y)) + Dyy(u(t,x,y))

    # Initial and boundary conditions
    bcs = [u(t_min,x,y) ~ analytic_sol_func(t_min,x,y),
           u(t,x_min,y) ~ analytic_sol_func(t,x_min,y),
           u(t,x_max,y) ~ analytic_sol_func(t,x_max,y),
           u(t,x,y_min) ~ analytic_sol_func(t,x,y_min),
           u(t,x,y_max) ~ analytic_sol_func(t,x,y_max)]

    # Space and time domains
    domains = [t ∈ Interval(t_min,t_max),
            x ∈ Interval(x_min,x_max),
            y ∈ Interval(y_min,y_max)]

    # Space and time domains
    @named pdesys = PDESystem([eq],bcs,domains,[t,x,y],[u(t,x,y)])

    dx = 0.1; dy = 0.2

    # Test against exact solution
    Nx = floor(Int64, (x_max - x_min) / dx) + 1
    Ny = floor(Int64, (y_max - y_min) / dy) + 1
    @variables u[1:Nx,1:Ny](t)
    r_space_x = x_min:dx:x_max
    r_space_y = y_min:dy:y_max
    asf = reshape([analytic_sol_func(t_max,r_space_x[i],r_space_y[j]) for j in 1:Ny for i in 1:Nx],(Nx,Ny))
    # Method of lines discretization
    order = 2
    discretization = MOLFiniteDifference([x=>dx,y=>dy],t;centered_order=order)
    prob = ModelingToolkit.discretize(pdesys,discretization)

    # Solution of the ODE system
    sol = solve(prob,Tsit5())

    # Test against exact solution
    sol′ = reshape([sol[u[(i-1)*Ny+j]][end] for i in 1:Nx for j in 1:Ny],(Nx,Ny))
    @test asf ≈ sol′ atol=0.4

    #Plot
    #using Plots
    #heatmap(sol′)
    #savefig("MOL_Linear_Diffusion_2D_Test00.png")
end

@testset "Test 01: Dt(u(t,x,y)) ~ Dx( a(x,y,u) * Dx(u(t,x,y))) + Dy( a(x,y,u) * Dy(u(t,x,y)))" begin

    # Variables, parameters, and derivatives
    @parameters t x y
    @variables u(..)
    Dx = Differential(x)
    Dy = Differential(y)
    Dt = Differential(t)
    t_min= 0.
    t_max = 2.0
    x_min = 0.
    x_max = 2.
    y_min = 0.
    y_max = 2.

    # Analytic solution
    analytic_sol_func(t,x,y) = exp(x+y)*cos(x+y+4t)

    # Equation
    eq = Dt(u(t,x,y)) ~ Dx( (u(t,x,y)^2 / exp(x+y)^2 + sin(x+y+4t)^2)^0.5 * Dx(u(t,x,y))) +
                        Dy( (u(t,x,y)^2 / exp(x+y)^2 + sin(x+y+4t)^2)^0.5 * Dy(u(t,x,y)))

    # Initial and boundary conditions
    bcs = [u(t_min,x,y) ~ analytic_sol_func(t_min,x,y),
           u(t,x_min,y) ~ analytic_sol_func(t,x_min,y),
           u(t,x_max,y) ~ analytic_sol_func(t,x_max,y),
           u(t,x,y_min) ~ analytic_sol_func(t,x,y_min),
           u(t,x,y_max) ~ analytic_sol_func(t,x,y_max)]

    # Space and time domains
    domains = [t ∈ Interval(t_min,t_max),
            x ∈ Interval(x_min,x_max),
            y ∈ Interval(y_min,y_max)]

    # Space and time domains
    @named pdesys = PDESystem([eq],bcs,domains,[t,x,y],[u(t,x,y)])

    # Method of lines discretization
    dx = 0.1; dy = 0.2
    discretization = MOLFiniteDifference([x=>dx,y=>dy],t)
    prob = ModelingToolkit.discretize(pdesys,discretization)

    # Solution of the ODE system
    sol = solve(prob,Rosenbrock23())

    # Test against exact solution
    Nx = floor(Int64, (x_max - x_min) / dx) + 1
    Ny = floor(Int64, (y_max - y_min) / dy) + 1
    @variables u[1:Nx,1:Ny](t)
    sol′ = reshape([sol[u[(i-1)*Ny+j]][end] for i in 1:Nx for j in 1:Ny],(Nx,Ny))
    r_space_x = x_min:dx:x_max
    r_space_y = y_min:dy:y_max
    asf = reshape([analytic_sol_func(t_max,r_space_x[i],r_space_y[j]) for j in 1:Ny for i in 1:Nx],(Nx,Ny))
    m = max(asf...)
    @test asf / m ≈ sol′ / m  atol=0.4 # TODO: use lower atol when g(x) is improved in MOL_discretize.jl

    #Plot
    #using Plots
    #heatmap(sol′)
    #savefig("MOL_NonLinear_Diffusion_2D_Test01.png")

end


#=
Problem 2
Each new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with 1 and 2, the first 10 terms will be:
1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
By considering the terms in the Fibonacci sequence whose values do not exceed four million, find the sum of the even-valued terms.
=#

last = 1
current = 1
temp = 0
sum = 0

while current <= 4000000
  temp = current + last
  last = current
  current = temp
  if(current % 2 == 0)
    sum += current
  end
end

println("The sum is: $(sum)")


# rng

#random number generators
#Computers are not capable of generating true random numbers
#but can generate sequences with statistical randomness

# 1 Julia's implemented solution
rand(100)

# LCP psuedo solutions

function rng_Park_Miller1998(n;x1=3)
    x=zeros(n)
    x[1] = x1
    u = zeros(n)
    for i in 2:n
        x[i] =mod(16807*x[i-1],2147483647)#7^5 \ 2^31-1 31th mason prime
    end
    u = x./2147483647
    return u
end
# implemented in Matlab 4 1990

rng_Park_Miller1998(100;x1=3)

function rng_randu(n;x1=3)
    x = zeros(n)
    x[1]=x1
    for i in 2:n
        x[i] = mod(65539*x[i-1],2147483648)
    end
    u = x./2147483648
    return u
end

rng_randu(100)

# iterative psudo solutions

function rng_LMap(n;x1=.4)
    x = zeros(n)
    x[1] = x1
    for i in 2:n
        x[i]=1-2*x[i-1]^2
    end
    return((x./2) .+.5)
end

rng_LMap(100)

# self - avoiding solutio

function rng_Halton(n;x1 = 3)
    b = zeros(Int(ceil(log(n)/log(x1))))
    u = zeros(n)
    for j in 1:n
        i = 1
        b[1] = b[1]+1
        while b[i]>(x1-1+eps())
            b[i]=0
            i=i+1
            b[i]=b[i]+1
        end

        u[j]=0
        for k=1:length(b)
            u[j] = u[j]+b[k]*Float64(x1)^(-k);
        end
    end
    return u
end

rng_Halton(100)


#=
# 1. Basic potential flow problem

This part introduces how to solve the basic potential flow problem in this package. We consider here a staggered, Cartesian grid with uniform cell size and of infinite extent. The basic (unbounded) potential flow problem is expressed as

$\mathsf{Ls} = -\mathsf{w},$

where $\mathsf{L}$ is the discrete 5-point Laplacian operator, $\mathsf{s}$ is the discrete streamfunction, and $\mathsf{w}$ is the discrete vorticity. Following the vortex-in-cell approach, the discrete vorticity is obtained by regularizing the vorticity from the $N_v$ vortex elements onto the cell vertices,

$\mathsf{w}_{i, j}=\sum_{q=1}^{N_{v}} \frac{1}{\Delta x} \Gamma_{v, q} d\left(\frac{\mathsf{x}_{i}-X_{q}}{\Delta x}\right) d\left(\frac{\mathsf{y}_{j}-Y_{q}}{\Delta x}\right)$

where $d$ is the $M_{4}'$ interpolation kernel, and $(X_{q},Y_{q})$ and $\Gamma_{v,q}$ are the position and strength of the $q$th vortex element.
=#

#=
## Flow around a point vortex
Now we will solve this discrete potential flow problem using `GridPotentialFlow.jl` to obtain the streamfunction field and velocity field around a single point vortex.
=#

# The first step in creating *any* `GridPotentialFlow` model is to create a `PhysicalGrid` to discretize the domain.
using GridPotentialFlow
Δx = 0.01
Lx = 2.0
xlim = (-Lx/2,Lx/2)
ylim = (-Lx/2,Lx/2)
g = PhysicalGrid(xlim,ylim,Δx);

# The second step is to create our point vortex using `Vortex`.
v = Vortex(0.0,0.0,1.0);

# Now we can create a `VortexModel` using the grid and an array containing the point vortex.
model = VortexModel(g,vortices=[v]);

# The discrete streamfunction `s` is then obtained using `streamfunction`.
s = streamfunction(model);
using Plots
plot(s,g,xlabel="x",ylabel="y")

# The function `streamfunction` returns a `Nodes` array. If we want to perform differential calculus operations this data, we can use the methods of the  `CartesianGrids` package. For example, we can easily obtain the velocity field from the streamfunction field using the `curl` operation.
q = curl(s);
plot(q,g,xlabel="x",ylabel="y")

#=
## Accuracy of the discretized Poisson equation
To verify that the discretization technique of the Poisson equation is second-order accurate, we perform a mesh refinement analysis for a flow consisting of point vortices of random strenght that are randomly positioned on the lower-left quadrant of a square domain.
=#

# The error is calculated by comparing the numerically calculated streamfunction field with the exact solution for the streamfunction field. We therefore need a function to calculate the exact streamfunction.
function ψ_vortex!(ψ::Nodes{Dual,nx,ny},vortex::Vortex,g::PhysicalGrid) where {nx,ny}
    x,y = coordinates(ψ,g)
    for i in 2:nx-1, j in 2:ny-1
        r = sqrt((x[i]-vortex.x)^2+(y[j]-vortex.y)^2)
        ψ[i,j] = ψ_vortex(r,vortex.Γ)
    end
end

function ψ_vortex(r::Real,Γ::Real)
    return -Γ/(2π)*log(r)
end

# We create four vortices with random strenghts, randomly positioned in the lower-left quadrant of the domain.
nv = 4;
vl = Vortex.(-Lx/4 .+ 0.4*Lx*(rand(nv).-0.5),-Lx/4 .+ 0.4*Lx*(rand(nv).-0.5),0.5*rand(nv).+0.5);

# Next, we create a series of grids, with each grid doubling the number of grid points of the previous grid in each direction.
grids = [PhysicalGrid(xlim,ylim,Lx/(nx-2)) for nx in [2^p for p in 5:9]];

# The error is calculated as $\epsilon = \Vert \psi(\mathsf{x},\mathsf{y})/\Delta x - \mathsf{s} \Vert_2 / \Vert \mathsf{s} \Vert_2$, for which we use the `norm` function.
using LinearAlgebra: norm

# We now loop over the grids and calculate the error.
errors = []
gridspacings = []
for g in grids
    ## LGF
    local model = VortexModel(g,vortices=vl)
    s_lgf = streamfunction(model);

    ## Exact solution
    s_exact = Nodes(Dual,size(g))
    s_temp = Nodes(Dual,size(g))
    for v in vl
        ψ_vortex!(s_temp,v,g)
        s_exact += s_temp
    end

    ## Bring to same reference level. A constant value can be added/subtracted to any potential flow solution
    s_lgf .-= s_lgf[g.I0[1],2];
    s_exact .-= s_exact[g.I0[1],2];

    error = s_lgf-s_exact
    idx = g.I0[1] + 1 : g.N[1] - 1 # Only look at top right corner
    push!(errors,norm(error[idx,idx])/norm(s_lgf[idx,idx]))
    push!(gridspacings,g.Δx)
end

# And finally, we create a log-log plot of the error versus the grid spacing.
firstorder=(errors[end]/gridspacings[end].*gridspacings).^1
secondorder=(sqrt(errors[end])/gridspacings[end].*gridspacings).^2
p=plot(gridspacings,errors,xaxis=:log,yaxis=:log,marker=:circle,lab="error",xlabel="dx",legend=:bottomright,title="Error")
plot!(gridspacings,firstorder,lab="1st order",linestyle=:dot,linecolor=:black)
plot!(gridspacings,secondorder,lab="2nd order",linestyle=:dash,linecolor=:black)

#=
## Corotating point vortices
Now we will try advancing a vortex model in time. The simplest unsteady vortex model consists of two point vortices. When both point vortices have the same strength, they will rotate around each other on a trajectory that is easy to describe analytically. In this example, we will compare the analytical and simulated trajectories during one revolution.
=#

# We create two vortices at a distance $d$ from each other and give them a strength $\Gamma$
d = Lx/2
Γ = 1
v1 = Vortex(d/2,0.0,Γ);
v2 = Vortex(-d/2,0.0,Γ);

# We can analytically determine the time $T$ it takes for these vortices to complete one revolution around their centroid.
Vθ = Γ/(2*π*d); # Analytical tangential velocity
T = π*d/Vθ # Analytical period

# Let's now create a vortex model with the two point vortices.
model = VortexModel(g,vortices=[v1,v2]);

# If we update the positions of the point vortices repeatedly to simulate the model advancing in time from $t=0$ to $t=T$, we can check if they end up again at their original positions.
tspan = (0.0,T);

# To step in time, we can simply update the position of the $q$th vortex as $X^{n+1}_q = X^{n}_q + Δt Ẋ^{n}_q$ (forward Euler) in a for-loop. Alternatively, we can make use of the `OrdinaryDiffEq.jl` package and use, for example, their fourth-order Runge-Kutta time stepping scheme.

import OrdinaryDiffEq

#= To construct the `ODEProblem` from this package, we will have to provide a right-hand side function that computes the velocities $Ẋ$ of the vortices, which is the local flow velocity at their positions. These velocities are obtained using `vortexvelocities`, which regularizes the vorticity to the grid, solves the potential flow system, and interpolates the velocities from the grid to the vortex locations as

$\left(U_{q}, V_{q}\right)=\sum_{i, j} \mathsf{v}_{i j} d\left(\frac{\mathsf{x}_{i}-X_{q}}{\Delta x}\right)\left(\frac{\mathsf{y}_{j}-Y_{q}}{\Delta x}\right),$

where $v$ is the velocity field on the nodes.
=#

function rhs(X,model,t)
    setvortexpositions!(model,X)
    Ẋ = vortexvelocities!(model)
    return Ẋ
end

# We finally create the problem and solve it with a fourth-order Runge-Kutta time stepping scheme.
X = getvortexpositions(model)
prob = OrdinaryDiffEq.ODEProblem(rhs,X,tspan,model);
sol = OrdinaryDiffEq.solve(prob,dt=0.1,OrdinaryDiffEq.RK4(),dense=false,adaptive=false);

# When we plot the x- and y-position versus time of the first vortex, we see it completed one revolution.
plot(sol.t,map(s->s.u[1],sol.u),xlabel='t',label="x₁")
plot!(sol.t,map(s->s.v[1],sol.u),label="y₁")

#jl @testset "Corotating point vortices" begin
#jl     @test isapprox(sol.u[end].u[1], 0.5; atol = 1e-2)
#jl     @test isapprox(sol.u[end].u[2], -0.5; atol = 1e-2)
#jl     @test isapprox(sol.u[end].v[1], 0.0; atol = 1e-2)
#jl     @test isapprox(sol.u[end].v[2], 0.0; atol = 1e-2)
#jl end

#=
## Corotating vortex patches
A more complex example is the evolution of two circular regions of spatially uniform vorticity that have an equal radius $r_0$ and circulation $\Gamma$, and whose centers are separated by a distance $d_0$. If $r_0/d_0$ is small enough, these two vortex patches can be regarded as two point vortices with the same circulation and separation. We therefore assume the period of this system is approximately the same as the the period for the point vortices case.
=#

#md # ```@setup 1.-Basic-potential-flow-problem
#md # xlim = (-2,2);
#md # ylim = (-2,2);
#md # Δx = 0.05;
#md # g = PhysicalGrid(xlim,ylim,Δx);
#md # ```
#!md xlim = (-2,2);
#!md ylim = (-2,2);
#!md Δx = 0.05;
#!md g = PhysicalGrid(xlim,ylim,Δx);

# We discretize the vortex patches with point vortices arranged on concentric rings using the following function.

function vortexpatch!(vort,xc,yc,Γ,radius,nring)
    Δr = radius/(nring-1/2)
    dΓ = Γ/(1+8*nring*(nring-1)/2)
    push!(vort,Vortex(xc,yc,dΓ))
    for ir in 1:nring-1
        nθ = 8*ir
        for j = 0:nθ-1
            push!(vort,Vortex(xc + ir*Δr*cos(2π*j/nθ),yc + ir*Δr*sin(2π*j/nθ),dΓ))
        end
    end
    return vort
end

vortexpatch(xc,yc,Γ,radius,nring) = vortexpatch!(Vortex[],xc,yc,Γ,radius,nring)

# We will simulate two cases with different values for $r_0/d_0$, the ratio of the vortex patch radius to the distance between them.
d0 = d
anims = []
for r0 in [0.2*d0, 0.4*d0]
    vortices = vcat(vortexpatch(0.0,0.0+d0/2,Γ,r0,10),vortexpatch(0.0,0.0-d0/2,Γ,r0,10));
    vortexcolors = vcat(fill(:blue,length(vortices)÷2),fill(:red,length(vortices)÷2));
    local model = VortexModel(g,vortices=vortices);
    local X = getvortexpositions(model)
    local prob = OrdinaryDiffEq.ODEProblem(rhs,X,tspan,model);
    local sol = OrdinaryDiffEq.solve(prob,dt=0.1,OrdinaryDiffEq.RK4(),dense=false,adaptive=false);
    anim = @animate for i=1:length(sol.t)-1
        plot(xlims=xlim,ylims=ylim,ratio=:equal,legend=:none,title="r0/d0 = $(r0/d0)")
        scatter!(sol.u[i].u,sol.u[i].v,markerstrokewidth=0,markersize=3,color=vortexcolors)
    end
    push!(anims,anim)
end

gif(anims[1])

# If the ratio $r_0/d_0$ is big enough, the two vortex patches merge with eachother, a result that has been widely reported in literature [^1].

gif(anims[2])

# [^1]: Eldredge J. D. (2019) "Mathematical  Modeling  of  Unsteady  Inviscid  Flows, Interdisciplinary Applied Mathematics" *Springer*, vol. 50.


## Basic understanding of logit regression.

using DataFrames
using CSV
using GLM
using FreqTables

obs = CSV.File("./logit_regression/logit_regression_data.csv", header=true) |> DataFrame

## summary of methods.
## want to model variable p in range (0, 1). This is really hard to do.
## probability is linked with odds ratio = prob of success / prob of failure
## taking the log of this is the log odds ratio.
## how do we transform p ∈ (0, 1) to odds ∈ (0, ∞)
## then take the log of odds to map to (-∞, ∞)
## This transformation is called logit transformation. The other common choice is the probit transformation,

## so regression of p is
## log(p/(1-p)) =  linear predictors

## now lets see what this means in the sample data.
# for the purpose of illustration.
# The data set has 200 observations and the outcome variable used will be hon, indicating if a student is in
# an honors class or not. So our p = prob(hon=1).

# Model 1, no predictors
ols = glm(@formula(hon ~ 1), obs, Binomial(), LogitLink())
coef(ols) # so the intercept here is the log odds -1.12
# taking the exponential gives us the odds ratio. I.e. what are the odds of getting into a honors class
exp.(coef(ols))
exp.(coef(ols)) ./ (1 .+ exp.(coef(ols))) # we can get p back as well

# we can verify all of this by looking at a frequnecy table
# lets look at the FreqTables
#freqtable(obs, :hon)

# Model 2, a single predictor: female
ols = glm(@formula(hon ~ female), obs, Binomial(), LogitLink())
coef(ols)   ## gets the log odds
exp.(coef(ols)) ## gets the odds ratio
## and can even calculate the prob given whether person is male or female

# here theintercept of -1.471 is the log odds for males since male is the reference group (female = 0).
# so this means the odds ratio is 0.23

# Using the frequency for males, we can confirm this: log(.23) = -1.47.

# Model 3, using math
ols = glm(@formula(hon ~ math), obs, Binomial(), LogitLink())
coef(ols)   ## gets the log odds
exp.(coef(ols)) ## gets the odds ratio
## and can even calculate the prob given math score.

# Model 3, using combination
ols = glm(@formula(hon ~ math + female + read), obs, Binomial(), LogitLink())
coef(ols)   ## gets the log odds
exp.(coef(ols)) ## gets the odds ratio
## and can even calculate the prob given a different combination of predictors

# This fitted model says that, holding math and reading at a fixed value, the odds of getting into an
# honors class for females (female = 1)over the odds of getting into an honors class for males (female = 0)
# is exp(.979948) = 2.66

# Model 4, interaction term
ols = glm(@formula(hon ~ math + female + female*math), obs, Binomial(), LogitLink())
coef(ols)   ## gets the log odds
exp.(coef(ols)) ## gets the odds ratio


function trapezoidal_rule(n::Int, a = -1.0, b = 1.0, ::Type{T} = typeof((b-a)/n)) where {T}
    grid = EquispacedGrid(n, a, b)
    weights = step(grid) * ones(T, n)
    weights[1] /= 2
    weights[end] /= 2
    grid, weights
end

function rectangular_rule(n::Int, a = 0.0, b = 1.0, ::Type{T} = typeof((b-a)/n)) where {T}
    grid = PeriodicEquispacedGrid(n, a, b)
    weight = step(grid)
    grid, Fill(weight,length(grid))
end

# The implementation of the first and second Fejer rule and of Clenshaw-Curtis
# quadrature is based on the exposition in:
# J. Waldvogel, Fast construction of the Fejer and Clenshaw-Curtis quadrature
# rules, BIT 43(1):1-18, 2003.
function fejer_vector1!(v)
    n = length(v)
    T = eltype(v)
    v[1] = T(2)
    for k in 1:(n-1)>>1
        r = T(2)/(1-4k^2)*exp(im*k*T(pi)/n)
        v[k+1] = r
        v[n-k+1] = conj(r)
    end
    if iseven(n)
        v[n>>1+1] = 0
    end
    v
end

function fejer_first_rule(n::Int, ::Type{T} = Float64) where {T}
    v = zeros(Complex{T}, n)
    fejer_vector1!(v)
    ChebyshevNodes{T}(n), real(ifft(v))
end

function fejer_vector2!(v)
    n = length(v)
    T = eltype(v)
    v[1] = T(2)
    for k in 1:(n-1)>>1
        r = T(2)/(1-4k^2)
        v[k+1] = r
        v[n-k+1] = r
    end
    if iseven(n)
        k = n>>1-1
        v[k+1] = T(2)/(1-4k^2)
        v[n>>1+1] = T(n-3)/(2*(n>>1)-1) - 1
    end
    v
end

# TODO: verify, is this correct?
function fejer_second_rule(n::Int, ::Type{T} = Float64) where {T}
    v = zeros(T, n)
    weights = zeros(T, n+1)
    fejer_vector2!(v)
    weights[1:n] = real(ifft(v))
    weights[n+1] = weights[1]
    ChebyshevExtremae{T}(n+1), weights
end

function cc_vector_g!(g)
    n = length(g)
    T = eltype(g)
    w0cc = one(T) / (n^2-1+mod(n,2))
    for k in 1:n
        g[k] = -w0cc
    end
    if iseven(n)
        g[n>>1+1] = w0cc * ((2-mod(n,2))*n-1)
    end
    g
end

function clenshaw_curtis(n, ::Type{T} = Float64) where {T}
    v = zeros(T, n)
    g = zeros(T, n)
    weights = zeros(T, n+1)
    fejer_vector2!(v)
    cc_vector_g!(g)
    weights[1:n] = real(ifft(v+g))
    weights[n+1] = weights[1]
    ChebyshevExtremae{T}(n+1), weights
end

function rescale_cc_quad(x, w, a, b)
    y = (reverse(x).+1)/2 * (b-a) .+ a
    v = w .* (b-a)/2
    y,v
end

function rescale_fejer_quad(x, w, a, b)
    y = (x.+1)/2 * (b-a) .+ a
    v = w .* (b-a)/2
    y,v
end

function graded_rule(sigma, a, b, M, n, T = Float64, ndiff = 2)
    xg = zeros(T, 0)
    wg = zeros(T, 0)
    q1 = one(T)
    for m = M-1:-1:1
        x,w = BasisFunctions.fejer_first_rule(n, T)
        q0 = sigma*q1
        xs,ws = rescale_fejer_quad(x, w, q0, q1)
        xg = [xg; xs]
        wg = [wg; ws]
        q1 = q0
        n -= ndiff
    end
    x,w = BasisFunctions.fejer_first_rule(n, T)
    xs,ws = rescale_fejer_quad(x, w, 0, q1)
    xg = [xg; xs]
    wg = [wg; ws]
    a .+ xg*(b-a), wg * (b-a)
end


# ---
# title: 258. Add Digits
# id: problem258
# author: zhwang
# date: 2022-03-03
# difficulty: Easy
# categories: Math
# link: <https://leetcode.com/problems/add-digits/description/>
# hidden: true
# ---
#
# Given a non-negative integer `num`, repeatedly add all its digits until the
# result has only one digit.
#
# **Example:**
#
#
#
#     Input: 38
#     Output: 2
#     Explanation: The process is like: 3 + 8 = 11, 1 + 1 = 2.
#                  Since 2 has only one digit, return it.
#
#
# **Follow up:**
# Could you do it without any loop/recursion in O(1) runtime?
#
#
## @lc code=start
using LeetCode

function add_digits(num::Int)::Int
    num <= 9 && return num
    return iszero(num % 9) ? 9 : num % 9
end
## @lc code=end


using Plots

# number of points to randomly generate in the square [(0,0), (0,1), (1,1), (1,0)]
N = 100_000

"function checks weather a given point is in the circle of radius equal to 1"
is_in( (x,y) ) = if x^2 + y^2 < 1
    1
else
    0
end

all_points = [ (rand(), rand()) for _ in 1:N]
points_in = [ p for p in all_points if is_in(p) == 1]
points_out = [ p for p in all_points if is_in(p) == 0]

myPI = 4 * length(points_in) / N

plot( [(0,0), (0,1), (1,1), (1,0), (0,0)],
    xlims = (0,1), ylims = (0,1),
    aspect_ratio=:equal,
    leg=nothing)
plot!( [(cos(x), sin(x)) for x in 0:0.01:2*pi])
scatter!(points_in)
scatter!(points_out)

print("PI = $(myPI)")


function nlp_cvx_107_010(
    optimizer,
    objective_tol,
    primal_tol,
    dual_tol,
    termination_target = TERMINATION_TARGET_LOCAL,
    primal_target = PRIMAL_TARGET_LOCAL,
)
    # Test Goals:
    # - convex objective
    # - single simple constraint
    # Variants
    #   010 - no binding constraints
    #   011 - binding constraint
    #   012 - binding constraint, different starting point

    model = Model(optimizer)

    @variable(model, x)
    @variable(model, y)

    @objective(model, Min, (x - 0.5)^2 + (y - 0.5)^2)
    @NLconstraint(model, x^2 + y^2 <= 1)

    optimize!(model)

    check_status(model, FEASIBLE_PROBLEM, termination_target, primal_target)
    check_objective(model, 0, tol = objective_tol)
    return check_solution([x, y], [1 / 2, 1 / 2], tol = primal_tol)
end


  include("num_integration.jl")

  func(x) = x^2 * sin(2*x) + exp(-sin(3*x)) # function that will be integrated from 0 to 5

  # evaluating a numerical integration using monte_carlo
  function mc_int(n::Int64,func::Function)

    # main variables
    sum = 0.
    crude_mc = 0.

    # monte-carlo evaluation
    for i in 1:n
      x = rand()  # sorting numbers between 0 and 1
      fx = func(x)
      crude_mc += fx
      sum += fx*fx
    end

    crude_mc = crude_mc / n
    sum = sum / n
    variance = sum -(crude_mc*crude_mc)

    return crude_mc,  variance

  end

  # calculation
  n = 1000000
  a = 0
  b = 1

  It = integration(a,b,n,func)
  Imc = mc_int(n,func)

  println("Trapezoidal integration with $n steps = ",It)
  println("Monte-carlo integration with $n steps = ",Imc)


# Descriptive Statistics

#############################
#
#   Location
#
#############################

# Geometric mean
"""
    geomean(a)

Return the geometric mean of a collection.
"""
geomean(a) = exp(mean(log, a))

# Harmonic mean
"""
    harmmean(a)

Return the harmonic mean of a collection.
"""
harmmean(a) = inv(mean(inv, a))

# Generalized mean
"""
    genmean(a, p)

Return the generalized/power mean with exponent `p` of a real-valued array,
i.e. ``\\left( \\frac{1}{n} \\sum_{i=1}^n a_i^p \\right)^{\\frac{1}{p}}``, where `n = length(a)`.
It is taken to be the geometric mean when `p == 0`.
"""
function genmean(a, p::Real)
    if p == 0
        return geomean(a)
    end

    # At least one of `x` or `p` must not be an int to avoid domain errors when `p` is a negative int.
    # We choose `x` in order to exploit exponentiation by squaring when `p` is an int.
    r = mean(a) do x
        float(x)^p
    end
    return r^inv(p)
end

# compute mode, given the range of integer values
"""
    mode(a, [r])
    mode(a::AbstractArray, wv::AbstractWeights)

Return the mode (most common number) of an array, optionally
over a specified range `r` or weighted via a vector `wv`.
If several modes exist, the first one (in order of appearance) is returned.
"""
function mode(a::AbstractArray{T}, r::UnitRange{T}) where T<:Integer
    isempty(a) && throw(ArgumentError("mode is not defined for empty collections"))
    len = length(a)
    r0 = r[1]
    r1 = r[end]
    cnts = zeros(Int, length(r))
    mc = 0    # maximum count
    mv = r0   # a value corresponding to maximum count
    for i = 1:len
        @inbounds x = a[i]
        if r0 <= x <= r1
            @inbounds c = (cnts[x - r0 + 1] += 1)
            if c > mc
                mc = c
                mv = x
            end
        end
    end
    return mv
end

"""
    modes(a, [r])::Vector
    mode(a::AbstractArray, wv::AbstractWeights)::Vector

Return all modes (most common numbers) of an array, optionally over a
specified range `r` or weighted via vector `wv`.
"""
function modes(a::AbstractArray{T}, r::UnitRange{T}) where T<:Integer
    r0 = r[1]
    r1 = r[end]
    n = length(r)
    cnts = zeros(Int, n)
    # find the maximum count
    mc = 0
    for i = 1:length(a)
        @inbounds x = a[i]
        if r0 <= x <= r1
            @inbounds c = (cnts[x - r0 + 1] += 1)
            if c > mc
                mc = c
            end
        end
    end
    # find all values corresponding to maximum count
    ms = T[]
    for i = 1:n
        @inbounds if cnts[i] == mc
            push!(ms, r[i])
        end
    end
    return ms
end

# compute mode over arbitrary iterable
function mode(a)
    isempty(a) && throw(ArgumentError("mode is not defined for empty collections"))
    cnts = Dict{eltype(a),Int}()
    # first element
    mc = 1
    mv, st = iterate(a)
    cnts[mv] = 1
    # find the mode along with table construction
    y = iterate(a, st)
    while y !== nothing
        x, st = y
        if haskey(cnts, x)
            c = (cnts[x] += 1)
            if c > mc
                mc = c
                mv = x
            end
        else
            cnts[x] = 1
            # in this case: c = 1, and thus c > mc won't happen
        end
        y = iterate(a, st)
    end
    return mv
end

function modes(a)
    isempty(a) && throw(ArgumentError("mode is not defined for empty collections"))
    cnts = Dict{eltype(a),Int}()
    # first element
    mc = 1
    x, st = iterate(a)
    cnts[x] = 1
    # find the mode along with table construction
    y = iterate(a, st)
    while y !== nothing
        x, st = y
        if haskey(cnts, x)
            c = (cnts[x] += 1)
            if c > mc
                mc = c
            end
        else
            cnts[x] = 1
            # in this case: c = 1, and thus c > mc won't happen
        end
        y = iterate(a, st)
    end
    # find values corresponding to maximum counts
    return [x for (x, c) in cnts if c == mc]
end

# Weighted mode of arbitrary vectors of values
function mode(a::AbstractVector, wv::AbstractWeights{T}) where T <: Real
    isempty(a) && throw(ArgumentError("mode is not defined for empty collections"))
    length(a) == length(wv) ||
        throw(ArgumentError("data and weight vectors must be the same size, got $(length(a)) and $(length(wv))"))

    # Iterate through the data
    mv = first(a)
    mw = first(wv)
    weights = Dict{eltype(a), T}()
    for (x, w) in zip(a, wv)
        _w = get!(weights, x, zero(T)) + w
        if _w > mw
            mv = x
            mw = _w
        end
        weights[x] = _w
    end

    return mv
end

function modes(a::AbstractVector, wv::AbstractWeights{T}) where T <: Real
    isempty(a) && throw(ArgumentError("mode is not defined for empty collections"))
    length(a) == length(wv) ||
        throw(ArgumentError("data and weight vectors must be the same size, got $(length(a)) and $(length(wv))"))

    # Iterate through the data
    mw = first(wv)
    weights = Dict{eltype(a), T}()
    for (x, w) in zip(a, wv)
        _w = get!(weights, x, zero(T)) + w
        if _w > mw
            mw = _w
        end
        weights[x] = _w
    end

    # find values corresponding to maximum counts
    return [x for (x, w) in weights if w == mw]
end

#############################
#
#   quantile and friends
#
#############################

"""
    percentile(x, p)

Return the `p`th percentile of a collection `x`, i.e. `quantile(x, p / 100)`.
"""
percentile(x, p) = quantile(x, p * 0.01)

"""
    nquantile(x, n::Integer)

Return the n-quantiles of collection `x`, i.e. the values which
partition `v` into `n` subsets of nearly equal size.

Equivalent to `quantile(x, [0:n]/n)`. For example, `nquantiles(x, 5)`
returns a vector of quantiles, respectively at `[0.0, 0.2, 0.4, 0.6, 0.8, 1.0]`.
"""
nquantile(x, n::Integer) = quantile(x, (0:n)/n)

"""
    quantilerank(itr, value; method=:inc)

Compute the quantile position in the [0, 1] interval of `value` relative to collection `itr`.

Different definitions can be chosen via the `method` keyword argument.
Let `count_less` be the number of elements of `itr` that are less than `value`,
`count_equal` the number of elements of `itr` that are equal to `value`, `n` the length of `itr`,
`greatest_smaller` the highest value below `value` and `smallest_greater` the lowest value above `value`.
Then `method` supports the following definitions:

- `:inc` (default): Return a value in the range 0 to 1 inclusive.
Return `count_less / (n - 1)` if `value ∈ itr`, otherwise apply interpolation based on
definition 7 of quantile in Hyndman and Fan (1996)
(equivalent to Excel `PERCENTRANK` and `PERCENTRANK.INC`).
This definition corresponds to the lower semi-continuous inverse of
[`quantile`](@ref) with its default parameters.

- `:exc`: Return a value in the range 0 to 1 exclusive.
Return `(count_less + 1) / (n + 1)` if `value ∈ itr` otherwise apply interpolation
based on definition 6 of quantile in Hyndman and Fan (1996)
(equivalent to Excel `PERCENTRANK.EXC`).

- `:compete`: Return `count_less / (n - 1)` if `value ∈ itr`, otherwise
return `(count_less - 1) / (n - 1)`, without interpolation
(equivalent to MariaDB `PERCENT_RANK`, dplyr `percent_rank`).

- `:tied`: Return `(count_less + count_equal/2) / n`, without interpolation.
Based on the definition in Roscoe, J. T. (1975)
(equivalent to `"mean"` kind of SciPy `percentileofscore`).

- `:strict`: Return `count_less / n`, without interpolation
(equivalent to `"strict"` kind of SciPy `percentileofscore`).

- `:weak`: Return `(count_less + count_equal) / n`, without interpolation
(equivalent to `"weak"` kind of SciPy `percentileofscore`).

!!! note
    An `ArgumentError` is thrown if `itr` contains `NaN` or `missing` values
    or if `itr` contains fewer than two elements.

# References
Roscoe, J. T. (1975). [Fundamental Research Statistics for the Behavioral Sciences]
(http://www.bryanburnham.net/wp-content/uploads/2014/07/Fundamental-Statistics-for-the-Behavioral-Sciences-v2.0.pdf#page=57)",
2nd ed., New York : Holt, Rinehart and Winston.

Hyndman, R.J and Fan, Y. (1996) "[Sample Quantiles in Statistical Packages]
(https://www.amherst.edu/media/view/129116/original/Sample+Quantiles.pdf)",
*The American Statistician*, Vol. 50, No. 4, pp. 361-365.

# Examples
```julia
julia> using StatsBase

julia> v1 = [1, 1, 1, 2, 3, 4, 8, 11, 12, 13];

julia> v2 = [1, 2, 3, 5, 6, missing, 8];

julia> v3 = [1, 2, 3, 4, 4, 5, 6, 7, 8, 9];

julia> quantilerank(v1, 2)
0.3333333333333333

julia> quantilerank(v1, 2, method=:exc), quantilerank(v1, 2, method=:tied)
(0.36363636363636365, 0.35)

# use `skipmissing` for vectors with missing entries.
julia> quantilerank(skipmissing(v2), 4)
0.5

# use broadcasting with `Ref` to compute quantile rank for multiple values
julia> quantilerank.(Ref(v3), [4, 8])
2-element Vector{Float64}:
 0.3333333333333333
 0.8888888888888888
```
"""
function quantilerank(itr, value; method::Symbol=:inc)
    ((value isa Number && isnan(value)) || ismissing(value)) &&
        throw(ArgumentError("`value` cannot be NaN or missing"))
    any(x -> ismissing(x) || (x isa Number && isnan(x)), itr) &&
        throw(ArgumentError("`itr` cannot contain missing or NaN entries"))

    count_less = count_equal = n = 0
    greatest_smaller = smallest_greater = value
    for x in itr
        if x == value
            count_equal += 1
        elseif x < value
            count_less += 1
            if greatest_smaller == value || greatest_smaller < x
                greatest_smaller = x
            end
        else
            if smallest_greater == value || smallest_greater > x
                smallest_greater = x
            end
        end
        n += 1
    end

    n == 0 && throw(ArgumentError("`itr` is empty. Pass a collection with at least two elements"))
    n == 1 && throw(ArgumentError("`itr` has only 1 value. Pass a collection with at least two elements"))

    if method == :inc
        if greatest_smaller == value
            return 0.0
        elseif count_equal > 0
            return count_less / (n - 1)
        elseif smallest_greater == value
            return 1.0
        else
            lower = (count_less - 1) / (n - 1)
            upper = count_less / (n - 1)
            ratio = (value - greatest_smaller) / (smallest_greater - greatest_smaller)
            return lower + ratio * (upper - lower)
        end
    elseif method == :exc
        if count_less == 0 && count_equal == 0
            return 0.0
        elseif count_less == 0
            return 1.0 / (n + 1)
        elseif count_equal > 0
            return (count_less + 1) / (n + 1)
        elseif smallest_greater == value
            return 1.0
        else
            lower = count_less / (n + 1)
            upper = (count_less + 1) / (n + 1)
            ratio = (value - greatest_smaller) / (smallest_greater - greatest_smaller)
            return lower + ratio * (upper - lower)
        end
    elseif method == :compete
        if value > maximum(itr)
            return 1.0
        elseif value ≤ minimum(itr)
            return 0.0
        else
            value ∈ itr && (count_less += 1)
            return (count_less - 1) / (n - 1)
        end
    elseif method == :tied
        return (count_less + count_equal/2) / n
    elseif method == :strict
        return count_less / n
    elseif method == :weak
        return (count_less + count_equal) / n
    else
        throw(ArgumentError("method=:$method is not valid. Pass :inc, :exc, :compete, :tied, :strict or :weak."))
    end
end

"""
    percentilerank(itr, value; method=:inc)

Return the `q`th percentile of `value` in collection `itr`, i.e. [`quantilerank(itr, value)`](@ref) * 100.

See the [`quantilerank`](@ref) docstring for more details.
"""
percentilerank(itr, value; method::Symbol=:inc) = quantilerank(itr, value, method=method) * 100

#############################
#
#   Dispersion
#
#############################

# span, i.e. the range minimum(x):maximum(x)
"""
    span(x)

Return the span of a collection, i.e. the range `minimum(x):maximum(x)`.
The minimum and maximum of `x` are computed in one pass using `extrema`.
"""
span(x) = ((a, b) = extrema(x); a:b)

# Variation coefficient: std / mean
"""
    variation(x, m=mean(x))

Return the coefficient of variation of collection `x`, optionally specifying
a precomputed mean `m`. The coefficient of variation is the ratio of the
standard deviation to the mean.
"""
variation(x, m) = stdm(x, m) / m
variation(x) = ((m, s) = mean_and_std(x); s/m)

# Standard error of the mean: std / sqrt(len)
# Code taken from var in the Statistics stdlib module

# faster computation of real(conj(x)*y)
realXcY(x::Real, y::Real) = x*y
realXcY(x::Complex, y::Complex) = real(x)*real(y) + imag(x)*imag(y)

"""
    sem(x; mean=nothing)
    sem(x::AbstractArray[, weights::AbstractWeights]; mean=nothing)

Return the standard error of the mean for a collection `x`.
A pre-computed `mean` may be provided.

When not using weights, this is the (sample) standard deviation
divided by the sample size. If weights are used, the
variance of the sample mean is calculated as follows:

* `AnalyticWeights`: Not implemented.
* `FrequencyWeights`: ``\\frac{\\sum_{i=1}^n w_i (x_i - \\bar{x_i})^2}{(\\sum w_i) (\\sum w_i - 1)}``
* `ProbabilityWeights`: ``\\frac{n}{n-1} \\frac{\\sum_{i=1}^n w_i^2 (x_i - \\bar{x_i})^2}{\\left( \\sum w_i \\right)^2}``

The standard error is then the square root of the above quantities.

# References

Carl-Erik Särndal, Bengt Swensson, Jan Wretman (1992). Model Assisted Survey Sampling.
New York: Springer. pp. 51-53.
"""
function sem(x; mean=nothing)
    if isempty(x)
        # Return the NaN of the type that we would get for a nonempty x
        T = eltype(x)
        _mean = mean === nothing ? zero(T) / 1 : mean
        z = abs2(zero(T) - _mean)
        return oftype((z + z) / 2, NaN)
    elseif mean === nothing
        n = 0
        y = iterate(x)
        value, state = y
        # Use Welford algorithm as seen in (among other places)
        # Knuth's TAOCP, Vol 2, page 232, 3rd edition.
        _mean = value / 1
        sse = real(zero(_mean))
        while y !== nothing
            value, state = y
            y = iterate(x, state)
            n += 1
            new_mean = _mean + (value - _mean) / n
            sse += realXcY(value - _mean, value - new_mean)
            _mean = new_mean
        end
    else
        n = 1
        y = iterate(x)
        value, state = y
        sse = abs2(value - mean)
        while (y = iterate(x, state)) !== nothing
            value, state = y
            n += 1
            sse += abs2(value - mean)
        end
    end
    variance = sse / (n - 1)
    return sqrt(variance / n)
end

function sem(x::AbstractArray; mean=nothing)
    if isempty(x)
        # Return the NaN of the type that we would get for a nonempty x
        T = eltype(x)
        _mean = mean === nothing ? zero(T) / 1 : mean
        z = abs2(zero(T) - _mean)
        return oftype((z + z) / 2, NaN)
    end
    return sqrt(var(x; mean=mean, corrected=true) / length(x))
end

function sem(x::AbstractArray, weights::UnitWeights; mean=nothing)
    if length(x) ≠ length(weights)
        throw(DimensionMismatch("array and weights do not have the same length"))
    end
    return sem(x; mean=mean)
end

# Weighted methods for the above
sem(x::AbstractArray, weights::FrequencyWeights; mean=nothing) =
    sqrt(var(x, weights; mean=mean, corrected=true) / sum(weights))

function sem(x::AbstractArray, weights::ProbabilityWeights; mean=nothing)
    if isempty(x)
        # Return the NaN of the type that we would get for a nonempty x
        return var(x, weights; mean=mean, corrected=true) / 0
    else
        _mean = mean === nothing ? Statistics.mean(x, weights) : mean
        # sum of squared errors = sse
        sse = sum(Broadcast.instantiate(Broadcast.broadcasted(x, weights) do x_i, w
            return abs2(w * (x_i - _mean))
        end))
        n = count(!iszero, weights)
        return sqrt(sse * n / (n - 1)) / sum(weights)
    end
end

# Median absolute deviation
@irrational mad_constant 1.4826022185056018 BigFloat("1.482602218505601860547076529360423431326703202590312896536266275245674447622701")

"""
    mad(x; center=median(x), normalize=true)

Compute the median absolute deviation (MAD) of collection `x` around `center`
(by default, around the median).

If `normalize` is set to `true`, the MAD is multiplied by
`1 / quantile(Normal(), 3/4) ≈ 1.4826`, in order to obtain a consistent estimator
of the standard deviation under the assumption that the data is normally distributed.
"""
function mad(x; center=nothing, normalize::Union{Bool, Nothing}=nothing, constant=nothing)
    isempty(x) && throw(ArgumentError("mad is not defined for empty arrays"))
    T = eltype(x)
    # Knowing the eltype allows allocating a single array able to hold both original values
    # and differences from the center, instead of two arrays
    S = isconcretetype(T) ? promote_type(T, typeof(middle(zero(T)))) : T
    x2 = x isa AbstractArray ? copyto!(similar(x, S), x) : collect(S, x)
    c = center === nothing ? median!(x2) : center
    if isconcretetype(T)
        x2 .= abs.(x2 .- c)
    else
        x2 = abs.(x2 .- c)
    end
    m = median!(x2)
    if normalize isa Nothing
        Base.depwarn("the `normalize` keyword argument will be false by default in future releases: set it explicitly to silence this deprecation", :mad)
        normalize = true
    end
    if !isa(constant, Nothing)
        Base.depwarn("keyword argument `constant` is deprecated, use `normalize` instead or apply the multiplication directly", :mad)
        m * constant
    elseif normalize
        m * mad_constant
    else
        m
    end
end

"""
    StatsBase.mad!(x; center=median!(x), normalize=true)

Compute the median absolute deviation (MAD) of array `x` around `center`
(by default, around the median), overwriting `x` in the process.
`x` must be able to hold values of generated by calling `middle` on its elements
(for example an integer vector is not appropriate since `middle` can produce
non-integer values).

If `normalize` is set to `true`, the MAD is multiplied by
`1 / quantile(Normal(), 3/4) ≈ 1.4826`, in order to obtain a consistent estimator
of the standard deviation under the assumption that the data is normally distributed.
"""
function mad!(x::AbstractArray;
              center=median!(x),
              normalize::Union{Bool,Nothing}=true,
              constant=nothing)
    isempty(x) && throw(ArgumentError("mad is not defined for empty arrays"))
    x .= abs.(x .- center)
    m = median!(x)
    if normalize isa Nothing
        Base.depwarn("the `normalize` keyword argument will be false by default in future releases: set it explicitly to silence this deprecation", :mad)
        normalize = true
    end
    if !isa(constant, Nothing)
        Base.depwarn("keyword argument `constant` is deprecated, use `normalize` instead or apply the multiplication directly", :mad)
        m * constant
    elseif normalize
        m * mad_constant
    else
        m
    end
end

# Interquartile range
"""
    iqr(x)

Compute the interquartile range (IQR) of collection `x`, i.e. the 75th percentile
minus the 25th percentile.
"""
iqr(x) = (q = quantile(x, [.25, .75]); q[2] - q[1])

# Generalized variance
"""
    genvar(X)

Compute the generalized sample variance of `X`. If `X` is a vector, one-column matrix,
or other iterable, this is equivalent to the sample variance.
Otherwise if `X` is a matrix, this is equivalent to the determinant of the covariance
matrix of `X`.

!!! note
    The generalized sample variance will be 0 if the columns of the matrix of deviations
    are linearly dependent.
"""
genvar(X::AbstractMatrix) = size(X, 2) == 1 ? var(vec(X)) : det(cov(X))
genvar(itr) = var(itr)

# Total variation
"""
    totalvar(X)

Compute the total sample variance of `X`. If `X` is a vector, one-column matrix,
or other iterable, this is equivalent to the sample variance.
Otherwise if `X` is a matrix, this is equivalent to the sum of the diagonal elements
of the covariance matrix of `X`.
"""
totalvar(X::AbstractMatrix) = sum(var(X, dims=1))
totalvar(itr) = var(itr)

#############################
#
#   Z-scores
#
#############################

function _zscore!(Z::AbstractArray, X::AbstractArray, μ::Real, σ::Real)
    # Z and X are assumed to have the same size
    iσ = inv(σ)
    if μ == zero(μ)
        for i = 1 : length(X)
            @inbounds Z[i] = X[i] * iσ
        end
    else
        for i = 1 : length(X)
            @inbounds Z[i] = (X[i] - μ) * iσ
        end
    end
    return Z
end

@generated function _zscore!(Z::AbstractArray{S,N}, X::AbstractArray{T,N},
                             μ::AbstractArray, σ::AbstractArray) where {S,T,N}
    quote
        # Z and X are assumed to have the same size
        # μ and σ are assumed to have the same size, that is compatible with size(X)
        siz1 = size(X, 1)
        @nextract $N ud d->size(μ, d)
        if size(μ, 1) == 1 && siz1 > 1
            @nloops $N i d->(d>1 ? (1:size(X,d)) : (1:1)) d->(j_d = ud_d ==1 ? 1 : i_d) begin
                v = (@nref $N μ j)
                c = inv(@nref $N σ j)
                for i_1 = 1:siz1
                    (@nref $N Z i) = ((@nref $N X i) - v) * c
                end
            end
        else
            @nloops $N i X d->(j_d = ud_d ==1 ? 1 : i_d) begin
                (@nref $N Z i) = ((@nref $N X i) - (@nref $N μ j)) / (@nref $N σ j)
            end
        end
        return Z
    end
end

function _zscore_chksize(X::AbstractArray, μ::AbstractArray, σ::AbstractArray)
    size(μ) == size(σ) || throw(DimensionMismatch("μ and σ should have the same size."))
    for i=1:ndims(X)
        dμ_i = size(μ,i)
        (dμ_i == 1 || dμ_i == size(X,i)) || throw(DimensionMismatch("X and μ have incompatible sizes."))
    end
end

"""
    zscore!([Z], X, μ, σ)

Compute the z-scores of an array `X` with mean `μ` and standard deviation `σ`.
z-scores are the signed number of standard deviations above the mean that an
observation lies, i.e. ``(x - μ) / σ``.

If a destination array `Z` is provided, the scores are stored
in `Z` and it must have the same shape as `X`. Otherwise `X` is overwritten.
"""
function zscore!(Z::AbstractArray{ZT}, X::AbstractArray{T}, μ::Real, σ::Real) where {ZT<:AbstractFloat,T<:Real}
    size(Z) == size(X) || throw(DimensionMismatch("Z and X must have the same size."))
    _zscore!(Z, X, μ, σ)
end

function zscore!(Z::AbstractArray{<:AbstractFloat}, X::AbstractArray{<:Real},
                 μ::AbstractArray{<:Real}, σ::AbstractArray{<:Real})
    size(Z) == size(X) || throw(DimensionMismatch("Z and X must have the same size."))
    _zscore_chksize(X, μ, σ)
    _zscore!(Z, X, μ, σ)
end

zscore!(X::AbstractArray{<:AbstractFloat}, μ::Real, σ::Real) = _zscore!(X, X, μ, σ)

zscore!(X::AbstractArray{<:AbstractFloat}, μ::AbstractArray{<:Real}, σ::AbstractArray{<:Real}) =
    (_zscore_chksize(X, μ, σ); _zscore!(X, X, μ, σ))

"""
    zscore(X, [μ, σ])

Compute the z-scores of `X`, optionally specifying a precomputed mean `μ` and
standard deviation `σ`. z-scores are the signed number of standard deviations
above the mean that an observation lies, i.e. ``(x - μ) / σ``.

`μ` and `σ` should be both scalars or both arrays. The computation is broadcasting.
In particular, when `μ` and `σ` are arrays, they should have the same size, and
`size(μ, i) == 1  || size(μ, i) == size(X, i)` for each dimension.
"""
function zscore(X::AbstractArray{T}, μ::Real, σ::Real) where T<:Real
    ZT = typeof((zero(T) - zero(μ)) / one(σ))
    _zscore!(Array{ZT}(undef, size(X)), X, μ, σ)
end

function zscore(X::AbstractArray{T}, μ::AbstractArray{U}, σ::AbstractArray{S}) where {T<:Real,U<:Real,S<:Real}
    _zscore_chksize(X, μ, σ)
    ZT = typeof((zero(T) - zero(U)) / one(S))
    _zscore!(Array{ZT}(undef, size(X)), X, μ, σ)
end

zscore(X::AbstractArray{<:Real}) = ((μ, σ) = mean_and_std(X); zscore(X, μ, σ))
zscore(X::AbstractArray{<:Real}, dim::Int) = ((μ, σ) = mean_and_std(X, dim); zscore(X, μ, σ))

#############################
#
#   entropy and friends
#
#############################

"""
    entropy(p, [b])

Compute the entropy of a collection of probabilities `p`,
optionally specifying a real number `b` such that the entropy is scaled by `1/log(b)`.
Elements with probability 0 or 1 add 0 to the entropy.
"""
function entropy(p)
    if isempty(p)
        throw(ArgumentError("empty collections are not supported since they do not " *
                            "represent proper probability distributions"))
    end
    return -sum(xlogx, p)
end

entropy(p, b::Real) = entropy(p) / log(b)

"""
    renyientropy(p, α)

Compute the Rényi (generalized) entropy of order `α` of an array `p`.
"""
function renyientropy(p::AbstractArray{T}, α::Real) where T<:Real
    α < 0 && throw(ArgumentError("Order of Rényi entropy not legal, $(α) < 0."))

    s = zero(T)
    z = zero(T)
    scale = sum(p)

    if α ≈ 0
        for i = 1:length(p)
            @inbounds pi = p[i]
            if pi > z
                s += 1
            end
        end
        s = log(s / scale)
    elseif α ≈ 1
        for i = 1:length(p)
            @inbounds pi = p[i]
            if pi > z
                s -= pi * log(pi)
            end
        end
        s = s / scale
    elseif (isinf(α))
        s = -log(maximum(p))
    else # a normal Rényi entropy
        for i = 1:length(p)
            @inbounds pi = p[i]
            if pi > z
                s += pi ^ α
            end
        end
        s = log(s / scale) / (1 - α)
    end
    return s
end

"""
    crossentropy(p, q, [b])

Compute the cross entropy between `p` and `q`, optionally specifying a real
number `b` such that the result is scaled by `1/log(b)`.
"""
function crossentropy(p::AbstractArray{<:Real}, q::AbstractArray{<:Real})
    length(p) == length(q) || throw(DimensionMismatch("Inconsistent array length."))

    # handle empty collections
    if isempty(p)
        Base.depwarn(
            "support for empty collections will be removed since they do not " *
            "represent proper probability distributions",
            :crossentropy,
        )
        # return zero for empty arrays
        return xlogy(zero(eltype(p)), zero(eltype(q)))
    end

    # use pairwise summation (https://github.com/JuliaLang/julia/pull/31020)
    broadcasted = Broadcast.broadcasted(xlogy, vec(p), vec(q))
    return - sum(Broadcast.instantiate(broadcasted))
end

crossentropy(p::AbstractArray{<:Real}, q::AbstractArray{<:Real}, b::Real) =
    crossentropy(p,q) / log(b)

"""
    kldivergence(p, q, [b])

Compute the Kullback-Leibler divergence from `q` to `p`,
also called the relative entropy of `p` with respect to `q`,
that is the sum `pᵢ * log(pᵢ / qᵢ)`. Optionally a real number `b`
can be specified such that the divergence is scaled by `1/log(b)`.
"""
function kldivergence(p::AbstractArray{<:Real}, q::AbstractArray{<:Real})
    length(p) == length(q) || throw(DimensionMismatch("Inconsistent array length."))

    # handle empty collections
    if isempty(p)
        Base.depwarn(
            "support for empty collections will be removed since they do not "*
            "represent proper probability distributions",
            :kldivergence,
        )
        # return zero for empty arrays
        pzero = zero(eltype(p))
        qzero = zero(eltype(q))
        return xlogy(pzero, zero(pzero / qzero))
    end

    # use pairwise summation (https://github.com/JuliaLang/julia/pull/31020)
    broadcasted = Broadcast.broadcasted(vec(p), vec(q)) do pi, qi
        # handle pi = qi = 0, otherwise `NaN` is returned
        piqi = iszero(pi) && iszero(qi) ? zero(pi / qi) : pi / qi
        return xlogy(pi, piqi)
    end
    return sum(Broadcast.instantiate(broadcasted))
end

kldivergence(p::AbstractArray{<:Real}, q::AbstractArray{<:Real}, b::Real) =
    kldivergence(p,q) / log(b)

#############################
#
#   summary
#
#############################

struct SummaryStats{T<:Union{AbstractFloat,Missing}}
    mean::T
    min::T
    q25::T
    median::T
    q75::T
    max::T
    nobs::Int
    nmiss::Int
end

"""
    summarystats(a)

Compute summary statistics for a real-valued array `a`. Returns a
`SummaryStats` object containing the mean, minimum, 25th percentile,
median, 75th percentile, and maxmimum.
"""
function summarystats(a::AbstractArray{T}) where T<:Union{Real,Missing}
    # `mean` doesn't fail on empty input but rather returns `NaN`, so we can use the
    # return type to populate the `SummaryStats` structure.
    s = T >: Missing ? collect(skipmissing(a)) : a
    m = mean(s)
    R = typeof(m)
    n = length(a)
    ns = length(s)
    qs = if ns == 0
        R[NaN, NaN, NaN, NaN, NaN]
    elseif T >: Missing
        quantile!(s, [0.00, 0.25, 0.50, 0.75, 1.00])
    else
        quantile(s, [0.00, 0.25, 0.50, 0.75, 1.00])
    end
    SummaryStats{R}(m, qs..., n, n - ns)
end

function Base.show(io::IO, ss::SummaryStats)
    println(io, "Summary Stats:")
    @printf(io, "Length:         %i\n", ss.nobs)
    ss.nobs > 0 || return
    @printf(io, "Missing Count:  %i\n", ss.nmiss)
    @printf(io, "Mean:           %.6f\n", ss.mean)
    @printf(io, "Minimum:        %.6f\n", ss.min)
    @printf(io, "1st Quartile:   %.6f\n", ss.q25)
    @printf(io, "Median:         %.6f\n", ss.median)
    @printf(io, "3rd Quartile:   %.6f\n", ss.q75)
    @printf(io, "Maximum:        %.6f\n", ss.max)
end

"""
    describe(a)

Pretty-print the summary statistics provided by [`summarystats`](@ref):
the mean, minimum, 25th percentile, median, 75th percentile, and
maximum.
"""
DataAPI.describe(x) = describe(stdout, x)
function DataAPI.describe(io::IO, a::AbstractArray{T}) where T<:Union{Real,Missing}
    show(io, summarystats(a))
    println(io, "Type:           $(string(eltype(a)))")
end
function DataAPI.describe(io::IO, a::AbstractArray)
    println(io, "Summary Stats:")
    println(io, "Length:         $(length(a))")
    println(io, "Type:           $(string(eltype(a)))")
    println(io, "Number Unique:  $(length(unique(a)))")
    return
end


# Basic Linear algebra
# https://www.juliabox.com/notebook/notebooks/tutorials/intro-to-julia/11.%20Basic%20linear%20algebra.ipynb

A = [1 2 3;
     4 5 6;
     7 8 9]

@show A
@show A' # conjuate transpose
@show transpose(A)

x = fill(1.0, (3,))

@show typeof(A), typeof(x)

@show A * x
@show A' * A
@show A'A #allows us to write this wiout `*`

# Solving linear systems
# The problem `Ax=b` for square A is solved by `\` function

A = [1 3 2;
     3 2 4;
     1 9 3]

b = A*x
@show A\b # expected the value of `x`


"""
    iscoprime(a,b)

Check if the pair of polynomials `a` and `b` is coprime, that is, it has no common factors.

# Examples

```julia
julia> a = Polynomial([1,2,3],:s);
julia> b = Polynomial([1,2],:s);
julia> iscoprime(a,b)
true

julia> a = Polynomial([1,2,3],:s);
julia> b = a*Polynomial([1,2],:s);
julia> iscoprime(a,b)
false
```
"""
function iscoprime(a::Polynomial,b::Polynomial)
    da = degree(a)
    db = degree(b)
    dx = db-1
    S = sylvestermatrix(a,b)
    return rank(S)==(da+db)
end

"""
    isschurstable(a)

Check if the polynomial `a` is Schur stable, that is, it has all its roots inside the unit disk.

# Examples

```julia
julia> a = fromroots([-1/2,0, 2/3,-1/3im, 1/3im]);
julia> isschurstable(a)
true

julia> a = fromroots([2,0, 2/3,-3im, 3im]);
julia> !isschurstable(a)
false
```
"""
function isschurstable(a::Polynomial)
    r = roots(a)
    all(abs.(r) .<= 1)
end

"""
    ishurwitzstable(a::Polynomial)

Check if the `a` is Schur stable, that is, it has all its roots inside the left complex half-plane.

# Examples

```julia
julia> a = fromroots([-1, -2, -3+4im, -3-4im]);
julia> ishurwitzstable(a)
true

julia> a = fromroots([-1, -2, 3+4im, 3-4im]);
julia> !ishurwitzstable(a)
false
```
"""
function ishurwitzstable(a::Polynomial)
    r = roots(a)
    return all(real(r) .<= 0)
end

"""
    isconjsymmetric(a::Polynomial)

Check if the scalar univariate polynomial `a` is conjugate symmetric, that is, if `a=ã`.

For a scalar univariate polynomial `a` with real coefficients, check if `a(s)=a(-s)`. If the coefficients are complex, the condition then extends to `a(s)=ā(-s)`, where `ā` is obtained from `a` by complex-conjugating the coefficients. A necessary and sufficient condition for conjugate symmetry is that the coefficients with odd powers of the variable are zero and the coefficients with even powers are real.

# Examples
```julia
julia> a = Polynomial([1,0,2,0,3],:s)
Polynomial(1 + 2*s^2 + 3*s^4)

julia> isconjsymmetric(a)
true
```
"""
function isconjsymmetric(a::Polynomial)
    return isequal(a,cconj(a))
end

"""
    isconjsymmetric(a::LaurentPolynomial)

Check if the scalar univariate Laurent polynomial `a` is conjugate symmetric, that is, if `a=ã`.

For a scalar univariate Laurent polynomial `a` with real coefficients, check if `a(z)=a(1/z)`. If the coefficients are complex, the condition then extends to `a(z)=ā(1/z)`, where `ā` is obtained from `a` by complex-conjugating the coefficients. A necessary and sufficient condition is that the coefficients are real and their order in the coefficient vector can be flipped with no impact.

# Examples
```julia
julia> a = LaurentPolynomial([3,2,1,2,3],-2:2,:z)
LaurentPolynomial(3*z⁻² + 2*z⁻¹ + 1 + 2*z + 3*z²)

julia> isconjsymmetric(a)
true
```
"""
function isconjsymmetric(a::LaurentPolynomial)
    return isequal(a,dconj(a))
end


@doc raw"""
    ExponentiatedKernel([α=1])

The exponentiated kernel is a Mercer kernel given by:

```
    κ(x,y) = exp(α⋅xᵀy)   α > 0
```
where `α` is a positive scaling parameter.

# Examples

```jldoctest; setup = :(using MLKernels)
julia> ExponentiatedKernel()
ExponentiatedKernel{Float64}(1.0)

julia> ExponentiatedKernel(2)
ExponentiatedKernel{Float64}(2.0)

julia> ExponentiatedKernel(2.0f0)
ExponentiatedKernel{Float32}(2.0)
```
"""
struct ExponentiatedKernel{T<:AbstractFloat} <: MercerKernel{T}
    α::T
    function ExponentiatedKernel{T}(α::Real=T(1)) where {T<:AbstractFloat}
        @check_args(ExponentiatedKernel, α, α > zero(T), "α > 0")
        return new{T}(α)
    end
end
ExponentiatedKernel(α::T=1.0) where {T<:Real} = ExponentiatedKernel{promote_float(T)}(α)

@inline basefunction(::ExponentiatedKernel) = ScalarProduct()

@inline kappa(κ::ExponentiatedKernel{T}, xᵀy::T) where {T} = exp(κ.α*xᵀy)

function convert(
        ::Type{K},
        κ::ExponentiatedKernel
    ) where {K>:ExponentiatedKernel{T}} where T
    return ExponentiatedKernel{T}(κ.α)
end


#=
Implement integer exponentiation. That is, implement the pow(x, y) function, where x and y are integers and returns x^y.

Do this faster than the naive method of repeated multiplication.

For example, pow(2, 10) should return 1024.
=#

function pow(x::Int, y::Int)
    if y == 0
        return 1
    elseif y % 2 == 0
        return pow(x, Int(y/2)) * pow(x, Int(y/2))
    else
        return x * pow(x,Int((y-1)/2)) * pow(x,Int((y-1)/2))
    end
end