using BenchmarkTools

# preallocate arrays
A = rand(1000,1000)
B = rand(1000,1000)
C = rand(1000,1000)

# the inner loop is across columns, Row-Major
function row_major!(C,A,B)
    for i in 1:1000, j in 1:1000
    C[i,j] = A[i,j] + B[i,j]
    end
end
@btime row_major!(C,A,B) # 10s of

# the inner loop is down rows, Column-Major
function col_major!(C,A,B)
    for j in 1:1000, i in 1:1000
    C[i,j] = A[i,j] + B[i,j]
    end
end
@btime col_major!(C,A,B)

function col_major!(C,A,B)
    for j in 1:1000, i in 1:1000
    val = [A[i,j] + B[i,j]] # brackets create 1-element array
    C[i,j] = val[1]
    end
end
@btime col_major!(C,A,B)

function col_major!(C,A,B)
    for j in 1:1000, i in 1:1000
    val = A[i,j] + B[i,j] # omitting brackets
    C[i,j] = val
    end
end
@btime col_major!(C,A,B)

function no_prealloc(A,B)
    C = rand(1000,1000) # preallocate array inside of function
    for j in 1:1000, i in 1:1000
        val = A[i,j] + B[i,j]
        C[i,j] = val
    end
end
@btime no_prealloc(A,B)

C = rand(1000,1000) # preallocate array outside of function
function w_prealloc!(C,A,B)
    for j in 1:1000, i in 1:1000
    val = A[i,j] + B[i,j]
    C[i,j] = val
    end
end
@btime w_prealloc!(C,A,B)

function no_broadcast(A,B,C)
  tmp = A + B
  tmp + C
end
@btime no_broadcast(A,B,C)

function unfused(A,B,C)
  tmp = A .+ B
  tmp .+ C
end
@btime unfused(A,B,C)

function fused(A,B,C)
    A .+ B .+ C
end
@btime fused(A,B,C)

function basic()
    A = ones(1000,1000)
    B = A .- 2 # -1 matrix
    C = A + B # 0 matrix
    D = A - B # 2 matrix
end
basic()

function basic()
    A = zeros(10,10)
    B = A .- 2
    C = A + B
    D = A - B

    return B, D
end

β, δ = basic()

D = zeros(1000,1000)
function fused_1!(D,A,B,C)
    D = A .+ B .+ C
end
δ = fused_1!(D,A,B,C)

D = zeros(1000,1000)
function fused_2!(D,A,B,C)
    for j in 1:1000, i in 1:1000
        D[i,j] = A[i,j] + B[i,j] + C[i,j]
    end
end
δ = fused_2!(D,A,B,C)

D = zeros(1000,1000)
function fused_3!(D,A,B,C)
    D .= A .+ B .+ C
end
fused_3!(D,A,B,C)
δ = fused_1!(D,A,B,C)

@btime fused_1!(D,A,B,C)
@btime fused_2!(D,A,B,C)
@btime fused_3!(D,A,B,C)

a = [1,2,3]

function f!(a)
    a[1] = 10
end
f!(a)
a   # a = [10,2,3]

function g!(a)
    a[:] = [1,2]
end
g!(a)
a

a = 1
b = 1
a === b

a = [1,2,3]
b = [1,2,3]
a === b

@btime f!(a)
@btime g!(a)

function f_alloc(A)
    B = A[1:5,1:5]
end

function f_point(A)
    B = @view A[1:5,1:5]
end

@btime f_alloc(A) # allocate array, 80 ns
@btime f_point(A) # allocate pointer, 27 ns

A = rand(1000,1000)
B = f_alloc(A)
A .= zeros(1000,1000)
B # B is unchanged

A = rand(1000,1000) # Bind A to new 1000x1000 rand matrix
B = f_point(A) # Bind B to a pointer, which points at 5x5 slice of A
A .= zeros(1000,1000) # change contents of A
B # B changes
A = rand(1000,1000) # change what A is ''bound'' to
B # pts to old A's mem; we changed A's bindings; can't access mem B pts to?

ff(x,y) = 2x + 2y
ff(x::Int,y::Int) = 2x + y
ff(x::Float64,y::Float64) = x/y
@show ff(2,5)
@show ff(2.0,5.0)

ff(2.0,5)

ff(x::Number,y::Number) = 2x
ff(x::Union{Int,Float64},y::Union{Int,Float64}) = 10 + x

ff(2.0,5)

function h()
    a = ["1.0",2,3.0]
end
@btime h()

function hh()
    a = [1,2,3]
end
@btime hh()


# Starting point for dot operator sample for Learning Julia

# The dot operator applies an operation to a collection
arr1 = [2,4,6,8,10]
println(arr1)

result1 = arr1 .* 2 # multiply each element in the array by 2
println(result1)

# functions can also be applied to collections this way
result2 = sqrt.(arr1)
println(result2)

# custom functions can also be used this way
f(x) = 3x
result3 = f.(arr1)
println(result3)


using Metatheory

@testset "Reduction Basics" begin
  t = @theory begin
    ~a + ~a --> 2 * (~a)
    ~x / ~x --> 1
    ~x * 1 --> ~x
  end

  # basic theory to check that everything works
  @test rewrite(:(a + a), t) == :(2a)
  @test rewrite(:(a + (x * 1)), t) == :(a + x)
  @test rewrite(:(a + (a * 1)), t; order = :inner) == :(2a)
end

import Base.(+)
@testset "Extending Algebra Operators" begin
  t = @theory begin
    ~a + ~a --> 2(~a)
  end

  # Let's extend an operator from base, for sake of example
  function +(x::Symbol, y)
    rewrite(:($x + $y), t)
  end

  @test (:x + :x) == :(2x)
end

## Free Monoid

@testset "Free Monoid - Overriding identity" begin
  # support symbol literals
  symbol_monoid = @theory begin
    ~a ⋅ :ε --> ~a
    :ε ⋅ ~a --> ~a
    ~a::Symbol --> ~a
    ~a::Symbol ⋅ ~b::Symbol => Symbol(String(a) * String(b))
    # i |> error("unsupported ", i)
  end

  @test rewrite(:(ε ⋅ a ⋅ ε ⋅ b ⋅ c ⋅ (ε ⋅ ε ⋅ d) ⋅ e), symbol_monoid; order = :inner) == :abcde
end

## Interpolation should be possible at runtime

@testset "Calculator" begin
  calculator = @theory begin
    ~x::Number ⊕ ~y::Number => ~x + ~y
    ~x::Number ⊗ ~y::Number => ~x * ~y
    ~x::Number ⊖ ~y::Number => ~x ÷ ~y
    ~x::Symbol --> ~x
    ~x::Number --> ~x
  end
  a = 10

  @test rewrite(:(3 ⊕ 1 ⊕ $a), calculator; order = :inner) == 14
end

## Direct rules
@testset "Direct Rules" begin
  t = @theory begin
    # maps
    ~a * ~b => ((~a isa Number && ~b isa Number) ? ~a * ~b : _lhs_expr)
  end
  @test rewrite(:(3 * 1), t) == 3

  t = @theory begin
    # maps
    ~a::Number * ~b::Number => ~a * ~b
  end
  @test rewrite(:(3 * 1), t) == 3
end

## Take advantage of subtyping.
# Subtyping in Julia has been formalized in this paper
# [Julia Subtyping: A Rational Reconstruction](https://benchung.github.io/papers/jlsub.pdf)

abstract type Vehicle end
abstract type GroundVehicle <: Vehicle end
abstract type AirVehicle <: Vehicle end
struct Airplane <: AirVehicle end
struct Car <: GroundVehicle end

airpl = Airplane()
car = Car()

t = @theory begin
  ~a::AirVehicle * ~b => "flies"
  ~a::GroundVehicle * ~b => "doesnt_fly"
end

@testset "Subtyping" begin

  sf = rewrite(:($airpl * c), t)
  df = rewrite(:($car * c), t)

  @test sf == "flies"
  @test df == "doesnt_fly"
end

@testset "Interpolation" begin
  airpl = Airplane()
  car = Car()
  t = @theory begin
    airpl * ~b => "flies"
    car * ~b => "doesnt_fly"
  end

  sf = rewrite(:($airpl * c), t)
  df = rewrite(:($car * c), t)

  @test sf == "flies"
  @test df == "doesnt_fly"
end

@testset "Segment Variables" begin
  t = @theory begin
    f(~x, ~~y) => Expr(:call, :ok, (~~y)...)
  end
  sf = rewrite(:(f(1, 2, 3, 4)), t)
  @test sf == :(ok(2, 3, 4))

  t = @theory x y begin
    f(x, y...) => Expr(:call, :ok, y...)
  end
  sf = rewrite(:(f(1, 2, 3, 4)), t)
  @test sf == :(ok(2, 3, 4))

  t = @theory x y begin
    f(x, y...) --> ok(y...)
  end
  sf = rewrite(:(f(1, 2, 3, 4)), t)
  @test sf == :(ok(2, 3, 4))
end

module NonCall
using Metatheory
t = [@rule a b (a, b) --> ok(a, b)]

test() = rewrite(:(x, y), t)
end

@testset "Non-Call expressions" begin
  @test NonCall.test() == :(ok(x, y))
end

@testset "Pattern matcher can match on both function object references and name symbols" begin
  ex = :($(+)($(sin)(x)^2, $(cos)(x)^2))
  r = @rule(sin(~x)^2 + cos(~x)^2 --> 1)

  @test r(ex) == 1
end

@testset "Pattern variable as pattern term head" begin
  foo(x) = x + 2
  ex = :(($foo)(bar, 2, pazz))
  r = @rule ((~f)(~x, 2, ~y) => (~f)(2))

  @test r(ex) == 4
end

using TermInterface

using Metatheory.Syntax: @capture
@testset "Capture form" begin
  ex = :(a^a)

  #note that @test inserts a soft local scope (try-catch) that would gobble
  #the matches from assignment statements in @capture macro, so we call it
  #outside the test macro
  ret = @capture ex (~x)^(~x)
  @test ret
  @test @isdefined x
  @test x === :a

  ex = :(b^a)
  ret = @capture ex (~y)^(~y)
  @test !ret
  @test !(@isdefined y)

  ret = @capture :(a + b) (+)(~~z)
  @test ret
  @test @isdefined z
  @test all(z .=== arguments(:(a + b)))

  #a more typical way to use the @capture macro

  f(x) =
    if @capture x (~w)^(~w)
      w
    end

  @test f(:(b^b)) == :b
  @test isnothing(f(:(b + b)))

  x = 1
  r = (@capture x x)
  @test r == true
end

using TermInterface
@testset "Matchable struct" begin
  struct qux
    args
    qux(args...) = new(args)
  end
  TermInterface.operation(::qux) = qux
  TermInterface.istree(::qux) = true
  TermInterface.arguments(x::qux) = [x.args...]

  @capture qux(1, 2) qux(1, 2)

  @test (@rule qux(1, 2) => "hello")(qux(1, 2)) == "hello"
  @test (@rule qux(1, 2) => "hello")(1) === nothing
  @test (@rule 1 => "hello")(1) == "hello"
  @test (@rule 1 => "hello")(qux(1, 2)) === nothing
  @test (@capture qux(1, 2) qux(1, 2))
  @test false == (@capture qux(1, 2) qux(3, 4))
end


# linkage.jl

# converts a distance matrix to the vector format expected by the library
function dist_to_vec(m::Array{T,2}) where {T<:Real}
    # check that matrix is square
    rows = size(m, 1)
    if rows != size(m,2)
        error("dist_to_vec() expects a square matrix.")
    end
    v = T[]
    for c = 1:rows
        for r = (c+1):rows
            push!(v,m[r,c])
        end
    end
    return v
end

#
# d::Array{Float64,2} is the dissimilarity matrix between the points to cluster. You
#   can use the [Distances.jl](https://github.com/JuliaStats/Distances.jl) package to
#   generate dissimilarity matrix.
# method::Symbol is one of the following: `:single`, `:complete`, `:average`, `:weighted`, `:ward`, `:centroid`, `:median`.
#
# *NOTE:* The methods `:ward`, `:centroid`, and `:median` the function assumes that
# the distance metric used is the squared Euclidean distance (e.g. `SqEuclidean()` in Distances.jl).
# This is different to the R interface of fastcluster, which, for the `Ward.D2` method, operates on the
# squares of the distances that are passed to the `hclust` function. (The Python interface operates on the
# squares of the distances passed to the `linkage` function for all three methods, `:ward`, `:centroid`, and `:median`.)
# We choose this way in order to save on memory.
function linkage(d::Array{T,2}, method::Symbol) where {T<:Real}
    return linkage_ext(d, method, false)
end

# The linkage!(d::Array{T,2}, method::Symbol) version of the function
# allows fastcluster to write in the memory occupied by `d` (without
# restoring the memory to the original values afterwards. Use this
# version if memory is scarce and you do not care about the content
# of `d` afterwards.
function linkage!(d::Array{T,2}, method::Symbol) where {T<:Real}
    return linkage_ext(d, method, false)
end

# preserve_input::Bool is an optional argument that, if set to `false`, allows
#   fastcluster to write onto the memory occupied by `d`, thereby preserving
#   memory.
function linkage_ext(d::Array{T,2}, method::Symbol, preserve_input::Bool) where {T<:Real}

    # dictionary for linkage methods, from fastcluster
    mthidx = Dict(:single => 0,
              :complete  => 1,
              :average   => 2,
              :weighted  => 3,
              :ward      => 4,
              :centroid  => 5,
              :median    => 6 )

    nobs = size(d, 1)
    m = zeros(Int32,2*(nobs-1))
    height = zeros(Float64,nobs-1)

    # D = pairwise(metric, X, dims=1)
    d2 = dist_to_vec(d)

    # Cxx.jl version
    #@cxx hclust_fast(nobs, pointer(d), mthidx[method], pointer(m), pointer(height) )

    # call the C function
    t = ccall((:hclust_fast, libfastcluster),
        Int32,
        (Int32, Ptr{Cdouble},Int32, Ptr{Cdouble},Ptr{Cdouble}),
        nobs, d2, mthidx[method], m, height
        )

    return m, height

end

# This function returns the labels for the k-cluster cut.
# Labels are between 1 and k.
function cutree(m::Vector{Int32}, nobs::Int64, k::Int64)

    # allocate memory for new array
    labels = Vector{Int32}(undef, nobs)

    # Cxx.jl version
    #@cxx cutree_k(nobs, pointer(m), k, pointer(labels))

    t = ccall((:cutree_k, libfastcluster),
        Int32,
        (Int32, Ptr{Cdouble}, Int32, Ptr{Cint}),
        nobs, m, k, labels
        )

    labels .+= 1
    # convert this to Int64, better to use in Julia
    labels64::Vector{Int64} = labels

    return labels64

end


@testset "6.1 Hyperbolic sine" begin
    include("6.1.1 (c+d x)^m (a+b sinh)^n.jl")
    include("6.1.3 (e x)^m (a+b sinh(c+d x^n))^p.jl")
    include("6.1.4 (d+e x)^m sinh(a+b x+c x^2)^n.jl")
    include("6.1.5 Hyperbolic sine functions.jl")
    include("6.1.7 hyper^m (a+b sinh^n)^p.jl")
end

test/6 Hyperbolic functions/6.1 Hyperbolic sine/.jl


using IntervalMatrices: exp_overapproximation, correction_hull, _exp_remainder
import MathematicalSystems: discretize

const IM = IntervalMatrices

"""
    discretize(𝑆, δ; [algorithm], [exp_method], [sih_method], [set_operations])

Apply an approximation model to `S` obtaining a discrete initial value problem.

### Input

- `𝑆`             -- initial value problem for a continuous affine ODE with
                     non-deterministic inputs
- `δ`             -- step size
- `algorithm`     -- (optional, default: `"forward"`) the algorithm used to
                     compute the approximation model for the discretization;
                     choose among:

    - `"forward"`    -- use forward-time interpolation
    - `"backward"`   -- use backward-time interpolation
    - `"firstorder"` -- use first-order approximation of the ODE
    - `"nobloating"` -- do not bloat the initial states

- `exp_method`  -- (optional, default: `"base"`) the method used to take the matrix
                    exponential of the coefficient matrix; choose among:

    - `"base"` -- the scaling and squaring method implemented in Julia base,
                  see `?exp` for details
    - `"pade"` -- use Pade approximant method to compute matrix exponentials of
                  sparse matrices, implemented in `Expokit`
    - `"lazy"` -- compute a wrapper type around the matrix exponential, i.e. using
                  the lazy implementation `SparseMatrixExp` from `LazySets` and
                  the evaluation of the action of the matrix exponential using the
                  `expmv` implementation from `Expokit`

- `sih_method`  -- (optional, default: `"concrete"`) the method used to take the
                    symmetric interval hull operation; choose among:

    - `"concrete"` -- compute the full symmetric interval hull using the function
                      `symmetric_interval_hull` from `LazySets.Approximations`
    - `"lazy"`     -- compute a wrapper set type around symmetric interval hull
                      in a lazy way using `SymmetricIntervalHull`

- `set_operations`  -- (optional, default: `"lazy"`) set operations used for the
                       discretized initial states and transformed inputs; choose
                       among:

    - `"lazy"`     -- use lazy convex hull for the initial states and lazy linear
                      map for the inputs
    - `"zonotope"` -- use concrete zonotope operations (linear map and Minkowski sum)
                      and return zonotopes for both the initial states and the
                      inputs of the discretized system

### Output

The initial value problem of a discrete system.

### Algorithm

Let ``𝑆 : x' = Ax(t) + u(t)``, ``x(0) ∈ \\mathcal{X}_0``, ``u(t) ∈ U`` be the
given continuous affine ODE `𝑆`, where `U` is the set of non-deterministic inputs
and ``\\mathcal{X}_0`` is the set of initial states. Recall that the system
`𝑆` is called homogeneous whenever `U` is the empty set.

Given a step size ``δ``, this function computes a set, `Ω₀`, that guarantees to
contain all the trajectories of ``𝑆`` starting at any ``x(0) ∈ \\mathcal{X}_0``
and for any input function that satisfies ``u(t) ∈ U``, for any ``t ∈ [0, δ]``.

The initial value problem returned by this function consists of the set `Ω₀`
together with the coefficient matrix ``ϕ = e^{Aδ}`` and a transformed
set of inputs if `U` is non-empty.

In the literature, the method to obtain `Ω₀` is called the *approximation model*
and different alternatives have been proposed. See the argument `algorithm`
for available options. For the reference to the original papers, see the
docstring of each method `discretize_...`.

In the dense-time case, the transformation is such that the trajectories
of the given continuous system are included in the computed flowpipe of the
discretized system.

In the discrete-time case, there is no bloating of the initial states and the
input is assumed to remain constant between sampled times. Use the option
`algorithm="nobloating"` for this setting.

Several methods to compute the matrix exponential are availabe. Use `exp_method`
to select one. For very large systems, computing the full matrix exponential is
expensive hence it is preferable to compute the action of the matrix exponential
over vectors when needed, `e^{δA} v` for each `v`. Use the option
`exp_method="lazy"` for this purpose.
"""
function discretize(𝑆::InitialValueProblem{<:AbstractContinuousSystem},
                    δ::Float64;
                    algorithm::String="forward",
                    exp_method::String="base",
                    sih_method::String="concrete",
                    set_operations::String="lazy",
                    order::Int=2)

    if algorithm in ["forward", "backward"]
        return discretize_interpolation(𝑆, δ, algorithm=algorithm,
                    exp_method=exp_method, sih_method=sih_method, set_operations=set_operations)

    elseif algorithm == "firstorder"
        if set_operations != "lazy"
            throw(ArgumentError("the algorithm $algorithm with set operations=$set_operations is not implemented"))
        end
        return discretize_firstorder(𝑆, δ, exp_method=exp_method)

    elseif algorithm == "nobloating"
        if set_operations != "lazy"
            throw(ArgumentError("the algorithm $algorithm with set operations=$set_operations is not implemented"))
        end
        return discretize_nobloating(𝑆, δ, exp_method=exp_method)

    elseif algorithm == "interval_matrix"
        return discretize_interval_matrix(𝑆, δ, order;
                                          set_operations=set_operations)
    else
        throw(ArgumentError("the algorithm $algorithm is unknown"))
    end
end

"""
    exp_Aδ(A::AbstractMatrix, δ::Float64; [exp_method])

Compute the matrix exponential ``e^{Aδ}``.

### Input

- `A`           -- coefficient matrix
- `δ`           -- step size
- `exp_method`  -- (optional, default: `"base"`) the method used to take the matrix
                    exponential of the coefficient matrix; choose among:

    - `"base"` -- the scaling and squaring method implemented in Julia base,
                  see `?exp` for details
    - `"pade"` -- use Pade approximant method to compute matrix exponentials of
                  sparse matrices, implemented in `Expokit`
    - `"lazy"` -- compute a wrapper type around the matrix exponential, i.e. using
                  the lazy implementation `SparseMatrixExp` from `LazySets` and
                  the evaluation of the action of the matrix exponential using the
                  `expmv` implementation from `Expokit`

### Output

A matrix.
"""
function exp_Aδ(A::AbstractMatrix{Float64}, δ::Float64; exp_method="base")
    if exp_method == "base"
        return exp(Matrix(A*δ))
    elseif exp_method == "lazy"
        return SparseMatrixExp(A*δ)
    elseif exp_method == "pade"
        return padm(A*δ)
    else
       throw(ArgumentError("the exponentiation method $exp_method is unknown"))
    end
end

@inline function Pmatrix(A, δ, n)
    return [A*δ     sparse(δ*I, n, n)  spzeros(n, n)    ;
            spzeros(n, 2*n          )  sparse(δ*I, n, n);
            spzeros(n, 3*n          )                   ]
end

"""
    Φ₁(A, δ; [exp_method])

Compute the series

```math
Φ₁(A, δ) = ∑_{i=0}^∞ \\dfrac{δ^{i+1}}{(i+1)!}A^i,
```
where ``A`` is a square matrix of order ``n`` and ``δ ∈ \\mathbb{R}_{≥0}``.

### Input

- `A`           -- coefficient matrix
- `δ`           -- step size
- `exp_method`  -- (optional, default: `"base"`) the method used to take the matrix
                    exponential of the coefficient matrix; choose among:

    - `"base"` -- the scaling and squaring method implemented in Julia base,
                  see `?exp` for details
    - `"pade"` -- use Pade approximant method to compute matrix exponentials of
                  sparse matrices, implemented in `Expokit`
    - `"lazy"` -- compute a wrapper type around the matrix exponential, i.e. using
                  the lazy implementation `SparseMatrixExp` from `LazySets` and
                  the evaluation of the action of the matrix exponential using the
                  `expmv` implementation from `Expokit`
### Output

A matrix.

### Algorithm

We use the method from [1]. If ``A`` is invertible, ``Φ₁`` can be computed as

```math
Φ₁(A, δ) = A^{-1}(e^{δA} - I_n).
```

In the general case, implemented in this function, it can be computed as
submatrices of the block matrix

```math
P = \\exp \\begin{pmatrix}
Aδ && δI_n && 0 \\\\
0 && 0 && δI_n \\\\
0 && 0 && 0
\\end{pmatrix}.
```
It can be shown that `Φ₁(A, δ) = P[1:n, (n+1):2*n]`.

[1] Frehse, Goran, et al. "SpaceEx: Scalable verification of hybrid systems."
International Conference on Computer Aided Verification. Springer, Berlin,
Heidelberg, 2011.
"""
function Φ₁(A, δ; exp_method="base")
    n = size(A, 1)
    if exp_method == "base"
        P = exp(Matrix(Pmatrix(A, δ, n)))
        Φ₁_Aδ = P[1:n, (n+1):2*n]

    elseif exp_method == "lazy"
        P = SparseMatrixExp(Pmatrix(A, δ, n))
        Φ₁_Aδ = sparse(get_columns(P, (n+1):2*n)[1:n, :])

    elseif exp_method == "pade"
        P = padm(Pmatrix(A, δ, n))
        Φ₁_Aδ = P[1:n, (n+1):2*n]

    else
        throw(ArgumentError("the exponentiation method $exp_method is unknown"))
    end

    return Φ₁_Aδ
end

"""
    Φ₂(A, δ; [exp_method])

Compute the series

```math
Φ₂(A, δ) = ∑_{i=0}^∞ \\dfrac{δ^{i+2}}{(i+2)!}A^i,
```
where ``A`` is a square matrix of order ``n`` and ``δ ∈ \\mathbb{R}_{≥0}``.

### Input

- `A`           -- coefficient matrix
- `δ`           -- step size
- `exp_method`  -- (optional, default: `"base"`) the method used to take the matrix
                    exponential of the coefficient matrix; choose among:

    - `"base"` -- the scaling and squaring method implemented in Julia base,
                  see `?exp` for details
    - `"pade"` -- use Pade approximant method to compute matrix exponentials of
                  sparse matrices, implemented in `Expokit`
    - `"lazy"` -- compute a wrapper type around the matrix exponential, i.e. using
                  the lazy implementation `SparseMatrixExp` from `LazySets` and
                  the evaluation of the action of the matrix exponential using the
                  `expmv` implementation from `Expokit`

### Output

A matrix.

### Algorithm

We use the method from [1]. If ``A`` is invertible, ``Φ₂`` can be computed as

```math
Φ₂(A, δ) = A^{-2}(e^{δA} - I_n - δA).
```

In the general case, implemented in this function, it can be computed as
submatrices of the block matrix

```math
P = \\exp \\begin{pmatrix}
Aδ && δI_n && 0 \\\\
0 && 0 && δI_n \\\\
0 && 0 && 0
\\end{pmatrix}.
```
It can be shown that `Φ₂(A, δ) = P[1:n, (2*n+1):3*n]`.

[1] Frehse, Goran, et al. "SpaceEx: Scalable verification of hybrid systems."
International Conference on Computer Aided Verification. Springer, Berlin,
Heidelberg, 2011.
"""
function Φ₂(A, δ; exp_method="base")
    n = size(A, 1)
    if exp_method == "base"
        P = exp(Matrix(Pmatrix(A, δ, n)))
        Φ₂_Aδ = P[1:n, (2*n+1):3*n]

    elseif exp_method == "lazy"
        P = SparseMatrixExp(Pmatrix(A, δ, n))
        Φ₂_Aδ = sparse(get_columns(P, (2*n+1):3*n)[1:n, :])

    elseif exp_method == "pade"
        P = padm(Pmatrix(A, δ, n))
        Φ₂_Aδ = P[1:n, (2*n+1):3*n]

    else
       throw(ArgumentError("the exponentiation method $exp_method is unknown"))
    end

    return Φ₂_Aδ
end

"""
    discretize_firstorder(𝑆, δ; [p], [exp_method])

Apply a first-order approximation model to `S` obtaining a discrete initial
value problem.

### Input

- `𝑆`           -- initial value problem for a continuous affine ODE with
                   non-deterministic inputs
- `δ`           -- step size
- `p`           -- (optional, default: `Inf`) parameter in the considered norm
- `exp_method`  -- (optional, default: `"base"`) the method used to take the matrix
                    exponential of the coefficient matrix; choose among:

    - `"base"` -- the scaling and squaring method implemented in Julia base,
                  see `?exp` for details
    - `"pade"` -- use Pade approximant method to compute matrix exponentials of
                  sparse matrices, implemented in `Expokit`
    - `"lazy"` -- compute a wrapper type around the matrix exponential, i.e. using
                  the lazy implementation `SparseMatrixExp` from `LazySets` and
                  the evaluation of the action of the matrix exponential using the
                  `expmv` implementation from `Expokit`

### Output

The initial value problem for a discrete system. In particular:

- if the input system is homogeneous, a `LinearDiscreteSystem` is returned,
- otherwise a `ConstrainedLinearControlDiscreteSystem` is returned.

### Algorithm

Let us define some notation. Let

```math
𝑆 : x' = Ax(t) + u(t)
```
with ``x(0) ∈ \\mathcal{X}_0``, ``u(t) ∈ U`` be the given continuous affine ODE
`𝑆`, where `U` is the set of non-deterministic inputs and ``\\mathcal{X}_0``
is the set of initial states.

Define ``R_{\\mathcal{X}_0} = \\max_{x ∈ \\mathcal{X}_0} ‖x‖``,
`D_{\\mathcal{X}_0} = \\max_{x, y ∈ \\mathcal{X}_0} ‖x-y‖`` and
``R_{U} = \\max_{u ∈ U} ‖u‖``. If only the support functions of ``\\mathcal{X}_0``
and ``U`` are known, these values might be hard to compute for some norms. See
`Notes` below for details.

Let ``Ω₀`` be the set defined as:
```math
Ω₀ = ConvexHull(\\mathcal{X}_0, e^{δA}\\mathcal{X}_0 ⊕ δU ⊕ αB_p)
```
where ``α = (e^{δ ‖A‖} - 1 - δ‖A‖)*(R_{\\mathcal{X}_0} + R_{U} / ‖A‖)`` and
``B_p`` denotes the unit ball for the considered ``p``-norm.

It is proved in [Lemma 1, 1] that the set of states reachable by ``S`` in the time
interval ``[0, δ]``, which we denote ``R_{[0,δ]}(\\mathcal{X}_0)`` here,
is included in ``Ω₀``:

```math
R_{[0,δ]}(\\mathcal{X}_0) ⊆ Ω₀.
```

Moreover, if ``d_H(A, B)`` denotes the Hausdorff distance between the sets ``A``
and ``B`` in ``\\mathbb{R}^n``, then

```math
d_H(Ω₀, R_{[0,δ]}(\\mathcal{X}_0)) ≤ \\frac{1}{4}(e^{δ ‖A‖} - 1) D_{\\mathcal{X}_0} + 2α.
```
Hence, the approximation error can be made arbitrarily small by choosing ``δ``
small enough.

Here we allow ``U`` to be a sequence of time varying non-deterministic inputs.

### Notes

In this implementation, the infinity norm is used by default. Other usual norms
are ``p=1`` and ``p=2``. However, note that not all norms are supported; see the
documentation of `?norm` in `LazySets` for the supported norms.

See also [`discretize_interpolation`](@ref) for an alternative algorithm that
uses less conservative bounds.

[1] Le Guernic, C., & Girard, A., 2010, *Reachability analysis of linear systems
using support functions. Nonlinear Analysis: Hybrid Systems, 4(2), 250-262.*
"""
function discretize_firstorder(𝑆::InitialValueProblem,
                               δ::Float64;
                               p::Float64=Inf,
                               exp_method::String="base")

    # unwrap coefficient matrix and initial states
    A, X0 = 𝑆.s.A, 𝑆.x0

    # system size; A is assumed square
    n = size(A, 1)

    Anorm = opnorm(Matrix(A), p)
    κ = exp(δ*Anorm) - 1.0 - δ*Anorm
    RX0 = norm(X0, p)

    # compute exp(A*δ)
    ϕ = exp_Aδ(A, δ, exp_method=exp_method)

    if inputdim(𝑆) == 0
        α = κ * RX0
        □α = Ballp(p, zeros(n), α)
        Ω0 = ConvexHull(X0, ϕ * X0 ⊕ □α)
        return IVP(CLDS(ϕ, stateset(𝑆.s)), Ω0)

    elseif isaffine(𝑆)
        Uset = inputset(𝑆)
        if Uset isa ConstantInput
            U = next_set(Uset)
            RU = norm(U, p)

            # bloating coefficients
            α = κ*(RX0 + RU/Anorm)
            β = κ*RU/Anorm

            # transformation of the initial states
            □α = Ballp(p, zeros(n), α)
            Ω0 = ConvexHull(X0, ϕ*X0 ⊕ δ*U ⊕ □α)

            # transformation of the inputs
            □β = Ballp(p, zeros(n), β)
            Ud = ConstantInput(δ*U ⊕ □β)
            return IVP(CLCDS(ϕ, I(n), stateset(𝑆.s), Ud), Ω0)

        elseif Uset isa VaryingInput
            Ud = Vector{LazySet}(undef, length(Uset))
            for (i, Ui) in enumerate(Uset)
                RU = norm(Ui, p)

                # bloating factors
                α = κ*(RX0 + RU/Anorm)
                β = κ*RU/Anorm

                if i == 1
                    # transform initial states
                    □α = Ballp(p, zeros(n), α)
                    Ω0 = ConvexHull(X0, ϕ * X0 ⊕ δ * Ui ⊕ □α)
                end

                # transform inputs
                □β = Ballp(p, zeros(n), β)
                Ud[i] = δ*Ui ⊕ □β
            end
            Ud = VaryingInput(Ud)
            return IVP(CLCDS(ϕ, I(n), stateset(𝑆.s), Ud), Ω0)
        else
            throw(ArgumentError("input of type $(typeof(U)) is not allowed"))
        end
    else
        throw(ArgumentError("this function only applies to linear or affine systems"))
    end
end

"""
    discretize_nobloating(𝑆, δ; [exp_method])

Discretize a continuous system without bloating of the initial states, suitable
for discrete-time reachability.

### Input

- `𝑆`          -- a continuous system
- `δ`          -- step size
- `exp_method` -- (optional, default: `"base"`) the method used to take the matrix
                  exponential of the coefficient matrix; choose among:

    - `"base"` -- the scaling and squaring method implemented in Julia base,
                  see `?exp` for details
    - `"pade"` -- use Pade approximant method to compute matrix exponentials of
                  sparse matrices, implemented in `Expokit`
    - `"lazy"` -- compute a wrapper type around the matrix exponential, i.e. using
                  the lazy implementation `SparseMatrixExp` from `LazySets` and
                  the evaluation of the action of the matrix exponential using the
                  `expmv` implementation from `Expokit`

### Output

The initial value problem for a discrete system. In particular:

- if the input system is homogeneous, a `LinearDiscreteSystem` is returned,
- otherwise a `ConstrainedLinearControlDiscreteSystem` is returned.

### Algorithm

Let us define some notation. Let

```math
𝑆 : x' = Ax(t) + u(t)
```
with ``x(0) ∈ \\mathcal{X}_0``, ``u(t) ∈ U`` be the given continuous affine ODE
`𝑆`, where `U` is the set of non-deterministic inputs and ``\\mathcal{X}_0``
is the set of initial states.

The approximation model implemented in this function is such that there is no bloating,
i.e. we don't bloat the initial states and don't multiply the input by the step
size δ, as required for the dense time case.

The transformations are:

- ``Φ ← \\exp^{Aδ}``
- ``Ω₀ ← \\mathcal{X}_0``
- ``V ← Φ₁(A, δ)U(k)``, where ``Φ₁(A, δ)`` is defined in [`Φ₁`](@ref).

Here we allow ``U`` to be a sequence of time varying non-deterministic input sets.
"""
function discretize_nobloating(𝑆::InitialValueProblem{<:AbstractContinuousSystem},
                               δ::Float64;
                               exp_method::String="base")

    # unwrap coefficient matrix and initial states
    A, X0 = 𝑆.s.A, 𝑆.x0
    n = size(A, 1)

    # compute matrix ϕ = exp(Aδ)
    ϕ = exp_Aδ(A, δ, exp_method=exp_method)

    # initial states remain unchanged
    Ω0 = copy(X0)

    # early return for homogeneous systems
    if inputdim(𝑆) == 0
        return IVP(CLDS(ϕ, stateset(𝑆.s)), Ω0)
    end

    # compute matrix to transform the inputs
    Phi1Adelta = Φ₁(A, δ, exp_method=exp_method)
    U = inputset(𝑆)
    Ud = map(ui -> Phi1Adelta * ui, U)

    return IVP(CLCDS(ϕ, I(n), stateset(𝑆.s), Ud), Ω0)
end

"""
    discretize_interpolation(𝑆, δ; [algorithm], [exp_method], [sih_method])

Compute bloating factors using forward or backward interpolation.

### Input

- `𝑆`             -- a continuous system
- `δ`             -- step size
- `algorithm`     -- choose the algorithm to compute the approximation model
                     among `"forward"` and `"backward"`
- `exp_method`    -- (optional, default: `"base"`) the method used to take the matrix
                     exponential of the coefficient matrix; choose among:

    - `"base"` -- the scaling and squaring method implemented in Julia base,
                  see `?exp` for details
    - `"pade"` -- use Pade approximant method to compute matrix exponentials of
                  sparse matrices, implemented in `Expokit`
    - `"lazy"` -- compute a wrapper type around the matrix exponential, i.e. using
                  the lazy implementation `SparseMatrixExp` from `LazySets` and
                  the evaluation of the action of the matrix exponential using the
                  `expmv` implementation from `Expokit`

- `sih_method`    -- (optional, default: `"concrete"`) the method used to take the
                     symmetric interval hull operation; choose among:

    - `"concrete"` -- compute the full symmetric interval hull
    - `"lazy"`     -- compute a wrapper set type around symmetric interval hull in a
                      lazy way

- `set_operations` -- (optional, default: `"lazy"`) set operations used for the
                      discretized initial states and transformed inputs; choose
                      among:

    - `"lazy"`     -- use lazy convex hull for the initial states and lazy
                      linear map for the inputs
    - `"zonotope"` -- use concrete zonotope operations (linear map and Minkowski
                      sum) and return zonotopes for both the initial states and
                      the inputs of the discretized system

### Output

The initial value problem for a discrete system. In particular:

- if the input system is homogeneous, a `LinearDiscreteSystem` is returned,
- otherwise a `ConstrainedLinearControlDiscreteSystem` is returned.

## Algorithm

Let us define some notation. Let

```math
𝑆 : x' = Ax(t) + u(t)
```
with ``x(0) ∈ \\mathcal{X}_0``, ``u(t) ∈ U`` be the given continuous affine ODE
`𝑆`, where `U` is the set of non-deterministic inputs and ``\\mathcal{X}_0``
is the set of initial states.

The transformations are:

- ``Φ ← \\exp^{Aδ}``,
- ``Ω₀ ← ConvexHull(\\mathcal{X}_0, Φ\\mathcal{X}_0 ⊕ δU(0) ⊕ Eψ(U(0), δ) ⊕ E^+(\\mathcal{X}_0, δ))``,
- ``V ← δU(k) ⊕ Eψ(U(k), δ)``.

Here we allow ``U`` to be a sequence of time varying non-deterministic input sets.

For the definition of the sets ``Eψ`` and ``E^+`` see [1]. The  "backward" method
uses ``E^-``.

[1] Frehse, Goran, et al. "SpaceEx: Scalable verification of hybrid systems."
International Conference on Computer Aided Verification. Springer, Berlin,
Heidelberg, 2011.
"""
function discretize_interpolation(𝑆::InitialValueProblem{<:AbstractContinuousSystem},
                                  δ::Float64;
                                  algorithm::String="forward",
                                  exp_method::String="base",
                                  sih_method::String="concrete",
                                  set_operations::String="lazy")

    # used to dispatch on the value of the set operation
    set_operations = Symbol(set_operations)

    if sih_method == "concrete"
        sih = symmetric_interval_hull
    elseif sih_method == "lazy"
        sih = SymmetricIntervalHull
    else
        throw(ArgumentError("the method $sih_method is unknown"))
    end

    # unwrap coefficient matrix and initial states
    A, X0 = 𝑆.s.A, 𝑆.x0

    # compute matrix ϕ = exp(Aδ)
    ϕ = exp_Aδ(A, δ, exp_method=exp_method)

    # compute the transformation matrix to bloat the initial states
    Phi2Aabs = Φ₂(abs.(A), δ, exp_method=exp_method)

    if algorithm == "forward"
        Einit = sih(Phi2Aabs * sih((A * A) * X0))     # use E⁺
    elseif algorithm == "backward"
        Einit = sih(Phi2Aabs * sih((A * A * ϕ) * X0)) # use E⁻
    else
        throw(ArgumentError("the algorithm $algorithm is unknown"))
    end

    # early return for homogeneous systems
    if inputdim(𝑆) == 0
        Ω0 = _discretize_interpolation_homog(X0, ϕ, Einit, Val(set_operations))
        return IVP(CLDS(ϕ, stateset(𝑆.s)), Ω0)
    end

    U = inputset(𝑆)
    U0 = next_set(U, 1)

    Eψ0 = sih(Phi2Aabs * sih(A * U0))
    Ω0, Ud = _discretize_interpolation_inhomog(δ, U0, U, X0, ϕ, Einit, Eψ0, A, sih, Phi2Aabs, Val(set_operations))

    return IVP(CLCDS(ϕ, I(size(A, 1)), stateset(𝑆.s), Ud), Ω0)
end

# version using lazy sets and operations
function _discretize_interpolation_homog(X0, ϕ, Einit, set_operations::Val{:lazy})
    Ω0 = ConvexHull(X0, ϕ * X0 ⊕ Einit)
    return Ω0
end

# version using lazy sets and operations
function _discretize_interpolation_inhomog(δ, U0, U, X0, ϕ, Einit, Eψ0, A, sih, Phi2Aabs, set_operations::Val{:lazy})
    Ω0 = ConvexHull(X0, ϕ * X0 ⊕ δ*U0 ⊕ Eψ0 ⊕ Einit)

    if U isa ConstantInput
        Ud = ConstantInput(δ*U0 ⊕ Eψ0)

    elseif U isa VaryingInput
        Ud = Vector{LazySet}(undef, length(U))
        for (k, Uk) in enumerate(U)
            Eψk = sih(Phi2Aabs * sih(A * Uk))
            Ud[k] = δ * Uk ⊕ Eψk
        end
        Ud = VaryingInput(Ud)

    else
        throw(ArgumentError("input of type $(typeof(U)) is not allowed"))
    end
    return Ω0, Ud
end

# version using concrete operations with zonotopes
function _discretize_interpolation_homog(X0, ϕ, Einit, set_operations::Val{:zonotope})
    Einit = _convert_or_overapproximate(Zonotope, Einit)
    Z1 = _convert_or_overapproximate(Zonotope, X0)
    Z2 = linear_map(ϕ, minkowski_sum(Z1, Einit))
    Ω0 = overapproximate(ConvexHull(Z1, Z2), Zonotope)
    return Ω0
end

# version using concrete operations with zonotopes
function _discretize_interpolation_inhomog(δ, U0, U, X0, ϕ, Einit, Eψ0, A, sih, Phi2Aabs, set_operations::Val{:zonotope})
    n = size(A, 1)
    Einit = _convert_or_overapproximate(Zonotope, Einit)
    Eψ0 = _convert_or_overapproximate(Zonotope, Eψ0)
    Z1 = _convert_or_overapproximate(Zonotope, X0)
    ϕX0 = linear_map(ϕ, Z1)
    U0 = _convert_or_overapproximate(Zonotope, U0)
    δI = Matrix(δ*I, n, n)
    δU0 = linear_map(δI, U0)
    Z2 = reduce(minkowski_sum, [ϕX0, δU0, Eψ0, Einit])
    Ω0 = overapproximate(ConvexHull(Z1, Z2), Zonotope)

    if U isa ConstantInput
        Ud = ConstantInput(minkowski_sum(δU0, Eψ0))

    elseif U isa VaryingInput
        Ud = Vector{LazySet}(undef, length(U))
        for (k, Uk) in enumerate(U)
            Eψk = _convert_or_overapproximate(Zonotope, sih(Phi2Aabs * sih(A * Uk)))
            Uk = _convert_or_overapproximate(Zonotope, Uk)
            δUk = linear_map(δI, Uk)
            Ud[k] = minkowski_sum(δUk, Eψk)
        end
        Ud = VaryingInput(Ud)

    else
        throw(ArgumentError("input of type $(typeof(U)) is not allowed"))
    end
    return Ω0, Ud
end

"""
    discretize_interval_matrix(𝑆::InitialValueProblem, δ::Float64, order::Int)

Discretize an affine continuous system whose dynamics are described by an
interval matrix.

### Input

- `𝑆`              -- a continuous system
- `δ`              -- step size
- `order`          -- order for Taylor-series approximation, ≥ 2
- `set_operations` -- (optional, default: `"zonotope"`) set operations used for
                      the discretized initial states and transformed inputs;
                      choose among:

    - `"lazy"`     -- use lazy convex hull for the initial states and lazy
                      linear map for the inputs
    - `"zonotope"` -- use concrete zonotope operations (linear map and Minkowski
                      sum) and return zonotopes for both the initial states and
                      the inputs of the discretized system

### Output

The discretized system. In particular:

- if the input system is homogeneous, a `LinearDiscreteSystem` is returned,
- otherwise a `ConstrainedLinearControlDiscreteSystem` is returned.

## Algorithm

See the first two lines of Algorithm 1 in [1].

[1] M. Althoff, O. Stursberg, M. Buss. Reachability Analysis of Linear Systems
with Uncertain Parameters and Inputs. CDC 2007.
"""
function discretize_interval_matrix(𝑆::InitialValueProblem, δ::Float64,
                                    order::Int;
                                    set_operations::String="zonotope")
    @assert order > 1 "order must be > 1 but was $order"
    N = Float64

    # used to dispatch on the value of the set operation
    set_ops = Val(Symbol(set_operations))

    A, X0 = 𝑆.s.A, 𝑆.x0
    ϕ = exp_overapproximation(A, δ, order)
    F = correction_hull(A, δ, order)

    Ω0_homog = _discretize_interval_matrix_homog(X0, ϕ, F, set_ops)

    # early return for homogeneous systems
    if inputdim(𝑆) == 0
        return IVP(CLDS(ϕ, stateset(𝑆.s)), Ω0_homog)
    end

    U = inputset(𝑆)
    n = size(A, 1)

    # `ΣM` sums up the interval matrices that are multiplied with the inputs:
    # Σᵢ (Mᵢ * V) = (Σᵢ Mᵢ) * V

    A² = A * A
    Iδ = IntervalMatrix(Diagonal(fill(IntervalMatrices.Interval(δ), n)))
    IδW = Iδ + 1/2 * δ^2 * A + 1/6 * δ^3 * A²
    ΣM = IδW

    if order > 2
        # i = 2
        αᵢ₊₁ = 6 # factorial of (i+1)
        Aⁱ = A²
        δⁱ⁺¹ = δ^3
        @inbounds for i in 3:order
            αᵢ₊₁ *= i+1
            δⁱ⁺¹ *= δ
            Aⁱ *= A
            ΣM += (δⁱ⁺¹/αᵢ₊₁) * Aⁱ
        end
    end

    E = _exp_remainder(A, δ, order; n=n)
    ΣM += E * δ

    Ω0, Ud = _discretize_interval_matrix_inhomog(Ω0_homog, U, ΣM, set_ops)

    # create identity interval matrix
    B = IntervalMatrix(Diagonal(fill(IntervalMatrices.Interval(one(N)), n)))

    return IVP(CLCDS(ϕ, B, stateset(𝑆.s), Ud), Ω0)
end

# version using lazy sets and operations
function _discretize_interval_matrix_homog(X0, ϕ, F, set_ops::Val{:lazy})
    Ω0 = ConvexHull(X0, LinearMap(ϕ, X0)) + LinearMap(F, X0)
    return Ω0
end

# version using concrete operations with zonotopes
function _discretize_interval_matrix_homog(X0, ϕ, F, set_ops::Val{:zonotope})
    X1 = overapproximate(LinearMap(ϕ, X0), Zonotope)
    CH_X0_X1 = overapproximate(ConvexHull(X0, X1), Zonotope)
    F_X0 = overapproximate(LinearMap(F, X0), Zonotope)
    Ω0 = minkowski_sum(CH_X0_X1, F_X0)
    return Ω0
end

# version using lazy sets and operations
function _discretize_interval_matrix_inhomog(Ω0_homog, U, M, set_ops::Val{:lazy})
    U0 = next_set(U, 1)
    Ω0_inhomog = M * U0
    Ω0 = MinkowskiSum(Ω0_homog, Ω0_inhomog)
    Ud = _discretize_interval_matrix_inhomog_wrap_inputs(U, Ω0_inhomog)
    return Ω0, Ud
end

# version using concrete operations with zonotopes
function _discretize_interval_matrix_inhomog(Ω0_homog, U, M,
                                             set_ops::Val{:zonotope})
    U0 = next_set(U, 1)
    Ω0_inhomog = overapproximate(M * U0, Zonotope)
    Ω0 = minkowski_sum(Ω0_homog, Ω0_inhomog)
    Ud = _discretize_interval_matrix_inhomog_wrap_inputs(U, Ω0_inhomog)
    return Ω0, Ud
end

function _discretize_interval_matrix_inhomog_wrap_inputs(U, Ω0_inhomog)
    if U isa ConstantInput
        Ud = ConstantInput(Ω0_inhomog)
    elseif U isa VaryingInput
        throw(ArgumentError("varying inputs with interval matrices are not " *
                            "supported yet"))
    else
        throw(ArgumentError("input of type $(typeof(U)) is not allowed"))
    end
    return Ud
end

# fallback implementation for conversion (if applicable) or overapproximation
function _convert_or_overapproximate(X::Type{<:AbstractPolytope}, Y::LazySet)
    if applicable(convert, X, Y)
        return convert(X, Y)
    elseif applicable(overapproximate, Y, X)
        return overapproximate(Y, X)
    else
        return convert(X, overapproximate(Y, Hyperrectangle))
    end
end


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Description
#
#   Compute the equation of time.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# References
#
#   [1] Vallado, D. A (2013). Fundamentals of Astrodynamics and Applications.
#       Microcosm Press, Hawthorne, CA.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

export equation_of_time

"""
    equation_of_time(JD::Number)

Compute the difference between the Sun apparent local time and the Sun mean
local time [rad], which is called Equation of Time, at the Julian Day `JD`. The
algorithm was adapted from [1, p. 178, 277-279].

"""
function equation_of_time(JD::Number)
    # Constants
    deg2rad = π/180

    # Number of Julian centuries from J2000 epoch.
    T_UT1 = (JD-JD_J2000)/36525.0

    # Mean longitude of the Sun.
    λ_m = mod(280.460 + 36000.771T_UT1, 360)

    # Mean anomaly of the Sun.
    #
    # Here, we should use T_TBD (Barycentric Dynamical Time). However, it is
    # sufficient to use T_UT1 because this is a low precision computation [1].
    Ms = mod(357.5291092 + 35999.05034T_UT1, 360)*deg2rad

    # Auxiliary variables.
    sin_Ms  = sin(Ms)
    sin_2Ms = sin(2Ms)

    # Ecliptic latitude of the Sun.
    λ_ecliptic = mod(λ_m + 1.914666471sin_Ms + 0.019994643sin_2Ms, 360)*deg2rad

    # Compute the equation of time [rad].
    eot = -1.914666471sin_Ms     - 0.019994643sin_2Ms +
           2.466sin(2λ_ecliptic) - 0.0053sin(4λ_ecliptic)

    eot*deg2rad
end


@testset "plots_std.jl" begin
    n = 5000

    x = rand(n)

    ybm = rand.(GeneralizedExtremeValue.(x, 1.0, 0.1))
    fmbm = MaximumLikelihoodEVA(BlockMaxima(Variable("y", ybm), locationcov = [Variable("x", x)]), [0.0, 1.0, 1.0, 0.1])

    yte = rand.(GeneralizedPareto.(x, 0.1))
    fmte = MaximumLikelihoodEVA(ThresholdExceedance(Variable("y", yte), logscalecov = [Variable("x", x)]), [0.0, 1.0, 0.1])

    ys = rand(GeneralizedExtremeValue(0.0, 1.0, 0.1), n)
    fms = MaximumLikelihoodEVA(BlockMaxima(Variable("y", ys)), [0.0, 1.0, 0.1])

    @testset "standardize(y, μ, σ, ξ)" begin
        # Simple standardized values
        std̂ = Extremes.standardize(1, 0.0, 1.0, 0.1)

        @test std̂ ≈ 0.9531 atol = 0.0001

    end

    @testset "standardize(fm)" begin
        # Function returns vector with n values
        std̂ = Extremes.standardize(fmbm)

        @test length(std̂) == n

    end

    @testset "standardize(fm)" begin
        # Function returns vector with n values
        std̂ = Extremes.standardize(fmte)

        @test length(std̂) == n

    end

    @testset "standarddist(fm)" begin
        # BlockMaxima standard distribution is Gumbel
        dist = Extremes.standarddist(fmbm.model)

        @test dist == Gumbel()

    end

    @testset "standarddist(fm)" begin
        # ThresholdExceedance standard distribution is Exponential
        dist = Extremes.standarddist(fmte.model)

        @test dist == Exponential()

    end

    @testset "probplot_std_data(fm)" begin
        dfbm = probplot_std_data(fmbm)
        dfte = probplot_std_data(fmte)

        # Returns a dataframe with n values in column Model with BlockMaxima
        @test length(dfbm[:, :Model]) == n

        # Returns a dataframe with n values in column Empirical with BlockMaxima
        @test length(dfbm[:, :Empirical]) == n

        # Returns a dataframe with n values in column Model with ThresholdExceedance
        @test length(dfte[:, :Model]) == n

        # Returns a dataframe with n values in column Empirical with ThresholdExceedance
        @test length(dfte[:, :Empirical]) == n

    end

    @testset "qqplot_std_data(fm)" begin
        dfbm = qqplot_std_data(fmbm)
        dfte = qqplot_std_data(fmte)

        # Returns a dataframe with n values in column Model with BlockMaxima
        @test length(dfbm[:, :Model]) == n

        # Returns a dataframe with n values in column Empirical with BlockMaxima
        @test length(dfbm[:, :Empirical]) == n

        # Returns a dataframe with n values in column Model with ThresholdExceedance
        @test length(dfte[:, :Model]) == n

        # Returns a dataframe with n values in column Empirical with ThresholdExceedance
        @test length(dfte[:, :Empirical]) == n

    end

end


@doc raw"""
`PSDE`: Stratonovich Partitioned Stochastic Differential Equation

Defines a partitioned stochastic differential initial value problem
```math
\begin{aligned}
dq (t) &= v(t, q(t)) \, dt + B(t, q(t)) \circ dW , & q(t_{0}) &= q_{0} , \\
dp (t) &= f(t, q(t)) \, dt + G(t, q(t)) \circ dW , & p(t_{0}) &= p_{0}
\end{aligned}
```
with the drift vector fields ``v`` and ``f``, diffusion matrices ``B`` and ``G``,
initial conditions ``q_{0}`` and ``p_{0}``, the dynamical variables ``(q,p)`` taking
values in ``\mathbb{R}^{d} \times \mathbb{R}^{d}``, and the m-dimensional Wiener process W

### Parameters

* `DT <: Number`: data type
* `TT <: Real`: time step type
* `AT <: AbstractArray{DT}`: array type
* `vType <: Function`: type of `v`
* `fType <: Function`: type of `f`
* `BType <: Function`: type of `B`
* `GType <: Function`: type of `G`
* `pType <: Union{NamedTuple,Nothing}`: parameters type

### Fields

* `d`:  dimension of dynamical variable ``q`` and the vector field ``v``
* `m`:  dimension of the Wiener process
* `ns`: number of sample paths
* `v`:  function computing the drift vector field for the position variable ``q``
* `f`:  function computing the drift vector field for the momentum variable ``p``
* `B`:  function computing the d x m diffusion matrix for the position variable ``q``
* `G`:  function computing the d x m diffusion matrix for the momentum variable ``p``
* `t₀`: initial time
* `q₀`: initial condition for dynamical variable ``q`` (may be a random variable itself)
* `p₀`: initial condition for dynamical variable ``p`` (may be a random variable itself)
* `parameters`: either a `NamedTuple` containing the equations parameters or `nothing`
* `periodicity`: determines the periodicity of the state vector `q` for cutting periodic solutions

The functions `v`, `f`, `B` and `G`, providing the drift vector fields and diffusion matrices, take four arguments,
`v(t, q, p, v)`, `f(t, q, p, f)`, `B(t, q, p,  B)` and `G(t, q, p, G)`, where `t` is the current time, `(q, p)` is the
current solution vector, and `v`, `f`, `B` and `G` are the variables which hold the result
of evaluating the vector fields ``v``, ``f`` and the matrices ``B``, ``G`` on `t` and `(q,p)`.

### Constructors

```julia
PSDE(m, ns, v, f, B, G, t₀, q₀, p₀; parameters=NullParameters(), periodicity=zero(q₀[begin]))
PSDE(m, ns, v, f, B, G, q₀::StateVector, p₀::StateVector; kwargs...) = PSDE(m, ns, v, f, B, G, 0.0, q₀, p₀; kwargs...)
PSDE(m, ns, v, f, B, G, t₀, q₀::State, p₀::State; kwargs...) = PSDE(m, ns, v, f, B, G, t₀, [q₀], [p₀]; kwargs...)
PSDE(m, ns, v, f, B, G, q₀::State, p₀::State; kwargs...) = PSDE(m, ns, v, f, B, G, 0.0, q₀, p₀; kwargs...)
```

### Example

```julia
    function v(λ, t, q, v)
        v[1] = λ*q[1]
        v[2] = λ*q[2]
    end

    function B(μ, t, q, B)
        B[1] = μ*q[1]
        B[2] = μ*q[2]
    end

    t₀ = 0.
    q₀ = [1., 1.]
    λ  = 2.
    μ  = 1.

    v_sde = (t, q, v) -> v(λ, t, q, v)
    B_sde = (t, q, B) -> B(μ, t, q, B)

    sde = SDE(v_sde, B_sde, t₀, q₀)
```
"""
struct PSDE{dType <: Number, tType <: Real, arrayType <: AbstractArray{dType},
            vType <: Function, fType <: Function,
            BType <: Function, GType <: Function,
            pType <: OptionalParameters} <: AbstractEquationPSDE{Nothing,Nothing,Nothing}

    d::Int
    m::Int
    ns::Int
    v::vType
    f::fType
    B::BType
    G::GType
    t₀::tType
    q₀::Vector{arrayType}
    p₀::Vector{arrayType}
    parameters::pType
    periodicity::arrayType

    function PSDE(m, ns, v::vType, f::fType, B::BType, G::GType,
                  t₀::tType, q₀::Vector{arrayType}, p₀::Vector{arrayType};
                  parameters=NullParameters(), periodicity=zero(q₀[begin])) where {
                        dType <: Number, tType <: Real, arrayType <: AbstractArray{dType},
                        vType <: Function, fType <: Function,
                        BType <: Function, GType <: Function}

        d  = length(q₀[begin])
        ni = length(q₀)

        @assert length(q₀) == length(p₀)
        @assert all(length(q) == d for q in q₀)
        @assert all(length(p) == d for p in p₀)

        @assert ns ≥ 1
        @assert ni == 1 || ns == 1
        # either multiple deterministic initial conditions and one sample path
        # or one deterministic initial condition and multiple sample paths

        new{dType, tType, arrayType, vType, fType, BType, GType, typeof(parameters)}(d, m, ns, v, f, B, G, t₀, q₀, p₀, parameters, periodicity)
    end
end

PSDE(m, ns, v, f, B, G, q₀::StateVector, p₀::StateVector; kwargs...) = PSDE(m, ns, v, f, B, G, 0.0, q₀, p₀; kwargs...)
PSDE(m, ns, v, f, B, G, t₀, q₀::State, p₀::State; kwargs...) = PSDE(m, ns, v, f, B, G, t₀, [q₀], [p₀]; kwargs...)
PSDE(m, ns, v, f, B, G, q₀::State, p₀::State; kwargs...) = PSDE(m, ns, v, f, B, G, 0.0, q₀, p₀; kwargs...)

# const PSDEHT{HT,DT,TT,AT,VT,FT,BT,GT,PT} = PSDE{DT,TT,AT,VT,FT,BT,GT,HT,PT} # type alias for dispatch on Hamiltonian type parameter
# const PSDEPT{PT,DT,TT,AT,VT,FT,BT,GT,HT} = PSDE{DT,TT,AT,VT,FT,BT,GT,HT,PT} # type alias for dispatch on parameters type parameter
const PSDEPT{PT,DT,TT,AT,VT,FT,BT,GT} = PSDE{DT,TT,AT,VT,FT,BT,GT,PT} # type alias for dispatch on parameters type parameter

Base.hash(sde::PSDE, h::UInt) = hash(sde.d, hash(sde.m, hash(sde.ns,
                                hash(sde.v, hash(sde.f, hash(sde.B, hash(sde.G,
                                hash(sde.t₀, hash(sde.q₀, hash(sde.p₀,
                                hash(sde.parameters, hash(sde.periodicity, h))))))))))))

Base.:(==)(sde1::PSDE, sde2::PSDE) = (
                                sde1.d == sde2.d
                             && sde1.m == sde2.m
                             && sde1.ns == sde2.ns
                             && sde1.v == sde2.v
                             && sde1.f == sde2.f
                             && sde1.B == sde2.B
                             && sde1.G == sde2.G
                             && sde1.t₀ == sde2.t₀
                             && sde1.q₀ == sde2.q₀
                             && sde1.p₀ == sde2.p₀
                             && sde1.parameters  == sde2.parameters
                             && sde1.periodicity == sde2.periodicity)

function Base.similar(equ::PSDE, t₀::Real, q₀::StateVector, p₀::StateVector, ns::Int=equ.ns)
    @assert all([length(q) == ndims(equ) for q in q₀])
    @assert all([length(p) == ndims(equ) for p in p₀])
    PSDE(equ.m, ns, equ.v, equ.f, equ.B, equ.G, t₀, q₀, p₀; parameters=equ.parameters, periodicity=equ.periodicity)
end

Base.similar(equ::PSDE, t₀::Real, q₀::State, p₀::State, ns::Int=equ.ns; kwargs...) = similar(equ, t₀, [q₀], [p₀], ns; kwargs...)
Base.similar(equ::PSDE, q₀::Union{State,StateVector}, p₀::Union{State,StateVector}, ns::Int=equ.ns; kwargs...) = similar(equ, equ.t₀, q₀, p₀, ns; kwargs...)

Base.ndims(equ::PSDE) = equ.d
Base.axes(equ::PSDE) = axes(equ.q₀[begin])
GeometricBase.nsamples(equ::PSDE) = length(equ.q₀)
GeometricBase.periodicity(equ::PSDE) = equ.periodicity

initial_conditions(equ::PSDE) = (equ.t₀, equ.q₀, equ.p₀)

# hashamiltonian(::PSDEHT{<:Nothing}) = false
# hashamiltonian(::PSDEHT{<:Function}) = true

hasparameters(::PSDEPT{<:NullParameters}) = false
hasparameters(::PSDEPT{<:NamedTuple}) = true

_get_v(equ::PSDE) = hasparameters(equ) ? (t,q,p,v) -> equ.v(t, q, p, v, equ.parameters) : equ.v
_get_f(equ::PSDE) = hasparameters(equ) ? (t,q,p,f) -> equ.f(t, q, p, f, equ.parameters) : equ.f
_get_B(equ::PSDE) = hasparameters(equ) ? (t,q,p,B) -> equ.B(t, q, p, B, equ.parameters) : equ.B
_get_G(equ::PSDE) = hasparameters(equ) ? (t,q,p,G) -> equ.G(t, q, p, G, equ.parameters) : equ.G
# _get_h(equ::PSDE) = hasparameters(equ) ? (t,q,p) -> equ.h(t, q, p, equ.parameters) : equ.h

function functions(equ::PSDE)
    names = (:v,:f,:B,:G)
    equs  = (_get_v(equ), _get_f(equ), _get_B(equ), _get_G(equ))

    # if hashamiltonian(equ)
    #     names = (names..., :h)
    #     equs  = (equs..., _get_h(equ))
    # end

    NamedTuple{names}(equs)
end