println("\n\tEstimate autocorrelations");
println("\n\t...for N1...");
xcorr = [0:10:400];
ycorr1 = zeros( length( xcorr ), 5 );
for a in [1:5]
  # Calculate autocorrelations
  ycorr1[:,a] = autocorrelation(N1, v1, a, xcorr);
  # Normalize by dividing through by Ĉ[1,a]
  ycorr1[:,a] /= ycorr1[1,a];
end
println("\n\t...for N2 (See plots below)");
ycorr2 = zeros( length( xcorr ), 5 );
for a in [1:5]
  # Calculate autocorrelations
  ycorr2[:,a] = autocorrelation(N2, v2, a, xcorr);
  # Normalize by dividing through by Ĉ[1,a]
  ycorr2[:,a] /= ycorr2[1,a];
end

# Make plots for each alpha
println("\n\tPlacing estimated autocorrelation plots 'plots1' and 'plots2':");
using Gadfly
plots1 = Plot[];
plots2 = Plot[];
for i in [1:5]
  # Plot
  push!(plots1, Gadfly.plot ( x = xcorr, y = ycorr1[:,i], Geom.line, Geom.point,
      Guide.xlabel("Separation"), Guide.ylabel("Normalized Autocorrelation"),
      Guide.title(join(["Estimated Autocorrelation for Dataset",string(i),
                        "with Sample Size N1 = $N1"]," "," "))))
  push!(plots2, Gadfly.plot ( x = xcorr, y = ycorr2[:,i], Geom.line, Geom.point,
      Guide.xlabel("Separation"), Guide.ylabel("Normalized Autocorrelation"),
      Guide.title(join(["Estimated Autocorrelation for Dataset",string(i),
                        "with Sample Size N2 = $N2"]," "," "))))
end

# Estimate autocorrelation times
println("\n\tEstimating autocorrelation times τ1 and τ2");
τ1 = Float64[];
τ2 = Float64[];
for a in [1:5]
  corra = autocorrelation(N1, v1, a, ns);
  corra /= corra[1];                          # Normalize
  push!(τ1,sum(corra) - 0.5);                 # Get autocorrelation time
end
for a in [1:5]
  corra = autocorrelation(N2, v2, a, ns);
  corra /= corra[1];                          # Normalize
  push!(τ2,sum(corra) - 0.5);                 # Get autocorrelation time
end

println("\n\tEstimated Autocorrelation Times for N1 = $N1:\n");
for i in [1:5]
  println("\t\tτ̂1v$i:\t$(τ1[i])");
end
println("\n\tEstimated Autocorrelation Times for N2 = $N2:\n");
for i in [1:5]
  println("\t\tτ̂2v$i:\t$(τ2[i])");
end
println("\n\tThe estimated autocorrelation times are clearly far better for the larger sample.");


"""
## sample!

This subroutine returns the local coordinates and weighting coefficients
of the integrating points.

### Function
```julia
sample!(fin_el, s, wt)
```

### Arguments
```julia
* fin_el::FiniteElement      : Finite element type
* s::Matrix{Float64}        : Local coordinates (Updated)
* wt::Vector{Float64}       : Weighting coefficients (Updated)
```
"""
function sample!(fin_el::Line, s::Matrix{Float64} , wt::Vector{Float64})
  #
  # This subroutine returns the local coordinates and weighting coefficients
  # of the integrating points.
  #
  nip = size(s,1)
  (s[:,1], wt[:]) = QuadGK.gauss(Float64, nip)
end


# Soft thresholding function
@inline soft_threshold(x, ϵ) = max(x - ϵ, 0) + min(x + ϵ, 0)

# RPCA using the inexact Augumented Lagrange Multiplier (ALM).
# Given a observation matrix D, find row-rank matrix A and sparse matrix E
# so that D = A + E.
function inexact_alm_rpca(D::AbstractMatrix;
                          sparseness::Float64=1.0/sqrt(maximum(size(D))),
                          max_iter::Int=1000,
                          error_tol::Float64=1.0e-7,
                          ρ::Float64=1.5,
                          verbose::Bool=false,
                          nonnegativeA::Bool=false,
                          nonnegativeE::Bool=false)
    M, N = size(D)
    λ = sparseness

    A⁰, E⁰ = zeros(M, N), zeros(M, N)

    # initialize
    Y⁰ = copy(D)
    norm² = svdvals(Y⁰)[1] # can be tuned
    norm∞ = norm(vec(Y⁰), Inf) / λ
    dual_norm = max(norm², norm∞)
    d_norm = norm(D)
    Y⁰ /= dual_norm

    μ⁰ = 1.25 / norm²
    μ̄ = μ⁰ * 1.0e+7

    converged::Bool = false
    k = 0
    sv⁰ = 10

    Yᵏ, Aᵏ, Eᵏ, μᵏ, svᵏ = Y⁰, A⁰, E⁰, μ⁰, sv⁰
    while !converged
        # update sparse matrix E
        Eᵏ = soft_threshold(D - Aᵏ + 1/μᵏ * Yᵏ, λ * 1/μᵏ)
        # force non-negative (heuristic)
        if nonnegativeE
            Eᵏ = max(Eᵏ, 0)
        end

        U, S, V = svd(D - Eᵏ + μᵏ^-1 * Yᵏ)

        # trancate dimention
        svpᵏ = trunc(Int, sum(S .> μᵏ^-1))
        if svpᵏ < svᵏ
            svᵏ = min(svpᵏ + 1, N)
        else
            svᵏ = min(svpᵏ + round(0.05 * N), N)
        end

        # update row-rank matrix A
        Aᵏ = U[:,1:svpᵏ] * diagm(S[1:svpᵏ] - 1/μᵏ) * V[:,1:svpᵏ]'
        # force non-negative (heuristic)
        if nonnegativeA
            Aᵏ = max(Aᵏ, 0)
        end

        Z = D - Aᵏ - Eᵏ

        Yᵏ = Yᵏ + μᵏ * Z
        μᵏ = min(μᵏ * ρ, μ̄)

        objective = norm(Z) / d_norm
        verbose && println("#$(k) objective: $(objective)")

        if objective < error_tol
            if verbose
                println("converged")
            end
            converged = true
        end

        k = k + 1
        k >= max_iter && break
    end

    Aᵏ, Eᵏ
end


function η_karnik(u::Matrix{FuzzyNumber}, d2::Matrix{FuzzyNumber}; m::Real=2.0)
	m > 1 || error("fuzzifier m ∈ (1, ∞), got $m")
	levels = u[1, 1].levels
	num_levels = length(levels)
	N, c = size(u)

	η = Vector{FuzzyNumber}(undef, c)
	for i = 1:c
		grades = Vector{Interval}(undef, num_levels)
		for (lvl, α) in enumerate(levels)
			u_cut = cut(FuzzyVector(u[:, i]), α)
			d2_cut = cut(FuzzyVector(d2[:, i]), α)
			c_left = km_iwa(d2_cut, u_cut; bound="lower", m=m)
			c_right = km_iwa(d2_cut, u_cut; bound="upper", m=m)
			grades[lvl] = Interval(c_left, c_right)
		end
		η[i] = FuzzyNumber(levels, grades)
	end
	η
end

function u_lpcm(X⃗::FuzzyVector, C⃗::FuzzyVector, η::Real; m::Real=2.0)
	m > 1 || error("fuzzifier m ∈ (1, ∞), got $m")
	η > 0 || error("η must be suitable positive number")
	levels = X⃗[1].levels
	num_levels = length(levels)

	grades = Vector{Interval}(undef, num_levels)
	D2 = FuzzySets.d_interval(X⃗, C⃗, squared=true)
	for (lvl, α) in enumerate(levels)
		D2_cut = cut(D2, α)
		grades[lvl] = u_lpcm(D2_cut, η; m=m)
	end
	u = FuzzyNumber(levels, grades)
	u
end

function u_lpcm(d2::Interval, η::Real; m::Real=2.0)
	m > 1 || error("fuzzifier m ∈ (1, ∞)")
	η > 0 || error("η must be suitable positive number")
	power = 1 / (m - 1)

	u = Interval(1) / (Interval(1) + (d2 / η)^power)
	u
end


"""
    hahn(x, p::Integer, α, β, N::Integer)

Evaluate the Hahn polynomial with parameters `α`, `β`, `N` of degree `p` at `x`
using the three term recursion [Öffner, Zweidimensionale klassische und diskrete
orthogonale Polynome, Chapter 5].
"""
function hahn(x, p::Integer, α, β, N)
    T = typeof( (α+β+2)*x/(N*(α+1)) )
    a = one(T)
    b = 1 - (α+β+2)*x/(N*(α+1))

    if p <= 0
        return a
    elseif p == 1
        return b
    elseif typeof(N) <: Integer && p > N
        throw(ArgumentError("p==$p must not be larger than N==$N."))
    end

    for n in 2:p
        An = T( (n+α+β)*(n+α)*(N-n+1) ) / ( (2n-1+α+β)*(2n+α+β) )
        Cn = T( (n-1)*(n+α+β+N)*(n-1+β) ) / ( (2n-2+α+β)*(2n-1+α+β) )
        a, b = b, ( (An+Cn-x)*b - Cn*a ) / An
    end

    b
end


"""
    Logistic(μ,θ)

The *Logistic distribution* with location `μ` and scale `θ` has probability density function

```math
f(x; \\mu, \\theta) = \\frac{1}{4 \\theta} \\mathrm{sech}^2
\\left( \\frac{x - \\mu}{2 \\theta} \\right)
```

```julia
Logistic()       # Logistic distribution with zero location and unit scale, i.e. Logistic(0, 1)
Logistic(u)      # Logistic distribution with location u and unit scale, i.e. Logistic(u, 1)
Logistic(u, b)   # Logistic distribution with location u ans scale b

params(d)       # Get the parameters, i.e. (u, b)
location(d)     # Get the location parameter, i.e. u
scale(d)        # Get the scale parameter, i.e. b
```

External links

* [Logistic distribution on Wikipedia](http://en.wikipedia.org/wiki/Logistic_distribution)

"""
struct Logistic{T<:Real} <: ContinuousUnivariateDistribution
    μ::T
    θ::T
    Logistic{T}(µ::T, θ::T) where {T} = new{T}(µ, θ)
end

function Logistic(μ::T, θ::T; check_args=true) where {T <: Real}
    check_args && @check_args(Logistic, θ > zero(θ))
    return Logistic{T}(μ, θ)
end

Logistic(μ::Real, θ::Real) = Logistic(promote(μ, θ)...)
Logistic(μ::Integer, θ::Integer) = Logistic(float(μ), float(θ))
Logistic(μ::T) where {T <: Real} = Logistic(μ, one(T))
Logistic() = Logistic(0.0, 1.0, check_args=false)

@distr_support Logistic -Inf Inf

#### Conversions
function convert(::Type{Logistic{T}}, μ::S, θ::S) where {T <: Real, S <: Real}
    Logistic(T(μ), T(θ))
end
function convert(::Type{Logistic{T}}, d::Logistic{S}) where {T <: Real, S <: Real}
    Logistic(T(d.μ), T(d.θ), check_args=false)
end

#### Parameters

location(d::Logistic) = d.μ
scale(d::Logistic) = d.θ

params(d::Logistic) = (d.μ, d.θ)
@inline partype(d::Logistic{T}) where {T<:Real} = T

#### Statistics

mean(d::Logistic) = d.μ
median(d::Logistic) = d.μ
mode(d::Logistic) = d.μ

std(d::Logistic) = π * d.θ / sqrt3
var(d::Logistic) = (π * d.θ)^2 / 3
skewness(d::Logistic{T}) where {T<:Real} = zero(T)
kurtosis(d::Logistic{T}) where {T<:Real} = T(6)/5

entropy(d::Logistic) = log(d.θ) + 2

#### Evaluation

zval(d::Logistic, x::Real) = (x - d.μ) / d.θ
xval(d::Logistic, z::Real) = d.μ + z * d.θ

pdf(d::Logistic, x::Real) = (lz = logistic(-abs(zval(d, x))); lz*(1-lz)/d.θ)
logpdf(d::Logistic, x::Real) = (u = -abs(zval(d, x)); u - 2*log1pexp(u) - log(d.θ))

cdf(d::Logistic, x::Real) = logistic(zval(d, x))
ccdf(d::Logistic, x::Real) = logistic(-zval(d, x))
logcdf(d::Logistic, x::Real) = -log1pexp(-zval(d, x))
logccdf(d::Logistic, x::Real) = -log1pexp(zval(d, x))

quantile(d::Logistic, p::Real) = xval(d, logit(p))
cquantile(d::Logistic, p::Real) = xval(d, -logit(p))
invlogcdf(d::Logistic, lp::Real) = xval(d, -logexpm1(-lp))
invlogccdf(d::Logistic, lp::Real) = xval(d, logexpm1(-lp))

function gradlogpdf(d::Logistic, x::Real)
    e = exp(-zval(d, x))
    ((2e) / (1 + e) - 1) / d.θ
end

mgf(d::Logistic, t::Real) = exp(t * d.μ) / sinc(d.θ * t)

function cf(d::Logistic, t::Real)
    a = (π * t) * d.θ
    a == zero(a) ? complex(one(a)) : cis(t * d.μ) * (a / sinh(a))
end


"""
    function dwz_ranking(
        players::Vector{Player},
        ranking_scheme_to_update::String,
        last_tournament_id::String
    )

Update the players rankings using the Deutsche Wertungszahl Algorithm

Arguments
=========
- `players`:
    an array of players in the simulation.
- `ranking_scheme_to_update`:
    the name of the ranking scheme to update for each Player.
- `last_tournament_id`:
    the last tournament that was completed.
"""

function dwz_ranking(
    players::Vector{Player},
    ranking_scheme_to_update::String,
    last_tournament_id::String
)
    updatedRankings = Dict{Int64, Int64}();
    for player in players
        if haskey(player.doubles_results, last_tournament_id)
            player_ranking = player.rankings["Dwz"]
            teammate_ranking = search_dwz_ranking(player.doubles_results.partner_player_id, players)
            achieved_points = 0
            expected_points = 0
            n = length(player.doubles_results[last_tournament_id])
            for match in player.doubles_results[last_tournament_id].matches
                zA = (player_ranking + teammate_ranking) / 2
                zG = search_dwz_ranking(match.opponent1_player_id, players) + search_dwz_ranking(match.opponent2_player_id, players)
                wE = 1/(1+(10^((zG-zA)/400)))
                wA = match.result == "Won" ? 1 : 0
                achieved_points += wA
                expected_points += wE
            end
            if player.age < 20
                J = 5
            elseif player.age <= 25
                J = 10
            else
                J = 15
            end
            E0 = (player_ranking/1000)^4 + J
            a = player.age <= 20 && achieved_points > expected_points ? player_ranking/2000 : 1
            B = player_ranking < 1300 && achieved_points <= expected_points ? exp((1300-player_ranking)/150)-1 : 0
            E = a * E0 + B
            Zn = player_ranking + (800/(E+n))(achieved_points-expected_points)
            updatedRankings[player.id] = Zn
        else
            # If player didn't participate in tournament, their ranking doesn't change
            updatedRankings[player.id] = player.rankings["Dwz"];
        end
    end
    map(player -> player.rankings[ranking_scheme_to_update] = updatedRankings[player.id], players)
    return nothing
end

"""
    function search_dwz_ranking(
        player_id::Int64,
        players::Vector{Player}
    )::Float64

Search for a player's current Dwz Ranking

Arguments
=========
- `player_id`:
    the player whose ranking is under search.
- `players`:
    an array of players in the simulation.
"""

function search_dwz_ranking(
    player_id::Int64,
    players::Vector{Player}
)::Float64
    player_ranking = filter(p -> p.id == player_id, players)[1].rankings["Dwz"]
    return player_ranking
end


using FFTW
import LinearAlgebra: svd, diag, diagm, eigen
import QuantumInfo: eye

function hilbert(signal)
	# construct the Hilbert transform of the signal via the FFT
	# in essense, we just want to set negative frequency components to zero
	spectrum = fft(signal)
	n = length(signal)
	midpoint = ceil(Int, n/2)

	kernel = zeros(n)
	kernel[1] = 1
	if iseven(n)
		kernel[midpoint + 1] = 1
	end
	kernel[2:midpoint] .= 2
	ifft(kernel .* spectrum)
end

function KT_estimation(data, timeStep, order)
	#Perform a modified KT estimation to obtain the estimates of the parameters
	#from a set of data.
	#
	# function [freqs, Tcs, amps] = KT_estimation(data, timeStep, order)
	#
	# See ? Van Huffel, S. (1993). Enhanced resolution based on minimum variance estimation and exponential data modeling.
	#       Signal Processing, 33(3), 333-355. doi:10.1016/0165-1684(93)90130-3

	analyticSig = hilbert(data)

	#Create the raw Hankel matrix
	N = length(analyticSig)
	K = order
	M = trunc(Integer, N/2)-1
	L = N-M+1
	H = zeros(ComplexF64, L, M)
	for ct = 1:M
	    H[:,ct] = analyticSig[ct:ct+L-1]
	end

	#Try and seperate the signal and noise subspace via the svd
	U,S,V = svd(H)

	#Reconstruct the approximate Hankel matrix with the first K singular values
	#Here we can iterate and modify the singular values
	S_k = diagm(0=>S[1:K])
	#Estimate the variance from the rest of the singular values
	varEst = (1/((M-K)*L)) * sum(S[K+1:end].^2)
	Sfilt = (S_k.^2 - L*varEst*eye(K)) / S_k
	Hbar = U[:,1:K] * Sfilt * V[:,1:K]'

	#Reconstruct the data from the averaged anti-diagonals
	cleanedData = zeros(ComplexF64, N)
	tmpMat = reverse(Hbar,dims=2)
	idx = -L+1
	for ct = N:-1:1
	    cleanedData[ct] = mean(diag(tmpMat,idx))
	    idx += 1
	end

	#Create a cleaned Hankel matrix
	cleanedH = similar(H)
	cleanedAnalyticSig = hilbert(cleanedData)
	for ct = 1:M
	    cleanedH[:,ct] = cleanedAnalyticSig[ct:ct+L-1]
	end

	#Compute Q with total least squares
	#U_K1*Q = U_K2
	U,_,_ = svd(cleanedH)
	U_K = U[:,1:K]
	tmpMat = [U_K[1:end-1,:] U_K[2:end,:]]
	_,_,V = svd(tmpMat)
	n = size(U_K,2)
	V_AB = V[1:n,1+n:end]
	V_BB = V[1+n:end,1+n:end]
	Q = -V_AB/V_BB

	#Now poles are eigenvalues of Q
	poles, _ = eigen(Q)

	#Take the log and return the decay constant and frequency
	freqs = zeros(K)
	Tcs = zeros(K)
	for ct = 1:K
	    sk = log(poles[ct])
	    freqs[ct] = imag(sk)/2/pi/timeStep
	    Tcs[ct] = -1/real(sk)*timeStep
	end

	#Refit the data to get the amplitude
	A = zeros(ComplexF64, N, K)
	for ct = 1:K
	    A[:,ct] = poles[ct].^(0:N-1)
	end

	amps = A\cleanedData

	return freqs, Tcs, amps

end


# Create confusion matrix for any number of classes
function confusion_matrix(y_cond::Vector{Int}, y_pred::Vector{Int})

    # Generate empty confusion matrix
    classes = sort(unique([y_cond; y_pred]))
    conf_matrix = zeros(Int64, length(classes), length(classes))

    # Add counts
    for i in 1:length(y_cond)
        # translate label to index
        cond_class_index = findfirst(classes, y_cond[i])
        pred_class_index = findfirst(classes, y_pred[i])
        # predicted class is the row, condition class is the column
        conf_matrix[pred_class_index, cond_class_index] += 1
  end

  return conf_matrix
end

# plot confusion matrix
function plot_confusion_matrix(y_cond::Vector{Int},
                               y_pred::Vector{Int},
                               ylabel::String = "Var1",
                               xlabel::String = "Var2",
                               classes::Vector{String} = string.(sort(unique([y_cond; y_pred]))))
    # Number of classes
    num = length(classes)

    # Create DataFrame with data from confusion matrix
    axes = vcat([[classes[i] classes[j]] for i in 1:num, j in 1:num]...)
    df = DataFrame([axes confusion_matrix(y_cond, y_pred)[:]])

    # Colormap
    cmap = vcat(RGB(1,1,1), sequential_palette(255, 99, c=0.88, s=0.6, b=0.75, w=0.3, d=0.25,
                            wcolor=RGB(1,1,1), dcolor=RGB(0,0,1), logscale=false))

    # Plot
    plot(df, x="x2", y="x1", color="x3", Geom.rectbin,
         Coord.cartesian(yflip=true, xflip=false, fixed=true),
         Guide.YLabel(ylabel), Guide.XLabel(xlabel), Guide.colorkey("Count"),
         Scale.color_continuous(colormap = p->cmap[round(Int, p*99+1)], minvalue=0))
end

# Print accuracy stats
# Optional to print short, medium or long version
function print_binary_accuracy(y_cond::Vector{Int}, y_pred::Vector{Int}; size="short")

    # Check only 2 classes
    length(unique([y_cond; y_pred])) > 2 && throw("function is only for 2 classes")

    # Create confusion matrix
    CM = confusion_matrix(y_cond, y_pred)

    # Calculate metrics

    # 1st level metrics
    Population = sum(CM)          # Total number in population

    TP = CM[1, 1]                 # "True Positive" / "Hit"
    TN = CM[2, 2]                 # "True Negative" / "Correct Reject"
    FP = CM[1, 2]                 # "False Positive" / "False Alarm" / Type I Error
    FN = CM[2, 1]                 # "False Negative" / "Miss" / Type II Error

    CondPos = TP + FN             # Sum of Condition = Positive
    CondNeg = FP + TN             # Sum of Condition = Negative
    PredPos = TP + FP             # Sum of Prediction = Positive
    PredNeg = FN + TN             # Sum of Prediction = Negative

    # 2nd level metrics

    TPR = TP / CondPos            # True Positive Rate / Sensitivity / Hit Rate / Recall
    TNR = TN / CondNeg            # True Negative Rate / Specificity
    PPV = TP / PredPos            # Positive Predictive Value / Precision
    NPV = TN / PredNeg            # Negative Predictive Value
    FPR = FP / CondNeg            # False Positive Rate / Fall-out
    FDR = FP / PredNeg            # False Discovery Rate
    FNR = FN / CondNeg            # False Negative Rate
    FOR = FN / PredNeg            # False Omission Rate

    # 3rd level metrics

    Accuracy = (TP + TN) / Population                  # Accuracy Rate
    PosLR = TPR / FPR                                  # Positive Likelihood Ratio
    NegLR = FNR / TNR                                  # Negative Likelihood Ratio
    DiagOR = PosLR / NegLR                             # Diagnostic Odds Ratio

    # F Scores i.e. F_β = ((1 + β^2) * PPV * TPR)/ (β^2 * PPV + TPR)
    F_1 = (2 * PPV * TPR) / (PPV + TPR)                # F1 Score / Harmonic Mean of Precision and Recall
    F_2 = (5 * PPV * TPR) / (4 * PPV + TPR)            # F2 Score - weights recall higher than precision
    F_05 = (1.25 * PPV * TPR) / (0.25 * PPV + TPR)     # F0.5 Score - weights precision higher than recall

    # Matthews's Correlation Coefficient
    # Guideline:
    # +1 = perfect prediction, 0 = no better than random, -1 = total disagreement
    MCC = (TP*TN - FP*FN) / sqrt((TP+FP)*(TP+FN)*(TN+FP)*(TN+FN))

    # Kappa Coefficient - compares Observed Accuracy with Expected Accuracy (random chance)
    # Guidelines:
    # L&K: <0 = no agreement, 0-0.20 = slight, 0.21-0.40 = fair, 0.41-0.60 = moderate, 0.61-0.80 = substantial, 0.81-1 = almost perfect
    # Fleiss: <0.4 = poor, 0.40-0.75 = fair to good, >0.75 = excellent
    ObsAcc = (TP + TN) / Population                                    # Observed Accuracy
    ExpAcc = (CondPos * PredPos + CondNeg * PredNeg) / Population^2    # Expected Accuracy
    Kappa = (ObsAcc - ExpAcc) / (1 - ExpAcc)                           # Kappa

    # Display metrics
    if size == "short"
        @printf "Accuracy:    %f\n" Accuracy
        @printf "F_1:         %f\n" F_1
    elseif size == "medium"
        @printf "Accuracy:    %f\n" Accuracy
        @printf "F_1:         %f\n" F_1
        @printf "F_2:         %f\n" F_2
        @printf "F_05:        %f\n" F_05
        @printf "MCC:         %f\n" MCC
        @printf "Kappa:       %f\n" Kappa
    elseif size == "long"
        @printf "Accuracy:    %f\n" Accuracy
        @printf "F_1:         %f\n" F_1
        @printf "F_2:         %f\n" F_2
        @printf "F_05:        %f\n" F_05
        @printf "MCC:         %f\n" MCC
        @printf "Kappa:       %f\n" Kappa
        @printf "Precision:   %f\n" PPV
        @printf "Recall:      %f\n" TPR
        @printf "Specificity: %f\n" TNR
    end
end

# ROC Curve


### A Pluto.jl notebook ###
# v0.12.10

using Markdown
using InteractiveUtils

# ╔═╡ e077ab16-22e5-11eb-15ed-5f0654bf7328
using Pkg, DrWatson

# ╔═╡ e75a9a06-22e5-11eb-2e36-8d4f62b830ed
begin
	@quickactivate "StatisticsWithJuliaPlutoNotebooks"
	using DataFrames, CSV, Dates, StatsBase, Plots, TimeSeries
end

# ╔═╡ ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
md"## Listing4.20"

# ╔═╡ fa68607e-22e5-11eb-0558-c9a4d9f77426
begin
	data = CSV.read(datadir("temperatures.csv"),copycols = true)
	brisbane = data.Brisbane
	dates = [Date(
				Year(data.Year[i]),
				Month(data.Month[i]),
				Day(data.Day[i])
			) for i in 1:nrow(data)]

	window1, window2 = 7, 14
	d1 = values(moving(mean,TimeArray(dates,brisbane),window1))
	d2 = values(moving(mean,TimeArray(dates,brisbane),window2))
end;

# ╔═╡ 56bc1334-250c-11eb-3c15-f98b6097c3a6
begin
	grid = (2pi:-2pi/365:0) .+ pi/2
	monthsNames = Dates.monthname.(dates[1:31:365])

	plot(grid, d1[1:366],
		c=:blue, proj=:polar, label="Brisbane weekly average temp.")
	plot!(grid, d2[1:366],
		xticks=([mod.((11pi/6:-pi/6:0) .+ pi/2,2pi) ;], monthsNames),
		c=:red, proj=:polar,
		label="Brisbane fortnightly average temp.", legend=:outerbottom)
end

# ╔═╡ 475ff888-22e6-11eb-2354-09f8f40a8e12
md"## End of listing4.20"

# ╔═╡ Cell order:
# ╟─ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
# ╠═e077ab16-22e5-11eb-15ed-5f0654bf7328
# ╠═e75a9a06-22e5-11eb-2e36-8d4f62b830ed
# ╠═fa68607e-22e5-11eb-0558-c9a4d9f77426
# ╠═56bc1334-250c-11eb-3c15-f98b6097c3a6
# ╟─475ff888-22e6-11eb-2354-09f8f40a8e12


import RowEchelon

#=
Homework 3
Section 1.7
Problem 41
=#

import RowEchelon

#=
Homework 3
Section 1.7
Problem 5
=#

# Create the matrix
A = [3 -4 10 7 -4 ;
	 -5 -3 -7 -11 15 ;
	 4 3 5 2 1 ;
	 8 -7 23 4 15]

# Row Reduce the matrix
A_rref = RowEchelon.rref(A)

# Store the rows and columns of A
rows = size(A)[1]
cols = size(A)[2]

# Find the pivot columns
pivotCols = zeros(0)
for i = 1:rows
	for j = 1:cols
		if A_rref[i,j] != 0
			append!(pivotCols,j)
			break
		end
	end
end

# The columns we want are the pivot columns
B = Array{Float64}(undef, 4, 0)
for col in pivotCols
	global B
	B = hcat(B, A[:,Int64(col)])
end

# Row Reduce
B_rref = RowEchelon.rref(B)

# Print results:
println("Original Matrix:")
println(A)
print("Matrix made from columns ")
print(pivotCols)
println(":")
println(B)
println("Row Reduced:")
println(B_rref)


"""
    PoissonProcess{T<:Real} <: AbstractMarkovChain

##### Fields
- `rate::T` : the rate; i.e., the parameter "lambda".
"""
struct PoissonProcess{T<:Real} <: AbstractMarkovChain
    rate::T
end

"""
    Base.show(io::IO, pp::PoissonProcess) -> nothing

Show a `PoissonProcess` in a human-friendly manner.
"""
function Base.show(io::IO, pp::PoissonProcess)
    println(io, typeof(pp))
    println(io, " Rate (i.e. lambda):")
    println(io, pp.rate)
    return nothing
end

"""
    Base.eltype(pp::PoissonProcess) -> DataType

Return the type associated with `pp`'s `rate` parameter.
"""
function Base.eltype(pp::PoissonProcess)
    return typeof(pp.rate)
end

"""
    nexpectedarrivals(pp::PoissonProcess, intervallength::Real) -> Int

Return the expected number of arrivals `E[N(intervallength)]` over the time interval
`[0, intervallength]` for `pp`.

##### Complexity
-Time complexity: `O(1)`.
"""
function nexpectedarrivals(pp::PoissonProcess, intervallength::Real)
    return pp.rate*intervallength
end


"""
    entropy(P)

Computes the entropy of a distribution.
"""
function entropy(P; norm=false)
    if norm
        P /= sum(P)
    end
    return - sum(P[P.>0] .* log.(P[P.>0]))
end

"""
    relative_entropy(P, Q)

Computes the relative entropy of P to Q.
"""
relative_entropy(P, Q) = - sum(P .* log.(Q))

"""
    KL(P, Q)

Computes the Kullback-Leibler divergence of P to Q
"""
KL(P, Q) = relative_entropy(P, Q) - entropy(P)

"""
    utility(P, M)

Computes the average utility.
"""
function utility(P, M)
    return sum(P .* M)
end

function report_utility(P, M, bottom_species=nothing, top_species=nothing)
    @assert size(P) == size(M) "P and M should be of the same size"
    n, m = size(P)
    bottom_species == nothing && bottom_species == 1:n
    top_species == nothing && top_species == 1:m
    U = P .* M
    a, b = marginals(P)
    println("Bottom species")
    for i in 1:n
        println("\t$(bottom_species[i]): utility = $(sum(U[i,:]) / a[i]), entropy = $(
                                entropy(P[i,:] |> x -> x / sum(x)))")
    end
    println()
    println("Top species")
    for j in 1:m
        println("\t$(top_species[j]): utility = $(sum(U[:,j] / b[j])), entropy = $(
                                entropy(P[:,j] |> x -> x / sum(x)))")
    end
    println()
    println("Average utility = $(sum(U))")
    println("Average entropy = $(entropy(P))")
end


using Distributions
import Base.size, Base.length, Base.rand, Base.mean, Base.convert

doc"""
    MatrixNormal(M, U, V)
The *Matrix Normal distribution* with mean `M` and row and column covariances `U` and `V` has probability density function
$f(\mathbf{X}; \mathbf{M}, \mathbf{U}, \mathbf{V}) = \frac{\exp\left(
    -\frac{1}{2} \mathrm{tr}\left[
        \mathbf{V}^{-1} (\mathbf{X} - \mathbf{M})^\top \mathbf{U}^{-1}
        (\mathbf{X} - \mathbf{M})
    \right]
    \right)}{(2\pi)^{np/2} |\mathbf{V}|^{n/2} |\mathbf{U}|^{p/2}}$
when $\mathbf{X}$ is $n \times p$.
```julia
MatrixNormal(U, V)      # Matrix Normal with mean 0 and covariances U and V
MatrixNormal(M, U, V)      # Matrix Normal with mean M and covariances U and V
params(d)        # Get the parameters, i.e. (M, U, V)
mean(d)          # Get the mean, i.e., M
cov(d)           # Get the covariance, i.e., V ⊗ U
var(d)           # Get the marginal variances for individual entries
invcov(d)        # inverse of covariance
logdetcov(d)     # log determinant of covariance
logpdf(d, X)     # log of the pdf at point X
```
External links
* [Matrix Normal distribution on Wikipedia](http://en.wikipedia.org/wiki/Matrix_normal_distribution)
"""
immutable MatrixNormal{Mean<:AbstractMatrix, CovU<:AbstractPDMat, CovV<:AbstractPDMat} <: ContinuousMatrixDistribution
    M::Mean
    U::CovU
    V::CovV
end

### Constructors
typealias CovLike Union{AbstractMatrix, AbstractVector, AbstractPDMat}
normalize(σ::AbstractVector) = PDiagMat(abs2(σ))
normalize(Σ::AbstractMatrix) = PDMat(Σ)
normalize(Σ::AbstractPDMat) = Σ
normalize(d::Int, σ::Real) = PDScalMat(d, abs2(σ))

function MatrixNormal{Mean <: AbstractMatrix, CovU<:AbstractPDMat, CovV<:AbstractPDMat}(M::Mean, U::CovU, V::CovV)
    size(M) == (dim(U), dim(V)) || throw(DimensionMismatch("The dimensions of M, U, and V are inconsistent."))
    MatrixNormal{Mean, CovU, CovV}(M, U, V)
end
function MatrixNormal{Mean<:AbstractMatrix, CovU<:CovLike, CovV<:CovLike}(M::Mean, U::CovU, V::CovV)
    MatrixNormal(M, normalize(U), normalize(V))
end

MatrixNormal{CovU<:AbstractPDMat, CovV<:AbstractPDMat}(U::CovU, V::CovV) = MatrixNormal(zeros(dim(U), dim(V)), U, V)
MatrixNormal{CovU<:CovLike, CovV<:CovLike}(U::CovU, V::CovV) = MatrixNormal(normalize(U), normalize(V))

### Conversions
convert(::Type{MvNormal}, d::MatrixNormal) = MvNormal(vec(mean(d)), cov(d))

### interface
size(d::MatrixNormal) = (dim(d.U), dim(d.V))
params(d::MatrixNormal) = (d.M, d.U, d.V)
mean(d::MatrixNormal) = d.M
cov(d::MatrixNormal) = kron(full(d.V), full(d.U))
var(d::MatrixNormal) = diag(d.U) * diag(d.V)'
invcov(d::MatrixNormal) = kron(full(inv(d.V)), full(inv(d.U)))
logdetcov(d::MatrixNormal) = ((n, p) = size(d); n * logdet(d.V) + p * logdet(d.U))

rand(d::MatrixNormal) = reshape(rand(convert(MvNormal, d)), size(d))
entropy(d::MatrixNormal) = entropy(convert(MvNormal, d))

function logpdf(d::MatrixNormal, X::AbstractMatrix)
    size(X) == size(d) || error("Input matrix x has wrong size.")
    m = X - d.M
    n, p = size(d)
    invU = full(inv(d.U))
    invV = full(inv(d.V))
    lpdf = -0.5 * trace(invV * m' * invU * m)
    lpdf -= (n * p/2) * Float64(Distributions.log2π)
    lpdf -= 0.5 * logdetcov(d)
    lpdf
end


@doc doc"""
    LPDistLoss{P} <: DistanceLoss

The P-th power absolute distance loss. It is Lipschitz continuous
iff `P == 1`, convex if and only if `P >= 1`, and strictly convex
iff `P > 1`.

```math
L(r) = |r|^P
```
"""
struct LPDistLoss{P} <: DistanceLoss end

LPDistLoss(p::Number) = LPDistLoss{p}()

value(loss::LPDistLoss{P}, difference::Number) where {P} = abs(difference)^P
function deriv(loss::LPDistLoss{P}, difference::T)::promote_type(typeof(P),T) where {P,T<:Number}
    if difference == 0
        zero(difference)
    else
        P * difference * abs(difference)^(P-convert(typeof(P), 2))
    end
end
function deriv2(loss::LPDistLoss{P}, difference::T)::promote_type(typeof(P),T) where {P,T<:Number}
    if difference == 0
        zero(difference)
    else
        (abs2(P)-P) * abs(difference)^P / abs2(difference)
    end
end

isminimizable(::LPDistLoss{P}) where {P} = true
issymmetric(::LPDistLoss{P}) where {P} = true
isdifferentiable(::LPDistLoss{P}) where {P} = P > 1
isdifferentiable(::LPDistLoss{P}, at) where {P} = P > 1 || at != 0
istwicedifferentiable(::LPDistLoss{P}) where {P} = P > 1
istwicedifferentiable(::LPDistLoss{P}, at) where {P} = P > 1 || at != 0
islipschitzcont(::LPDistLoss{P}) where {P} = P == 1
islocallylipschitzcont(::LPDistLoss{P}) where {P} = P >= 1
isconvex(::LPDistLoss{P}) where {P} = P >= 1
isstrictlyconvex(::LPDistLoss{P}) where {P} = P > 1
isstronglyconvex(::LPDistLoss{P}) where {P} = P >= 2

# ===========================================================

@doc doc"""
    L1DistLoss <: DistanceLoss

The absolute distance loss.
Special case of the [`LPDistLoss`](@ref) with `P=1`.
It is Lipschitz continuous and convex, but not strictly convex.

```math
L(r) = |r|
```

---
```
              Lossfunction                     Derivative
      ┌────────────┬────────────┐      ┌────────────┬────────────┐
    3 │\.                     ./│    1 │            ┌------------│
      │ '\.                 ./' │      │            |            │
      │   \.               ./   │      │            |            │
      │    '\.           ./'    │      │_           |           _│
    L │      \.         ./      │   L' │            |            │
      │       '\.     ./'       │      │            |            │
      │         \.   ./         │      │            |            │
    0 │          '\./'          │   -1 │------------┘            │
      └────────────┴────────────┘      └────────────┴────────────┘
      -3                        3      -3                        3
                 ŷ - y                            ŷ - y
```
"""
const L1DistLoss = LPDistLoss{1}

value(loss::L1DistLoss, difference::Number) = abs(difference)
deriv(loss::L1DistLoss, difference::T) where {T<:Number} = convert(T, sign(difference))
deriv2(loss::L1DistLoss, difference::T) where {T<:Number} = zero(T)

isdifferentiable(::L1DistLoss) = false
isdifferentiable(::L1DistLoss, at) = at != 0
istwicedifferentiable(::L1DistLoss) = false
istwicedifferentiable(::L1DistLoss, at) = at != 0
islipschitzcont(::L1DistLoss) = true
isconvex(::L1DistLoss) = true
isstrictlyconvex(::L1DistLoss) = false
isstronglyconvex(::L1DistLoss) = false

# ===========================================================

@doc doc"""
    L2DistLoss <: DistanceLoss

The least squares loss.
Special case of the [`LPDistLoss`](@ref) with `P=2`.
It is strictly convex.

```math
L(r) = |r|^2
```

---
```
              Lossfunction                     Derivative
      ┌────────────┬────────────┐      ┌────────────┬────────────┐
    9 │\                       /│    3 │                   .r/   │
      │".                     ."│      │                 .r'     │
      │ ".                   ." │      │              _./'       │
      │  ".                 ."  │      │_           .r/         _│
    L │   ".               ."   │   L' │         _:/'            │
      │    '\.           ./'    │      │       .r'               │
      │      \.         ./      │      │     .r'                 │
    0 │        "-.___.-"        │   -3 │  _/r'                   │
      └────────────┴────────────┘      └────────────┴────────────┘
      -3                        3      -2                        2
                 ŷ - y                            ŷ - y
```
"""
const L2DistLoss = LPDistLoss{2}

value(loss::L2DistLoss, difference::Number) = abs2(difference)
deriv(loss::L2DistLoss, difference::T) where {T<:Number} = convert(T,2) * difference
deriv2(loss::L2DistLoss, difference::T) where {T<:Number} = convert(T,2)

isdifferentiable(::L2DistLoss) = true
isdifferentiable(::L2DistLoss, at) = true
istwicedifferentiable(::L2DistLoss) = true
istwicedifferentiable(::L2DistLoss, at) = true
islipschitzcont(::L2DistLoss) = false
isconvex(::L2DistLoss) = true
isstrictlyconvex(::L2DistLoss) = true
isstronglyconvex(::L2DistLoss) = true

# ===========================================================

@doc doc"""
    PeriodicLoss <: DistanceLoss

Measures distance on a circle of specified circumference `c`.

```math
L(r) = 1 - \cos \left( \frac{2 r \pi}{c} \right)
```
"""
struct PeriodicLoss{T<:AbstractFloat} <: DistanceLoss
    k::T   # k = 2π/circumference
    function PeriodicLoss{T}(circ::T) where T
        circ > 0 || error("circumference should be strictly positive")
        new{T}(convert(T, 2π/circ))
    end
end
PeriodicLoss(circ::T=1.0) where {T<:AbstractFloat} = PeriodicLoss{T}(circ)
PeriodicLoss(circ) = PeriodicLoss{Float64}(Float64(circ))

value(loss::PeriodicLoss, difference::T) where {T<:Number} = 1 - cos(difference*loss.k)
deriv(loss::PeriodicLoss, difference::T) where {T<:Number} = loss.k * sin(difference*loss.k)
deriv2(loss::PeriodicLoss, difference::T) where {T<:Number} = abs2(loss.k) * cos(difference*loss.k)

isdifferentiable(::PeriodicLoss) = true
isdifferentiable(::PeriodicLoss, at) = true
istwicedifferentiable(::PeriodicLoss) = true
istwicedifferentiable(::PeriodicLoss, at) = true
islipschitzcont(::PeriodicLoss) = true
isconvex(::PeriodicLoss) = false
isstrictlyconvex(::PeriodicLoss) = false
isstronglyconvex(::PeriodicLoss) = false

# ===========================================================

@doc doc"""
    HuberLoss <: DistanceLoss

Loss function commonly used for robustness to outliers.
For large values of `d` it becomes close to the [`L1DistLoss`](@ref),
while for small values of `d` it resembles the [`L2DistLoss`](@ref).
It is Lipschitz continuous and convex, but not strictly convex.

```math
L(r) = \begin{cases} \frac{r^2}{2} & \quad \text{if } | r | \le \alpha \\ \alpha | r | - \frac{\alpha^3}{2} & \quad \text{otherwise}\\ \end{cases}
```

---
```
              Lossfunction (d=1)               Derivative
      ┌────────────┬────────────┐      ┌────────────┬────────────┐
    2 │                         │    1 │                .+-------│
      │                         │      │              ./'        │
      │\.                     ./│      │             ./          │
      │ '.                   .' │      │_           ./          _│
    L │   \.               ./   │   L' │           /'            │
      │     \.           ./     │      │          /'             │
      │      '.         .'      │      │        ./'              │
    0 │        '-.___.-'        │   -1 │-------+'                │
      └────────────┴────────────┘      └────────────┴────────────┘
      -2                        2      -2                        2
                 ŷ - y                            ŷ - y
```
"""
struct HuberLoss{T<:AbstractFloat} <: DistanceLoss
    d::T   # boundary between quadratic and linear loss
    function HuberLoss{T}(d::T) where T
        d > 0 || error("Huber crossover parameter must be strictly positive.")
        new{T}(d)
    end
end
HuberLoss(d::T=1.0) where {T<:AbstractFloat} = HuberLoss{T}(d)
HuberLoss(d) = HuberLoss{Float64}(Float64(d))

function value(loss::HuberLoss{T1}, difference::T2) where {T1,T2<:Number}
    T = promote_type(T1,T2)
    abs_diff = abs(difference)
    if abs_diff <= loss.d
        return convert(T,0.5)*abs2(difference)   # quadratic
    else
        return (loss.d*abs_diff) - convert(T,0.5)*abs2(loss.d)   # linear
    end
end
function deriv(loss::HuberLoss{T1}, difference::T2) where {T1,T2<:Number}
    T = promote_type(T1,T2)
    if abs(difference) <= loss.d
        return convert(T,difference)   # quadratic
    else
        return loss.d*convert(T,sign(difference))   # linear
    end
end
function deriv2(loss::HuberLoss{T1}, difference::T2) where {T1,T2<:Number}
    T = promote_type(T1,T2)
    abs(difference) <= loss.d ? one(T) : zero(T)
end

isdifferentiable(::HuberLoss) = true
isdifferentiable(l::HuberLoss, at) = true
istwicedifferentiable(::HuberLoss) = false
istwicedifferentiable(l::HuberLoss, at) = at != abs(l.d)
islipschitzcont(::HuberLoss) = true
isconvex(::HuberLoss) = true
isstrictlyconvex(::HuberLoss) = false
isstronglyconvex(::HuberLoss) = false
issymmetric(::HuberLoss) = true

# ===========================================================

@doc doc"""
    L1EpsilonInsLoss <: DistanceLoss

The ``ϵ``-insensitive loss. Typically used in linear support vector
regression. It ignores deviances smaller than ``ϵ``, but penalizes
larger deviances linarily.
It is Lipschitz continuous and convex, but not strictly convex.

```math
L(r) = \max \{ 0, | r | - \epsilon \}
```

---
```
              Lossfunction (ϵ=1)               Derivative
      ┌────────────┬────────────┐      ┌────────────┬────────────┐
    2 │\                       /│    1 │                  ┌------│
      │ \                     / │      │                  |      │
      │  \                   /  │      │                  |      │
      │   \                 /   │      │_      ___________!     _│
    L │    \               /    │   L' │      |                  │
      │     \             /     │      │      |                  │
      │      \           /      │      │      |                  │
    0 │       \_________/       │   -1 │------┘                  │
      └────────────┴────────────┘      └────────────┴────────────┘
      -3                        3      -2                        2
                 ŷ - y                            ŷ - y
```
"""
struct L1EpsilonInsLoss{T<:AbstractFloat} <: DistanceLoss
    ε::T

    function L1EpsilonInsLoss{T}(ɛ::T) where T
        ɛ > 0 || error("ɛ must be strictly positive")
        new{T}(ɛ)
    end
end
const EpsilonInsLoss = L1EpsilonInsLoss
@inline L1EpsilonInsLoss(ε::T) where {T<:AbstractFloat} = L1EpsilonInsLoss{T}(ε)
@inline L1EpsilonInsLoss(ε::Number) = L1EpsilonInsLoss{Float64}(Float64(ε))

function value(loss::L1EpsilonInsLoss{T1}, difference::T2) where {T1,T2<:Number}
    T = promote_type(T1,T2)
    max(zero(T), abs(difference) - loss.ε)
end
function deriv(loss::L1EpsilonInsLoss{T1}, difference::T2) where {T1,T2<:Number}
    T = promote_type(T1,T2)
    abs(difference) <= loss.ε ? zero(T) : convert(T,sign(difference))
end
deriv2(loss::L1EpsilonInsLoss{T1}, difference::T2) where {T1,T2<:Number} = zero(promote_type(T1,T2))

issymmetric(::L1EpsilonInsLoss) = true
isdifferentiable(::L1EpsilonInsLoss) = false
isdifferentiable(loss::L1EpsilonInsLoss, at) = abs(at) != loss.ε
istwicedifferentiable(::L1EpsilonInsLoss) = false
istwicedifferentiable(loss::L1EpsilonInsLoss, at) = abs(at) != loss.ε
islipschitzcont(::L1EpsilonInsLoss) = true
isconvex(::L1EpsilonInsLoss) = true
isstrictlyconvex(::L1EpsilonInsLoss) = false
isstronglyconvex(::L1EpsilonInsLoss) = false

# ===========================================================

@doc doc"""
    L2EpsilonInsLoss <: DistanceLoss

The quadratic ``ϵ``-insensitive loss.
Typically used in linear support vector regression.
It ignores deviances smaller than ``ϵ``, but penalizes
larger deviances quadratically. It is convex, but not strictly convex.

```math
L(r) = \max \{ 0, | r | - \epsilon \}^2
```

---
```
              Lossfunction (ϵ=0.5)             Derivative
      ┌────────────┬────────────┐      ┌────────────┬────────────┐
    8 │                         │    1 │                  /      │
      │:                       :│      │                 /       │
      │'.                     .'│      │                /        │
      │ \.                   ./ │      │_         _____/        _│
    L │  \.                 ./  │   L' │         /               │
      │   \.               ./   │      │        /                │
      │    '\.           ./'    │      │       /                 │
    0 │      '-._______.-'      │   -1 │      /                  │
      └────────────┴────────────┘      └────────────┴────────────┘
      -3                        3      -2                        2
                 ŷ - y                            ŷ - y
```
"""
struct L2EpsilonInsLoss{T<:AbstractFloat} <: DistanceLoss
    ε::T

    function L2EpsilonInsLoss{T}(ɛ::T) where T
        ɛ > 0 || error("ɛ must be strictly positive")
        new{T}(ɛ)
    end
end
L2EpsilonInsLoss(ε::T) where {T<:AbstractFloat} = L2EpsilonInsLoss{T}(ε)
L2EpsilonInsLoss(ε) = L2EpsilonInsLoss{Float64}(Float64(ε))

function value(loss::L2EpsilonInsLoss{T1}, difference::T2) where {T1,T2<:Number}
    T = promote_type(T1,T2)
    abs2(max(zero(T), abs(difference) - loss.ε))
end
function deriv(loss::L2EpsilonInsLoss{T1}, difference::T2) where {T1,T2<:Number}
    T = promote_type(T1,T2)
    absr = abs(difference)
    absr <= loss.ε ? zero(T) : convert(T,2)*sign(difference)*(absr - loss.ε)
end
function deriv2(loss::L2EpsilonInsLoss{T1}, difference::T2) where {T1,T2<:Number}
    T = promote_type(T1,T2)
    abs(difference) <= loss.ε ? zero(T) : convert(T,2)
end

issymmetric(::L2EpsilonInsLoss) = true
isdifferentiable(::L2EpsilonInsLoss) = true
isdifferentiable(::L2EpsilonInsLoss, at) = true
istwicedifferentiable(::L2EpsilonInsLoss) = false
istwicedifferentiable(loss::L2EpsilonInsLoss, at) = abs(at) != loss.ε
islipschitzcont(::L2EpsilonInsLoss) = false
isconvex(::L2EpsilonInsLoss) = true
isstrictlyconvex(::L2EpsilonInsLoss) = true
isstronglyconvex(::L2EpsilonInsLoss) = true

# ===========================================================

@doc doc"""
    LogitDistLoss <: DistanceLoss

The distance-based logistic loss for regression.
It is strictly convex and Lipschitz continuous.

```math
L(r) = - \ln \frac{4 e^r}{(1 + e^r)^2}
```

---
```
              Lossfunction                     Derivative
      ┌────────────┬────────────┐      ┌────────────┬────────────┐
    2 │                         │    1 │                   _--'''│
      │\                       /│      │                ./'      │
      │ \.                   ./ │      │              ./         │
      │  '.                 .'  │      │_           ./          _│
    L │   '.               .'   │   L' │           ./            │
      │     \.           ./     │      │         ./              │
      │      '.         .'      │      │       ./                │
    0 │        '-.___.-'        │   -1 │___.-''                  │
      └────────────┴────────────┘      └────────────┴────────────┘
      -3                        3      -4                        4
                 ŷ - y                            ŷ - y
```
"""
struct LogitDistLoss <: DistanceLoss end

function value(loss::LogitDistLoss, difference::Number)
    er = exp(difference)
    T = typeof(er)
    -log(convert(T,4)) - difference + 2log(one(T) + er)
end
function deriv(loss::LogitDistLoss, difference::T) where T<:Number
    tanh(difference / convert(T,2))
end
function deriv2(loss::LogitDistLoss, difference::Number)
    er = exp(difference)
    T = typeof(er)
    convert(T,2)*er / abs2(one(T) + er)
end

issymmetric(::LogitDistLoss) = true
isdifferentiable(::LogitDistLoss) = true
isdifferentiable(::LogitDistLoss, at) = true
istwicedifferentiable(::LogitDistLoss) = true
istwicedifferentiable(::LogitDistLoss, at) = true
islipschitzcont(::LogitDistLoss) = true
isconvex(::LogitDistLoss) = true
isstrictlyconvex(::LogitDistLoss) = true
isstronglyconvex(::LogitDistLoss) = false

# ===========================================================
@doc doc"""
    QuantileLoss <: DistanceLoss

The distance-based quantile loss, also known as pinball loss,
can be used to estimate conditional τ-quantiles.
It is Lipschitz continuous and convex, but not strictly convex.
Furthermore it is symmetric if and only if `τ = 1/2`.

```math
L(r) = \begin{cases} -\left( 1 - \tau  \right) r & \quad \text{if } r < 0 \\ \tau r & \quad \text{if } r \ge 0 \\ \end{cases}
```

---
```
              Lossfunction (τ=0.7)             Derivative
      ┌────────────┬────────────┐      ┌────────────┬────────────┐
    2 │'\                       │  0.3 │            ┌------------│
      │  \.                     │      │            |            │
      │   '\                    │      │_           |           _│
      │     \.                  │      │            |            │
    L │      '\              ._-│   L' │            |            │
      │        \.         ..-'  │      │            |            │
      │         '.     _r/'     │      │            |            │
    0 │           '_./'         │ -0.7 │------------┘            │
      └────────────┴────────────┘      └────────────┴────────────┘
      -3                        3      -3                        3
                 ŷ - y                            ŷ - y
```
"""
struct QuantileLoss{T <: AbstractFloat} <: DistanceLoss
    τ::T
end

function value(loss::QuantileLoss{T1}, diff::T2) where {T1, T2 <: Number}
    T = promote_type(T1, T2)
    diff * (convert(T,diff > 0) - loss.τ)
end
function deriv(loss::QuantileLoss{T1}, diff::T2) where {T1, T2 <: Number}
    T = promote_type(T1, T2)
    convert(T,diff > 0) - loss.τ
end
deriv2(::QuantileLoss{T1}, diff::T2) where {T1, T2 <: Number} = zero(promote_type(T1, T2))

issymmetric(loss::QuantileLoss) = loss.τ == 0.5
isdifferentiable(::QuantileLoss) = false
isdifferentiable(::QuantileLoss, at) = at != 0
istwicedifferentiable(::QuantileLoss) = false
istwicedifferentiable(::QuantileLoss, at) = at != 0
islipschitzcont(::QuantileLoss) = true
islipschitzcont_deriv(::QuantileLoss) = true
isconvex(::QuantileLoss) = true
isstrictlyconvex(::QuantileLoss) = false
isstronglyconvex(::QuantileLoss) = false

# ======================================================

@doc doc"""
    LogCoshLoss <: DistanceLoss

The log cosh loss is twice differentiable, strongly convex,
Lipschitz continous function.

```math
L(r) = log ( cosh ( x ))
```
---
```
           Lossfunction                     Derivative
      ┌────────────┬────────────┐      ┌────────────┬────────────┐
  2.5 │\                       /│    1 │                 .-------│
      │".                     ."│      │                |        │
      │ ".                   ." │      │               /         │
      │  ".                 ."  │      │_           . "         _│
    L │   ".               ."   │   L' │         /"              │
      │    '\.           ./'    │      │       ."                │
      │      \.         ./      │      │       |                 │
    0 │        "-. _ .-"        │   -1 │------"                  │
      └────────────┴────────────┘      └────────────┴────────────┘
      -3                        3      -3                        3
                 ŷ - y                            ŷ - y
```
"""

struct LogCoshLoss <: DistanceLoss end

_softplus(x::T) where T<:Number = x > zero(T) ? x + log1p(exp(-x)) : log1p(exp(x))
_log_cosh(x::T) where T<:Number = x + _softplus(-2x) - log(convert(T, 2))

function value(loss::LogCoshLoss, diff::T) where {T <: Number}
  _log_cosh(diff)
end

function deriv(loss::LogCoshLoss, diff::T) where {T <: Number}
  tanh.(diff)
end

function deriv2(::LogCoshLoss, diff::T) where {T <: Number}
  (sech.(diff))^2
end

issymmetric(loss::LogCoshLoss)           = true
isdifferentiable(::LogCoshLoss)          = true
isdifferentiable(::LogCoshLoss, at)      = true
istwicedifferentiable(::LogCoshLoss, at) = true
istwicedifferentiable(::LogCoshLoss)     = true
islipschitzcont(::LogCoshLoss)           = true
isconvex(::LogCoshLoss)                  = true
isstrictlyconvex(::LogCoshLoss)          = true
isstronglyconvex(::LogCoshLoss)          = true


@doc raw"""
```
binarize(Yen(), img)
```

Computes the binarization threshold value using Yen's maximum correlation criterion for
bilevel thresholding.

# Output

Returns the binarized image as an `Array{Gray{Bool},2}`.

# Details

This algorithm uses the concept of *entropic correlation* of a gray level histogram to produce a threshold
value.

Let ``f_1, f_2, \ldots, f_I`` be the frequencies in the various bins of the
histogram and ``I`` the number of bins. With ``N = \sum_{i=1}^{I}f_i``, let
``p_i = \frac{f_i}{N}`` (``i = 1, \ldots, I``) denote the probability
distribution of gray levels. From this distribution one derives two additional
distributions. The first defined for discrete values ``1`` to ``s`` and the
other, from ``s+1`` to ``I``. These distributions are

```math
A: \frac{p_1}{P_s}, \frac{p_2}{P_s}, \ldots, \frac{p_s}{P_s}
\quad \text{and} \quad
B: \frac{p_{s+1}}{1-P_s}, \ldots, \frac{p_n}{1-P_s}
\quad \text{where} \quad
P_s = \sum_{i=1}^{s}p_i.
```
The entropic correlations associated with each distribution are

```math
C(A) = -\ln \sum_{i=1}^{s} \left( \frac{p_i}{P_s} \right)^2 \quad \text{and} \quad C(B) = -\ln \sum_{i=s+1}^{I} \left( \frac{p_i}{1 - P_s} \right)^2.
```

Combining these two entropic correlation functions we have

```math
\psi(s) = -\ln \sum_{i=1}^{s} \left( \frac{p_i}{P_s} \right)^2 -\ln \sum_{i=s+1}^{I} \left( \frac{p_i}{1 - P_s} \right)^2.
```
Finding the discrete value ``s`` which maximises the function ``\psi(s)`` produces
the sought-after threshold value (i.e. the bin which determines the threshold).

# Arguments

The function argument is described in more detail below.

##  `img`

An `AbstractArray` representing an image. The image is automatically converted
to `Gray` in order to construct the requisite graylevel histogram.

# Example

Binarize the "cameraman" image in the `TestImages` package.

```julia
using TestImages, ImageBinarization

img = testimage("cameraman")
img_binary = binarize(Yen(), img)
```

# Reference

1. Yen JC, Chang FJ, Chang S (1995), “A New Criterion for Automatic Multilevel Thresholding”, IEEE Trans. on Image Processing 4 (3): 370-378, [doi:10.1109/83.366472](https://doi.org/10.1109/83.366472)
"""
function binarize(algorithm::Yen,  img::AbstractArray{T,2}) where T <: Colorant
  img₀₁ = zeros(Gray{Bool}, axes(img))
  edges, counts = build_histogram(img,  256)
  t = find_threshold(HistogramThresholding.Yen(), counts[1:end], edges)
  for i in CartesianIndices(img)
    img₀₁[i] = img[i] < t ? 0 : 1
  end
  img₀₁
end


module TestFunctions

struct Rosenbrock
    f
    ∇f
    argmin
    Rosenbrock(; a = 1, b = 5) =
    new(x->(a - x[1])^2 + b * (x[2] - x[1]^2)^2,
        x->[
            -2 * (a - x[1]) - 4 * b * x[1] * (x[2] - x[1]^2),
            2 * b * (x[2] - x[1]^2)
        ],
        [a, a^2])
end

end # module


using Plots

@fastmath function SIR_fs(N,bet,gamm)
    final_size = zeros(N+1,1);
    final_size[2] = 1;
    for Z2 in 0:N
        @inbounds for Z1 in Z2+1:N-1
            p1 = 1 / ( 1 + gamm/(bet*(N-Z1)));
            final_size[Z1+2] = final_size[Z1+2] + final_size[Z1+1]*p1;
            final_size[Z1+1] = final_size[Z1+1]*(1-p1);
        end
    end
    return final_size;
end

N = 20;
bet = 2/(N-1);
gamm = 1.0;

final_size = SIR_fs(N,bet,gamm);

@time final_size = SIR_fs(N,bet,gamm);

bar(0:N,final_size)


"""
    fourier_pade(u, degree_num, degree_den, num_output=length(u))

Compute the Fourier-Padé reconstruction of `u` with degrees
`(degree_num, degree_den)` and evaluate it at `num_output`
equispaced points, cf.
Driscoll and Fornberg (2001) A Padé-based algorithm for overcoming the Gibbs phenomenon,
doi: 10.1023/A:1016648530648.
"""
function fourier_pade(u, degree_num, degree_den, num_output=length(u))
  N = degree_num + degree_den
  modes = length(u) ÷ 2 + 1
  if modes <= N
    throw(ArgumentError("Cannot perform a Fourier-Padé reconstruction with degrees ($degree_num, $degree_den) given $(length(u)) real coefficients corresponding to $modes complex modes."))
  end

  uhat = rfft(u)
  uhat ./= length(u)

  # denominator
  col_den = uhat[degree_num+2:N+1]
  row_den = zeros(eltype(col_den), degree_den+1)
  row_den[1:min(degree_num+2,degree_den+1)] = uhat[degree_num+2:-1:max(1, degree_num+2-degree_den)]
  Z = nullspace(Matrix(Toeplitz(col_den, row_den)))
  den_p = zeros(eltype(uhat), num_output)
  den_p[1:degree_den+1] = Z[:, end]
  den_p ./= den_p[findfirst(!iszero, den_p)]

  # numerator
  col_num = uhat[1:degree_num+1]
  col_num[1] /= 2
  row_num = zeros(eltype(col_num), degree_den+1)
  row_num[1] = col_num[1]
  A = Toeplitz(col_num, row_num)
  num_p = zeros(eltype(uhat), num_output)
  mul!(view(num_p, 1:degree_num+1), A, view(den_p, 1:degree_den+1))

  # evaluate Fourier-Padé approximation
  bfft_plan = plan_bfft(num_p)
  2 .* real.((bfft_plan * num_p) ./ (bfft_plan * den_p))
end

function reference(::typeof(fourier_pade))
"""
@article{driscoll2001pade,
  title={A {P}ad{\'e}-based algorithm for overcoming the {G}ibbs phenomenon},
  author={Driscoll, Tobin A and Fornberg, Bengt},
  journal={Numerical Algorithms},
  volume={26},
  number={1},
  pages={77--92},
  year={2001},
  publisher={Springer},
  doi={10.1023/A:1016648530648}
}
"""
end


"""
    Student's t

* Parametrization
parametrized in \\nu

* Score

* Fisher Information

* `time_varying_params` map.

* Default link
"""
TDist

function score!(score_til::Matrix{T}, y::T, ::Type{TDist}, param::Matrix{T}, t::Int) where T
    score_til[t, 1] = 0.5 * (((y^2)*(param[t, 1] + 1)/(param[t, 1] * y^2 + param[t, 1]^2)) -
                      1/param[t, 1] -
                      log((y^2)/(param[t, 1]) + 1) -
                      digamma(param[t, 1] / 2) + digamma((param[t, 1] + 1) / 2))
    return
end

function fisher_information!(aux::AuxiliaryLinAlg{T}, ::Type{TDist}, param::Matrix{T}, t::Int) where T
    aux.fisher[1, 1] = 0.5 * (0.5 * trigamma(0.5 * param[t, 1]) - 0.5 * trigamma( 0.5 * (param[t, 1] + 1.0) ) -
                             (param[t, 1] + 5.0) / (param[t, 1] * (param[t, 1] + 3.0) * (param[t, 1] + 1.0)))
    return
end

function log_likelihood(::Type{TDist}, y::Vector{T}, param::Matrix{T}, n::Int) where T
    loglik = zero(T)
    for t in 1:n
        loglik -= 0.5 * log(param[t, 1]) + logbeta(0.5, param[t, 1]/2) + ((param[t, 1] + 1)/2) *
                  log(1 + (y[t]^2)/param[t, 1])
    end
    return -loglik
end

# Links
function link!(param_tilde::Matrix{T}, ::Type{TDist}, param::Matrix{T}, t::Int) where T
    param_tilde[t, 1] = link(LogLink, param[t, 1], zero(T))
    return
end
function unlink!(param::Matrix{T}, ::Type{TDist}, param_tilde::Matrix{T}, t::Int) where T
    param[t, 1] = unlink(LogLink, param_tilde[t, 1], zero(T))
    return
end
function jacobian_link!(aux::AuxiliaryLinAlg{T}, ::Type{TDist}, param::Matrix{T}, t::Int) where T
    aux.jac[1] = jacobian_link(LogLink, param[t, 1], zero(T))
    return
end

# utils
function update_dist(::Type{TDist}, param::Matrix{T}, t::Int) where T
    return TDist(param[t, 1])
end

function params_sdm(d::TDist)
    return Distributions.params(d)
end

function num_params(::Type{TDist})
    return 1
end


"""
    @polyeval(z, p, N)

Evaluate the polynomial ``\\sum_k c[k] z^{k-1}`` for the coefficients `c[1]`, `c[2]`, ..., `c[N]`;
that is, the coefficients are given in ascending order by power of `z`.  This macro expands
to efficient inline code that uses either Horner's method.

```jldoctest

julia> p = randn(5)
5-element Array{Float64,1}:
  0.106455
  0.0351716
 -0.204764
 -0.87642
  0.260407

julia> @polyeval(0.5, p, 5)
-0.020427265391245605

```
"""
macro polyeval(x, p, N)
    if N==1
        return :($(esc(p))[1])
    elseif N==2
        return :($(esc(p))[1] + $(esc(x))*$(esc(p))[2])
    else
        nm1 = N-1
        ex = :($(esc(p))[$nm1] + $(esc(x))*$(esc(p))[$N])
        for i in N-2:-1:1
            ex = :($(esc(p))[$i] + $(esc(x))*$ex)
        end
    end
    return ex
end

"""
    ConvergenceError([msg, val, niter, err])

Exception that is to be thrown when an iterative procedure fails
to converge.

"""
mutable struct ConvergenceError <: Exception
    msg::String
    val::Float64
    niter::Int
    err::Float64
    ConvergenceError() = new("Calculations failed to converge", NaN, -1, NaN)
    ConvergenceError(msg) = new(msg, NaN, -1, NaN)
    ConvergenceError(msg, val) = new(msg, val, -1, NaN)
    ConvergenceError(msg, val, niter) = new(msg, val, niter, NaN)
    ConvergenceError(msg, val, niter, err) = new(msg, val, niter, err)
end

"""
    calcz(vm, b0, c0, [EPS, [MAXITER, [relax]]])

Calculates the compressibility factor using a virial equation:

`z = 1 + b₀/z + c₀/vₘ²`

The algorithm initially uses as an initial guess only the b₀ virial
coefficient. Then it uses a Newton-Raphson algorithm to compute the
compressibility factor

 * `b0` b₀ parameter of the virial equation
 * `c0` c₀ parameter of the virial equation
 * `EPS` Convergence criterium
 * `MAXITER` Maximim number of iterations
 * `relax` Sub-relaxation parameter if convergence is difficult.
"""
function calcz(b0, c0, EPS=1e-8, MAXITER=200, relax=1.0)

    z = (1.0 + sqrt(1.0 + 4*b0)) / 2.0
    dz = 0.0

    for i = 1:MAXITER
        f = -c0 + z*(-b0 + z*(-1.0 + z))
        df = -b0 + z*(-2.0 + 3.0*z)
        dz = -f / df
        if abs(dz) < EPS
            return z + dz
        end
        z = z + relax*dz
    end
    throw(ConvergenceError("Compressibility factor failed to converge properly!",
                           z, MAXITER, abs(dz)))

end


# ---
# title: 1515. Best Position for a Service Centre
# id: problem1515
# author: Tian Jun
# date: 2020-10-31
# difficulty: Hard
# categories: Geometry
# link: <https://leetcode.com/problems/best-position-for-a-service-centre/description/>
# hidden: true
# ---
#
# A delivery company wants to build a new service centre in a new city. The
# company knows the positions of all the customers in this city on a 2D-Map and
# wants to build the new centre in a position such that **the sum of the
# euclidean distances to all customers is minimum**.
#
# Given an array `positions` where `positions[i] = [xi, yi]` is the position of
# the `ith` customer on the map, return _the minimum sum of the euclidean
# distances_ to all customers.
#
# In other words, you need to choose the position of the service centre
# `[xcentre, ycentre]` such that the following formula is minimized:
#
# ![](https://assets.leetcode.com/uploads/2020/06/25/q4_edited.jpg)
#
# Answers within `10^-5` of the actual value will be accepted.
#
#
#
# **Example 1:**
#
# ![](https://assets.leetcode.com/uploads/2020/06/25/q4_e1.jpg)
#
#
#
#     Input: positions = [[0,1],[1,0],[1,2],[2,1]]
#     Output: 4.00000
#     Explanation: As shown, you can see that choosing [xcentre, ycentre] = [1, 1] will make the distance to each customer = 1, the sum of all distances is 4 which is the minimum possible we can achieve.
#
#
# **Example 2:**
#
# ![](https://assets.leetcode.com/uploads/2020/06/25/q4_e3.jpg)
#
#
#
#     Input: positions = [[1,1],[3,3]]
#     Output: 2.82843
#     Explanation: The minimum possible sum of distances = sqrt(2) + sqrt(2) = 2.82843
#
#
# **Example 3:**
#
#
#
#     Input: positions = [[1,1]]
#     Output: 0.00000
#
#
# **Example 4:**
#
#
#
#     Input: positions = [[1,1],[0,0],[2,0]]
#     Output: 2.73205
#     Explanation: At the first glance, you may think that locating the centre at [1, 0] will achieve the minimum sum, but locating it at [1, 0] will make the sum of distances = 3.
#     Try to locate the centre at [1.0, 0.5773502711] you will see that the sum of distances is 2.73205.
#     Be careful with the precision!
#
#
# **Example 5:**
#
#
#
#     Input: positions = [[0,1],[3,2],[4,5],[7,6],[8,9],[11,1],[2,12]]
#     Output: 32.94036
#     Explanation: You can use [4.3460852395, 4.9813795505] as the position of the centre.
#
#
#
#
# **Constraints:**
#
#   * `1 <= positions.length <= 50`
#   * `positions[i].length == 2`
#   * `0 <= positions[i][0], positions[i][1] <= 100`
#
#
## @lc code=start
using LeetCode

## add your code here:
## @lc code=end


using EconPDEs

struct WangWangYangModel
    μ::Float64
    σ::Float64
    r::Float64
    ρ::Float64
    γ::Float64
    ψ::Float64
    wmax::Float64
end

function WangWangYangModel(;μ = 0.015, σ = 0.1, r = 0.035, ρ = 0.04, γ = 3, ψ = 1.1, wmax = 5000.0)
    WangWangYangModel(μ, σ, r, ρ, γ, ψ, wmax)
end

function initialize_stategrid(m::WangWangYangModel; n = 500)
    OrderedDict(:w => range(0.0, stop = sqrt(m.wmax), length = n).^2)
end

function initialize_y(m::WangWangYangModel, stategrid)
    OrderedDict(:p => 1 .+ stategrid[:w])
end

function (m::WangWangYangModel)(state::NamedTuple, y::NamedTuple)
    μ = m.μ ;  σ = m.σ ;  r = m.r ;  ρ = m.ρ ;  γ = m.γ ;  ψ = m.ψ ; wmax = m.wmax
    w = state.w
    p, pw, pww = y.p, y.pw, y.pww
    c = (r + ψ * (ρ - r)) * p * pw^(-ψ)
    μw = (r - μ + σ^2) * w + 1 - c
    # One only needs a ghost node if μw <= 0 (since w^2p_ww = 0). In this case, we obtain a formula for pw so that c <= 1
    if w ≈ 0.0 && μw <= 0.0
       pw = ((r + ψ * (ρ - r)) * p)^(1 / ψ)
       c = (r + ψ * (ρ - r)) * p * pw^(-ψ)
       μw = (r - μ + σ^2) * w + 1 - c
    end
    # At the top, I use the solution of the unconstrainted, i.e. pw = 1 (I could also do reflecting boundary but less elegant)
    pt = (((r + ψ * (ρ - r)) * pw^(1 - ψ) - ψ * ρ) / (ψ - 1) + μ - γ * σ^2 / 2) * p + ((r - μ + γ * σ^2) * w + 1) * pw + σ^2 * w^2 / 2  * (pww - γ * pw^2 / p)
    μw = 1 + (r - μ + σ^2) * w - c
    return (pt,), (μw,), (w = w, p = p, pw = pw, pww = pww, μw = μw, c = c)
end

m = WangWangYangModel()
stategrid = initialize_stategrid(m)
y0 = initialize_y(m, stategrid)
y, result, distance = pdesolve(m, stategrid, y0; bc = OrderedDict(:pw => (1.0, 1.0)))


export jacobian,  StabilityType

@with_kw struct jacobian
    ode
    jac_func
    u
    p
    t
end

"""
    StabilityType

Store the information about stability.
"""
@with_kw struct StabilityType
    stable ::Bool
    unstable ::Bool
    saddle  ::Bool
    damping ::Bool
end

"""
Construct the jacobian struct type

Argument
--------
- `ode` {ODE function}: with the form `f(du,u,p,t)`
- `u` {Array}: initial values
- `p` {Array}: parameters

Return
------
- `Jacobian` {struct}

Warning
-------
NameTuple definition of `ode` function is unrecommended. Due to the incompatibility with `ModelingToolkit.modelingtoolkitize`
"""
function jacobian(ode, u, p; t=nothing)
    prob = ODEProblem(ode, u, t, p)
    de =  ModelingToolkit.modelingtoolkitize(prob)

    jac_func = ModelingToolkit.generate_jacobian(de, expression=Val{false})[2]
    return jacobian(ode=ode, jac_func=jac_func, u=u, p=p, t=t)
end

"""Use DEmeta for jacobian generation"""
function jacobian(de::T; t= nothing) where T<:DEmeta
    return jacobian(de.func, de.u0, de.p; t=t)
end

"""
Calculate the jacobian of given initial valuables and parameters.
"""
function (self::jacobian)(u,p;t=nothing)
    l = length(u)
    j = zeros(l,l)
    self.jac_func(j, u, p, t)
    return j
end

"""
Get jacobian from state variable with default paramter set
"""
function (self::jacobian)(u;t=nothing)
    return self(u, self.p; t=t)
end

"""
Get jacobian from default state.
"""
function (self::jacobian)()
    return self(self.u, self.p; t=self.t)
end

function StabilityType(jac_matrix)
    es = eigvals(jac_matrix)

    return StabilityType(
    stable = is_stable(es),
    unstable = is_unstable(es),
    saddle = is_saddle(es),
    damping = is_damping(es)
    )
end

"""
Stability identification
"""
function is_stable(es) :: Bool
    method(e) = real(e) >= 0. ? true : false
    return judge_jac(es, method, true)
end

"""
Unstability identification
"""
function is_unstable(es) :: Bool
    method(e) = real(e) >= 0. ? true : false
    return judge_jac(es, method, false)
end

"""
Damping identification
"""
function is_damping(es) :: Bool
   method(e) = imag(e) != 0. ? true : false
   return judge_jac(es, method, false)
end

"""
Saddle point identification
"""
function is_saddle(es) :: Bool
    real_es = real.(es)
    if (maximum(real_es) > 0. ) & (minimum(real_es) < 0.)
        saddle = true
    else
        saddle = false
    end
    return saddle
end

"""
General method for judging jacobian
"""
function judge_jac(es, eigval_method, default_bool)
    feature = default_bool
    for e in es
        if eigval_method(e)
            feature = !feature
            break
        end
    end
    return feature
end


# Estimate polynomial linear regression model with a half-T prior.

using DynamicHMCModels

ProjDir = @__DIR__
cd(ProjDir)

# Import the dataset.

data = CSV.read(joinpath("..", "..", "data", "Howell1.csv"), DataFrame)

# Use only adults and standardize

df = filter(row -> row[:age] >= 18, data);
df[!, :weight] = convert(Vector{Float64}, df[:, :weight]);
df[!, :weight_s] = (df[:, :weight] .- mean(df[:, :weight])) / std(df[:, :weight]);
df[!, :weight_s2] = df[:, :weight_s] .^ 2;

# Define a structure to hold the data: observables, covariates,
# and the degrees of freedom for the prior.

"""
Linear regression model ``y ∼ Xβ + ϵ``, where ``ϵ ∼ N(0, σ²)`` IID.
Flat prior for `β`, half-T for `σ`.
"""
Base.@kwdef mutable struct LinearRegressionModel{Ty <: AbstractVector, Tx <: AbstractMatrix,
Tv <: Real}
    "Observations."
    y::Ty
    "Covariates"
    x::Tx
    "Degrees of freedom for prior."
    v::Tv
end

# Write a function to return a properly dimensioned transformation.

function make_transformation(model::LinearRegressionModel)
  as((β = as(Array, size(model.x, 2)), σ = asℝ₊))
end

N = size(df, 1)
x = hcat(ones(N), hcat(df[:, :weight_s], df[:, :weight_s2]));
model = LinearRegressionModel(;y = df[:, :height], x=x, v=1.0)

# Pack parameters *as a single argument*.

function (model::LinearRegressionModel)(θ)
    @unpack y, x, v = model   # extract data
    @unpack β, σ = θ                 # extract parameters
    loglikelihood(Normal(0, σ), y .- x*β) + logpdf(TDist(v), σ)
end

# Evaluate at model function at some initial valuues

println()
model((β = [1.0, 2.0, 3.0], σ = 1.0)) |> display
println()

# Wrap the problem with a transformation, then use Flux for the gradient.

P = TransformedLogDensity(make_transformation(model), model)
∇P = ADgradient(:ForwardDiff, P);

# Tune and sample.

results = mcmc_with_warmup(Random.GLOBAL_RNG, ∇P, 1000)
posterior = P.transformation.(results.chain)

p = as_particles(posterior)
display(p)

stan_result = "
Iterations = 1:1000
Thinning interval = 1
Chains = 1,2,3,4
Samples per chain = 1000

Empirical Posterior Estimates:
           Mean         SD       Naive SE       MCSE      ESS
    a 154.609019750 0.36158389 0.0057171433 0.0071845548 1000
   b1   5.838431778 0.27920926 0.0044146860 0.0048693502 1000
   b2  -0.009985954 0.22897191 0.0036203637 0.0047224478 1000
sigma   5.110136300 0.19096315 0.0030193925 0.0030728192 1000

Quantiles:
          2.5%        25.0%        50.0%       75.0%        97.5%
    a 153.92392500 154.3567500 154.60700000 154.8502500 155.32100000
   b1   5.27846200   5.6493250   5.83991000   6.0276275   6.39728200
   b2  -0.45954687  -0.1668285  -0.01382935   0.1423620   0.43600905
sigma   4.76114350   4.9816850   5.10326000   5.2300450   5.51500975
";

# end of m4.5d.jl


using NLsolve, StaticArrays

function f!(F, x)
    F[1] = (x[1]+3)*(x[2]^3-7)+18
    F[2] = sin(x[2]*exp(x[1])-1)
end

function j!(J, x)
    J[1, 1] = x[2]^3-7
    J[1, 2] = 3*x[2]^2*(x[1]+3)
    u = exp(x[1])*cos(x[2]*exp(x[1])-1)
    J[2, 1] = x[2]*u
    J[2, 2] = u
end

nlsolve(f!, j!, @MArray [ 0.1; 1.2], )


using LinearAlgebra: I, nullspace
using UniqueVectors: UniqueVector
using StaticArrays: MMatrix

"""
    parametrize_subspace(M::AbstractMatrix{<:Real}, v::AbstractVector{<:Real})

`M` is a `K-n × K` full rank matrix and `v` is a `K-n` length vector. The equation `Mx = v`
describes an `n`-dimensional affine subspace. Compute `a` and `b` so that this space is
parametrized as `x = a + b * x[1:n]`. The columns of `b` are a basis for the null space of
`M` and `a` is a solution of `Mx=v`.
"""
function parametrize_subspace(M::AbstractMatrix{<:Real}, v::AbstractVector{<:Real})
    n = size(M, 2) - size(M, 1)
    @assert n >= 0
    b = nullspace(M)
    N = inv([transpose(b); M]) # this is square because M is full rank
    a = N[:, (n+1):end] * v
    return a, b
end

"""
    intersect_linear_quadratic(A::AbstractMatrix{<:Real},
    B::AbstractVector{<:Real}, C::Real, a::AbstractVector{<:Real},
    b::AbstractVector{<:Real})

Intersect the line `x = a + b * t` with the quadratic `xᵀAx + Bᵀx + C = 0` and return the
two intersection points.
"""
function intersect_linear_quadratic(A::AbstractMatrix{<:Real},
    B::AbstractVector{<:Real}, C::Real, a::AbstractVector{<:Real},
    b::AbstractVector{<:Real})
    quadratic = transpose(b) * A * b
    linear = transpose(a) * A * b + transpose(b) * A * a + transpose(B) * b
    constant = transpose(a) * A * a + transpose(B) * a + C
    t1, t2 = solve_quadratic(quadratic, linear, constant)
    return a + b * t1, a + b * t2
end

"""
    solve_quadratic(a::Real, b::Real, c::Real)

Find the two solutions of `ax² + bx + c = 0`.
"""
function solve_quadratic(a::Real, b::Real, c::Real)
    t1 = -b/(2 * a)
    t2 = sqrt(b^2 - 4 * a * c)/(2 * a)
    return t1 + t2, t1 - t2
end

"""
    sign_of_permutation(a1::AbstractVector{T}, a2::AbstractVector{T}) where T

Given two arrays which are permutations of each other, find the sign of the permutation.
"""
function sign_of_permutation(a1::AbstractVector{T}, a2::AbstractVector{T}) where T
    n = length(a1)
    @assert length(a2) == n
    inds = [findfirst(isequal(x), a2) for x in a1]
    m = MMatrix{n,n,Int}(zeros(n,n)) # det is faster for small matrices using StaticArrays
    for i in 1:n
        m[i, inds[i]] = 1
    end
    return sign(det(m))
end

"""
    first_setdiff_index(l1, l2)

Given two collections `l1` and `l2` find the index of the first element of `l1` that is
not in `l2`. If no such element exists return `0`.
"""
function first_setdiff_index(l1, l2)
    for (i, e) in enumerate(l1)
        if !(e in l2)
            return i
        end
    end
    return 0
end

"""
    first_setdiff(l1, l2)

Given two collections `l1` and `l2` find the first element of `l1` that is not in `l2`.
If no such element exists return `nothing`.
"""
function first_setdiff(l1, l2)
    i = first_setdiff_index(l1, l2)
    return i > 0 ? l1[i] : nothing
end

"""
    triangulated_lattice(v1::Vector{<:Real}, v2::Vector{<:Real}, n::Int, m::Int)

Subdivide a parallelogram with sides `v1` and `v2` into `n*m` similar parallelograms.
Divide each in two pieces to find a triangulation. Return the points and a
TriangulatedComplex.
"""
function triangulated_lattice(v1::Vector{<:Real}, v2::Vector{<:Real}, n::Int, m::Int)
    @assert length(v1) == length(v2) == 2
    inds = UniqueVector([(i, j) for i in 1:(n+1) for j in 1:(m+1)])
    points = [Point(v1 * (i-1)/n + v2 * (j-1)/m) for (i, j) in inds]
    simplices = Simplex{2, 3}[]
    for i in 1:n
        for j in 1:m
            p11 = points[findfirst(isequal((i,j)), inds)]
            p12 = points[findfirst(isequal((i,j+1)), inds)]
            p21 = points[findfirst(isequal((i+1,j)), inds)]
            p22 = points[findfirst(isequal((i+1,j+1)), inds)]
            push!(simplices, Simplex([p11, p12, p21]))
            push!(simplices, Simplex([p12, p22, p21]))
        end
    end
    return points, TriangulatedComplex(simplices)
end


# calculate probability by bayes theorem

module ProbsBayes
    using DataFrames, CSV, FreqTables

    function main()
        data_path = joinpath(split(@__FILE__, "src")[1], "data/sensor_data_600.txt")
        df_org = CSV.read(data_path, DataFrame,
                      header=["date", "time", "ir", "lidar"],
                      delim=' ')

        hours = [Int64(floor(e/10000)) for e in df_org.time]

        # add hour array to df
        df_h = DataFrame(hour=hours)
        df_new = hcat(df_org, df_h)

        # P(t)
        freqs = freqtable(df_new, :lidar, :hour)
        probs = freqs / length(df_org.lidar)
        keys = names(probs, 2)
        p_t = [sum(probs[begin:end, Name(key)]) for key in keys]
        println("P(t) = $(p_t)")

        # P(z)
        probs_trans = transpose(probs)
        keys_trans = names(probs_trans, 2)
        p_z = [sum(probs_trans[begin:end, Name(key)]) for key in keys_trans]
        println("P(z) = $(p_z)")

        # P(t|z)
        for (i, key) in enumerate(keys_trans)
            probs_trans[begin:end, Name(key)] /= p_z[i]
        end
        cond_t_z = probs_trans

        p_z_630 = p_z[findfirst(keys_trans .== 630)]
        p_t_13 = p_t[findfirst(keys .== 13)]
        p_t_13_z_630 = cond_t_z[Name(13), Name(630)]

        bayes = p_t_13_z_630 * p_z_630 /p_t_13

        cond_z_t = probs / p_t[findfirst(keys .== 13)]
        answer = cond_z_t[Name(630), Name(13)]

        println("P(z=630) = $(p_z_630)")
        println("P(t=13) = $(p_t_13)")
        println("P(t=13|z=630) = $(p_t_13_z_630)")
        println("Bayes P(z=630|t=13) = $(bayes)")
        println("Answer P(z=630|t=13) = $(answer)")
    end
end


import Base.length
import Base.isempty
import Base.getindex
import Base.setindex!
import Base.firstindex
import Base.lastindex
import Base.sum

# Use a binary tree to describe a categorical distribution
# This allows for O(1) read, O(log n) write and O(log n) generation of a random index
# Optimized for categorical distributions with a large number of possible outcomes

# Should this be a subtype of AbstractVector?
struct CategoricalTree{T<:Real}
    ns::Vector{<:Integer} # number of nodes at each layer of the tree (`ns[end]` should be 1, i.e. the root of the tree
    as::Vector{Vector{T}} # store each layer into a separate array. Each parent node contains the sum of its children
end

function CategoricalTree(a::Vector{T}) where T<:Real
    n = length(a)
    ns::Vector{Int64} = [n]
    while n > 1
        if isodd(n)
            n = Int((n+1)/2)
            push!(ns,n)
        else
            n = Int(n/2)
            push!(ns,n)
        end
    end
    as = [zeros(T,k) for k in ns]
    as[1] = a
    for i in 1:(length(ns)-1)
        for j in 2:2:ns[i]
            as[i+1][Int(j/2)] = as[i][j-1] + as[i][j]
        end
        if isodd(ns[i])
            as[i+1][ns[i+1]] = as[i][ns[i]]
        end
    end
    return CategoricalTree{T}(ns,as)
end

isempty(ct::CategoricalTree{<:Real}) = isempty(ct.as[1])
length(ct::CategoricalTree{<:Real}) = length(ct.as[1])

firstindex(::CategoricalTree) = 1
lastindex(ct::CategoricalTree) = length(ct)

getindex(ct::CategoricalTree{<:Real}, i) = ct.as[1][i]

function setindex!(ct::CategoricalTree{<:Real}, x, i)
    ct.as[1][i] = x
    j = i
    for k in 2:length(ct.ns)
        j = isodd(j) ? Int((j+1)/2) : Int(j/2)
        if isodd(ct.ns[k-1]) && j == ct.ns[k]
                ct.as[k][j] = ct.as[k-1][2*j-1]
        else
            ct.as[k][j] = ct.as[k-1][2*j-1] + ct.as[k-1][2*j]
        end
    end
end

# since we already computed the sum, we might as well make use of it
sum(ct::CategoricalTree{<:Real}) = ct.as[end][1]

"""
    rand_categorical(ct::CategoricalTree{<:Real}, a0)

Pick one index according to a categorical distribution described by `ct`.
"""
function rand_categorical(ct::CategoricalTree{<:Real}, a0 = sum(ct))
    r = rand()*sum(ct)
    i = length(ct.ns) - 1
    j = 1
    while i > 1
        if r <= ct.as[i][j]
            j = 2*j - 1
        else
            r -= ct.as[i][j]
            j = 2*j + 1
        end
        i -= 1
    end
    if r <= ct.as[i][j]
        return j
    else
        return j+1
    end
end


using PyPlot

A = [1.0 2.0
     2.0 1.0]

evals, evecs = eig(A)
evecs =(evecs[:,1], evecs[:,2])

fig, ax = subplots()
for spine in ["left", "bottom"]
    ax[:spines][spine][:set_position]("zero")
end

for spine in ["right", "top"]
    ax[:spines][spine][:set_color]("none")
end
ax[:grid](alpha=0.4)

xmin, xmax = -3, 3
ymin, ymax = -3, 3
ax[:set_xlim](xmin, xmax)
ax[:set_ylim](ymin, ymax)

for v in evecs
    # Plot each eigenvector
    ax[:annotate](" ", xy=v, xytext=[0, 0],
                  arrowprops={"facecolor"=>"blue",
                              "shrink"=>0,
                              "alpha"=>0.6,
                              "width"=>0.5})

    # Plot the image of each eigenvector
    v = A * v
    ax[:annotate](" ", xy=v, xytext=[0, 0],
                  arrowprops={"facecolor"=>"red",
                              "shrink"=>0,
                              "alpha"=>0.6,
                              "width"=>0.5})
end

x = linspace(xmin, xmax, 3)
for v in evecs
    a = v[2] / v[1]
    ax[:plot](x, a .* x, "b-", lw=0.4)
end


#=

Purpose: Use the Schur method to find the bounded solution of the
         discrete Lyapunov equation:

         X = B + A*X*A'.

Inputs:  A (n*n)
         B (n*n) = Symmetric positive semidefinite

Outputs: X (n*n) = Symmetric positive semidefinite solution.

Based on Octave code written by A.S. Hodel.

=#

function dlyap{T<:AbstractFloat}(a::Array{T,2},b::Array{T,2})

  n = size(a,1)
  x = zeros(n,n)
  j = n

  (s,u) = schur(a)
  b = u'b*u

  while j > 0
    j1 = j
    if j == 1
      block = 1
    elseif s[j,j-1] > 2*eps(T)*norm(s)
      block = 2
      j -= 1
    else
      block = 1
    end
    lhs = kron(s[j:j1,j:j1],s) - I # I = eye(block*n)
    rhs = vec(b[:,j:j1])
    if j1 < n
      rhs2 = s*(x[:,(j1+1):n]*s[j:j1,(j1+1):n]')
      rhs += vec(rhs2)
    end
    w = -lhs\rhs
    x[:,j] = w[1:n]
    if block == 2
      x[:,j1] = w[(n+1):block*n]
    end
    j -= 1
  end

  x = u*x*u'

  return x

end


using Plots
using Optim

# Calculate dose-response
# flux reaction:
function reaction(conc,Km,Vmax)
    10 .^Vmax.*conc./(10 .^Km .+ conc)
end

Km = 0
Vmax = 1
conc = collect(0:.01:2)

flux = reaction(conc, Km, Vmax)
plot(conc,flux)

# simulate data with 7% std
conc_sim = [.1,.5,1,2]
flux_sim = reaction(conc_sim,  Km, Vmax)
flux_sim = randn(length(flux_sim))*0.07.*flux_sim+flux_sim
std_sim = flux_sim * .07

scatter!(conc_sim,flux_sim,yerror=std_sim,grid=false,linealpha = 0)

# negative log-likelihood for simulated data
function L(conc,flux,std,p)
    X = reaction(conc,p[1],p[2])
    LL = -sum(-length(X)*log(2π)/2 .- length(X).*log.(abs.(std)) .- (sum((flux .- X).^2) ./ (2std.^2)))
end

# maximum likelihood fit
init = [1.0,1.0]        # start parameters Km,Vmax
optimum = optimize(p -> L(conc_sim,flux_sim,std_sim,p),init,Newton())

### We will continue with the profile likelihood on July, 10th


include("../algoritmos-busqueda/generar-vecino.jl")
include("../learner/euclidean-1-NN.jl")
include("../algoritmos-busqueda/busqueda-local.jl")
include("../utils/funcion-objetivo.jl")

function HeuristicaEnfriamiento(F_w, F_w_nuevo, T)
    return rand() < exp((F_w - F_w_nuevo)/ (T*1.380649*10^(-23)))
end

"""
    Expresón para pasar avanzar en la construcción de la T
"""
macro SiguienteT(ß, T)
    quote
        $(esc(T))  / (1 + $(esc(ß))*$(esc(T)))
    end
end

"""
    Expresión para calcular la beta
"""
macro calculaß(T, T_f, M)
    quote
        ($(esc(T)) - $(esc(T_f)))/($(esc(M))*$(esc(T)) *$(esc(T_f)))
    end
end

"""
function EnfriamientoSimulado(
    evaluaciones_maximas_funcion_objetivo::Int,
    numero_atributos::Int,
    umbral_atributo::Real,
    F,  # fitness
    probabilidad_aceptar_peor = 0.3,
    temperatura_final = 10^(-3),
    maximo_vecinos,
    maximo_vecinos_aceptados
    )::Vector{<:Real}
Devuelve mejor vector de peso encontrando de acorde
al algoritmo de enfriamiento simulado
"""
function EnfriamientoSimulado(
    evaluaciones_maximas_funcion_objetivo::Int,
    numero_atributos::Int,
    umbral_atributo::Real,
    F,  # fitness
    probabilidad_aceptar_peor,
    temperatura_final,
    maximo_vecinos,
    maximo_vecinos_aceptados
    )::Vector{<:Real}
    µ = 0.3
    M = 15000/maximo_vecinos

    # inicializaciones
    w = rand(numero_atributos) # vector de tamaño atributo uniformement inicializado
    # Ponemos a cero atributos menores que umbral atributo
    w = map(x-> (x<umbral_atributo) ? 0 : x , w)
    w_mejor = w
    F_w = F(w)
    F_w_mejor = F_w

    vecinos_aceptados = 1
    evaluaciones = 0
    primera_iteracion = true

    T = µ * F_w / -log(probabilidad_aceptar_peor)
    ß = (T - temperatura_final) / (M * T * temperatura_final)
    while ((evaluaciones < evaluaciones_maximas_funcion_objetivo
        && vecinos_aceptados > 0
        &&
        T <= temperatura_final
    )
        ||
        primera_iteracion
    )
        #reseteamos valores
        vecinos_aceptados = 0
        vecinos_generados = 0
        primera_iteracion = false

        while( vecinos_generados < maximo_vecinos &&
            vecinos_aceptados < maximo_vecinos_aceptados)
            w_n = GenNeighbourhood(w, 0, 1)
            F_w_vecino = F(w_n)
            vecinos_generados += 1
            ß = @calculaß(T, temperatura_final, M)

            if(F_w < F_w_vecino || HeuristicaEnfriamiento(F_w, F_w_vecino, T))
                vecinos_aceptados += 1
                w = w_n
                F_w = F_w_vecino
                if F_w_vecino > F_w_mejor
                    w_mejor = w_n
                    F_w_mejor = F_w_vecino
                end
            end
        end
        evaluaciones += 1
        T = @SiguienteT(ß, T)
    end
    return w_mejor
end

"""
    BL_LearnerOneNN(data::Matrix{<:Real}, labels::Vector{<:Real})
Devuelve un clasificado 1-NN refinado por unos pesos en la distancia euclídea.
Ese vector de pesos se ha obtenido a partir de un algoritmo de búsqueda local
"""
function EnfriamientoSimulado_LearnerOneNN(data::Matrix{<:Real}, labels)
    # Creamos función objetivo
    a = 0.5
    umbral_tasa_reduccion = 0.1
    F = CrearFuncionObjetivo(data, labels, a, umbral_tasa_reduccion )

    numero_atributos = size(data)[2] # los atributos son las columnas
    # Criterios de acorde a los requisitos
    probabilidad_aceptar_peor = 0.3
    temperatura_final = 10^(-3)
    numero_maximo_vecinos_sin_mejora = 20 * numero_atributos
    evaluaciones_maximas_funcion_objetivo = 15000
    maximo_vecinos = 10*numero_atributos
    maximo_vecinos_aceptados = 0.1*maximo_vecinos
    # Buscamos pesos de acorde a parámetros calculados
    w = EnfriamientoSimulado(
        evaluaciones_maximas_funcion_objetivo,
        numero_atributos,
        umbral_tasa_reduccion,
        F,  # fitness
        probabilidad_aceptar_peor,
        temperatura_final,
        maximo_vecinos,
        maximo_vecinos_aceptados
    )

    return WeightedLearnerEuclideanOneNN(w, data, labels), F(w), w
end


# Copyright (c) 2021 Idiap Research Institute, http://www.idiap.ch/
#  Niccolò Antonello <nantonel@idiap.ch>

export LogWeight

"""
`LogWeight(x)`

| Set                                 |     ``\\oplus``      |  ``\\otimes``  | ``\\bar{0}`` | ``\\bar{1}`` |
|:-----------------------------------:|:--------------------:|:--------------:|:------------:|:------------:|
|``\\mathbb{R}\\cup\\{\\pm\\infty\\}``|``\\log(e^{x}+e^{y})``|     ``+``      |``-\\infty``  |   ``0``      |
"""
struct LogWeight{T <: AbstractFloat} <: Semiring
  x::T
end

LogWeight(x::Number) = LogWeight(float(x))

zero(::Type{LogWeight{T}}) where T = LogWeight{T}(T(-Inf))
one(::Type{LogWeight{T}}) where T = LogWeight{T}(zero(T))

*(a::LogWeight{T}, b::LogWeight{T}) where {T <: AbstractFloat} = LogWeight{T}(a.x + b.x)
+(a::LogWeight{T}, b::LogWeight{T}) where {T <: AbstractFloat} = LogWeight{T}(logadd(a.x,b.x))
/(a::LogWeight{T}, b::LogWeight{T}) where {T <: AbstractFloat} = LogWeight{T}(a.x - b.x)
reverse(a::LogWeight) = a
function logadd(y::T, x::T) where {T <: AbstractFloat}
  if isinf(x) return y end
  if isinf(y) return x end
  if x < y
    diff = x-y
    x = y
  else
    diff = y-x
  end
  return x + log1p(exp(diff))
end

# parsing
parse(::Type{S},str) where {T, S <: LogWeight{T}} = S(parse(T,str))

#properties
iscommulative(::Type{W}) where {W <: LogWeight} = true
isleft(::Type{W}) where {W <: LogWeight} = true
isright(::Type{W}) where {W <: LogWeight}= true
isweaklydivisible(::Type{W}) where {W <: LogWeight}= true
iscomplete(::Type{W}) where {W <: LogWeight}= true


"""
    cheby1(n, x)

Compute the Chebyshev polynomial of the first kind of order n at point x.

This family of Chebyshev polynomials are a special case of the Jacobi polynomials
with α, β = -1/2.
"""
function cheby1(n, x)
    return Jacobi.jacobi(n, -0.5, -0.5, x)
end

"""
    cheby2(n, x)

Compute the Chebyshev polynomial of the second kind of order n at point x.

This family of Chebyshev polynomials are a special case of the Jacobi polynomials
with α, β = 1/2.
"""
function cheby2(n, x)
    return Jacobi.jacobi(n, 0.5, 0.5, x)
end


#=
gauss2.jl
=#

#using ImagePhantoms #: Object, Object2d

export Gauss2
export phantom, radon, spectrum, fwhm2spread

"""
    Gauss2 <: AbstractShape2
"""
struct Gauss2 <: AbstractShape2 end

# constructors

"""
    Gauss2(cx, cy, wx, wy=wx, ϕ=0, value::Number=1)
    Gauss2(center::NTuple{2,RealU}, width::NTuple{2,RealU}=(1,1), ϕ::RealU=0, v=1)
    Gauss2([6-vector])
    Gauss2(w, v=1) (isotropic of width `w`)
Construct `Gauss2` object from parameters;
here `width` = FWHM (full-width at half-maximum).

In 1D, the formula is `g(x) = exp(-π ((x - cx) / sx)^2)`
where `sx = fwhm2spread(w) = w * sqrt(π / log(16))`,
which, for `cx=0`,  has 1D FT `G(ν) = sx^2 exp(π (sx νx)^2)`.
"""
function Gauss2(
    cx::RealU,
    cy::RealU,
    wx::RealU,
    wy::RealU = wx,
    ϕ::RealU = 0,
    value::Number = 1,
)
    (cx, cy, wx, wy) = promote(cx, cy, wx, wy)
    Object(Gauss2(), (cx,cy), (wx,wy), ϕ, value)
end

function Gauss2(
    center::NTuple{2,RealU},
    width::NTuple{2,RealU} = (1,1) .* oneunit(center[1]),
    ϕ::RealU = 0,
    value::Number = 1,
)
    Gauss2(center..., width..., ϕ, value)
end

function Gauss2(v::AbstractVector{<:Number})
    length(v) == 6 || throw(ArgumentError("$v wrong length"))
    Gauss2(v...)
end

Gauss2(w::RealU, v::Number = 1) = Gauss2((zero(w),zero(w)), (w,w), 0, v)

# helper

"""
    s = fwhm2spread(w)
Convert FWHM `w` to equivalent Gaussian spread `s` for ``\\exp(-π (x/s)^2)``.
`exp(-π (fwhm/2/s)^2) = 1/2` means
`fwhm/2/s) = sqrt(log(2)/π)`
`2s/fwhm = sqrt(π/log(2))`
`2s = fwhm * sqrt(π/log(2))`
`s = fwhm * sqrt(π / log(16))`
"""
@inline fwhm2spread(w) = w * sqrt(π / log(16))

# methods

"""
    phantom(ob::Object2d{Gauss2})
Returns function of `(x,y)` for making image.
"""
function phantom(ob::Object2d{Gauss2})
    return (x,y) -> ob.value * # trick due to fwhm to spread scaling:
        exp(-π * sum(abs2.(coords(ob, x, y))) / fwhm2spread(1)^2)
end

"""
    radon_gauss2(r, ϕ, cx, cy, wx, wy, θ)
Radon transform at `(r,ϕ)` of 2D Gaussian.
"""
function radon_gauss2(r, ϕ, cx, cy, wx, wy, θ)
    (sx, sy) = fwhm2spread.((wx, wy))
    (sinϕ, cosϕ) = sincos(ϕ)
    r -= cx * cosϕ + cy * sinϕ # Radon translation property
    (sinϕ, cosϕ) = sincos(ϕ - θ) # Radon rotation property
    s = sqrt(abs2(sx * cosϕ) + abs2(sy * sinϕ)) # by Fourier-slice Thm.
    return sx * sy / s * exp(-π * abs2(r / s))
end

"""
    radon(ob::Object2d{Gauss2})
Returns function of `(r,ϕ)` for making a sinogram.
"""
radon(ob::Object2d{Gauss2}) = (r,ϕ) -> ob.value *
    radon_gauss2(r, ϕ, ob.center..., ob.width..., ob.angle[1])

function spectrum_gauss2(fx, fy, cx, cy, wx, wy, θ)
    (sx, sy) = fwhm2spread.((wx, wy))
    (kx, ky) = rotate2d(fx, fy, θ) # rotate first, then translate
    return sx * exp(-2im*π*fx*cx) *
           sy * exp(-2im*π*fy*cy) * exp(-π * (abs2(sx*kx) + abs2(sy*ky)))
end

"""
    spectrum(ob::Object2d{Gauss2})
Returns function of ``(f_x,f_y)`` for the spectrum (2D Fourier transform).
"""
spectrum(ob::Object2d{Gauss2}) = (fx,fy) -> ob.value *
    spectrum_gauss2(fx, fy, ob.center..., ob.width..., ob.angle[1])


export cwt

"""
    cwt(signal, Λ)

Calculate the continuous wavelet transform of a signal given filter bank Λ.

# Arguments
 - `signal::Vector{Float64}`: Time series.
 - `Λ::Array{Float64, 2}`: Fourier transformed filters arranged by [frequency, scale].
"""
function cwt(signal::Vector{Float64}, Λ::Array{Float64,2})
    signal_ft = fft(signal, 1)
    W = ifft(signal_ft.*Λ, 1)
    return W
end

"""
    cwt(signal, Λ, ω0, δt, δj)

Convenience method for cwt that constructs the filter bank Λ given a signal.

# Arguments
 - `signal::Vector{Float64}`: Time series.
 - `F::FilterBank1dBlock`: Fourier transformed filters and metadata
"""
function cwt(signal::Vector{Float64}, F::FilterBank1dBlock)
    W = cwt(signal, F.Λ)
    return W
end


@doc raw"""
    rating_constraint!(canonical::Canonical,
                            rating_data::Vector{Tuple{String, Float64}},
                            cons_name::Symbol,
                            var_names::Tuple{Symbol, Symbol})

Constructs constraint from rating data and related variable tuple.

# Constraint

``` var1[r[1], t]^2 + var2[r[1], t]^2 <= r[2]^2 ```

where r in rating data and t in time steps.

# LaTeX

`` x_1^2 + x_2^2 \leq r^2 ``

# Arguments
* canonical::Canonical : the canonical model built in PowerSimulations
* rating_data::Vector{Tuple{String, Float64}} : rating data name (1) and value (2)
* cons_name::Symbol : name of the constraint
* var_names::Tuple{Symbol, Symbol} : the names of the variables
- : var_names[1] : var1
- : var_names[2] : var2
"""
function rating_constraint!(canonical::Canonical,
                            rating_data::Vector{Tuple{String, Float64}},
                            cons_name::Symbol,
                            var_names::Tuple{Symbol, Symbol})
    time_steps = model_time_steps(canonical)
    var1 = get_variable(canonical, var_names[1])
    var2 = get_variable(canonical, var_names[2])
    add_cons_container!(canonical, cons_name, (r[1] for r in rating_data), time_steps)
    constraint = get_constraint(canonical, cons_name)

    for r in rating_data
        for t in time_steps
          constraint[r[1], t] = JuMP.@constraint(canonical.JuMPmodel, var1[r[1], t]^2 + var2[r[1], t]^2 <= r[2]^2)
        end
    end

    return

end


# Hessian Eigenmaps (HLLE)
# ---------------------------
# Hessian eigenmaps: Locally linear embedding techniques for high-dimensional data,
# D. Donoho and C. Grimes, Proc Natl Acad Sci U S A. 2003 May 13; 100(10): 5591–5596

import Combinatorics: combinations

#### HLLE type
struct HLLE{T <: AbstractFloat} <: SpectralResult
    k::Int
    λ::AbstractVector{T}
    proj::Projection{T}

    HLLE{T}(k::Int, λ::AbstractVector{T}, proj::Projection{T}) where T = new(k, λ, proj)
end

## properties
outdim(M::HLLE) = size(M.proj, 1)
projection(M::HLLE) = M.proj

eigvals(M::HLLE) = M.λ
neighbors(M::HLLE) = M.k

## show & dump
function show(io::IO, M::HLLE)
    print(io, "Hessian Eigenmaps(outdim = $(outdim(M)), neighbors = $(neighbors(M)))")
end

function dump(io::IO, M::HLLE)
    show(io, M)
    println(io, "eigenvalues: ")
    Base.showarray(io, transpose(M.λ), header=false, repr=false)
    println(io)
    println(io, "projection:")
    Base.showarray(io, M.proj, header=false, repr=false)
end

## interface functions
function transform(::Type{HLLE}, X::DenseMatrix{T}; d::Int=2, k::Int=12) where T<:AbstractFloat
    n = size(X, 2)

    # Identify neighbors
    D, I = find_nn(X, k)

    # Obtain tangent coordinates and develop Hessian estimator
    hs = round(Int, d*(d+1)/2)
    W = spzeros(hs*n,n)
    for i=1:n
        # re-center points in neighborhood
        μ = mean(X[:,I[:,i]], dims=2)
        N = X[:,I[:,i]] .- μ
        # calculate tangent coordinates
        #tc = svd(transpose(N)).U[:,1:d]
        tc = svd(N).V[:,1:d]

        # Develop Hessian estimator
        Yi = [ones(k) tc zeros(k,hs)]
        for ii=1:d
            Yi[:,d+ii+1] = tc[:,ii].^2
        end
        yi = 2(1+d)
        for (ii,jj) in combinations(1:d,2)
            Yi[:, yi] = tc[:, ii] .* tc[:, jj]
            yi += 1
        end
        F = qr(Yi)
        H = transpose(Matrix(F.Q)[:,d+2:end])
        W[(i-1)*hs .+ (1:hs),I[:,i]] = H
    end

    # decomposition
    λ, V = decompose(transpose(W)*W, d)
    return HLLE{T}(k, λ, transpose(V) .* sqrt(n))
end