using MatrixFactorizations, LinearAlgebra, Random, ArrayLayouts, Test
using LinearAlgebra: BlasComplex, BlasFloat, BlasReal, rmul!, lmul!, require_one_based_indexing, checksquare

n = 10

# Split n into 2 parts for tests needing two matrices
n1 = div(n, 2)
n2 = 2*n1

Random.seed!(1234321)

areal = randn(n,n)/2
aimg  = randn(n,n)/2
a2real = randn(n,n)/2
a2img  = randn(n,n)/2
breal = randn(n,2)/2
bimg  = randn(n,2)/2

using MatrixFactorizations: RQPackedQ

using MatrixFactorizations: rq, rq!
const Our=MatrixFactorizations

@testset "RQ" begin
    @testset "LAPACK $elty" for elty in (Float32,Float64,ComplexF32,ComplexF64)
        @testset "Compare with LAPACK (square $elty)" begin
            n = 10
            A = randn(n,n)
            R, Q = @inferred rq(A)
            Ṟ, Q̄ = Our._rqfactUnblocked!(copy(A))
            fla,τla = LinearAlgebra.LAPACK.gerqf!(copy(A))
            @test Q.factors ≈ fla
            @test Q.τ ≈ τla
            @test Q.factors ≈ Q̄.factors
            @test Q.τ ≈ Q̄.τ
            @test R ≈ Ṟ
            @test Q ≈ Q̄
        end
        @testset "Compare with LAPACK (rectangular $elty)" begin
            n = 10
            A = randn(n,n+2)
            R, Q = rq(A)
            Ṟ, Q̄ = Our._rqfactUnblocked!(copy(A))
            fla,τla = LinearAlgebra.LAPACK.gerqf!(copy(A))
            @test Q.factors ≈ fla
            @test Q.τ ≈ τla
            @test Q.factors ≈ Q̄.factors
            @test Q.τ ≈ Q̄.τ
            @test R ≈ Ṟ
            @test Q ≈ Q̄

            A = randn(n+2,n)
            R, Q = rq(A)
            Ṟ, Q̄ = Our._rqfactUnblocked!(copy(A))
            fla,τla = LinearAlgebra.LAPACK.gerqf!(copy(A))
            @test Q.factors ≈ fla
            @test Q.τ ≈ τla
            @test Q.factors ≈ Q̄.factors
            @test Q.τ ≈ Q̄.τ
            @test R ≈ Ṟ
            @test Q ≈ Q̄
        end
    end
    @testset for eltya in (Float32, Float64, ComplexF32, ComplexF64, BigFloat, Complex{BigFloat}, Int)
    # @testset for eltya in (Float32,ComplexF32)
        raw_a = eltya == Int ? rand(1:7, n, n) : convert(Matrix{eltya}, eltya <: Complex ? complex.(areal, aimg) : areal)
        raw_a2 = eltya == Int ? rand(1:7, n, n) : convert(Matrix{eltya}, eltya <: Complex ? complex.(a2real, a2img) : a2real)
        asym = raw_a' + raw_a                  # symmetric indefinite
        apd  = raw_a' * raw_a                 # symmetric positive-definite
        ε = εa = eps(abs(float(one(eltya))))

        @testset for eltyb in (Float32, Float64, ComplexF32, ComplexF64, Int)
        # @info "stubbed eltyb loop"
        # @testset for eltyb in (eltya, )
            raw_b = eltyb == Int ? rand(1:5, n, 2) : convert(Matrix{eltyb}, eltyb <: Complex ? complex.(breal, bimg) : breal)
            εb = eps(abs(float(one(eltyb))))
            ε = max(εa, εb)
            tab = promote_type(eltya, eltyb)

            @testset "RQ decomposition of a Number" begin
                α = rand(eltyb)
                aα = fill(α, 1, 1)
                @test rq(α).R * rq(α).Q ≈ rq(aα).R * rq(aα).Q
                @test abs(rq(α).Q[1,1]) ≈ one(eltyb)
            end

            for (a, b) in ((raw_a, raw_b),
                (view(raw_a, 1:n-1, 1:n-1), view(raw_b, 1:n-1, 1)))
                a_1 = size(a, 1)
                @testset "RQ decomposition" begin
                    rqa   = @inferred rq(a)
                    @inferred rq(a)
                    q, r  = rqa.Q, rqa.R
                    @test_throws ErrorException rqa.Z
                    @test q'*q ≈ Matrix(I, a_1, a_1)
                    @test q*q' ≈ Matrix(I, a_1, a_1)
                    @test q'*Matrix(1.0I, a_1, a_1)' ≈ q'
                    @test q'q ≈ Matrix(I, a_1, a_1)
                    @test Matrix(1.0I, a_1, a_1)'q' ≈ q'
                    @test r*q ≈ a
                    @test a*(rqa\b) ≈ b atol=5000ε
                    @test Array(rqa) ≈ a
                    sq = size(q.factors, 2)
                    @test *(Matrix{eltyb}(I, sq, sq), adjoint(q)) * q ≈ Matrix(I, sq, sq) atol=5000ε
                    if eltya != Int
                        @test Matrix{eltyb}(I, a_1, a_1)*q ≈ convert(AbstractMatrix{tab}, q)
                        ac = copy(a)
                        # would need rectangular ldiv! method
                        @test_throws DimensionMismatch rq!(a[:, 1:5])\b == rq!(view(ac, :, 1:5))\b
                    end
                    rqstring = sprint((t, s) -> show(t, "text/plain", s), rqa)
                    rstring  = sprint((t, s) -> show(t, "text/plain", s), r)
                    qstring  = sprint((t, s) -> show(t, "text/plain", s), q)
                    @test rqstring == "$(summary(rqa))\nR factor:\n$rstring\nQ factor:\n$qstring"
                end
            end
            if eltya != Int
                @testset "Matmul with RQ factorizations" begin
                    a = raw_a

                    rqa = rq(a[:,1:n1])
                    q, r = rqa.Q, rqa.R
                    @test rmul!(copy(q'), q) ≈ Matrix(I, n1, n1)
                    @test_throws DimensionMismatch rmul!(Matrix{eltya}(I, n1+1, n1+1),q)
                    @test rmul!(copy(q), adjoint(q)) ≈ Matrix(I, n1, n1)
                    @test_throws DimensionMismatch rmul!(Matrix{eltya}(I, n1+1, n1+1),adjoint(q))
                    @test_throws ErrorException size(q,-1)
                    @test_throws DimensionMismatch q * Matrix{Int8}(I, n+4, n+4)
                end
            end
        end
        @testset "Wide RQ" begin
            m = n-2
            A = raw_a[1:m,1:n]
            R,Q = rq(A)
            @test Q'*Q ≈ Matrix(I, n, n)
            @test istriu(R)
            @test hcat(zeros(m,n-m),R)*Q ≈ A
            # test the perverse padded product
            @test R*Q ≈ A
            Qm = Matrix(Q)
            @test Qm' * Qm ≈ Matrix(I, n, n)
            @test hcat(zeros(m,n-m),R)*Qm ≈ A
        end
        @testset "Tall RQ" begin
            p = n-2
            A = raw_a[1:n,1:p]
            R,Q = rq(A)
            @test Q'*Q ≈ Matrix(I, p, p)
            @test istriu(R,p-n)
            @test R*Q ≈ A
            Qm = Matrix(Q)
            @test Qm' * Qm ≈ Matrix(I, p, p)
            @test R*Qm ≈ A
        end
    end

    @testset "transpose errors" begin
        @test_throws MethodError transpose(rq(randn(3,3)))
        @test_throws MethodError adjoint(rq(randn(3,3)))
        @test_throws MethodError transpose(rq(big.(randn(3,3))))
        @test_throws MethodError adjoint(rq(big.(randn(3,3))))
    end

    @testset "Issue 7304" begin
        A = [-√.5 -√.5; -√.5 √.5]
        Q = rq(A).Q
        @test norm(A+Q) < eps()
    end

    @testset "rq on AbstractVector" begin
        vl = [3.0, 4.0]
        for Tl in (Float32, Float64)
            for T in (Tl, Complex{Tl})
                v = convert(Vector{T}, vl)
                nv, nm = rq(v)
                @test nv*nm ≈ v
            end
        end
    end

    @testset "Issue 24589. Promotion of rational matrices" begin
        A = rand(1//1:5//5, 4,3)
        @test first(rq(A)) == first(rq(float(A)))
    end

    # omit "Issue Test Factorization fallbacks for rectangular problems"

    @testset "lmul!/rmul! $elty" for elty in (:real, :cplx)
        s = elty == :real ? 0.0 : 0.25im
        A = randn(100,100) .+ s
        R,Q = rq(A)
        x = randn(100) .+ s
        b = randn(100,2) .+ s
        @test lmul!(Q, copy(x)) ≈ Matrix(Q)*x
        @test lmul!(Q, copy(b)) ≈ Matrix(Q)*b
        @test lmul!(Q', copy(x)) ≈ Matrix(Q)'*x
        @test lmul!(Q', copy(b)) ≈ Matrix(Q)'*b
        c = randn(2,100) .+ s
        @test rmul!(copy(c), Q) ≈ c*Matrix(Q)
        @test rmul!(copy(c), Q') ≈ c*Matrix(Q')

        A = randn(103,100) .+ s
        R,Q = rq(A)
        x = randn(100) .+ s
        b = randn(100,2) .+ s
        @test lmul!(Q, copy(x)) ≈ Matrix(Q)*x
        @test lmul!(Q, copy(b)) ≈ Matrix(Q)*b
        @test lmul!(Q', copy(x)) ≈ Matrix(Q)'*x
        @test lmul!(Q', copy(b)) ≈ Matrix(Q)'*b
        c = randn(2,100) .+ s
        @test rmul!(copy(c), Q) ≈ c*Matrix(Q)
        @test rmul!(copy(c), Q') ≈ c*Matrix(Q')
    end
end


"""
A module which provides common astrodynamics
calculations.

# Extended help

**Exports**

$(EXPORTS)

**Imports**

$(IMPORTS)
"""
module Calculations

export Circular, Elliptical, Parabolic, Hyperbolic
export conic, keplerian, cartesian
export perifocal, semimajor_axis
export kepler, lambert, lambert_universal, lambert_lancaster_blanchard
export specific_angular_momentum_vector, specific_angular_momentum
export specific_energy, C3, v_infinity, specific_potential_energy
export eccentricity_vector, eccentricity, semi_parameter
export distance, speed, periapsis_radius, apoapsis_radius, period
export true_anomoly, mean_motion, time_since_periapsis
export hohmann, SOI, SOA, normalize, redimension, analyticalhalo

using DocStringExtensions
using Unitful
using Requires
using Contour
using StaticArrays
using AstroTime
using LinearAlgebra

import Dates
import Roots: find_zero

@template (FUNCTIONS, METHODS, MACROS) =
    """
    $(SIGNATURES)
    $(DOCSTRING)
    $(METHODLIST)
    """

@template (TYPES, CONSTANTS) =
    """
    $(TYPEDEF)
    $(DOCSTRING)
    """

Unitful.@derived_dimension MassParameter Unitful.𝐋^3/Unitful.𝐓^2

@doc """
A unit dimension alias for length^3 / time^2. This is a common
dimension used in astrodynamics calculations.
"""
MassParameter

include(joinpath("R2BP",  "Conics.jl"))
include(joinpath("R2BP",  "R2BPCalculations.jl"))
include(joinpath("R2BP",  "Kepler.jl"))
include(joinpath("R2BP",  "Lambert.jl"))
include(joinpath("CR3BP", "CR3BPCalculations.jl"))

end # module


using AbstractSphericalHarmonics
using LinearAlgebra: norm
using Random
using StaticArrays
using Test

const bitsign = AbstractSphericalHarmonics.bitsign

chop(x) = abs2(x) < 100eps(x) ? zero(x) : x
chop(x::Complex) = Complex(chop(real(x)), chop(imag(x)))
chop(x::SArray) = chop.(x)

function integrate(f::AbstractMatrix, g::AbstractMatrix, lmax::Integer)
    sz = ash_grid_size(lmax)
    @assert size(f) == size(g) == sz

    s = zero(eltype(f)) * zero(eltype(g)) * zero(Float64)
    for pt in CartesianIndices(sz)
        theta, phi = ash_point_coord(pt, lmax)
        dtheta, dphi = ash_point_delta(pt, lmax)
        s += conj(f[pt]) * g[pt] * sin(theta) * dtheta * dphi
    end

    return s
end

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

# Half angle formulae:
#     sin(θ/2)^2 = (1-cos(θ))/2
#     cos(θ/2)^2 = (1+cos(θ))/2

# These sYlm are taken from Wikipedia and black-holes.org:
# <https://en.wikipedia.org/wiki/Spherical_harmonics> and
# <https://en.wikipedia.org/wiki/Spin-weighted_spherical_harmonics>
# <https://www.black-holes.org/SpinWeightedSphericalHarmonics.nb>
function sYlm(s::Integer, l::Integer, m::Integer, θ::Real, ϕ::Real)
    @assert abs(s) ≤ l
    @assert -l ≤ m ≤ l

    # Parity:
    s < 0 && return bitsign(s + m) * conj(sYlm(-s, l, -m, θ, ϕ))

    (s, l, m) == (0, 0, 0) && return sqrt(1 / 4π)
    (s, l, m) == (0, 1, -1) && return sqrt(3 / 8π) * sin(θ) * cis(-ϕ)
    (s, l, m) == (0, 1, 0) && return sqrt(3 / 4π) * cos(θ)
    (s, l, m) == (0, 1, +1) && return -sqrt(3 / 8π) * sin(θ) * cis(ϕ)
    (s, l, m) == (0, 2, -2) && return sqrt(15 / 2π) * cos(θ / 2)^2 * sin(θ / 2)^2 * cis(-2ϕ)
    (s, l, m) == (0, 2, -1) && return -sqrt(15 / 2π) * cos(θ / 2) * sin(θ / 2) * (-cos(θ / 2)^2 + sin(θ / 2)^2) * cis(-ϕ)
    (s, l, m) == (0, 2, 0) && return sqrt(5 / 4π) * (cos(θ / 2)^4 - 4 * cos(θ / 2)^2 * sin(θ / 2)^2 + sin(θ / 2)^4)
    (s, l, m) == (0, 2, +1) && return -sqrt(15 / 2π) * cos(θ / 2) * sin(θ / 2) * (cos(θ / 2)^2 - sin(θ / 2)^2) * cis(ϕ)
    (s, l, m) == (0, 2, +2) && return sqrt(15 / 2π) * cos(θ / 2)^2 * sin(θ / 2)^2 * cis(2ϕ)

    (s, l, m) == (+1, 1, -1) && return -sqrt(3 / 16π) * (1 + cos(θ)) * cis(-ϕ)
    (s, l, m) == (+1, 1, 0) && return sqrt(3 / 8π) * sin(θ)
    (s, l, m) == (+1, 1, +1) && return -sqrt(3 / 16π) * (1 - cos(θ)) * cis(ϕ)
    (s, l, m) == (+1, 2, -2) && return -sqrt(5 / π) * cos(θ / 2)^3 * sin(θ / 2) * cis(-2ϕ)
    (s, l, m) == (+1, 2, -1) && return -sqrt(5 / 4π) * cos(θ / 2)^2 * (cos(θ / 2)^2 - 3 * sin(θ / 2)^2) * cis(-ϕ)
    (s, l, m) == (+1, 2, 0) && return sqrt(15 / 2π) * cos(θ / 2) * sin(θ / 2) * (cos(θ / 2)^2 - sin(θ / 2)^2)
    (s, l, m) == (+1, 2, +1) && return sqrt(5 / 4π) * sin(θ / 2)^2 * (-3 * cos(θ / 2)^2 + sin(θ / 2)^2) * cis(ϕ)
    (s, l, m) == (+1, 2, +2) && return sqrt(5 / π) * cos(θ / 2) * sin(θ / 2)^3 * cis(2ϕ)

    (s, l, m) == (+2, 2, -2) && return sqrt(5 / 4π) * cos(θ / 2)^4 * cis(-2ϕ)
    (s, l, m) == (+2, 2, -1) && return -sqrt(5 / π) * cos(θ / 2)^3 * sin(θ / 2) * cis(-ϕ)
    (s, l, m) == (+2, 2, 0) && return sqrt(15 / 2π) * cos(θ / 2)^2 * sin(θ / 2)^2
    (s, l, m) == (+2, 2, +1) && return -sqrt(5 / π) * cos(θ / 2) * sin(θ / 2)^3 * cis(ϕ)
    (s, l, m) == (+2, 2, +2) && return sqrt(5 / 4π) * sin(θ / 2)^4 * cis(2ϕ)

    @assert false
end

# ð F = - (sin θ)^s (∂_θ + i / sin(θ) ∂_ϕ) (sin θ)^-s F
# (Calculated manually from sYlm above)
function ðsYlm(s::Integer, l::Integer, m::Integer, θ::Real, ϕ::Real)
    @assert abs(s) ≤ l
    @assert -l ≤ m ≤ l

    (s, l, m) == (0, 0, 0) && return 0
    (s, l, m) == (0, 1, -1) && return -sqrt(3 / 8π) * (1 + cos(θ)) * cis(-ϕ)
    (s, l, m) == (0, 1, 0) && return sqrt(3 / 4π) * sin(θ)
    (s, l, m) == (0, 1, +1) && return -sqrt(3 / 8π) * (1 - cos(θ)) * cis(ϕ)
    (s, l, m) == (0, 2, -2) && return -sqrt(15 / 8π) * (1 + cos(θ)) * sin(θ) * cis(-2ϕ)
    (s, l, m) == (0, 2, -1) && return -sqrt(15 / 2π) * cos(θ / 2)^2 * (-1 + 2 * cos(θ)) * cis(-ϕ)
    (s, l, m) == (0, 2, 0) && return sqrt(45 / 16π) * sin(2θ)
    (s, l, m) == (0, 2, +1) && return -sqrt(15 / 2π) * (1 + 2 * cos(θ)) * sin(θ / 2)^2 * cis(ϕ)
    (s, l, m) == (0, 2, +2) && return -sqrt(15 / 8π) * (-1 + cos(θ)) * sin(θ) * cis(2ϕ)

    (s, l, m) == (+1, 1, -1) && return 0
    (s, l, m) == (+1, 1, 0) && return 0
    (s, l, m) == (+1, 1, +1) && return 0
    (s, l, m) == (+1, 2, -2) && return sqrt(5 / π) * cos(θ / 2)^4 * cis(-2ϕ)
    (s, l, m) == (+1, 2, -1) && return -sqrt(5 / π) * cos(θ / 2)^2 * sin(θ) * cis(-ϕ)
    (s, l, m) == (+1, 2, 0) && return sqrt(15 / 8π) * sin(θ)^2
    (s, l, m) == (+1, 2, +1) && return -sqrt(5 / π) * sin(θ / 2)^2 * sin(θ) * cis(ϕ)
    (s, l, m) == (+1, 2, +2) && return sqrt(5 / π) * sin(θ / 2)^4 * cis(2ϕ)

    (s, l, m) == (+2, 2, -2) && return 0
    (s, l, m) == (+2, 2, -1) && return 0
    (s, l, m) == (+2, 2, 0) && return 0
    (s, l, m) == (+2, 2, +1) && return 0
    (s, l, m) == (+2, 2, +2) && return 0

    @assert false
end

# ð̄ F = - (sin θ)^-s (∂_θ - i / sin(θ) ∂_ϕ) (sin θ)^s F
# (Calculated manually from sYlm above)
function ð̄sYlm(s::Integer, l::Integer, m::Integer, θ::Real, ϕ::Real)
    @assert abs(s) ≤ l
    @assert -l ≤ m ≤ l

    (s, l, m) == (0, 0, 0) && return 0
    (s, l, m) == (0, 1, -1) && return sqrt(3 / 8π) * (1 - cos(θ)) * cis(-ϕ)
    (s, l, m) == (0, 1, 0) && return sqrt(3 / 4π) * sin(θ)
    (s, l, m) == (0, 1, +1) && return sqrt(3 / 8π) * (1 + cos(θ)) * cis(ϕ)
    (s, l, m) == (0, 1, +1) && return -sqrt(3 / 8π) * (1 - cos(θ)) * cis(ϕ)
    (s, l, m) == (0, 2, -2) && return -sqrt(15 / 8π) * (-1 + cos(θ)) * sin(θ) * cis(-2ϕ)
    (s, l, m) == (0, 2, -1) && return sqrt(15 / 2π) * (1 + 2 * cos(θ)) * sin(θ / 2)^2 * cis(-ϕ)
    (s, l, m) == (0, 2, 0) && return sqrt(45 / 16π) * sin(2θ)
    (s, l, m) == (0, 2, +1) && return sqrt(15 / 2π) * cos(θ / 2)^2 * (-1 + 2 * cos(θ)) * cis(ϕ)
    (s, l, m) == (0, 2, +2) && return -sqrt(15 / 8π) * (1 + cos(θ)) * sin(θ) * cis(2ϕ)

    (s, l, m) == (+1, 1, -1) && return -sqrt(3 / 4π) * sin(θ) * cis(-ϕ)
    (s, l, m) == (+1, 1, 0) && return -sqrt(3 / 2π) * cos(θ)
    (s, l, m) == (+1, 1, +1) && return sqrt(3 / 4π) * sin(θ) * cis(ϕ)
    (s, l, m) == (+1, 2, -2) && return -sqrt(45 / 16π) * sin(θ)^2 * cis(-2ϕ)
    (s, l, m) == (+1, 2, -1) && return -sqrt(45 / 16π) * sin(2θ) * cis(-ϕ)
    (s, l, m) == (+1, 2, 0) && return -sqrt(15 / 32π) * (1 + 3 * cos(2θ))
    (s, l, m) == (+1, 2, +1) && return sqrt(45 / 16π) * sin(2θ) * cis(ϕ)
    (s, l, m) == (+1, 2, +2) && return -sqrt(45 / 16π) * sin(θ)^2 * cis(2ϕ)

    (s, l, m) == (+2, 2, -2) && return sqrt(5 / 4π) * (1 + cos(θ)) * sin(θ) * cis(-2ϕ)
    (s, l, m) == (+2, 2, -1) && return sqrt(5 / 4π) * (cos(θ) + cos(2θ)) * cis(-ϕ)
    (s, l, m) == (+2, 2, 0) && return -sqrt(15 / 2π) * cos(θ) * sin(θ)
    (s, l, m) == (+2, 2, +1) && return sqrt(5 / 4π) * (cos(θ) - cos(2θ)) * cis(ϕ)
    (s, l, m) == (+2, 2, +2) && return -sqrt(5 / π) * sin(θ / 2)^2 * sin(θ) * cis(2ϕ)

    @assert false
end

function rand_tensor(::Val{D}, ::Type{T}, lmax::Int) where {D,T}
    sz = ash_grid_size(lmax)
    Dims = Tuple{[2 for d in 1:D]...}
    f = randn(SArray{Dims,T}, sz)
    return f
end

function const_tensor(::Val{D}, ::Type{T}, lmax::Int) where {D,T}
    sz = ash_grid_size(lmax)
    Dims = Tuple{[2 for d in 1:D]...}
    α = randn(SArray{Dims,T})
    f = fill(α, sz)
    return f
end

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

@testset "Mode indices" begin
    for lmax in 0:20
        nmodes = ash_nmodes(lmax)
        for s in -4:4, l in abs(s):lmax, m in (-l):l
            ind = ash_mode_index(s, l, m, lmax)
            @test length(nmodes) ≡ length(ind)
            @test all(1 ≤ ind[d] ≤ nmodes[d] for d in 1:length(nmodes))
            l′, m′ = ash_mode_numbers(s, ind, lmax)
            @test l′ == l && m′ == m
        end
    end
end

Random.seed!(100)
modes = [
    (name="(s=$s,l=$l,m=$m)", spin=s, el=l, m=m, fun=(θ, ϕ) -> sYlm(s, l, m, θ, ϕ), modes=(l′, m′) -> l′ == l && m′ == m) for
    s in -2:+2 for l in abs(s):2 for m in (-l):l
]
@testset "Simple transforms: $(mode.name)" for mode in modes
    for lmax in (mode.el):20
        sz = ash_grid_size(lmax)
        f = Array{Complex{Float64}}(undef, sz)
        for ij in CartesianIndices(sz)
            θ, ϕ = ash_point_coord(ij, lmax)
            f[ij] = mode.fun(θ, ϕ)
        end
        # function setvalue(ij::CartesianIndex{2})
        #     θ, ϕ = ash_point_coord(ij, lmax)
        #     return Complex{Float64}(mode.fun(θ, ϕ))
        # end
        # f = map(setvalue, CartesianIndices(sz))

        flm = ash_transform(f, mode.spin, lmax)
        @test all(
            isapprox(flm[ash_mode_index(mode.spin, l, m, lmax)], mode.modes(l, m); atol=100eps()) for l in abs(mode.spin):lmax for
            m in (-l):l
        )

        f′ = ash_evaluate(flm, mode.spin, lmax)
        @test isapprox(f′, f; atol=1000eps())
    end
end

Random.seed!(100)
@testset "Parity" begin
    for iter in 1:20
        lmax = rand(0:100)
        sz = ash_grid_size(lmax)
        nmodes = ash_nmodes(lmax)

        s = rand(-4:4)
        abs(s) ≤ lmax || continue
        l = rand(abs(s):lmax)
        m = rand((-l):l)

        flm = zeros(Complex{Float64}, nmodes)
        flm[ash_mode_index(s, l, m, lmax)] = 1

        flm′ = zeros(Complex{Float64}, nmodes)
        flm′[ash_mode_index(-s, l, -m, lmax)] = 1

        f = ash_evaluate(flm, s, lmax)
        f′ = ash_evaluate(flm′, -s, lmax)

        # Phase: conj(sYlm) = (-1)^(s+m) (-s)Yl(-m)
        @test conj(f) ≈ bitsign(s + m) * f′
    end
end

Random.seed!(100)
@testset "Linearity" begin
    for iter in 1:20
        lmax = rand(0:100)
        sz = ash_grid_size(lmax)
        nmodes = ash_nmodes(lmax)

        spin = rand(-4:4)

        f = randn(Complex{Float64}, sz)
        g = randn(Complex{Float64}, sz)
        α = randn(Complex{Float64})
        h = f + α * g

        flm = ash_transform(f, spin, lmax)
        glm = ash_transform(g, spin, lmax)
        hlm = ash_transform(h, spin, lmax)

        if !(flm + α * glm ≈ hlm)
            @show iter lmax sz nmodes spin
            @show α
            @show any(isnan, f) any(isnan, g) any(isnan, h)
            @show any(isnan, flm) any(isnan, glm) any(isnan, hlm)
            @show f[1] g[1] h[1]
            @show flm[1] glm[1] hlm[1]
        end
        @test flm + α * glm ≈ hlm

        flm = randn(Complex{Float64}, nmodes)
        glm = randn(Complex{Float64}, nmodes)
        hlm = flm + α * glm

        f = ash_evaluate(flm, spin, lmax)
        g = ash_evaluate(glm, spin, lmax)
        h = ash_evaluate(hlm, spin, lmax)

        @test f + α * g ≈ h
    end
end

Random.seed!(100)
@testset "Orthonormality" begin
    for iter in 1:20
        lmax = rand(0:100)
        nmodes = ash_nmodes(lmax)

        smax = min(4, lmax ÷ 2)
        spin = rand((-smax):smax)

        flm = zeros(Complex{Float64}, nmodes)
        glm = zeros(Complex{Float64}, nmodes)

        # The produce will have lh = lf + lg
        lf = rand(abs(spin):(lmax ÷ 2))
        mf = rand((-lf):lf)
        lg = rand(abs(spin):(lmax - lf))
        mg = rand((-lg):lg)

        flm[ash_mode_index(spin, lf, mf, lmax)] = 1
        glm[ash_mode_index(spin, lg, mg, lmax)] = 1

        f = ash_evaluate(flm, spin, lmax)
        g = ash_evaluate(glm, spin, lmax)

        @test isapprox(integrate(f, f, lmax), 1; atol=20 / lmax^2)
        @test isapprox(integrate(f, g, lmax), (lf == lg) * (mf == mg); atol=1 / lmax^2)

        h = conj(f) .* f
        hlm = ash_transform(h, 0, lmax)
        @test isapprox(hlm[ash_mode_index(0, 0, 0, lmax)], sqrt(1 / 4π); atol=sqrt(eps()))

        h = conj(f) .* g
        hlm = ash_transform(h, 0, lmax)
        @test isapprox(hlm[ash_mode_index(0, 0, 0, lmax)], (lf == lg) * (mf == mg) * sqrt(1 / 4π); atol=sqrt(eps()))
    end
end

Random.seed!(100)
modes = [
    (
        name="(s=$s,l=$l,m=$m)",
        spin=s,
        el=l,
        fun=(θ, ϕ) -> sYlm(s, l, m, θ, ϕ),
        ðfun=(θ, ϕ) -> ðsYlm(s, l, m, θ, ϕ),
        ð̄fun=(θ, ϕ) -> ð̄sYlm(s, l, m, θ, ϕ),
    ) for s in 0:+2 for l in abs(s):2 for m in (-l):l
]
@testset "Simple derivatives (eth, eth-bar): $(mode.name)" for mode in modes
    for lmax in (mode.el):20
        sz = ash_grid_size(lmax)
        f = Array{Complex{Float64}}(undef, sz)
        ðf₀ = Array{Complex{Float64}}(undef, sz)
        ð̄f₀ = Array{Complex{Float64}}(undef, sz)
        for ij in CartesianIndices(sz)
            θ, ϕ = ash_point_coord(ij, lmax)
            f[ij] = mode.fun(θ, ϕ)
            ðf₀[ij] = mode.ðfun(θ, ϕ)
            ð̄f₀[ij] = mode.ð̄fun(θ, ϕ)
        end

        flm = ash_transform(f, mode.spin, lmax)

        ðflm = ash_eth(flm, mode.spin, lmax)
        ðf = ash_evaluate(ðflm, mode.spin + 1, lmax)
        @test isapprox(ðf, ðf₀; atol=10000eps())

        ð̄flm = ash_ethbar(flm, mode.spin, lmax)
        ð̄f = ash_evaluate(ð̄flm, mode.spin - 1, lmax)
        @test isapprox(ð̄f, ð̄f₀; atol=10000eps())
    end
end

Random.seed!(100)
@testset "Eigenvectors of Laplacian" begin
    for iter in 1:20
        lmax = rand(0:100)
        nmodes = ash_nmodes(lmax)

        flm = zeros(Complex{Float64}, nmodes)

        l = rand(0:lmax)
        m = rand((-l):l)
        flm[ash_mode_index(0, l, m, lmax)] = 1

        ðflm = ash_eth(flm, 0, lmax)
        ð̄ðflm = ash_ethbar(ðflm, +1, lmax)

        ð̄flm = ash_ethbar(flm, 0, lmax)
        ðð̄flm = ash_eth(ð̄flm, -1, lmax)

        f = ash_evaluate(flm, 0, lmax)
        ð̄ðf = ash_evaluate(ð̄ðflm, 0, lmax)
        ðð̄f = ash_evaluate(ðð̄flm, 0, lmax)

        @test isapprox(ð̄ðf, -l * (l + 1) * f; atol=(lmax + 1)^2 * 100eps())
        @test isapprox(ðð̄f, -l * (l + 1) * f; atol=(lmax + 1)^2 * 100eps())
    end
end

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

Random.seed!(100)
@testset "Arbitrary modes" begin
    for iter in 1:20
        lmax = rand(0:100)
        nmodes = ash_nmodes(lmax)

        flm = zeros(Complex{Float64}, nmodes)

        s = rand(-4:4)
        abs(s) ≤ lmax || continue
        l = rand(abs(s):lmax)
        m = rand((-l):l)
        flm[ash_mode_index(s, l, m, lmax)] = 1

        f = ash_evaluate(flm, s, lmax)

        f′ = [
            begin
                θ, ϕ = ash_point_coord(ij, lmax)
                # We need the increased precision of `BigFloat` for `l >≈ 50`
                # AbstractSphericalHarmonics.sYlm(Val(s), Val(l), Val(m), θ, ϕ)
                Complex{Float64}(AbstractSphericalHarmonics.sYlm(Val(s), Val(l), Val(m), big(θ), big(ϕ)))
            end for ij in CartesianIndices(f)
        ]

        @test f ≈ f′
    end
end

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

Random.seed!(100)
@testset "Tensors on the sphere (rank $D)" for D in 0:4
    for iter in 1:20
        lmax = rand(0:100)

        f = rand_tensor(Val(D), Complex{Float64}, lmax)

        t = Tensor{D}(f, lmax)
        st = SpinTensor{D}(t)
        t′ = Tensor{D}(st)
        st′ = SpinTensor{D}(t′)
        t″ = Tensor{D}(st′)

        @test st′ ≈ st
        @test t″ ≈ t′
    end
end

Random.seed!(100)
@testset "Linearity of tensors on the sphere (rank $D)" for D in 0:4
    for iter in 1:20
        lmax = rand(0:100)

        f = rand_tensor(Val(D), Complex{Float64}, lmax)
        g = rand_tensor(Val(D), Complex{Float64}, lmax)
        α = randn(Complex{Float64})

        h = f + α * g

        t = Tensor{D}(f, lmax)
        u = Tensor{D}(g, lmax)
        v = Tensor{D}(h, lmax)

        st = SpinTensor{D}(t)
        su = SpinTensor{D}(u)
        sv = SpinTensor{D}(v)

        @test sv ≈ st + α * su
    end
end

@testset "Simple derivatives of tensors on the sphere" begin
    for lmax in 1:20
        sz = ash_grid_size(lmax)

        # 1

        s = Tensor{0}(fill(Scalar{Complex{Float64}}(1), sz), lmax)
        ds₀ = Tensor{1}(fill(SVector{2,Complex{Float64}}(0, 0), sz), lmax)

        s̃ = SpinTensor(s)
        ds̃ = tensor_gradient(s̃)
        ds = Tensor(ds̃)

        @test isapprox(ds, ds₀; atol=(lmax + 1)^2 * 1000eps())

        # x

        x = Tensor{0}([
            begin
                θ, ϕ = ash_point_coord(ij, lmax)
                Scalar{Complex{Float64}}(sin(θ) * cos(ϕ))
            end for ij in CartesianIndices(sz)
        ], lmax)
        dx₀ = Tensor{1}([
            begin
                θ, ϕ = ash_point_coord(ij, lmax)
                SVector{2,Complex{Float64}}(cos(θ) * cos(ϕ), -sin(ϕ))
            end for ij in CartesianIndices(sz)
        ], lmax)

        x̃ = SpinTensor(x)
        dx̃ = tensor_gradient(x̃)
        dx = Tensor(dx̃)

        @test isapprox(dx, dx₀; atol=(lmax + 1)^2 * 100eps())

        # y

        y = Tensor{0}([
            begin
                θ, ϕ = ash_point_coord(ij, lmax)
                Scalar{Complex{Float64}}(sin(θ) * sin(ϕ))
            end for ij in CartesianIndices(sz)
        ], lmax)
        dy₀ = Tensor{1}([
            begin
                θ, ϕ = ash_point_coord(ij, lmax)
                SVector{2,Complex{Float64}}(cos(θ) * sin(ϕ), cos(ϕ))
            end for ij in CartesianIndices(sz)
        ], lmax)

        ỹ = SpinTensor(y)
        dỹ = tensor_gradient(ỹ)
        dy = Tensor(dỹ)

        @test isapprox(dy, dy₀; atol=(lmax + 1)^2 * 100eps())

        # z

        z = Tensor{0}([
            begin
                θ, ϕ = ash_point_coord(ij, lmax)
                Scalar{Complex{Float64}}(cos(θ))
            end for ij in CartesianIndices(sz)
        ], lmax)
        dz₀ = Tensor{1}([
            begin
                θ, ϕ = ash_point_coord(ij, lmax)
                SVector{2,Complex{Float64}}(-sin(θ), 0)
            end for ij in CartesianIndices(sz)
        ], lmax)

        z̃ = SpinTensor(z)
        dz̃ = tensor_gradient(z̃)
        dz = Tensor(dz̃)

        @test isapprox(dz, dz₀; atol=(lmax + 1)^2 * 100eps())

        if lmax ≥ 2

            # grad x
            #
            # q_θθ = 1
            # q_ϕϕ = sin(ϕ)^2
            #
            # Γ^θ_ϕϕ = - cos(θ) sin(θ)
            # Γ^ϕ_θϕ = cos(θ) / sin(θ)
            # Γ^ϕ_ϕθ = cos(θ) / sin(θ)
            #
            # x = sin(θ) cos(ϕ)
            # y = sin(θ) sin(ϕ)
            # z = cos(θ)
            #
            # ∇x_θ = cos(θ) cos(ϕ)
            # ∇x_ϕ = - sin(θ) sin(ϕ)
            #
            # ∇∇x_θθ = - sin(θ) cos(ϕ)
            # ∇∇x_ϕϕ = - sin(θ)^3 cos(ϕ)

            gradx = Tensor{1}([
                begin
                    θ, ϕ = ash_point_coord(ij, lmax)
                    SVector{2,Complex{Float64}}(cos(θ) * cos(ϕ), -sin(ϕ))
                end for ij in CartesianIndices(sz)
            ], lmax)
            dgradx₀ = Tensor{2}(
                [
                    begin
                        θ, ϕ = ash_point_coord(ij, lmax)
                        SMatrix{2,2,Complex{Float64}}(-sin(θ) * cos(ϕ), 0, 0, -sin(θ) * cos(ϕ))
                    end for ij in CartesianIndices(sz)
                ], lmax
            )

            gradx̃ = SpinTensor(gradx)
            dgradx̃ = tensor_gradient(gradx̃)
            dgradx = Tensor(dgradx̃)

            @test isapprox(dgradx, dgradx₀; atol=(lmax + 1)^2 * 100eps())

            ddgradx₀ = Tensor{3}(
                [
                    begin
                        θ, ϕ = ash_point_coord(ij, lmax)
                        SArray{Tuple{2,2,2},Complex{Float64}}(-cos(θ) * cos(ϕ), 0, 0, -cos(θ) * cos(ϕ), sin(ϕ), 0, 0, sin(ϕ))
                    end for ij in CartesianIndices(sz)
                ],
                lmax,
            )

            ddgradx̃ = tensor_gradient(dgradx̃)
            ddgradx = Tensor(ddgradx̃)

            @test isapprox(ddgradx, ddgradx₀; atol=(lmax + 1)^2 * 1000eps())

            # curl x
            #
            # curl x_θ = sin(ϕ)
            # curl x_ϕ = cos(θ) sin(θ) cos(ϕ)
            #
            # ∇ curl x_θθ = - sin(θ) cos(ϕ)
            # ∇ curl x_ϕϕ = - sin(θ)^3 cos(ϕ)

            curlx = Tensor{1}([
                begin
                    θ, ϕ = ash_point_coord(ij, lmax)
                    SVector{2,Complex{Float64}}(sin(ϕ), cos(θ) * cos(ϕ))
                end for ij in CartesianIndices(sz)
            ], lmax)
            dcurlx₀ = Tensor{2}(
                [
                    begin
                        θ, ϕ = ash_point_coord(ij, lmax)
                        SMatrix{2,2,Complex{Float64}}(0, -sin(θ) * cos(ϕ), sin(θ) * cos(ϕ), 0)
                    end for ij in CartesianIndices(sz)
                ], lmax
            )

            curlx̃ = SpinTensor(curlx)
            dcurlx̃ = tensor_gradient(curlx̃)
            dcurlx = Tensor(dcurlx̃)

            @test isapprox(dcurlx, dcurlx₀; atol=(lmax + 1)^2 * 100eps())
        end
    end
end

Random.seed!(100)
@testset "Derivatives of tensors on the sphere (rank $D)" for D in 0:3
    for iter in 1:20
        lmax = rand(0:100)

        f = rand_tensor(Val(D), Complex{Float64}, lmax)
        g = rand_tensor(Val(D), Complex{Float64}, lmax)
        α = randn(Complex{Float64})

        # Linear combination
        s = f + α * g

        tf = Tensor{D}(f, lmax)
        tg = Tensor{D}(g, lmax)
        ts = Tensor{D}(s, lmax)

        stf = SpinTensor{D}(tf)
        stg = SpinTensor{D}(tg)
        sts = SpinTensor{D}(ts)

        dstf = tensor_gradient(stf)
        dstg = tensor_gradient(stg)
        dsts = tensor_gradient(sts)

        dtf = Tensor{D + 1}(dstf)
        dtg = Tensor{D + 1}(dstg)
        dts = Tensor{D + 1}(dsts)

        @test dts ≈ dtf + α * dtg

        dstf′ = zero(dstf)
        dstg′ = zero(dstg)
        dsts′ = zero(dsts)
        tensor_gradient!(dstf′, stf)
        tensor_gradient!(dstg′, stg)
        tensor_gradient!(dsts′, sts)
        @test dstf′ ≈ dstf
        @test dstg′ ≈ dstg
        @test dsts′ ≈ dsts

        if D == 0
            # Constant function (derivative is zero)
            c = const_tensor(Val(D), Complex{Float64}, lmax)
            # Product of two functions
            p = map(.*, f, g)

            tc = Tensor{D}(c, lmax)
            tp = Tensor{D}(p, lmax)

            stc = SpinTensor{D}(tc)
            stp = SpinTensor{D}(tp)

            dstc = tensor_gradient(stc)
            dstp = tensor_gradient(stp)

            dtc = Tensor{D + 1}(dstc)
            dtp = Tensor{D + 1}(dstp)

            dtc′ = Tensor{D + 1}(zero(dtc.values), lmax)
            @test isapprox(dtc, dtc′; atol=sqrt(eps()))
            dtp′ = Tensor{D + 1}(
                map((dx, x) -> dx * x[], dtf.values, tg.values) + map((x, dx) -> x[] * dx, tf.values, dtg.values), lmax
            )
            # TODO: This needs smooth input, not noise
            # if !isapprox(dtp, dtp′; atol=1 / lmax^2)
            #     @show D iter lmax
            #     @show maximum(map(x -> maximum(abs.(x)), dtp.values - dtp′.values))
            # end
            @test_skip isapprox(dtp, dtp′; atol=1 / lmax^2)
        end
    end
end

Random.seed!(100)
@testset "Filtering tensors on the sphere D=$D" for D in 0:4
    for lmax in 0:20
        sz = ash_grid_size(lmax)
        t = Tensor{D}(rand_tensor(Val(D), Complex{Float64}, lmax), lmax)
        st = SpinTensor(t)
        st′ = filter_modes(st)
        # This is only a weak test
        lfilter = lmax * 2 ÷ 3
        for (ij, cs) in zip(CartesianIndices(st′.coeffs), st′.coeffs)
            s = count(Tuple(ij) .== 1) - count(Tuple(ij) .== 2)
            for l in abs(s):lmax, m in (-l):l
                if l ≤ lfilter
                    @test cs[ash_mode_index(s, l, m, lmax)] ≠ 0
                else
                    @test abs(cs[ash_mode_index(s, l, m, lmax)]) ≤ 1000eps()
                end
            end
        end
        # Ideas:
        # - test effect of filtering on particular modes
        # - test that filtering and derivatives commute
        # - test that filtering is linear
    end
end

Random.seed!(100)
@testset "Calculate Ricci scalar" begin
    for iter in 1:20
        lmax = rand(2:100)
        sz = ash_grid_size(lmax)

        # q_ab = (1, 0, 0, 1)
        q = Tensor{2}(fill(SMatrix{2,2,Complex{Float64}}(1, 0, 0, 1), sz), lmax)
        q̃ = SpinTensor(q)
        # qu^ab
        qu = q
        # q_ab,c
        dq̃ = tensor_gradient(q̃)
        dq = Tensor(dq̃)
        # Γ^a_bc = q^ad (q_dc,b + q_bd,c - q_bc,d) / 2
        Γ = Tensor{3}(
            [
                SArray{Tuple{2,2,2}}(
                    sum(qu[a, d] * (dq[d, c, b] + dq[b, d, c] - dq[b, c, d]) / 2 for d in 1:2) for a in 1:2, b in 1:2, c in 1:2
                ) for (qu, dq) in zip(qu.values, dq.values)
            ],
            lmax,
        )
        # dΓ^a_bc,d
        Γ̃ = SpinTensor(Γ)
        dΓ̃ = tensor_gradient(Γ̃)
        dΓ = Tensor(dΓ̃)
        # R_ab = dΓ^c_ab,c - dΓ^c_ac,b + Γ^c_ab Γ^d_cd - Γ^c_ad Γ^d_bc
        R = Tensor{2}(
            [
                SMatrix{2,2}(
                    sum(dΓ[c, a, b, c] - dΓ[c, a, c, b] for c in 1:2) +
                    sum(Γ[c, a, b] * Γ[d, c, d] - Γ[c, a, d] * Γ[d, b, c] for c in 1:2, d in 1:2) for a in 1:2, b in 1:2
                ) for (Γ, dΓ) in zip(Γ.values, dΓ.values)
            ],
            lmax,
        )
        Rsc = Tensor{0}([Scalar(sum(qu[a, b] * R[a, b] for a in 1:2, b in 1:2)) for (qu, R) in zip(qu.values, R.values)], lmax)

        @test isapprox(map(x -> x[], Rsc.values), zeros(sz); atol=(lmax + 1)^4 * 10eps())

        # println("q:")
        # display(map(x -> chop.(x), q.values))
        # println()
        # println("Γ:")
        # display(map(x -> chop.(x), Γ.values))
        # println()
        # println("R:")
        # display(map(x -> chop.(x), R.values))
        # println()
        # println("Rsc:")
        # display(map(x -> chop.(x)[], Rsc.values))
        # println()
    end
end


function wdot(wx::AbstractArray{T}, x::AbstractArray{T},
                 wy::AbstractArray{T}, y::AbstractArray{T}) where T
    out = zero(T)
    @inbounds @simd for i in 1:length(x)
        out += conj(wx[i]*x[i]) * wy[i]*y[i]
    end
    return out
end

wnorm(w, x) = sqrt(real(wdot(w, x, w, x)))
assess_convergence(f, ftol) = assess_convergence(NaN, NaN, f, NaN, ftol)
function assess_convergence(x,
                            x_previous,
                            f,
                            xtol,
                            ftol)
    x_converged, f_converged = false, false

    if !any(isnan, x_previous) && chebyshev(x, x_previous) <= xtol
        x_converged = true
    end

    if maximum(abs, f) <= ftol
        f_converged = true
    end

    converged = x_converged || f_converged

    return x_converged, f_converged, converged
end

function check_isfinite(x::AbstractArray)
    if any(!isfinite, x)
        i = findall(!isfinite, x)
        throw(IsFiniteException(i))
    end
end

"""
    qrdelete!(Q, R, k)

Delete the left-most column of F = Q[:, 1:k] * R[1:k, 1:k] by updating Q and R.

Only Q[:, 1:(k-1)] and R[1:(k-1), 1:(k-1)] are valid on exit.
"""
function qrdelete!(Q::AbstractMatrix, R::AbstractMatrix, k::Int)
  n, m = size(Q)
  m == LinearAlgebra.checksquare(R) || throw(DimensionMismatch())
  1 ≤ k ≤ m || throw(ArgumentError())

  # apply Givens rotations
  for i in 2:k
      g = first(givens(R, i - 1, i, i))
      lmul!(g, R)
      rmul!(Q, g')
  end

  # move columns of R
  @inbounds for j in 1:(k-1)
    for i in 1:(k-1)
      R[i, j] = R[i, j + 1]
    end
  end

  Q, R
end

"""
    qradd!(Q, R, v, k)

Replace the right-most column of F = Q[:, 1:k] * R[1:k, 1:k] with v by updating Q and R.

This implementation modifies vector v as well. Only Q[:, 1:k] and R[1:k, 1:k] are valid on
exit.
"""
function qradd!(Q::AbstractMatrix, R::AbstractMatrix, v::AbstractVector, k::Int)
  n, m = size(Q)
  n == length(v) || throw(DimensionMismatch())
  m == LinearAlgebra.checksquare(R) || throw(DimensionMismatch())
  1 ≤ k ≤ m || throw(ArgumentError())

  @inbounds for i in 1:(k-1)
    q = view(Q, :, i)
    r = dot(q, v)

    R[i, k] = r
    axpy!(-r, q, v)
  end

  @inbounds begin
    d = norm(v)
    R[k, k] = d
    @. Q[:, k] = v / d
  end

  Q, R
end


export decompose, ms_decompose, cst_rkf, eps_rkf, weights, normlz

#------------------------------------------------------------------------
eps_rkf = eps::Float64 -> function (S)
  i :: Int = 1;
  while i<= length(S) && S[i]/S[1] > eps
    i+= 1;
  end
  i-1;
end

cst_rkf = r::Int64 -> function (S) return r end

#------------------------------------------------------------------------
# Decomposition of the pencil of matrices
function decompose(H::Vector{Matrix{C}}, lambda::Vector, rkf::Function) where C
    n = length(H)

    H0 = sum(H[i]*lambda[i] for i in 1:length(lambda))

    U, S, V = svd(H0)       # H0= U*diag(S)*V'
    r = rkf(S)

    Sr  = S[1:r]
    Sri = diagm([one(C)/S[i] for i in 1:r])

    M = Matrix{C}[]
    for i in 1:length(H)
    	push!(M, Sri*(U[:,1:r]')*H[i]*(V[:,1:r]))
    end

    if r > 1
        Xi, E, DiagInfo = diagonalization(M)
    else
        Xi = fill(zero(C),n,r)
        for i in 1:n
            Xi[i,1] = M[i][1,1]
        end
        E  = fill(1.0,1,1)
        DiagInfo = Dict{String,Any}( "case" => "1x1" )
    end

    Uxi = (U[:,1:r].*Sr')*E
    Vxi = (E\ V[:,1:r]')

    Info = Dict{String,Any}( "diagonalization" => DiagInfo)
    return Xi, Uxi, Vxi, Info
end

#------------------------------------------------------------------------
"""
```
decompose(σ :: Series{C,M}, rkf :: Function)
```
Decompose the series ``σ`` as a weighted sum of exponentials.
Return ``ω``, ``Ξ`` where
 - ``ω`` is the vector of weights,
 - ``Ξ`` is the matrix of frequency points, stored per row.
The list of monomials of degree ``\\leq {d-1 \\over 2}`` are used to construct
the Hankel matrix, where ``d`` is the maximal degree of the moments in ``σ``.

The optional argument `rkf` is the rank function used to determine the numerical rank from the vector S of singular values. Its default value `eps_rkf(1.e-6)` determines the rank as the first i s.t. S[i+1]/S[i]< 1.e-6 where S is the vector of singular values.

If the rank function cst_rkf(r) is used, the SVD is truncated at rank r.
"""
function decompose(sigma::Series{R,M},
                   rkf::Function = eps_rkf(1.e-6),
                   weps::Float64=1.e-5) where {R, M}

    d = maxdegree(sigma)
    X = variables(sigma)

    d0 = div(d-1,2); d1 = d-1-d0
    B0 = monomials(X, seq(0:d0))
    B1 = monomials(X, seq(0:d1))

    H = Matrix{R}[hankel(sigma, B0, B1)]
    for x in X
        push!(H, hankel(sigma, B0, [b*x for b in B1]))
    end

    lambda = [1.0]
    Xi, Uxi, Vxi = decompose(H, lambda,  rkf)

    n, r = size(Xi)

    w = fill(one(eltype(Xi)),r)

    for i in 1:r
        w[i] = Xi[1,i]
        Xi[:,i]/= Xi[1,i]
        w[i]*= Uxi[1,i]*Vxi[i,1]
    end

    # remove weights below threshold weps
    I = Bool[abs(w[i])>weps for i in 1:length(w)]
    return w[I], Xi[2:end,I]

end

#------------------------------------------------------------------------
function ms_decompose(sigma::Series{R,M},
                      rkf::Function = eps_rkf(1.e-6),
                      weps::Float64=1.e-5) where {R, M}
    return decompose(sigma,rkf,weps)
end
#------------------------------------------------------------------------
function normlz(M::AbstractMatrix,i)
    diagm(0 => [1/M[i,j] for j in 1:size(M,1)])*M
end


import GreyDecision.Utility: makeminmax
import GreyDecision.GreyNumbers: GreyNumber
import GreyDecision.Wpm: wpm

@testset "WPM" begin
    tol = 0.00001
    weights = [0.221, 0.159, 0.175, 0.127, 0.117, 0.201]
    fns = [maximum, minimum, minimum, maximum, minimum, maximum]
    decmat = [
            3.0 12.5 2 120 14 3
            5.0 15 3 110 38 4
            3.0 13 2 120 19 3
            4.0 14 2 100 31 4
            3.0 15 1.5 125 40 4
        ]
    nrow, ncol = size(decmat)
    # Converting white decision matrix to
    # grey decision matrix
    greydecmat = Array{GreyNumber{Float64}, 2}(undef, nrow, ncol)

    for i in 1:nrow
        for j in 1:ncol
            greydecmat[i, j] = GreyNumber(decmat[i, j])
        end
    end

    result = wpm(greydecmat, weights, fns)

    scores = result[:scores]
    @test isapprox(scores[1].a, 0.7975224331331, atol = tol)
    @test isapprox(scores[2].a, 0.7532541470585, atol = tol)
    @test isapprox(scores[3].a, 0.7647463553356, atol = tol)
    @test isapprox(scores[4].a, 0.7873956894791, atol = tol)
    @test isapprox(scores[5].a, 0.7674278741782, atol = tol)

    @test result[:bestIndex] == 1
    @test result[:ranking] == [2, 3, 5, 4, 1]
end


using LinearAlgebra

xrange(::Type{T}, len=11) where {T} = range(-one(T), one(T), length = len)

##################################
# UNNORMALIZED LEGENDRE FUNCTIONS
##################################

# P_0^0 is constant. Verify the output is invariant for several inputs.
@testset "Constant P_0^0 ($T)" for T in NumTypes
    @test @inferred(legendre(LegendreUnitNorm(), 0, 0, T(0.1))) isa T
    @test all(legendre(LegendreUnitNorm(), 0, 0, x) == one(T) for x in xrange(T))
end

# Match P_ℓ^{m=0} terms for ℓ=1...9 where the analytical expressions
# have been taken from a table of equations, assuming x = cos(π/4).
@testset "Analytic checks for P_ℓ^0(cos(π/4)) ($T)" for T in NumTypes
    LMAX = 9
    plm = Matrix{T}(undef, LMAX+1, LMAX+1)
    legendre!(LegendreUnitNorm(), plm, LMAX, LMAX, sqrt(T(2))/2)
    @test plm[2, 1] ≈  sqrt(T(2))/2
    @test plm[3, 1] ≈  T(1)/4
    @test plm[4, 1] ≈ -sqrt(T(2))/8
    @test plm[5, 1] ≈ -T(13)/32
    @test plm[6, 1] ≈ -17sqrt(T(2))/64
    @test plm[7, 1] ≈ -T(19)/128
    @test plm[8, 1] ≈  23sqrt(T(2))/256
    @test plm[9, 1] ≈  T(611)/2048
    @test plm[10,1] ≈  827sqrt(T(2))/4096
end

#################################################
# ORTHONORMAL & SPHERICAL HARMONIC NORMALIZATION
#################################################

# P_0^0 is constant. Verify the output is invariant for several inputs.
@testset "Constant P_0^0 ($T)" for T in NumTypes
    @test @inferred(legendre(LegendreFourPiNorm(), 0, 0, T(0.1))) isa T
    @test @inferred(legendre(LegendreOrthoNorm(),  0, 0, T(0.1))) isa T
    @test @inferred(legendre(LegendreSphereNorm(), 0, 0, T(0.1))) isa T
    @test all(legendre(LegendreFourPiNorm(), 0, 0, x) == one(T)           for x in xrange(T))
    @test all(legendre(LegendreOrthoNorm(),  0, 0, x) == sqrt(T(0.5))     for x in xrange(T))
    @test all(legendre(LegendreSphereNorm(), 0, 0, x) == sqrt(inv(4T(π))) for x in xrange(T))
end

# The initialization condition
#
#   P_ℓ^ℓ(x) = (-1)^ℓ (2ℓ-1)!! (1-x^2)^(ℓ/2)
#
# can be used with extended-precision computations for special values
# of ℓ and m. To account for the spherical harmonic normalization,
# the formula can be simplified to
#
#   λ_ℓ^ℓ(x) = (-1)^ℓ 1/sqrt(4π) sqrt((2ℓ+1)!!/(2ℓ)!!) (1-x^2)^(ℓ/2)
#
# We'll compute the coefficient separately since it's "easy", and then
# we can use convenient angles that also expand to an exact analytic
# value for (1-x^2)^(ℓ/2).
#
# Assuming x = cos(θ), the (1-x^2)^(ℓ/2) term simplifies to an
# expression of \sin(θ):
#
#   (1-x^2)^(ℓ/2) = (sin^2 θ)^(ℓ/2) = (sin θ)^ℓ
#
# For θ = π/4:
#
#   * even ℓ => 1 / 2^(ℓ/2)
#   * odd ℓ => 1 / (2^(k/2) sqrt(2)) where k=floor(ℓ/2).
@testset "Analytic checks for λ_ℓ^m(cos(π/4))" begin
    function SphericalPll_coeff(T, ℓ)
        ratio(x) = sqrt(T(2x + 1) / T(2x))
        factorials = mapreduce(ratio, *, 1:ℓ, init=one(T))
        sign = (ℓ % 2 == 0) ? 1 : -1
        return sign * sqrt(inv(4T(π))) * factorials
    end

    LMAX = 99
    Λ = fill(0.0, LMAX+1, LMAX+1)
    legendre!(LegendreSphereNorm(), Λ, LMAX, LMAX, cosd(45.0))

    @test Λ[3,3]   ≈ SphericalPll_coeff(Float64,2)  / (2^1)
    @test Λ[4,4]   ≈ SphericalPll_coeff(Float64,3)  / (2^1 * sqrt(2))
    @test Λ[21,21] ≈ SphericalPll_coeff(Float64,20) / (2^10)
    @test Λ[22,22] ≈ SphericalPll_coeff(Float64,21) / (2^10 * sqrt(2))
    @test Λ[99,99] ≈ SphericalPll_coeff(Float64,98) / (2^49)
    @test Λ[100,100] ≈ SphericalPll_coeff(Float64,99) / (2^49 * sqrt(2))
end

# The normal P_ℓ^m should be equal to the spherical harmonic normalized
# λ_ℓ^m if we manually normalize them.
@testset "Equality of N_ℓ^m × P_ℓ^m and λ_ℓ^m ($T)" for T in NumTypes
    LMAX = 5
    atol = max(eps(T(100)), eps(100.0)) # maximum(plm_norm) is of order 10²
    plm_norm = zeros(T, LMAX+1, LMAX+1)
    plm_sphr = zeros(T, LMAX+1, LMAX+1)

    lmat = tril(repeat(collect(0:LMAX), 1, LMAX+1))
    mmat = tril(repeat(collect(0:LMAX)', LMAX+1, 1))
    nlm = tril(Nlm.(T, lmat, mmat))
    for x in xrange(T)
        legendre!(LegendreUnitNorm(),   plm_norm, LMAX, LMAX, x)
        legendre!(LegendreSphereNorm(), plm_sphr, LMAX, LMAX, x)
        @test all(isapprox.(nlm.*plm_norm, plm_sphr, atol=atol))
    end
end

# The orthonormal O_ℓ^m should be equal to the spherical harmonic normalized
# λ_ℓ^m if we manually normalize them.
@testset "Equality of (2π)^(-1/2) O_ℓ^m and λ_ℓ^m ($T)" for T in NumTypes
    LMAX = 5
    atol = max(eps(T(100)), eps(100.0)) # maximum(plm_norm) is of order 10²
    plm_orth = zeros(T, LMAX+1, LMAX+1)
    plm_sphr = zeros(T, LMAX+1, LMAX+1)
    for x in xrange(T)
        legendre!(LegendreOrthoNorm(),  plm_orth, LMAX, LMAX, x)
        legendre!(LegendreSphereNorm(), plm_sphr, LMAX, LMAX, x)
        @test all(isapprox.(plm_orth./sqrt(2T(π)), plm_sphr, atol=atol))
    end
end

# The spherical and 4π-normalized functions should be the same up to the
# differing factor of 1/√4π
@testset "Equality of (4π)^(-1/2) F_ℓ^m and λ_ℓ^m ($T)" for T in NumTypes
    LMAX = 5
    atol = max(eps(4T(π)), eps(4.0π))
    plm_4pi  = zeros(T, LMAX+1, LMAX+1)
    plm_sphr = zeros(T, LMAX+1, LMAX+1)
    for x in xrange(T)
        legendre!(LegendreFourPiNorm(), plm_4pi,  LMAX, LMAX, x)
        legendre!(LegendreSphereNorm(), plm_sphr, LMAX, LMAX, x)
        @test all(isapprox.(plm_4pi./sqrt(4T(π)), plm_sphr, atol=atol))
    end
end

################################
# Complex domain
################################

# Trivial case: real-only complex arguments should be identical to the real case
@testset "Complex arguments, real axis ($T)" for T in NumTypes
    LMAX = 10
    x = xrange(T, 100)
    z = complex(x)
    @test Plm.(0:LMAX, 0:LMAX, x) == real.(Plm.(0:LMAX, 0:LMAX, z))
    @test λlm.(0:LMAX, 0:LMAX, x) == real.(λlm.(0:LMAX, 0:LMAX, z))
end

################################
# Other analytic checks / issues
################################

@testset "Numerical stability/accuracy" begin
    # issue CMB.jl#11
    #   Overall accuracy --- fixed by adding FMA y=(1-x^2) and pairwise diagonal iteration
    x = sind(-57.5)
    lmax = 3 * 720
    Λ = λlm.(0:lmax, 0:lmax, x)
    # The amplitude bound of 1.5 is a very rough limit --- simply checking for
    # unbounded growth.
    @test all(abs.(Λ[end,:]) .< 1.5)

    # PR#16
    #   Increase accuracy of μ coefficient for spherical norm.
    #   Also test ν, and both already equivalent for unit norm.
    @testset "coeffs μ, ν ($norm)" for norm in (LegendreSphereNorm(), LegendreUnitNorm())
        lrng = 1:30_000
        μ(T, l) = AssociatedLegendrePolynomials.coeff_μ(norm, T, l)
        ν(T, l) = AssociatedLegendrePolynomials.coeff_ν(norm, T, l)
        @test μ.(Float64, lrng) == Float64.(μ.(BigFloat, lrng))
        @test ν.(Float64, lrng) == Float64.(ν.(BigFloat, lrng))
    end
end


########## Script ##########

function main(input_name)

    # open file and store content into an input string
    input = open(join(["inputs//", input_name]), "r") do io
        read(io, String)
    end

    # split input into a data vector
    data = String.(split(input, "\r\n"))

    # tables from day10 instructions
    pairs = Dict('('=>')', '['=>']', '{'=>'}', '<'=>'>')
    score1 = Dict(')'=>3, ']'=>57, '}'=>1197, '>'=>25137)
    score2 = Dict(')'=>1, ']'=>2, '}'=>3, '>'=>4)

    # part 1 and 2
    total_score1 = 0
    total_scores2 = Int[]
    for line in data

        opened_chars = Char[]
        corrupted = false
        for char in line
            if char in keys(pairs)
                append!(opened_chars, [char])
            elseif char in values(pairs)
                if char == pairs[opened_chars[end]]
                    pop!(opened_chars)
                else
                    total_score1 += score1[char]
                    corrupted = true
                    break
                end
            end
        end

        if !corrupted
            total_score2 = 0
            for char in reverse(opened_chars)
                total_score2 *= 5
                total_score2 += score2[pairs[char]]
            end
            append!(total_scores2, [total_score2])
        end

    end
    sort!(total_scores2)

    println("Part 1: ", total_score1)
    println("Part 2: ", total_scores2[(end + 1) ÷ 2])

end

main("day10.txt")


@testset "Test quadratures" begin

    using jFEMTools
    jF = jFEMTools

    # Triangle Quadratures
    ###############################################

    # Test Strang Quadrature
    for i in 1:5
        quad_rule = jF.QuadratureRule{jF.Triangle}(jF.Strang(),i)
        @test sum(quad_rule.weights) ≈ 0.5
    end

end


# =====================================
# MNA 1
#
# system type: affine continuous system
# state dimension: 578
# =====================================
using ReachabilityBenchmarks, MathematicalSystems, MAT, SparseArrays

function mna1_model()
    file = matopen(@relpath "mna1.mat")

    # system matrix
    A = sparse(read(file, "A"))

    # affine term
    b = sparsevec(570:578, [fill(-0.1, 5); fill(-0.2, 4)], 578)

    # affine continuous system
    S = @system(x' = Ax + b)

    return S
end


using Snopt
using DiffResults, ForwardDiff

function rosenbrock(x)

    f = (1 - x[1])^2 + 100*(x[2] - x[1]^2)^2

    return f

end

function opt_wrapper(x)
    result = DiffResults.GradientResult(x)
    result = ForwardDiff.gradient!(result, rosenbrock, x)
    f = DiffResults.value(result)
    g = DiffResults.gradient(result)

    fail = false

    c = []
    dcdx = []

    return f, c, g, dcdx, fail
end

x0 = [4.0; 4.0]
lb = [-5.0; -5.0]
ub = [5.0; 5.0]
options = Dict{String, Any}()
options["Derivative option"] = 1
options["Verify level"] = 1
options["Major optimality tolerance"] = 1e-6

xopt, fopt, info = snopt(opt_wrapper, x0, lb, ub, options)


"""
	EconomicDispatch(hours,
					 nodes::Nodes,
					 powerplants::PowerPlants,
					 renewables::Renewables,
					 )

Creates an economic dispatch model from power plants and renewables.
Solves using JuMP. Returns ED_mod, P_opt, P_R_opt, P_R_opt_dist, price.
"""
function EconomicDispatch(hours,
						  nodes::Nodes,
			   		      powerplants::PowerPlants,
						  renewables::Renewables,
			   			  )

	T = hours
	G = powerplants.unit
	R = renewables.unit
	J = nodes.id

	# Initialise JuMP model: Economic dispatch
	ED_mod = JuMP.Model(with_optimizer(Gurobi.Optimizer))

	# Variables
	@variable(ED_mod, P[T, G] >= 0)
	@variable(ED_mod, P_R[T, R] >= 0)

	# Constraints
	# Market clearing/power balance
	@constraint(ED_mod, MarketClearing[t = T],
	    sum(P[t, g] for g in G) +
			sum(P_R[t, r] for r in R) == nodes.systemload[t] -
			sum(nodes.exchange[j][t] for j in J));

	# Upper generation limit
	@constraint(ED_mod, GenerationLimitUp[g = G, t = T],
	    P[t, g] <= powerplants.pmax[g][t]);

	# Lower generation limit
	@constraint(ED_mod, GenerationLimitDown[g = G, t = T],
	    P[t, g] >= powerplants.pmin[g][t]);

	# Upper generation limit for renewables
	@constraint(ED_mod, ResGenerationLimitUp[r = R, t = T],
	    P_R[t, r] <= renewables.infeed[r][t]);

	# # Ramp-up limit
	# @constraint(ED_mod, RampUp[g = G, t = T; t > T[1]],
	# 	P[t, g] - P[t-1, g] <= powerplants.rup[g])
	#
	# # Ramp-down limit
	# @constraint(ED_mod, RampDown[g = G, t = T; t > T[1]],
	# 	-(P[t, g] - P[t-1, g]) <= powerplants.rdn[g])

	# Objective function
	@objective(ED_mod, Min,
		sum(powerplants.mc[g][1] * P[t, g] for g in G, t in T));

	# @objective(ED_mod, Min,
	#     sum(powerplants.mc[g][1] * P[t, g] for g in G, t in T) +
	# 		sum(0 * P_R[t, r] for r in R, t in T));

	# Initiate optimisation process
	JuMP.optimize!(ED_mod)

	# Export results
	P_opt = JuMP.value.(P)
	P_R_opt = JuMP.value.(P_R)
	price = JuMP.dual.(MarketClearing)

	# Correct RES dispatch
	# Total summed RES generation from dispatch for each t
	ResTotalGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T)), T)

	for t in T
		ResTotalGeneration[t] = sum(P_R_opt[t, :])
	end

	# Maximum output possible
	ResMaxGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T)), T)
	for t in T
		ResMaxGeneration[t] = sum(renewables.infeed[r][t] for r in R)
	end

	# Redistribution among RES
	ResUnitGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T), length(R)), T, R)

	for t in T
		for r in R
			ResUnitGeneration[t, r] = (renewables.infeed[r][t] / ResMaxGeneration[t]) *
			ResTotalGeneration[t]
		end
	end

	# Distributed, corrected P_R_opt
	P_R_opt_dist = ResUnitGeneration

	return(ED_mod,
	       P_opt,
		   P_R_opt,
		   P_R_opt_dist,
		   price
	       )
end

"""
	EconomicDispatch(hours,
					 nodes::Nodes,
					 powerplants::PowerPlants,
					 renewables::Renewables,
					 storages::Storages,
					 )

Creates an economic dispatch model from power plants, renewables, and storages.
Initial storage level is fixed to zero. Solves using JuMP. Returns ED_mod,
P_opt, P_R_opt, P_R_opt_dist, P_S_opt, D_S_opt, L_S_opt, price.
"""
### Economic dispatch with renewable energy sources and storages
function EconomicDispatch(hours,
						  nodes::Nodes,
			   		      powerplants::PowerPlants,
						  renewables::Renewables,
						  storages::Storages,
			   			  )

	T = hours
	G = powerplants.unit
	R = renewables.unit
	S = storages.unit
	J = nodes.id

	# Initialise JuMP model: Economic dispatch
	ED_mod = JuMP.Model(with_optimizer(Gurobi.Optimizer))

	# Variables
	@variable(ED_mod, P[T, G] >= 0)
	@variable(ED_mod, P_R[T, R] >= 0)

	# Storage voriables
	@variable(ED_mod, P_S[T, S] >= 0)	# Power generation from storage unit
	@variable(ED_mod, D_S[T, S] >= 0)	# Electricity demand from storage unit
	@variable(ED_mod, L_S[T, S] >= 0)	# Storage level

	# Fix storage level in period 0 to zero
	for s in S
		fix(L_S[T[1], s], 0; force = true)
	end

	# Constraints
	# Market clearing/power balance
	@constraint(ED_mod, MarketClearing[t = T],
	    sum(P[t, g] for g in G) +
		sum(P_R[t, r] for r in R) +
		sum(P_S[t, s] for s in S) -
		sum(D_S[t, s] for s in S)
			== nodes.systemload[t] -
			sum(nodes.exchange[j][t] for j in J));

	# Upper generation limit
	@constraint(ED_mod, GenerationLimitUp[g = G, t = T],
	    P[t, g] <= powerplants.pmax[g][t]);

	# Lower generation limit
	@constraint(ED_mod, GenerationLimitDown[g = G, t = T],
	    P[t, g] >= powerplants.pmin[g][t]);

	# Upper generation limit for renewables
	@constraint(ED_mod, ResGenerationLimitUp[r = R, t = T],
	    P_R[t, r] <= renewables.infeed[r][t]);

	# # Ramp-up limit
	# @constraint(ED_mod, RampUp[g = G, t = T; t > T[1]],
	# 	P[t, g] - P[t-1, g] <= powerplants.rup[g])
	#
	# # Ramp-down limit
	# @constraint(ED_mod, RampDown[g = G, t = T; t > T[1]],
	# 	-(P[t, g] - P[t-1, g]) <= powerplants.rdn[g])

	### Storage constraints
	# Maximum storage power output
	@constraint(ED_mod, StoragePowerOutput[s = S, t = T],
	    P_S[t, s] <= storages.power[s]);

	# Maximum storage power input
	@constraint(ED_mod, StoragePowerInput[s = S, t = T],
	    D_S[t, s] <= storages.power[s]);

	# Generation limit determined by remaining storage level
	@constraint(ED_mod, StorageLevelGen[s = S, t = T],
	    P_S[t, s] <= L_S[t, s]);

	# Storace capacity limit
	@constraint(ED_mod, StorageLevelCap[s = S, t = T],
	    L_S[t, s] <= storages.storage[s]);

	# Storage
	@constraint(ED_mod, Storage[s = S, t = T; t > T[1]],
		L_S[t-1, s] - P_S[t-1, s] + storages.efficiency[s] * D_S[t-1, s] == L_S[t, s]);

	# Objective function
	@objective(ED_mod, Min,
	    sum(powerplants.mc[g][1] * P[t, g] for g in G, t in T));

	# @objective(ED_mod, Min,
	#     sum(powerplants.mc[g][1] * P[t, g] for g in G, t in T) +
	# 		sum(0 * P_R[t, r] for r in R, t in T));

	# Initiate optimisation process
	JuMP.optimize!(ED_mod)

	# Export results
	P_opt = JuMP.value.(P)
	P_R_opt = JuMP.value.(P_R)
	P_S_opt = JuMP.value.(P_S)
	D_S_opt = JuMP.value.(D_S)
	L_S_opt = JuMP.value.(L_S)
	price = JuMP.dual.(MarketClearing)

	# Correct RES dispatch
	# Total summed RES generation from dispatch for each t
	ResTotalGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T)), T)

	for t in T
		ResTotalGeneration[t] = sum(P_R_opt[t, :])
	end

	# Maximum output possible
	ResMaxGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T)), T)
	for t in T
		ResMaxGeneration[t] = sum(renewables.infeed[r][t] for r in R)
	end

	# Redistribution among RES
	ResUnitGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T), length(R)), T, R)

	for t in T
		for r in R
			ResUnitGeneration[t, r] = (renewables.infeed[r][t] / ResMaxGeneration[t]) *
			ResTotalGeneration[t]
		end
	end

	# Distributed, corrected P_R_opt
	P_R_opt_dist = ResUnitGeneration

	return(ED_mod,
	       P_opt,
		   P_R_opt,
		   P_R_opt_dist,
		   P_S_opt,
		   D_S_opt,
		   L_S_opt,
		   price
	       )
end

"""
	EconomicDispatch(hours,
					 nodes::Nodes,
					 powerplants::PowerPlants,
					 renewables::Renewables,
					 storages::Storages,
					 P_S_opt_prev,
					 D_S_opt_prev,
					 L_S_opt_prev,
					 )

Creates an economic dispatch model from power plants, renewables, and storages.
Initial storage level, generation, and demand is initialised  with parameters
from the previous model run. Solves using JuMP. Returns ED_mod, P_opt, P_R_opt,
P_R_opt_dist, P_S_opt, D_S_opt, L_S_opt, price.
"""
function EconomicDispatch(hours,
						  nodes::Nodes,
			   		      powerplants::PowerPlants,
						  renewables::Renewables,
						  storages::Storages,
						  P_S_opt_prev,
						  D_S_opt_prev,
						  L_S_opt_prev,
			   			  )

	T = hours
	G = powerplants.unit
	R = renewables.unit
	S = storages.unit
	J = nodes.id

	# Initialise JuMP model: Economic dispatch
	ED_mod = JuMP.Model(with_optimizer(Gurobi.Optimizer))

	# Variables
	@variable(ED_mod, P[T, G] >= 0)
	@variable(ED_mod, P_R[T, R] >= 0)

	# Storage voriables
	@variable(ED_mod, P_S[T, S] >= 0)	# Power generation from storage unit
	@variable(ED_mod, D_S[T, S] >= 0)	# Electricity demand from storage unit
	@variable(ED_mod, L_S[T, S] >= 0)	# Storage level

	# Fix storage parameters in first hour to the last hour value of the
	# previous day slice.
	for s in S
		fix(P_S[T[1], s], P_S_opt_prev[(T[1]), s]; force = true)
		fix(D_S[T[1], s], D_S_opt_prev[(T[1]), s]; force = true)
		fix(L_S[T[1], s], L_S_opt_prev[(T[1]), s]; force = true)
	end

	# Constraints
	# Market clearing/power balance
	@constraint(ED_mod, MarketClearing[t = T],
	    sum(P[t, g] for g in G) +
		sum(P_R[t, r] for r in R) +
		sum(P_S[t, s] for s in S) -
		sum(D_S[t, s] for s in S)
			== nodes.systemload[t] -
			sum(nodes.exchange[j][t] for j in J));

	# Upper generation limit
	@constraint(ED_mod, GenerationLimitUp[g = G, t = T],
	    P[t, g] <= powerplants.pmax[g][t]);

	# Lower generation limit
	@constraint(ED_mod, GenerationLimitDown[g = G, t = T],
	    P[t, g] >= powerplants.pmin[g][t]);

	# Upper generation limit for renewables
	@constraint(ED_mod, ResGenerationLimitUp[r = R, t = T],
	    P_R[t, r] <= renewables.infeed[r][t]);

	# # Ramp-up limit
	# @constraint(ED_mod, RampUp[g = G, t = T; t > T[1]],
	# 	P[t, g] - P[t-1, g] <= powerplants.rup[g])
	#
	# # Ramp-down limit
	# @constraint(ED_mod, RampDown[g = G, t = T; t > T[1]],
	# 	-(P[t, g] - P[t-1, g]) <= powerplants.rdn[g])

	### Storage constraints
	# Maximum storage power output
	@constraint(ED_mod, StoragePowerOutput[s = S, t = T],
	    P_S[t, s] <= storages.power[s]);

	# Maximum storage power input
	@constraint(ED_mod, StoragePowerInput[s = S, t = T],
	    D_S[t, s] <= storages.power[s]);

	# Generation limit determined by remaining storage level
	@constraint(ED_mod, StorageLevelGen[s = S, t = T],
	    P_S[t, s] <= L_S[t, s]);

	# Storace capacity limit
	@constraint(ED_mod, StorageLevelCap[s = S, t = T],
	    L_S[t, s] <= storages.storage[s]);

	# Storage
	@constraint(ED_mod, Storage[s = S, t = T; t > T[1]],
		L_S[t-1, s] - P_S[t-1, s] + storages.efficiency[s] * D_S[t-1, s] == L_S[t, s]);

	# Objective function
	@objective(ED_mod, Min,
	    sum(powerplants.mc[g][1] * P[t, g] for g in G, t in T));

	# @objective(ED_mod, Min,
	#     sum(powerplants.mc[g][1] * P[t, g] for g in G, t in T) +
	# 		sum(0 * P_R[t, r] for r in R, t in T));

	# Initiate optimisation process
	JuMP.optimize!(ED_mod)

	# Export results
	P_opt = JuMP.value.(P)
	P_R_opt = JuMP.value.(P_R)
	P_S_opt = JuMP.value.(P_S)
	D_S_opt = JuMP.value.(D_S)
	L_S_opt = JuMP.value.(L_S)
	price = JuMP.dual.(MarketClearing)

	# Correct RES dispatch
	# Total summed RES generation from dispatch for each t
	ResTotalGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T)), T)

	for t in T
		ResTotalGeneration[t] = sum(P_R_opt[t, :])
	end

	# Maximum output possible
	ResMaxGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T)), T)
	for t in T
		ResMaxGeneration[t] = sum(renewables.infeed[r][t] for r in R)
	end

	# Redistribution among RES
	ResUnitGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T), length(R)), T, R)

	for t in T
		for r in R
			ResUnitGeneration[t, r] = (renewables.infeed[r][t] / ResMaxGeneration[t]) *
			ResTotalGeneration[t]
		end
	end

	# Distributed, corrected P_R_opt
	P_R_opt_dist = ResUnitGeneration

	return(ED_mod,
	       P_opt,
		   P_R_opt,
		   P_R_opt_dist,
		   P_S_opt,
		   D_S_opt,
		   L_S_opt,
		   price
	       )
end

#### 80%
### Economic dispatch with renewable energy sources and storages
function EconomicDispatch80(hours,
						  nodes::Nodes,
			   		      powerplants::PowerPlants,
						  renewables::Renewables,
						  storages::Storages,
			   			  )

	T = hours
	G = powerplants.unit
	R = renewables.unit
	S = storages.unit
	J = nodes.id

	# Initialise JuMP model: Economic dispatch
	ED_mod = JuMP.Model(with_optimizer(Gurobi.Optimizer))

	# Variables
	@variable(ED_mod, P[T, G] >= 0)
	@variable(ED_mod, P_R[T, R] >= 0)

	# Storage voriables
	@variable(ED_mod, P_S[T, S] >= 0)	# Power generation from storage unit
	@variable(ED_mod, D_S[T, S] >= 0)	# Electricity demand from storage unit
	@variable(ED_mod, L_S[T, S] >= 0)	# Storage level

	# Fix storage level in period 0 to zero
	for s in S
		fix(L_S[T[1], s], 0; force = true)
	end

	# Constraints
	# Market clearing/power balance
	@constraint(ED_mod, MarketClearing[t = T],
	    sum(P[t, g] for g in G) +
		sum(P_R[t, r] for r in R) +
		sum(P_S[t, s] for s in S) -
		sum(D_S[t, s] for s in S)
			== nodes.systemload[t] -
			sum(nodes.exchange[j][t] for j in J));

	# Upper generation limit
	@constraint(ED_mod, GenerationLimitUp[g = G, t = T],
	    P[t, g] <= powerplants.pmax[g][t]);

	# Lower generation limit
	@constraint(ED_mod, GenerationLimitDown[g = G, t = T],
	    P[t, g] >= powerplants.pmin[g][t]);

	# Upper generation limit for renewables
	@constraint(ED_mod, ResGenerationLimitUp[r = R, t = T],
	    P_R[t, r] <= renewables.infeed[r][t]);

	# # Ramp-up limit
	# @constraint(ED_mod, RampUp[g = G, t = T; t > T[1]],
	# 	P[t, g] - P[t-1, g] <= powerplants.rup[g])
	#
	# # Ramp-down limit
	# @constraint(ED_mod, RampDown[g = G, t = T; t > T[1]],
	# 	-(P[t, g] - P[t-1, g]) <= powerplants.rdn[g])

	### Storage constraints
	# Maximum storage power output
	@constraint(ED_mod, StoragePowerOutput[s = S, t = T],
	    P_S[t, s] <= storages.power[s]);

	# Maximum storage power input
	@constraint(ED_mod, StoragePowerInput[s = S, t = T],
	    D_S[t, s] <= storages.power[s]);

	# Generation limit determined by remaining storage level
	@constraint(ED_mod, StorageLevelGen[s = S, t = T],
	    P_S[t, s] <= L_S[t, s]);

	# Storace capacity limit
	@constraint(ED_mod, StorageLevelCap[s = S, t = T],
	    L_S[t, s] <= 0.8*storages.storage[s]);

	# Storage
	@constraint(ED_mod, Storage[s = S, t = T; t > T[1]],
		L_S[t-1, s] - P_S[t-1, s] + storages.efficiency[s] * D_S[t-1, s] == L_S[t, s]);

	# Objective function
	@objective(ED_mod, Min,
	    sum(powerplants.mc[g][1] * P[t, g] for g in G, t in T));

	# @objective(ED_mod, Min,
	#     sum(powerplants.mc[g][1] * P[t, g] for g in G, t in T) +
	# 		sum(0 * P_R[t, r] for r in R, t in T));

	# Initiate optimisation process
	JuMP.optimize!(ED_mod)

	# Export results
	P_opt = JuMP.value.(P)
	P_R_opt = JuMP.value.(P_R)
	P_S_opt = JuMP.value.(P_S)
	D_S_opt = JuMP.value.(D_S)
	L_S_opt = JuMP.value.(L_S)
	price = JuMP.dual.(MarketClearing)

	# Correct RES dispatch
	# Total summed RES generation from dispatch for each t
	ResTotalGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T)), T)

	for t in T
		ResTotalGeneration[t] = sum(P_R_opt[t, :])
	end

	# Maximum output possible
	ResMaxGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T)), T)
	for t in T
		ResMaxGeneration[t] = sum(renewables.infeed[r][t] for r in R)
	end

	# Redistribution among RES
	ResUnitGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T), length(R)), T, R)

	for t in T
		for r in R
			ResUnitGeneration[t, r] = (renewables.infeed[r][t] / ResMaxGeneration[t]) *
			ResTotalGeneration[t]
		end
	end

	# Distributed, corrected P_R_opt
	P_R_opt_dist = ResUnitGeneration

	return(ED_mod,
	       P_opt,
		   P_R_opt,
		   P_R_opt_dist,
		   P_S_opt,
		   D_S_opt,
		   L_S_opt,
		   price
	       )
end

function EconomicDispatch80(hours,
						  nodes::Nodes,
			   		      powerplants::PowerPlants,
						  renewables::Renewables,
						  storages::Storages,
						  P_S_opt_prev,
						  D_S_opt_prev,
						  L_S_opt_prev,
			   			  )

	T = hours
	G = powerplants.unit
	R = renewables.unit
	S = storages.unit
	J = nodes.id

	# Initialise JuMP model: Economic dispatch
	ED_mod = JuMP.Model(with_optimizer(Gurobi.Optimizer))

	# Variables
	@variable(ED_mod, P[T, G] >= 0)
	@variable(ED_mod, P_R[T, R] >= 0)

	# Storage voriables
	@variable(ED_mod, P_S[T, S] >= 0)	# Power generation from storage unit
	@variable(ED_mod, D_S[T, S] >= 0)	# Electricity demand from storage unit
	@variable(ED_mod, L_S[T, S] >= 0)	# Storage level

	# Fix storage parameters in first hour to the last hour value of the
	# previous day slice.
	for s in S
		fix(P_S[T[1], s], P_S_opt_prev[(T[1]), s]; force = true)
		fix(D_S[T[1], s], D_S_opt_prev[(T[1]), s]; force = true)
		fix(L_S[T[1], s], L_S_opt_prev[(T[1]), s]; force = true)
	end

	# Constraints
	# Market clearing/power balance
	@constraint(ED_mod, MarketClearing[t = T],
	    sum(P[t, g] for g in G) +
		sum(P_R[t, r] for r in R) +
		sum(P_S[t, s] for s in S) -
		sum(D_S[t, s] for s in S)
			== nodes.systemload[t] -
			sum(nodes.exchange[j][t] for j in J));

	# Upper generation limit
	@constraint(ED_mod, GenerationLimitUp[g = G, t = T],
	    P[t, g] <= powerplants.pmax[g][t]);

	# Lower generation limit
	@constraint(ED_mod, GenerationLimitDown[g = G, t = T],
	    P[t, g] >= powerplants.pmin[g][t]);

	# Upper generation limit for renewables
	@constraint(ED_mod, ResGenerationLimitUp[r = R, t = T],
	    P_R[t, r] <= renewables.infeed[r][t]);

	# # Ramp-up limit
	# @constraint(ED_mod, RampUp[g = G, t = T; t > T[1]],
	# 	P[t, g] - P[t-1, g] <= powerplants.rup[g])
	#
	# # Ramp-down limit
	# @constraint(ED_mod, RampDown[g = G, t = T; t > T[1]],
	# 	-(P[t, g] - P[t-1, g]) <= powerplants.rdn[g])

	### Storage constraints
	# Maximum storage power output
	@constraint(ED_mod, StoragePowerOutput[s = S, t = T],
	    P_S[t, s] <= storages.power[s]);

	# Maximum storage power input
	@constraint(ED_mod, StoragePowerInput[s = S, t = T],
	    D_S[t, s] <= storages.power[s]);

	# Generation limit determined by remaining storage level
	@constraint(ED_mod, StorageLevelGen[s = S, t = T],
	    P_S[t, s] <= L_S[t, s]);

	# Storace capacity limit
	@constraint(ED_mod, StorageLevelCap[s = S, t = T],
	    L_S[t, s] <= 0.8*storages.storage[s]);

	# Storage
	@constraint(ED_mod, Storage[s = S, t = T; t > T[1]],
		L_S[t-1, s] - P_S[t-1, s] + storages.efficiency[s] * D_S[t-1, s] == L_S[t, s]);

	# Objective function
	@objective(ED_mod, Min,
	    sum(powerplants.mc[g][1] * P[t, g] for g in G, t in T));

	# @objective(ED_mod, Min,
	#     sum(powerplants.mc[g][1] * P[t, g] for g in G, t in T) +
	# 		sum(0 * P_R[t, r] for r in R, t in T));

	# Initiate optimisation process
	JuMP.optimize!(ED_mod)

	# Export results
	P_opt = JuMP.value.(P)
	P_R_opt = JuMP.value.(P_R)
	P_S_opt = JuMP.value.(P_S)
	D_S_opt = JuMP.value.(D_S)
	L_S_opt = JuMP.value.(L_S)
	price = JuMP.dual.(MarketClearing)

	# Correct RES dispatch
	# Total summed RES generation from dispatch for each t
	ResTotalGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T)), T)

	for t in T
		ResTotalGeneration[t] = sum(P_R_opt[t, :])
	end

	# Maximum output possible
	ResMaxGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T)), T)
	for t in T
		ResMaxGeneration[t] = sum(renewables.infeed[r][t] for r in R)
	end

	# Redistribution among RES
	ResUnitGeneration = JuMP.Containers.DenseAxisArray(zeros(length(T), length(R)), T, R)

	for t in T
		for r in R
			ResUnitGeneration[t, r] = (renewables.infeed[r][t] / ResMaxGeneration[t]) *
			ResTotalGeneration[t]
		end
	end

	# Distributed, corrected P_R_opt
	P_R_opt_dist = ResUnitGeneration

	return(ED_mod,
	       P_opt,
		   P_R_opt,
		   P_R_opt_dist,
		   P_S_opt,
		   D_S_opt,
		   L_S_opt,
		   price
	       )
end


using LinearAlgebra;
using SparseArrays;
using IterativeSolvers;
using MKLSparse; #<! Impportant for the Iterative Solver (Much faster sparseMat * denseVec)

# using Parameters #<! Might help with packing / unpacking of parameters

# https://docs.julialang.org/en/v1/manual/unicode-input/

@enum LinearSolverMode modeAuto = 1 modeItertaive modeDirect
@enum ConvergenceFlag convNumItr = 1 convAdmm convPrimDual

function SolveQuadraticProgram!(vX, mP, vQ, mA, vL, vU, LinSysSolInit, LinSysSol!;
    numIterations = 5000, ϵAbs = 1e-6, ϵRel = 1e-6,
    ρ = 1, σ = 1e-6, α = 1.6, δ = 1e-6, adptΡ::Bool = false,
    fctrΡ = 5, numItrConv = 25, numItrPolish = 10, ϵMinres = 1e-6, numItrMinres = 500)

    numElementsX            = size(vX, 1);
    numRowsP, numColsP      = size(mP);
    numElementsQ            = size(vQ, 1);
    numRowsA, numColsA      = size(mA);
    numElementsL            = size(vL, 1);
    numElementsU            = size(vU, 1);

    # TODO: Numbers should be optimized
    MAX_NUM_ROWS_L  = 5000;
    MAX_DENSITY     = 0.4;

    ρ¹ = 1 / ρ;
    α¹ = 1 - α;

    convFlag    = convNumItr;
    ϵAdmm       = min(ϵAbs, ϵRel) * 1e-2;

    vXX, vZZ, tuSolver = LinSysSolInit(vX, mP, vQ, mA, ρ, ρ¹, σ, numElementsX, numRowsA);

    vXP = zeros(numElementsX); #<! Previous iteration of vX
    vZ  = zeros(numRowsA);
    vY  = zeros(numRowsA);
    vZP = zeros(numRowsA); #<! Previous iteration of vZ

    ρρ = ρ;

    for ii in 1:numIterations
        changedΡ = false;
        if (adptΡ && ((ρρ * fctrΡ < ρ) || (ρρ > fctrΡ * ρ)))
            ρ   = ρρ;
            ρ¹  = 1 / ρ;

            changedΡ = true;
        end

        LinSysSol!(tuSolver, vXX, vZZ, vX, mP, vQ, mA, vZ, vY, ρ, ρ¹, σ, numElementsX, numRowsA, changedΡ);

        copyto!(vXP, vX);
        @. vX = α * vXX + α¹ * vX;

        copyto!(vZP, vZ);
        @. vZ = clamp(α * vZZ + α¹ * vZ + ρ¹ * vY, vL, vU); #<! Projection
        @. vY = vY + ρ * (α * vZZ + α¹ * vZP - vZ);

        if (mod(ii, numItrConv) == 0)
            ρρ, convFlag = CheckConvergence(vX, mP, vQ, mA, vZ, vY, vXP, vZP, ρ, ρρ, adptΡ, ϵAbs, ϵRel, ϵAdmm, convFlag);

            if(convFlag ≠ convNumItr)
                break;
            end
        end

    end

    return convFlag;

end

function CheckConvergence(vX, mP, vQ, mA, vZ, vY, vXP, vZP, ρ, ρρ, adptΡ, ϵAbs, ϵRel, ϵAdmm, convFlag)
    #TODO: Use buffers for the intermediate allocations (mP * vX, mA * vX, ...)
    MIN_VAL_Ρ = 1e-3;
    MAX_VAL_Ρ = 1e6;

    # Pre Computation
    normResPrim = norm(mA * vX - vZ, Inf);
    normResDual = norm(mP * vX + vQ + mA' * vY, Inf);

    maxNormPrim = max(norm(mA * vX, Inf), norm(vZ, Inf));
    maxNormDual = max(norm(mP * vX, Inf), norm(mA' * vY, Inf), norm(vQ, Inf));

    # Adaptive Rho
    if (adptΡ)
        numeratorVal    = normResPrim * maxNormDual;
        denominatorVal  = normResDual * maxNormPrim;
        ρρ              = clamp(ρ * sqrt(numeratorVal / denominatorVal), MIN_VAL_Ρ, MAX_VAL_Ρ);
    end

    # Termination
    epsPrim = ϵAbs + ϵRel * maxNormPrim;
    epsDual = ϵAbs + ϵRel * maxNormDual;

    if ((normResPrim < epsPrim) && (normResDual < epsDual))
        convFlag = convPrimDual;
    end
    if ((norm(vX - vXP, Inf) <= ϵAdmm) && (norm(vZ - vZP, Inf) <= ϵAdmm))
        convFlag = convAdmm;
    end

    return ρρ, convFlag;

end

# Left for reference
function SolveQuadraticProgramRef!(vX, mP, vQ, mA, vL, vU;
    numIterations = 5000, ϵAbs = 1e-6, ϵRel = 1e-6,
    ρ = 1e6, σ = 1e-6, α = 1.6, δ = 1e-6, adptΡ::Bool = false,
    fctrΡ = 5, numItrPolish = 10, linSolverMode::LinearSolverMode = modeAuto,
    ϵPcg = 1e-9, numItrPcg = 15000, ϵMinres = 1e-6, numItrMinres = 500, numItrConv = 25)

    numElementsX            = size(vX, 1);
    numRowsP, numColsP      = size(mP);
    numElementsQ            = size(vQ, 1);
    numRowsA, numColsA      = size(mA);
    numElementsL            = size(vL, 1);
    numElementsU            = size(vU, 1);

    # TODO: Numbers should be optimized
    MAX_NUM_ROWS_L  = 5000;
    MAX_DENSITY     = 0.4;

    MIN_VAL_Ρ = 1e-3;
    MAX_VAL_Ρ = 1e6;

    ρ¹ = 1 / ρ;
    α¹ = 1 - α;

    if (linSolverMode == modeItertaive)
        directSol = false;
    elseif (linSolverMode == modeDirect)
        directSol = true;
    else
        numRowsL    = numRowsP + numRowsA;
        numNonZeros = nnz(mP) + nnz(mA);
        nnzDensity  = numNonZeros / (numRowsL * numRowsL);
        if ((numRowsL <= MAX_NUM_ROWS_L) && (nnzDensity <= MAX_DENSITY))
            directSol = true;
        else
            directSol = false;
        end
    end

    convFlag    = convNumItr;
    ϵAdmm       = min(ϵAbs, ϵRel) * 1e-2;

    vXX = copy(vX);
    vXP = zeros(numElementsX); #<! Previous iteration of vX
    vZ  = zeros(numRowsA);
    vY  = zeros(numRowsA);
    vZZ = zeros(numRowsA);
    vZP = zeros(numRowsA); #<! Previous iteration of vZ

    vV  = zeros(numElementsX + numRowsA);
    vT1 = @view vV[1:numElementsX];
    vT2 = @view vV[(numElementsX + 1):end];

    if (adptΡ)
        # ̂ρ = ρ; #<! Won't work
        ρρ = ρ;

        mAA = mA' * mA;
        mPI = mP + sparse(σ * I, numElementsX, numElementsX);
    end

    if (directSol)
        hDL = ldlt([mP + sparse(σ * I, numElementsX, numElementsX) mA'; mA sparse(-ρ¹ * I, numRowsA, numRowsA)]);
    else
        mL = mP + sparse(σ * I, numElementsX, numElementsX) + (ρ * (mA' * mA));
        # sCgSolver = Krylov.CgSolver(numElementsX, numElementsX, typeof(vX)); #<! For Krylov based
        # mL = LinearOperator(Float64, numElements, numElements, true, true, (vU, vW, α, β) -> begin
        #         mul!(vU, mAA, vW);
        #         mul!(vU, mPI, vW, one(Float64), ρ);
        #     end);
    end

    for ii in 1:numIterations
        if (adptΡ && ((ρρ * fctrΡ < ρ) || (ρρ > fctrΡ * ρ)))
            ρ   = ρρ;
            ρ¹  = 1 / ρ;
            if (directSol)
                hDL = ldlt([mP + sparse(σ * I, numElementsX, numElementsX) mA'; mA sparse(-ρ¹ * I, numRowsA, numRowsA)]);
            else
                mL = mPI + (ρ * mAA);
                #     mL = LinearOperator(Float64, numElements, numElements, true, true, (vU, vW, α, β) -> begin
                #     mul!(vU, mAA, vW);
                #     mul!(vU, mPI, vW, one(Float64), ρ);
                # end);
            end
        end
        if (directSol)
            # vV = hDL \ [σ * vX - vQ; vZ - ρ¹ * vY];
            # @. vXX = vV[1:numElementsX];
            # @. vZZ = vZ + ρ¹ * (vV[(numElementsX + 1):end] - vY);
            @. vT1 = σ * vX - vQ;
            @. vT2 = vZ - ρ¹ * vY;
            # ldiv!(hDL, vV); #<! No support in SuiteSparse
            vV .= hDL \ vV; #<! Like copy!() / copyto!()
            @. vXX = vV[1:numElementsX];
            @. vZZ = vZ + ρ¹ * (vT2 - vY);
        else
            @. vT2 = ρ * vZ - vY;
            mul!(vT1, mA', vT2);
            @. vT1 = σ * vX - vQ + vT1;
            IterativeSolvers.cg!(vXX, mL, vT1, abstol = ϵPcg, maxiter = numItrPcg);
            # cg!(vXX, mL, σ * vX - vQ + mA' * (ρ * vZ - vY), abstol = ϵPcg, maxiter = numItrPcg);
            mul!(vZZ, mA, vXX);
        end

        copyto!(vXP, vX);
        @. vX = α * vXX + α¹ * vX;

        copyto!(vZP, vZ);
        @. vZ = clamp(α * vZZ + α¹ * vZ + ρ¹ * vY, vL, vU); #<! Projection
        @. vY = vY + ρ * (α * vZZ + α¹ * vZP - vZ);

        if (mod(ii, numItrConv) == 0)
            # Pre Computation
            normResPrim = norm(mA * vX - vZ, Inf);
            normResDual = norm(mP * vX + vQ + mA' * vY, Inf);

            maxNormPrim = max(norm(mA * vX, Inf), norm(vZ, Inf));
            maxNormDual = max(norm(mP * vX, Inf), norm(mA' * vY, Inf), norm(vQ, Inf));

            # Adaptive Rho
            if (adptΡ)
                numeratorVal    = normResPrim * maxNormDual;
                denominatorVal  = normResDual * maxNormPrim;
                ρρ              = clamp(ρ * sqrt(numeratorVal / denominatorVal), MIN_VAL_Ρ, MAX_VAL_Ρ);
            end

            # Termination
            epsPrim = ϵAbs + ϵRel * maxNormPrim;
            epsDual = ϵAbs + ϵRel * maxNormDual;

            if ((normResPrim < epsPrim) && (normResDual < epsDual))
                convFlag = convPrimDual;
                break;
            end
            if ((norm(vX - vXP, Inf) <= ϵAdmm) && (norm(vZ - vZP, Inf) <= ϵAdmm))
                convFlag = convAdmm;
                break;
            end
        end

    end

    return convFlag;

end


struct Discretization{LType, UType, PType}
  low_mat::LType                        # Lower triangular matrix of LU composition
  upp_mat::UType                        # Upper triangular matrix of LU composition
  perm_array::PType                     # Permutation array of LU composition
  num_steps_year::Int64                 # Number of time steps per astronomical year
  mesh::Mesh
  model::Model                          # Physical model
  annual_temperature::Array{Float64, 2} # Annual temperature at every time step in an astronomical year
  rhs::Vector{Float64}
  last_rhs::Vector{Float64}
end

function Discretization(mesh, model, num_steps_year; run_garbage_collector = true)
    low_mat, upp_mat, perm_array = compute_lu_matrices(mesh, model, num_steps_year)

    if run_garbage_collector
      GC.gc()
    end

    annual_temperature = fill(5.0, mesh.dof, num_steps_year) # Magic initialization
    rhs     = zeros(mesh.dof)  # TODO: The EBM Fortran code initializes the RHS to zero... Maybe we want to initialize it differently
    last_rhs = zeros(mesh.dof)

    Discretization(low_mat, upp_mat, perm_array, num_steps_year, mesh, model, annual_temperature, rhs, last_rhs)
end

Base.size(discretization::Discretization) = size(discretization.mesh)

function Base.show(io::IO, discretization::Discretization)
  nx, ny = size(discretization)
  print(io, "Discretization() with ", nx, "×", ny, " degrees of freedom")
end

function compute_lu_matrices(mesh, model, num_steps_year)
  mat = compute_matrix(mesh,num_steps_year,model)
  lu_dec = lu(mat)
  low_mat = sparse(lu_dec.L)
  upp_mat = sparse(lu_dec.U)

  return low_mat, upp_mat, lu_dec.p
end


module WeakGradTestsSeq
   using Gridap
   using GridapGeosciences
   import Gridap.Fields: ∇, divergence
   using Plots
   using Test

   include("../ConvergenceAnalysisTools.jl")
   include("../WeakGradTests.jl")

   n_values=generate_n_values(2)
   hs=[2.0/n for n in n_values]
   model_args_series=[(n,) for n in n_values]

   @time ahs0,ak0errors,as0=convergence_study(compute_error_weak_grad,hs,model_args_series,0,4)
   @test as0 ≈ 0.8346320885900106

   n_values=generate_n_values(2,n_max=50)
   hs=[2.0/n for n in n_values]
   model_args_series=[(n,) for n in n_values]

   @time ahs1,ak1errors,as1=convergence_study(compute_error_weak_grad,hs,model_args_series,1,8)
   @test as1 ≈ 1.1034326200306834

end


#------------------------------------------------------------------------------# Mean
"""
Mean of a single series.

```julia
y = randn(100)

o = Mean()
fit!(o, y)

o = Mean(y)
```
"""
type Mean{W <: Weight} <: OnlineStat{ScalarInput}
    value::Float64
    weight::W
end
Mean(wt::Weight = EqualWeight()) = Mean(0.0, wt)
_fit!(o::Mean, y::Real, γ::Float64) = (o.value = smooth(o.value, y, γ))
function _fitbatch!{W <: BatchWeight}(o::Mean{W}, y::AVec, γ::Float64)
    o.value = smooth(o.value, mean(y), γ)
end
Base.mean(o::Mean) = value(o)
center(o::Mean, x::Real) = x - mean(o)
_merge!(o::Mean, o2::Mean, γ) = _fit!(o, mean(o2), γ)

#-----------------------------------------------------------------------------# Means
"""
Means of multiple series, similar to `mean(x, 1)`.

```julia
x = randn(1000, 5)
o = Means(5)
fit!(o, x)
mean(o)
```
"""
type Means{W <: Weight} <: OnlineStat{VectorInput}
    value::VecF
    weight::W
end
Means(p::Int, wgt::Weight = EqualWeight()) = Means(zeros(p), wgt)
_fit!(o::Means, y::AVec, γ::Float64) = smooth!(o.value, y, γ)
function _fitbatch!{W <: BatchWeight}(o::Means{W}, y::AMat, γ::Float64)
    smooth!(o.value, row(mean(y, 1), 1), γ)
end
Base.mean(o::Means) = value(o)
center{T<:Real}(o::Means, x::AVec{T}) = x - mean(o)
_merge!(o::Means, o2::Means, γ) = _fit!(o, mean(o2), γ)

#--------------------------------------------------------------------------# Variance
"""
Univariate variance.

```julia
y = randn(100)
o = Variance(y)
mean(o)
var(o)
std(o)
```
"""
type Variance{W <: Weight} <: OnlineStat{ScalarInput}
    value::Float64
    μ::Float64
    weight::W
end
Variance(wgt::Weight = EqualWeight()) = Variance(0.0, 0.0, wgt)
function _fit!(o::Variance, y::Real, γ::Float64)
    μ = o.μ
    o.μ = smooth(o.μ, y, γ)
    o.value = smooth(o.value, (y - o.μ) * (y - μ), γ)
end
Base.var(o::Variance) = value(o)
Base.std(o::Variance) = sqrt(var(o))
Base.mean(o::Variance) = o.μ
value(o::Variance) = nobs(o) < 2 ? 0.0 : o.value * unbias(o)
center(o::Variance, x::Real) = x - mean(o)
function StatsBase.zscore(o::Variance, x::Real)
    σ = std(o)
    σ == 0.0 ? 1.0 : center(o, x) / σ
end
function _merge!(o::Variance, o2::Variance, γ)
    δ = mean(o2) - mean(o)
    o.value = smooth(o.value, o2.value, γ) + δ ^ 2 * γ * (1.0 - γ)
    o.μ = smooth(o.μ, o2.μ, γ)
end

#-------------------------------------------------------------------------# Variances
"""
Variances of a multiple series, similar to `var(x, 1)`.

```julia
o = Variances(x, EqualWeight())
o = Variances(x)
fit!(o, x2)

mean(o)
var(o)
std(o)
```
"""
type Variances{W <: Weight} <: OnlineStat{VectorInput}
    value::VecF
    μ::VecF
    μold::VecF  # avoid allocation in update
    weight::W
end
function Variances(p::Integer, wgt::Weight = EqualWeight())
    Variances(zeros(p), zeros(p), zeros(p), wgt)
end
function _fit!(o::Variances, y::AVec, γ::Float64)
    copy!(o.μold, o.μ)
    smooth!(o.μ, y, γ)
    for i in eachindex(y)
        o.value[i] = smooth(o.value[i], (y[i] - o.μ[i]) * (y[i] - o.μold[i]), γ)
    end
end
Base.var(o::Variances) = value(o)
Base.std(o::Variances) = sqrt(value(o))
Base.mean(o::Variances) = o.μ
value(o::Variances) = nobs(o) < 2 ? zeros(o.value) : o.value * unbias(o)
center{T<:Real}(o::Variances, x::AVec{T}) = x - mean(o)
function StatsBase.zscore{T<:Real}(o::Variances, x::AVec{T})
    σs = std(o)
    for j in eachindex(σs)
        @inbounds if σs[j] == 0.0
            σs[j] = 1.0
        end
    end
    center(o, x) ./ σs
end
function StatsBase.zscore{T<:Real}(o::Variances, x::AMat{T})
    σs = std(o)
    for j in eachindex(σs)
        if σs[j] == 0.0
            σs[j] = 1.0
        end
    end
    StatsBase.zscore(x, mean(o)', σs')
end
function _merge!(o::Variances, o2::Variances, γ::Float64)
    δ = mean(o2) - mean(o)
    for i in eachindex(o.value)
        o.value[i] = smooth(o.value[i], o2.value[i], γ) + δ[i] ^ 2 * γ * (1.0 - γ)
        o.μ[i] = smooth(o.μ[i], o2.μ[i], γ)
    end
end

#-------------------------------------------------------------------------# CovMatrix
"""
Covariance matrix, similar to `cov(x)`.

```julia
o = CovMatrix(x, EqualWeight())
o = CovMatrix(x)
fit!(o, x2)

cor(o)
cov(o)
mean(o)
var(o)
```
"""
type CovMatrix{W <: Weight} <: OnlineStat{VectorInput}
    value::MatF
    cormat::MatF
    A::MatF  # X'X / n
    B::VecF  # X * 1' / n (column means)
    weight::W
end
function CovMatrix(p::Integer, wgt::Weight = EqualWeight())
    CovMatrix(zeros(p, p), zeros(p,p), zeros(p, p), zeros(p), wgt)
end
function _fit!(o::CovMatrix, x::AVec, γ::Float64)
    rank1_smooth!(o.A, x, γ)
    smooth!(o.B, x, γ)
    o
end
function _fitbatch!(o::CovMatrix, x::AMat, γ::Float64)
    n2 = size(x, 1)
    smooth!(o.B, vec(mean(x, 1)), γ)
    BLAS.syrk!('U', 'T', γ / n2, x, 1.0 - γ, o.A)
    o
end
function value(o::CovMatrix)
    copy!(o.value, unbias(o) * (o.A - BLAS.syrk('U', 'N', 1.0, o.B)))
    _covfill!(o.value)
    o.value
end
function _covfill!(A::MatF)  # fill in lower triangle
    p = size(A, 1)
    for j in 1:p, i in j+1:p
        @inbounds A[i, j] = A[j, i]
    end
end
Base.mean(o::CovMatrix) = o.B
Base.cov(o::CovMatrix) = value(o)
Base.var(o::CovMatrix) = diag(value(o))
Base.std(o::CovMatrix) = [sqrt(x) for x in var(o) ]
function Base.cor(o::CovMatrix)
    copy!(o.cormat, value(o))
    v = 1.0 ./ sqrt(diag(o.cormat))
    scale!(o.cormat, v)
    scale!(v, o.cormat)
    o.cormat
end
function _merge!(o::CovMatrix, o2::CovMatrix, γ::Float64)
    smooth!(o.A, o2.A, γ)
    smooth!(o.B, o2.B, γ)
end

#---------------------------------------------------------------------------# Extrema
"""
Extrema (maximum and minimum).

```julia
o = Extrema(y)
fit!(o, y2)
extrema(o)
```
"""
type Extrema <: OnlineStat{ScalarInput}
    min::Float64
    max::Float64
    weight::EqualWeight
    Extrema() = new(Inf, -Inf, EqualWeight())
    Extrema{T<:Real}(y::AVec{T}) = new(minimum(y), maximum(y), EqualWeight(length(y)))
end
function _fit!(o::Extrema, y::Real, γ::Float64)
    o.min = min(o.min, y)
    o.max = max(o.max, y)
    o
end
Base.extrema(o::Extrema) = (o.min, o.max)
value(o::Extrema) = extrema(o)
function _merge!(o::Extrema, o2::Extrema, γ::Float64)
    o.min = min(o.min, o2.min)
    o.max = max(o.max, o2.max)
end

#-----------------------------------------------------------------------# QuantileSGD
"""
Approximate quantiles via stochastic gradient descent.

```julia
o = QuantileSGD(y, LearningRate())
o = QuantileSGD(y, tau = [.25, .5, .75])
fit!(o, y2)
```
"""
type QuantileSGD{W <: StochasticWeight} <: OnlineStat{ScalarInput}
    value::VecF
    τ::VecF
    weight::W
end
function QuantileSGD(wgt::StochasticWeight = LearningRate();
        tau::VecF = [0.25, 0.5, 0.75], value::VecF = zeros(length(tau))
    )
    for i in 1:length(tau)
        @assert 0 < tau[i] < 1
    end
    QuantileSGD(value, tau, wgt)
end
function _fit!(o::QuantileSGD, y::Float64, γ::Float64)
    @inbounds for i in 1:length(o.τ)
        v = Float64(y < o.value[i]) - o.τ[i]
        o.value[i] = subgrad(o.value[i], γ, v)
    end
end
function _fitbatch!{T <: Real}(o::QuantileSGD, y::AVec{T}, γ::Float64)
    n2 = length(y)
    γ = γ / n2
    @inbounds for yi in y
        for i in 1:length(o.τ)
            v = Float64(yi < o.value[i]) - o.τ[i]
            o.value[i] = subgrad(o.value[i], γ, v)
        end
    end
end
function Base.show(io::IO, o::QuantileSGD)
    printheader(io, "QuantileSGD, τ = $(o.τ)")
    print_value_and_nobs(io, o)
end

#------------------------------------------------------------------------# QuantileMM
"""
Approximate quantiles via an online MM algorithm.  Typically more accurate than
`QuantileSGD`.

```julia
o = QuantileMM(y, LearningRate())
o = QuantileMM(y, tau = [.25, .5, .75])
fit!(o, y2)
```
"""
type QuantileMM{W <: Weight} <: OnlineStat{ScalarInput}
    value::VecF
    τ::VecF
    # "sufficient statistics"
    s::VecF
    t::VecF
    o::Float64

    weight::W
end
function QuantileMM(wgt::Weight = LearningRate();
        tau::VecF = [0.25, 0.5, 0.75], value::VecF = zeros(length(tau))
    )
    p = length(tau)
    for i in 1:p
        @assert 0 < tau[i] < 1
    end
    QuantileMM(value, tau, zeros(p), zeros(p), 0.0, wgt)
end
function _fit!(o::QuantileMM, y::Real, γ::Float64)
    o.o = smooth(o.o, 1.0, γ)
    @inbounds for j in 1:length(o.τ)
        w::Float64 = 1.0 / (abs(y - o.value[j]) + _ϵ)
        o.s[j] = smooth(o.s[j], w * y, γ)
        o.t[j] = smooth(o.t[j], w, γ)
        o.value[j] = (o.s[j] + o.o * (2.0 * o.τ[j] - 1.0)) / o.t[j]
    end
end
function _fitbatch!{T <: Real}(o::QuantileMM, y::AVec{T}, γ::Float64)
    n2 = length(y)
    γ = γ / n2
    o.o = smooth(o.o, 1.0, γ)
    @inbounds for yi in y
        for j in 1:length(o.τ)
            w::Float64 = 1.0 / abs(yi - o.value[j])
            o.s[j] = smooth(o.s[j], w * yi, γ)
            o.t[j] = smooth(o.t[j], w, γ)
        end
    end
    @inbounds for j in 1:length(o.τ)
        o.value[j] = (o.s[j] + o.o * (2.0 * o.τ[j] - 1.0)) / o.t[j]
    end
    o
end
function Base.show(io::IO, o::QuantileMM)
    printheader(io, "QuantileMM, τ = $(o.τ)")
    print_value_and_nobs(io, o)
end

# #----------------------------------------------------------------# QuantileExperiment
#
# type QuantileExperiment <: OnlineStat{ScalarInput}
#     value::VecF
#     τ::VecF
#     weight::EqualWeight
# end
# function QuantileMM(wgt::Weight = LearningRate();
#         tau::VecF = [0.25, 0.5, 0.75], value::VecF = zeros(length(tau))
#     )
#     p = length(tau)
#     for i in 1:p
#         @assert 0 < tau[i] < 1
#     end
#     QuantileMM(value, tau, zeros(p), zeros(p), 0.0, wgt)
# end

#---------------------------------------------------------------------------# Moments
"""
Univariate, first four moments.  Provides `mean`, `var`, `skewness`, `kurtosis`

```julia
o = Moments(x, EqualWeight())
o = Moments(x)
fit!(o, x2)

mean(o)
var(o)
std(o)
StatsBase.skewness(o)
StatsBase.kurtosis(o)
```
"""
type Moments{W <: Weight} <: OnlineStat{ScalarInput}
    value::VecF
    weight::W
    n::Int
    nups::Int
end
Moments(wgt::Weight = EqualWeight()) = Moments(zeros(4), wgt, 0, 0)
function _fit!(o::Moments, y::Real, γ::Float64)
    o.value[1] = smooth(o.value[1], y, γ)
    o.value[2] = smooth(o.value[2], y * y, γ)
    o.value[3] = smooth(o.value[3], y * y * y, γ)
    o.value[4] = smooth(o.value[4], y * y * y * y, γ)
end
Base.mean(o::Moments) = value(o)[1]
Base.var(o::Moments) = (value(o)[2] - value(o)[1] ^ 2) * unbias(o)
Base.std(o::Moments) = sqrt(var(o))
function StatsBase.skewness(o::Moments)
    v = value(o)
    (v[3] - 3.0 * v[1] * var(o) - v[1] ^ 3) / var(o) ^ 1.5
end
function StatsBase.kurtosis(o::Moments)
    v = value(o)
    (v[4] - 4.0 * v[1] * v[3] + 6.0 * v[1] ^ 2 * v[2] - 3.0 * v[1] ^ 4) / var(o) ^ 2 - 3.0
end
function Base.show(io::IO, o::Moments)
    printheader(io, "Moments")
    print_item(io, "mean", mean(o))
    print_item(io, "var", var(o))
    print_item(io, "skewness", skewness(o))
    print_item(io, "kurtosis", kurtosis(o))
    print_item(io, "nobs", nobs(o))
end

#------------------------------------------------------------------------# Diff/Diffs
"""
Track the last value and the last difference.

```julia
o = Diff()
o = Diff(y)
```
"""
type Diff{T <: Real} <: OnlineStat{ScalarInput}
    diff::T
    lastval::T
    weight::EqualWeight
end
Diff() = Diff(0.0, 0.0, EqualWeight())
Diff{T<:Real}(::Type{T}) = Diff(zero(T), zero(T), EqualWeight())
Diff{T<:Real}(x::AVec{T}) = (o = Diff(T); fit!(o, x); o)
value(o::Diff) = o.diff
Base.last(o::Diff) = o.lastval
Base.diff(o::Diff) = o.diff
function _fit!{T<:AbstractFloat}(o::Diff{T}, x::Real, γ::Float64)
    v = convert(T, x)
    o.diff = (nobs(o) == 0 ? zero(T) : v - last(o))
    o.lastval = v
end
function _fit!{T<:Integer}(o::Diff{T}, x::Real, γ::Float64)
    v = round(T, x)
    o.diff = (nobs(o) == 0 ? zero(T) : v - last(o))
    o.lastval = v
end

"""
Track the last value and the last difference for multiple series.  Ignores `Weight`.

```julia
o = Diffs()
o = Diffs(y)
```
"""
type Diffs{T <: Real} <: OnlineStat{VectorInput}
    diffs::Vector{T}
    lastvals::Vector{T}
    weight::EqualWeight
end
Diffs(p::Integer) = Diffs(zeros(p), zeros(p), EqualWeight())
Diffs{T<:Real}(::Type{T}, p::Integer) = Diffs(zeros(T,p), zeros(T,p), EqualWeight())
Diffs{T<:Real}(x::AMat{T}) = (o = Diffs(T,ncols(x)); fit!(o, x); o)

value(o::Diffs) = o.diffs
Base.last(o::Diffs) = o.lastvals
Base.diff(o::Diffs) = o.diffs
function _fit!{T<:Real}(o::Diffs{T}, x::AVec{T}, γ::Float64)
    o.diffs = (nobs(o) == 0 ? zeros(T,length(o.diffs)) : x - last(o))
    o.lastvals = collect(x)
    o
end

#-------------------------------------------------------------------# Sum/Sums
"""
Track the running sum.  Ignores `Weight`.

```julia
o = Sum()
o = Sum(y)
```
"""
type Sum{T <: Real} <: OnlineStat{ScalarInput}
    sum::T
    weight::EqualWeight
end
Sum() = Sum(0.0, EqualWeight())
Sum{T<:Real}(::Type{T}) = Sum(zero(T), EqualWeight())
Sum{T<:Real}(x::AVec{T}) = (o = Sum(T); fit!(o, x); o)
value(o::Sum) = o.sum
Base.sum(o::Sum) = o.sum
function _fit!{T<:AbstractFloat}(o::Sum{T}, x::Real, γ::Float64)
    v = convert(T, x)
    o.sum += v
end
function _fit!{T<:Integer}(o::Sum{T}, x::Real, γ::Float64)
    v = round(T, x)
    o.sum += v
end

"""
Track the running sum for multiple series.  Ignores `Weight`.

```julia
o = Sums()
o = Sums(y)
```
"""
type Sums{T <: Real} <: OnlineStat{VectorInput}
    sums::Vector{T}
    weight::EqualWeight
end
Sums(p::Integer) = Sums(zeros(p), EqualWeight())
Sums{T<:Real}(::Type{T}, p::Integer) = Sums(zeros(T,p), EqualWeight())
Sums{T<:Real}(x::AMat{T}) = (o = Sums(T, ncols(x)); fit!(o, x); o)

value(o::Sums) = o.sums
Base.sum(o::Sums) = o.sums
function _fit!{T<:Real}(o::Sums{T}, x::AVec{T}, γ::Float64)
    for i in eachindex(x)
        o.sums[i] += x[i]
    end
end

#----------------------------------------------------------# convenience constructors
for nm in [:Mean, :Variance, :Moments]
    @eval begin
        function $nm{T <: Real}(y::AVec{T}, wgt::Weight = EqualWeight(); kw...)
            o = $nm(wgt; kw...)
            fit!(o, y)
            o
        end
    end
end
for nm in [:QuantileSGD, :QuantileMM]
    @eval begin
        function $nm{T <: Real}(y::AVec{T}, wgt::Weight = LearningRate(); kw...)
            o = $nm(wgt; kw...)
            fit!(o, y)
            o
        end
    end
end
for nm in [:Means, :Variances, :CovMatrix]
    @eval begin
        function $nm{T <: Real}(y::AMat{T}, wgt::Weight = EqualWeight())
            o = $nm(size(y, 2), wgt)
            fit!(o, y, size(y, 1))
            o
        end
    end
end
for nm in [:Variances]
    @eval begin
        function $nm{T <: Real}(y::AMat{T}, wgt::Weight = EqualWeight())
            o = $nm(size(y, 2), wgt)
            fit!(o, y)
            o
        end
    end
end


immutable Erlang <: ContinuousUnivariateDistribution
    gammad::Gamma

    function Erlang(α::Real, θ::Real)
        isinteger(α) || error("Erlang shape parameter must be an integer")
        new(Gamma(α, θ))
    end

    Erlang(α::Real) = Erlang(α, 1.0)
    Erlang() = new(Gamma())
end

@distr_support Erlang 0.0 Inf

show(io::IO, d::Erlang) = ((α, β) = params(d); show_oneline(io, d, [(:α, α), (:β, β)]))

#### Parameters

shape(d::Erlang) = int(shape(d.gammad))
scale(d::Erlang) = scale(d.gammad)
rate(d::Erlang) = rate(d.gammad)
params(d::Erlang) = ((α, β) = params(d.gammad); (int(α), β))

#### Statistics

mean(d::Erlang) = mean(d.gammad)
var(d::Erlang) = var(d.gammad)
median(d::Erlang) = median(d.gammad)
skewness(d::Erlang) = skewness(d.gammad)
kurtosis(d::Erlang) = kurtosis(d.gammad)

mode(d::Erlang) = mode(d.gammad)
entropy(d::Erlang) = entropy(d.gammad)

#### Evaluation

pdf(d::Erlang, x::Float64) = pdf(d.gammad, x)
logpdf(d::Erlang, x::Float64) = logpdf(d.gammad, x)

cdf(d::Erlang, x::Float64) = cdf(d.gammad, x)
ccdf(d::Erlang, x::Float64) = ccdf(d.gammad, x)
logcdf(d::Erlang, x::Float64) = logcdf(d.gammad, x)
logccdf(d::Erlang, x::Float64) = logccdf(d.gammad, x)

quantile(d::Erlang, p::Float64) = quantile(d.gammad, p)
cquantile(d::Erlang, p::Float64) = cquantile(d.gammad, p)
invlogcdf(d::Erlang, lp::Float64) = invlogcdf(d.gammad, lp)
invlogccdf(d::Erlang, lp::Float64) = invlogccdf(d.gammad, lp)

mgf(d::Erlang, t::Real) = mgf(d.nested_gamma, t)
cf(d::Erlang, t::Real) = cf(d.nested_gamma, t)

#### Sampling

rand(d::Erlang) = rand(d.gammad)


using GLM, QuantileRegressions, CSV, Winston, StatsBase

# Load data
url = "http://vincentarelbundock.github.io/Rdatasets/csv/quantreg/engel.csv"
df = CSV.read(download(url))
ResultQR = qreg(@formula(foodexp ~ income), df, .5)

# Fit quantile regression for a bunch of different quantiles
QNum = 35; # Number of equally spaced quantiles
DataPlot = reduce(vcat, [[coeftable(qreg(@formula(foodexp ~ income), df, i/(QNum+1), IP())).cols[2][2] coeftable(qreg(@formula(foodexp ~ income), df, i/(QNum+1), IP())).cols[3][2]] for i in 1:QNum])

# Fit OLS model to compare
ResultLM = lm(@formula(foodexp ~ income), df) # Fit the model using OLS from the GLM-package
ols = vcat([[coeftable(ResultLM).cols[1][2] coeftable(ResultLM).cols[2][2]] for i in 1:QNum]...)

PlotDF = DataFrame(
    X = range(1, length=QNum, stop=QNum)/(QNum+1),
    Y = DataPlot[:,1],
    ols = ols[:,1],
    Ymin = DataPlot[:,1] - 1.96 * DataPlot[:,2],
    Ymax = DataPlot[:,1] + 1.96 * DataPlot[:,2])

x = PlotDF[:X]
p = FramedPlot()
βτ   = Curve(x, PlotDF[:Y])
βols = Curve(x, PlotDF[:ols] , "color", "red")
ci_low  = Curve(x, PlotDF[:Ymin], "type" , "dash")
ci_high = Curve(x, PlotDF[:Ymax], "type" , "dash")
setattr(βτ,"label","β(τ)")
setattr(βols, "label", "β(ols)")
lgnd = Legend(.8,.2,[βτ, βols])
add(p, βτ, βols, ci_low, ci_high, lgnd)
setattr(p, "title" , "Quantile regression: Food Expenditure ~ Income")
setattr(p, "xlabel", "Quantiles")
setattr(p, "ylabel", "Coefficient on Income")
savefig(p, Pkg.dir("QuantileRegressions")"/examples/qreg_example_plot.png")