"""
    struct ProductSpace{S<:ElementarySpace, N} <: CompositeSpace{S}

A `ProductSpace` is a tensor product space of `N` vector spaces of type
`S<:ElementarySpace`. Only tensor products between [`ElementarySpace`](@ref) objects of the
same type are allowed.
"""
struct ProductSpace{S<:ElementarySpace, N} <: CompositeSpace{S}
    spaces::NTuple{N, S}
end
ProductSpace(spaces::Vararg{S,N}) where {S<:ElementarySpace, N} =
    ProductSpace{S,N}(spaces)
ProductSpace{S,N}(spaces::Vararg{S,N}) where {S<:ElementarySpace, N} =
    ProductSpace{S,N}(spaces)
ProductSpace{S}(spaces) where {S<:ElementarySpace} = ProductSpace{S,length(spaces)}(spaces)
ProductSpace(P::ProductSpace) = P

# Corresponding methods
#-----------------------
"""
    dims(::ProductSpace{S,N}) -> Dims{N} = NTuple{N,Int}

Return the dimensions of the spaces in the tensor product space as a tuple of integers.
"""
dims(P::ProductSpace) = map(dim, P.spaces)
dim(P::ProductSpace, n::Int) = dim(P.spaces[n])
dim(P::ProductSpace) = prod(dims(P))

Base.axes(P::ProductSpace) = map(axes, P.spaces)
Base.axes(P::ProductSpace, n::Int) = axes(P.spaces[n])

dual(P::ProductSpace{<:ElementarySpace,0}) = P
dual(P::ProductSpace) = ProductSpace(map(dual, reverse(P.spaces)))

# Base.conj(P::ProductSpace) = ProductSpace(map(conj, P.spaces))

function Base.show(io::IO, P::ProductSpace{S}) where {S<:ElementarySpace}
    spaces = P.spaces
    if length(spaces) == 0
        print(io,"ProductSpace{", S, ",0}")
    end
    if length(spaces) == 1
        print(io,"ProductSpace")
    end
    print(io,"(")
    for i in 1:length(spaces)
        i==1 || print(io," ⊗ ")
        show(io, spaces[i])
    end
    print(io,")")
end

# more specific methods
"""
    sectors(P::ProductSpace{S,N}) where {S<:ElementarySpace}

Return an iterator over all possible combinations of sectors (represented as an
`NTuple{N,sectortype(S)}`) that can appear within the tensor product space `P`.
"""
sectors(P::ProductSpace) = _sectors(P, sectortype(P))
_sectors(P::ProductSpace{<:ElementarySpace, N}, ::Type{Trivial}) where {N} =
    (ntuple(n->Trivial(), StaticLength{N}()),) # speed up sectors for ungraded spaces
_sectors(P::ProductSpace{<:ElementarySpace, N}, ::Type{<:Sector}) where {N} =
    product(map(sectors, P.spaces)...)

"""
    hassector(P::ProductSpace{S,N}, s::NTuple{N,sectortype(S)}) where {S<:ElementarySpace}
    -> Bool

Query whether `P` has a non-zero degeneracy of sector `s`, representing a combination of
sectors on the individual tensor indices.
"""
hassector(V::ProductSpace{<:ElementarySpace,N}, s::NTuple{N}) where {N} =
    reduce(&, map(hassector, V.spaces, s); init = true)

"""
    dims(P::ProductSpace{S,N}, s::NTuple{N,sectortype(S)}) where {S<:ElementarySpace}
    -> Dims{N} = NTuple{N,Int}

Return the degeneracy dimensions corresponding to a tuple of sectors `s` for each of the
spaces in the tensor product `P`.
"""
dims(P::ProductSpace{<:ElementarySpace, N}, sector::NTuple{N,<:Sector}) where {N} =
    map(dim, P.spaces, sector)

"""
    dims(P::ProductSpace{S,N}, s::NTuple{N,sectortype(S)}) where {S<:ElementarySpace}
    -> Int

Return the total degeneracy dimension corresponding to a tuple of sectors for each of the
spaces in the tensor product, obtained as `prod(dims(P, s))``.
"""
dim(P::ProductSpace{<:ElementarySpace, N}, sector::NTuple{N,<:Sector}) where {N} =
    reduce(*, dims(P, sector); init = 1)

Base.axes(P::ProductSpace{<:ElementarySpace,N}, sectors::NTuple{N,<:Sector}) where {N} =
    map(axes, P.spaces, sectors)

"""
    blocksectors(P::ProductSpace)

Return an iterator over the different unique coupled sector labels, i.e. the different
fusion outputs that can be obtained by fusing the sectors present in the different spaces
that make up the `ProductSpace` instance.
"""
function blocksectors(P::ProductSpace{S,N}) where {S,N}
    G = sectortype(S)
    if G == Trivial
        return TrivialOrEmptyIterator(dim(P) == 0)
    end
    bs = Vector{G}()
    if N == 0
        push!(bs, one(G))
    elseif N == 1
        for s in sectors(P)
            push!(bs, first(s))
        end
    else
        for s in sectors(P)
            for c in ⊗(s...)
                if !(c in bs)
                    push!(bs, c)
                end
            end
        end
        # return foldl(union!, Set{G}(), (⊗(s...) for s in sectors(P)))
    end
    return bs
end

"""
    blockdim(P::ProductSpace, c::Sector)

Return the total dimension of a coupled sector `c` in the product space, by summing over
all `dim(P, s)` for all tuples of sectors `s::NTuple{N,<:Sector}` that can fuse to  `c`,
counted with the correct multiplicity (i.e. number of ways in which `s` can fuse to `c`).
"""
function blockdim(P::ProductSpace, c::Sector)
    sectortype(P) == typeof(c) || throw(SectorMismatch())
    d = 0
    for s in sectors(P)
        ds = dim(P, s)
        d += length(fusiontrees(s, c))*ds
    end
    return d
end

Base.:(==)(P1::ProductSpace, P2::ProductSpace) = (P1.spaces == P2.spaces)

Base.hash(P::ProductSpace, h::UInt) = hash(P.spaces, h)

# Default construction from product of spaces
#---------------------------------------------
⊗(V1::S, V2::S) where {S<:ElementarySpace}= ProductSpace((V1, V2))
⊗(P1::ProductSpace{S}, V2::S) where {S<:ElementarySpace} =
    ProductSpace(tuple(P1.spaces..., V2))
⊗(V1::S, P2::ProductSpace{S}) where {S<:ElementarySpace} =
    ProductSpace(tuple(V1, P2.spaces...))
⊗(P1::ProductSpace{S}, P2::ProductSpace{S}) where {S<:ElementarySpace} =
    ProductSpace(tuple(P1.spaces..., P2.spaces...))
⊗(P::ProductSpace{S,0}, ::ProductSpace{S,0}) where {S<:ElementarySpace} = P
⊗(P::ProductSpace{S}, ::ProductSpace{S,0}) where {S<:ElementarySpace} = P
⊗(::ProductSpace{S,0}, P::ProductSpace{S}) where {S<:ElementarySpace} = P
⊗(V::ElementarySpace) = ProductSpace((V,))
⊗(P::ProductSpace) = P

# unit element with respect to the monoidal structure of taking tensor products
"""
    one(::S) where {S<:ElementarySpace} -> ProductSpace{S,0}
    one(::ProductSpace{S}) where {S<:ElementarySpace} -> ProductSpace{S,0}

Return a tensor product of zero spaces of type `S`, i.e. this is the unit object under the
tensor product operation, such that `V ⊗ one(V) == V`.
"""
Base.one(V::VectorSpace) = one(typeof(V))
Base.one(::Type{<:ProductSpace{S}}) where {S<:ElementarySpace} = ProductSpace{S,0}(())
Base.one(::Type{S}) where {S<:ElementarySpace} = ProductSpace{S,0}(())

Base.convert(::Type{<:ProductSpace}, V::ElementarySpace) = ProductSpace((V,))
Base.:^(V::ElementarySpace, N::Int) = ProductSpace{typeof(V), N}(ntuple(n->V, N))
Base.:^(V::ProductSpace, N::Int) = ⊗(ntuple(n->V, N)...)
Base.literal_pow(::typeof(^), V::ElementarySpace, p::Val{N}) where N =
    ProductSpace{typeof(V), N}(ntuple(n->V, p))
Base.convert(::Type{S}, P::ProductSpace{S,0}) where {S<:ElementarySpace} = oneunit(S)
Base.convert(::Type{S}, P::ProductSpace{S}) where {S<:ElementarySpace} = fuse(P.spaces...)
fuse(P::ProductSpace{S,0}) where {S<:ElementarySpace} = oneunit(S)
fuse(P::ProductSpace{S}) where {S<:ElementarySpace} = fuse(P.spaces...)

# Functionality for extracting and iterating over spaces
#--------------------------------------------------------
Base.length(P::ProductSpace) = length(P.spaces)
Base.getindex(P::ProductSpace, n::Integer) = P.spaces[n]
# Base.getindex(P::ProductSpace{S}, I::NTuple{N,Integer}) where {S<:ElementarySpace,N} =
#     ProductSpace{S,N}(TupleTools.getindices(P.spaces, I))

Base.iterate(P::ProductSpace, args...) = Base.iterate(P.spaces, args...)
# Base.indexed_iterate(P::ProductSpace, args...) = Base.indexed_iterate(P.spaces,  args...)

Base.eltype(::Type{<:ProductSpace{S}}) where {S<:ElementarySpace} = S
Base.eltype(P::ProductSpace) = eltype(typeof(P))

Base.IteratorEltype(::Type{<:ProductSpace}) = Base.HasEltype()
Base.IteratorSize(::Type{<:ProductSpace}) = Base.HasLength()


include("utils.jl")

@testset "Sphere" begin
    M = Sphere(2)
    @testset "Sphere Basics" begin
        @test repr(M) == "Sphere(2, ℝ)"
        @test typeof(get_embedding(M)) === Euclidean{Tuple{3},ℝ}
        @test representation_size(M) == (3,)
        @test injectivity_radius(M) == π
        @test injectivity_radius(M, ExponentialRetraction()) == π
        @test injectivity_radius(M, ProjectionRetraction()) == π / 2
        @test base_manifold(M) === M
        @test !is_manifold_point(M, [1.0, 0.0, 0.0, 0.0])
        @test !is_tangent_vector(M, [1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0])
        @test_throws DomainError is_manifold_point(M, [2.0, 0.0, 0.0], true)
        @test !is_manifold_point(M, [2.0, 0.0, 0.0])
        @test !is_tangent_vector(M, [1.0, 0.0, 0.0], [1.0, 0.0, 0.0])
        @test_throws DomainError is_tangent_vector(
            M,
            [1.0, 0.0, 0.0],
            [1.0, 0.0, 0.0],
            true,
        )
        @test injectivity_radius(M, [1.0, 0.0, 0.0], ProjectionRetraction()) == π / 2
    end
    types = [Vector{Float64}]
    TEST_FLOAT32 && push!(types, Vector{Float32})
    TEST_STATIC_SIZED && push!(types, MVector{3,Float64})

    basis_types = (DefaultOrthonormalBasis(), ProjectedOrthonormalBasis(:svd))
    for T in types
        @testset "Type $T" begin
            pts = [
                convert(T, [1.0, 0.0, 0.0]),
                convert(T, [0.0, 1.0, 0.0]),
                convert(T, [0.0, 0.0, 1.0]),
            ]
            test_manifold(
                M,
                pts,
                test_reverse_diff=isa(T, Vector),
                test_project_tangent=true,
                test_musical_isomorphisms=true,
                test_default_vector_transport=true,
                vector_transport_methods=[
                    ParallelTransport(),
                    SchildsLadderTransport(),
                    PoleLadderTransport(),
                ],
                test_mutating_rand=isa(T, Vector),
                point_distributions=[Manifolds.uniform_distribution(M, pts[1])],
                tvector_distributions=[
                    Manifolds.normal_tvector_distribution(M, pts[1], 1.0),
                ],
                basis_types_vecs=(DiagonalizingOrthonormalBasis([0.0, 1.0, 2.0]),),
                basis_types_to_from=basis_types,
                test_vee_hat=false,
                retraction_methods=[ProjectionRetraction(), ExponentialRetraction()],
                inverse_retraction_methods=[ProjectionInverseRetraction()],
                is_tangent_atol_multiplier=1,
            )
            @test isapprox(-pts[1], exp(M, pts[1], log(M, pts[1], -pts[1])))
        end
    end

    @testset "Distribution tests" begin
        usd_mvector = Manifolds.uniform_distribution(M, @MVector [1.0, 0.0, 0.0])
        @test isa(rand(usd_mvector), MVector)

        gtsd_mvector =
            Manifolds.normal_tvector_distribution(M, (@MVector [1.0, 0.0, 0.0]), 1.0)
        @test isa(rand(gtsd_mvector), MVector)
    end

    @testset "Embedding test" begin
        p = [1.0, 0.0, 0.0]
        X = [0.0, 1.0, 1.0]
        @test embed(M, p) == p
        q = similar(p)
        embed!(M, q, p)
        @test q == p
        @test embed(M, p, X) == X
        Y = similar(X)
        embed!(M, Y, p, X)
        @test Y == X
    end

    @testset "DefaultOrthonormalBasis edge cases" begin
        B = DefaultOrthonormalBasis(ℝ)
        n = manifold_dimension(M)
        p = [1; zeros(n)]
        for i in 1:n  # p'x ± 1
            vcoord = [j == i for j in 1:n]
            v = [0; vcoord]
            @test get_coordinates(M, p, v, B) ≈ vcoord
            @test get_vector(M, p, vcoord, B) ≈ v
            @test get_coordinates(M, -p, v, B) ≈ vcoord
            @test get_vector(M, -p, vcoord, B) ≈ v
        end
        p = [0; 1; zeros(n - 1)] # p'x = 0
        for i in 1:n
            vcoord = [j == i for j in 1:n]
            v = get_vector(M, p, vcoord, B)
            @test is_tangent_vector(M, p, v)
            @test get_coordinates(M, p, v, B) ≈ vcoord
        end
    end

    @testset "log edge case" begin
        n = manifold_dimension(M)
        x = normalize(randn(n + 1))
        v = log(M, x, -x)
        @test norm(v) ≈ π
        @test isapprox(dot(x, v), 0; atol=1e-12)
        vexp = normalize(project(M, x, [1, zeros(n)...]))
        @test v ≈ π * vexp

        x = [1, zeros(n)...]
        v = log(M, x, -x)
        @test norm(v) ≈ π
        @test isapprox(dot(x, v), 0; atol=1e-12)
        vexp = normalize(project(M, x, [0, 1, zeros(n - 1)...]))
        @test v ≈ π * vexp
    end

    @testset "Complex Sphere" begin
        M = Sphere(2, ℂ)
        @test repr(M) == "Sphere(2, ℂ)"
        @test typeof(get_embedding(M)) === Euclidean{Tuple{3},ℂ}
        @test representation_size(M) == (3,)
        p = [1.0, 1.0im, 1.0]
        q = project(M, p)
        @test is_manifold_point(M, q)
        Y = [2.0, 1.0im, 20.0]
        X = project(M, q, Y)
        @test is_tangent_vector(M, q, X, true; atol=10^(-14))
    end

    @testset "Quaternion Sphere" begin
        M = Sphere(2, ℍ)
        @test repr(M) == "Sphere(2, ℍ)"
        @test typeof(get_embedding(M)) === Euclidean{Tuple{3},ℍ}
        @test representation_size(M) == (3,)
        p = [Quaternion(1.0), Quaternion(1.0im), Quaternion(0.0, 0.0, -1.0, 0.0)]
        q = project(M, p)
        @test is_manifold_point(M, q)
        Y = [Quaternion(2.0), Quaternion(1.0im), Quaternion(0.0, 0.0, 20.0, 0.0)]
        X = project(M, q, Y)
        @test is_tangent_vector(M, q, X, true; atol=10^(-14))
    end

    @testset "Array Sphere" begin
        M = ArraySphere(2, 2; field=ℝ)
        @test repr(M) == "ArraySphere(2, 2; field = ℝ)"
        @test typeof(get_embedding(M)) === Euclidean{Tuple{2,2},ℝ}
        @test representation_size(M) == (2, 2)
        p = ones(2, 2)
        q = project(M, p)
        @test is_manifold_point(M, q)
        Y = [1.0 0.0; 0.0 1.1]
        X = project(M, q, Y)
        @test is_tangent_vector(M, q, X)
        M = ArraySphere(2, 2; field=ℂ)

        @test repr(M) == "ArraySphere(2, 2; field = ℂ)"
        @test typeof(get_embedding(M)) === Euclidean{Tuple{2,2},ℂ}
        @test representation_size(M) == (2, 2)
    end
end


# ---
# title: 405. Convert a Number to Hexadecimal
# id: problem405
# author: Tian Jun
# date: 2020-10-31
# difficulty: Easy
# categories: Bit Manipulation
# link: <https://leetcode.com/problems/convert-a-number-to-hexadecimal/description/>
# hidden: true
# ---
#
# Given an integer, write an algorithm to convert it to hexadecimal. For
# negative integer, [two’s
# complement](https://en.wikipedia.org/wiki/Two%27s_complement) method is used.
#
# **Note:**
#
#   1. All letters in hexadecimal (`a-f`) must be in lowercase.
#   2. The hexadecimal string must not contain extra leading `0`s. If the number is zero, it is represented by a single zero character `'0'`; otherwise, the first character in the hexadecimal string will not be the zero character.
#   3. The given number is guaranteed to fit within the range of a 32-bit signed integer.
#   4. You **must not use _any_ method provided by the library** which converts/formats the number to hex directly.
#
# **Example 1:**
#
#
#
#     Input:
#     26
#
#     Output:
#     "1a"
#
#
# **Example 2:**
#
#
#
#     Input:
#     -1
#
#     Output:
#     "ffffffff"
#
#
#
## @lc code=start
using LeetCode

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


#Physical constants

module constants

export M_sun, M_earth, AU, pc, G, kB, amu, c_ang, cc, c_kms, mu_gas, m_H, m_CO, m_12CO, m_13CO, m_C18O, number_densities, molnames, arcsec, deg, fftspace, MCO, lam0_12CO, lam0_13CO, lam0_C18O, lam0s, nyquist_factor, r_out_factor, Av_sigmaH, n_z_interpolator, X_H2, rho_gas_critical
export RADMC_SIZEAU_SHIFT
export DiskJockeyException, ModelException, ImageException
export DISKJOCKEY_VERSION

DISKJOCKEY_VERSION = v"0.1.5"

# Conversion from astronomical units to CGS units
M_sun = 1.99e33 # [g]
M_earth = 5.97219e27 # [g]
AU = 1.4959787066e13 # [cm]
pc = 3.0856776e18 # [cm]
G = 6.67259e-8 # [cm3 g-1 s-2]
kB = 1.380658e-16 # [erg K^-1] Boltzmann constant
c_ang = 2.99792458e18 # [A s^-1]
cc = 2.99792458e10 # [cm s^-1]
c_kms = 2.99792458e5 # [km s^-1]

# Conversion from degrees to radians
deg = pi/180. # [radians]

mu_gas = 2.37 # mean molecular weight of circumstellar gas
m_H = 1.6733e-24 # g mass of hydrogen atom

#Atomic
amu = 1.6605402e-24 # [g]

#CO
m_CO = 28.01 * amu #molecular weight of CO in g

m_12CO = 27.9949 * amu # [g]
m_13CO = 28.9983 * amu # [g]
m_C18O = 29.9992 * amu # [g]

atomic_massses = Dict([("12CO", m_12CO), ("13CO", m_13CO), ("C18O", m_C18O)])

# Using numbers from Charlie Qi
# Number ratios measured relative to all single H nuclei (not H2) [unitless]
f_12CO = 7.5e-5
X_12CO = 2 * f_12CO
X_13CO = 1/69. * X_12CO
X_C18O = 1/557. * X_12CO

# molecular hydrogen number ratio to gas [unitless]
# [n_H2/n_gas] = 0.8
X_H2 = 0.8

# To go from rho_gas to n_12CO
# n_12CO = rho_gas * X_H2 *

# Old number densities calculated as
# 12CO = 7.5e-5 / (mu_gas * amu)

# New number density calculated as
# 12CO = 0.8 * (2 * 7.5e-5)/ (mu_gas * amu)

# new/old = 1.6
# This means that the new total disk mass we use will be 1/1.6 times smaller than the old time.

# Use this to multiply against rho_gas to get n_12CO or n_13CO
number_densities = Dict{String, Float64}([("12CO", X_H2 * X_12CO/(mu_gas * amu)), ("13CO", X_H2 * X_13CO/(mu_gas * amu)), ("C18O", X_H2 * X_C18O/(mu_gas * amu))])

molnames = Dict{String, String}([("12CO", "co"), ("13CO", "13co"), ("C18O", "c18o")])

# Species can be "12CO", "13CO", etc.
# Transition can be "3-2", "2-1", etc.

# Key to this dictionary is then species * transition

# Rest frame wavelengths
lam0s = Dict{String, Float64}([("12CO2-1", cc/230.538e9 * 1e4 ),
            ("13CO2-1", cc/220.39868420e9 * 1e4),
            ("13CO3-2", cc/330.58796530e9 * 1e4),
            ("C18O2-1", cc/219.56035410e9 * 1e4),
            ("12CO3-2", cc/345.7959899e9 * 1e4)]) # microns

# Convert from Av to hydrogen *nuclei* column density for interstellar dust
# Av_sigmaH = 1.59e21 # [cm^-2] # "standard" number
Av_sigmaH = 1.0e20 # [cm^-2] # Charlie's number

# convert from arcseconds to radians
arcsec = pi / (180.0 * 3600) # [radians]  = 1/206265 radian/arcsec

# Used when determining the necessary number of pixels in an image, given distance. Anything below
# 2 is not Nyquist sampled. This is currently set to 2.2 to provide a degree of oversampling.
nyquist_factor = 2.2

# The bound at which to force the density to zero
# n_H2 = 100 [1/cm^3] average H2 number density in diffuse ISM
rho_gas_critical = (100.0/X_H2) * mu_gas * amu # [g/cm^3]

# The number of z points to use for the density interpolator
n_z_interpolator = 64
rho_gas_zero = 1e-50

# Fractional shift that exists between commanded size of image and actual size (actual size bigger)
# Therefore, use this to demand a size that is slightly smaller, so that the actual synthesized
# image comes out being the proper size.
RADMC_SIZEAU_SHIFT = 1.4233758746704833e-5

"Oftentimes it is necessary to get a symmetric coordinate array that spans ``N``
 elements from `-width` to `+width`, but makes sure that the middle point lands
 on ``0``. The indices go from ``0`` to ``N -1.``
 `linspace` returns  the end points inclusive, wheras we want to leave out the
 right endpoint, because we are sampling the function in a cyclic manner."
function fftspace(width::Real, N::Int)
    @assert(N % 2 == 0, "N must be even.")

    dx = width * 2.0 / N
    xx = Array{Float64}(undef, N)
    for i=1:N
        xx[i] = -width + (i - 1) * dx
    end
    return xx
end

# Convert result from MCMC to M_sun
function MCO(logCO::Float64)
    # Convert to Earth mass
    CO_earth = 10^logCO

    # Convert from Earth to M_sun
    CO_sun = CO_earth * M_earth/M_sun

    # Convert from 12CO to total gas mass using the mass ratio of 1e-4
    gas_mass = CO_sun / X_12CO

    logM_gas = log10(gas_mass)

    return logM_gas
end

# Abstract exception that serves as a catch-all for any error generated by the DiskJockey package.
abstract type DiskJockeyException <: Exception end

# Define the various types of Errors that we can Raise
mutable struct ModelException <: DiskJockeyException
    msg::String
end

mutable struct ImageException <: DiskJockeyException
    msg::String
end

end # Module


# # Quasi-Linear forced-dissipative barotropic QG beta-plane turbulence
#
#md # This example can be run online via [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/generated/barotropicqgql_betaforced.ipynb).
#md # Also, it can be viewed as a Jupyter notebook via [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/generated/barotropicqgql_betaforced.ipynb).
#
# A simulation of forced-dissipative barotropic quasi-geostrophic turbulence on
# a beta plane under the *quasi-linear approximation*. The dynamics include
# linear drag and stochastic excitation.

using FourierFlows, Plots, Statistics, Printf, Random

using FourierFlows: parsevalsum
using FFTW: irfft
using Random: seed!
using Statistics: mean

import GeophysicalFlows.BarotropicQGQL
import GeophysicalFlows.BarotropicQGQL: energy, enstrophy

# ## Choosing a device: CPU or GPU

dev = CPU()     # Device (CPU/GPU)
nothing # hide

# ## Numerical parameters and time-stepping parameters

      n = 128            # 2D resolution = n^2
stepper = "FilteredRK4"  # timestepper
     dt = 0.05           # timestep
 nsteps = 8000           # total number of time-steps
 nsubs  = 10             # number of time-steps for intermediate logging/plotting (nsteps must be multiple of nsubs)
nothing # hide

# ## Physical parameters

L = 2π        # domain size
β = 10.0      # planetary PV gradient
μ = 0.01      # bottom drag
nothing # hide

# ## Forcing

# We force the vorticity equation with stochastic excitation that is delta-correlated
# in time and while spatially homogeneously and isotropically correlated. The forcing
# has a spectrum with power in a ring in wavenumber space of radious $k_f$ and
# width $\delta k_f$, and it injects energy per unit area and per unit time equal
# to $\varepsilon$.

forcing_wavenumber = 14.0    # the central forcing wavenumber for a spectrum that is a ring in wavenumber space
forcing_bandwidth  = 1.5     # the width of the forcing spectrum
ε = 0.001                    # energy input rate by the forcing

gr  = TwoDGrid(n, L)

K = @. sqrt(gr.Krsq)                      # a 2D array with the total wavenumber
k = [gr.kr[i] for i=1:gr.nkr, j=1:gr.nl]  # a 2D array with the zonal wavenumber

forcing_spectrum = @. exp(-(K - forcing_wavenumber)^2 / (2 * forcing_bandwidth^2))
forcing_spectrum[K .<  2 * 2π/L] .= 0    # no power at low wavenumbers
forcing_spectrum[K .> 20 * 2π/L] .= 0    # no power at high wavenumbers
forcing_spectrum[k .< 2π/L ] .= 0        # make sure forcing does not have power at k=0
ε0 = parsevalsum(forcing_spectrum .* gr.invKrsq / 2, gr) / (gr.Lx * gr.Ly)
@. forcing_spectrum *= ε/ε0               # normalize forcing to inject energy at rate ε

seed!(1234) # reset of the random number generator for reproducibility
nothing # hide

# Next we construct function `calcF!` that computes a forcing realization every timestep
function calcF!(Fh, sol, t, clock, vars, params, grid)
  ξ = ArrayType(dev)(exp.(2π * im * rand(eltype(grid), size(sol))) / sqrt(clock.dt))
  @. Fh = ξ * sqrt.(forcing_spectrum)
  Fh[abs.(grid.Krsq) .== 0] .= 0
  return nothing
end
nothing # hide

# ## Problem setup
# We initialize a `Problem` by providing a set of keyword arguments. Not providing
# a viscosity coefficient ν leads to the module's default value: ν=0. In this
# example numerical instability due to accumulation of enstrophy in high wavenumbers
# is taken care with the `FilteredTimestepper` we picked.
prob = BarotropicQGQL.Problem(dev; nx=n, Lx=L, β=β, μ=μ, dt=dt, stepper=stepper,
                              calcF=calcF!, stochastic=true)
nothing # hide

# and define some shortcuts.
sol, cl, vs, pr, gr = prob.sol, prob.clock, prob.vars, prob.params, prob.grid
x, y = gr.x, gr.y
nothing # hide

# First let's see how a forcing realization looks like.
calcF!(vs.Fh, sol, 0.0, cl, vs, pr, gr)

heatmap(x, y, irfft(vs.Fh, gr.nx),
     aspectratio = 1,
               c = :balance,
            clim = (-8, 8),
           xlims = (-gr.Lx/2, gr.Lx/2),
           ylims = (-gr.Ly/2, gr.Ly/2),
          xticks = -3:3,
          yticks = -3:3,
          xlabel = "x",
          ylabel = "y",
           title = "a forcing realization",
      framestyle = :box)

# ## Setting initial conditions

# Our initial condition is simply fluid at rest.
BarotropicQGQL.set_zeta!(prob, zeros(gr.nx, gr.ny))
nothing # hide

# ## Diagnostics

# Create Diagnostics -- `energy` and `enstrophy` are functions imported at the top.
E = Diagnostic(energy, prob; nsteps=nsteps)
Z = Diagnostic(enstrophy, prob; nsteps=nsteps)
nothing # hide

# We can also define our custom diagnostics via functions.
function zetaMean(prob)
  sol = prob.sol
  sol[1, :]
end

zMean = Diagnostic(zetaMean, prob; nsteps=nsteps, freq=10)  # the zonal-mean vorticity
nothing # hide

# We combile all diags in a list.
diags = [E, Z, zMean] # A list of Diagnostics types passed to "stepforward!" will  be updated every timestep.
nothing # hide

# ## Output

# We choose folder for outputing `.jld2` files and snapshots (`.png` files).
filepath = "."
plotpath = "./plots_forcedbetaQLturb"
plotname = "snapshots"
filename = joinpath(filepath, "forcedbetaQLturb.jld2")
nothing # hide

# Do some basic file management,
if isfile(filename); rm(filename); end
if !isdir(plotpath); mkdir(plotpath); end
nothing # hide

# and then create Output.
get_sol(prob) = sol # extracts the Fourier-transformed solution
get_u(prob) = irfft(im*g.l.*g.invKrsq.*sol, g.nx)
out = Output(prob, filename, (:sol, get_sol), (:u, get_u))
nothing # hide

# ## Visualizing the simulation

# We define a function that plots the vorticity and streamfunction fields, the
# corresponding zonal-mean vorticity and zonal-mean zonal velocity and timeseries
# of energy and enstrophy.

function plot_output(prob)
  ζ̄, ζ′= prob.vars.Zeta, prob.vars.zeta
  ζ = @. ζ̄ + ζ′
  ψ̄, ψ′= prob.vars.Psi,  prob.vars.psi
  ψ = @. ψ̄ + ψ′
  ζ̄ₘ = mean(ζ̄, dims=1)'
  ūₘ = mean(prob.vars.U, dims=1)'

  pζ = heatmap(x, y, ζ,
       aspectratio = 1,
            legend = false,
                 c = :balance,
              clim = (-8, 8),
             xlims = (-gr.Lx/2, gr.Lx/2),
             ylims = (-gr.Ly/2, gr.Ly/2),
            xticks = -3:3,
            yticks = -3:3,
            xlabel = "x",
            ylabel = "y",
             title = "vorticity ζ=∂v/∂x-∂u/∂y",
        framestyle = :box)

  pψ = contourf(x, y, ψ,
            levels = -0.32:0.04:0.32,
       aspectratio = 1,
         linewidth = 1,
            legend = false,
              clim = (-0.22, 0.22),
                 c = :viridis,
             xlims = (-gr.Lx/2, gr.Lx/2),
             ylims = (-gr.Ly/2, gr.Ly/2),
            xticks = -3:3,
            yticks = -3:3,
            xlabel = "x",
            ylabel = "y",
             title = "streamfunction ψ",
        framestyle = :box)

  pζm = plot(ζ̄ₘ, y,
            legend = false,
         linewidth = 2,
             alpha = 0.7,
            yticks = -3:3,
             xlims = (-3, 3),
            xlabel = "zonal mean ζ",
            ylabel = "y")
  plot!(pζm, 0*y, y, linestyle=:dash, linecolor=:black)

  pum = plot(ūₘ, y,
            legend = false,
         linewidth = 2,
             alpha = 0.7,
            yticks = -3:3,
             xlims = (-0.5, 0.5),
            xlabel = "zonal mean u",
            ylabel = "y")
  plot!(pum, 0*y, y, linestyle=:dash, linecolor=:black)

  pE = plot(1,
             label = "energy",
         linewidth = 2,
             alpha = 0.7,
             xlims = (-0.1, 4.1),
             ylims = (0, 0.05),
            xlabel = "μt")

  pZ = plot(1,
             label = "enstrophy",
         linecolor = :red,
            legend = :bottomright,
         linewidth = 2,
             alpha = 0.7,
             xlims = (-0.1, 4.1),
             ylims = (0, 5),
            xlabel = "μt")

  l = @layout grid(2, 3)
  p = plot(pζ, pζm, pE, pψ, pum, pZ, layout=l, size = (1000, 600), dpi=150)

  return p
end

# ## Time-stepping the `Problem` forward

# We time-step the `Problem` forward in time.

p = plot_output(prob)

startwalltime = time()

anim = @animate for j=0:Int(nsteps/nsubs)

  cfl = cl.dt*maximum([maximum(vs.v)/gr.dy, maximum(vs.u+vs.U)/gr.dx])

  log = @sprintf("step: %04d, t: %d, cfl: %.2f, E: %.4f, Q: %.4f, walltime: %.2f min",
    cl.step, cl.t, cfl, E.data[E.i], Z.data[Z.i],
    (time()-startwalltime)/60)

  if j%(1000/nsubs)==0; println(log) end

  p[1][1][:z] = @. vs.zeta + vs.Zeta
  p[1][:title] = "vorticity, μt="*@sprintf("%.2f", μ*cl.t)
  p[4][1][:z] = @. vs.psi + vs.Psi
  p[2][1][:x] = mean(vs.Zeta, dims=1)'
  p[5][1][:x] = mean(vs.U, dims=1)'
  push!(p[3][1], μ*E.t[E.i], E.data[E.i])
  push!(p[6][1], μ*Z.t[Z.i], Z.data[Z.i])

  stepforward!(prob, diags, nsubs)
  BarotropicQGQL.updatevars!(prob)

end

mp4(anim, "barotropicqgql_betaforced.mp4", fps=18)

# ## Save

# Finally save the last snapshot.
savename = @sprintf("%s_%09d.png", joinpath(plotpath, plotname), cl.step)
savefig(savename)


"""
Main module for `LLLplus.jl` -- lattice reduction and related tools for Julia.

As an example of the functions in the package, see [`lll`](@ref), which does
Lenstra–Lenstra–Lovász lattice reduction of a matrix.

"""
module LLLplus

using LinearAlgebra
using Printf

export
    lll,
    cvp,
    svp,
    hkz,ishkzreduced,
    brun,
    gauss,
    sizereduction,
    seysen,
    vblast,
    subsetsum,lagariasodlyzko,mdsubsetsum,
    integerfeasibility,
    rationalapprox,spigotBBP,
    hardsphere, hard_sphere,
    issizereduced,islllreduced,orthogonalitydefect,hermitefactor,seysencond,
    gen_qary_b,gen_qary!,
    dataTypeForGram,intTypeGivenBitsRequired

include("lll.jl")          # lll, gauss, sizereduction
include("l2.jl")           # lll on Rational{Integer} with `l2`
include("cvp.jl")          # cvp, svp
include("hkz.jl")
include("bkz.jl")
include("brun.jl")
include("seysen.jl")
include("vblast.jl")
include("applications.jl") # subsetsum, integerfeasibility, rationalapprox
include("utilities.jl")
include("latticegen.jl")

include("hard_sphere.jl")  # may be deprecated in future

end # LLLplus module


module RankWrapper

using JavaCall

const AbstractStorelessUnivariateStatistic = @jimport org.hipparchus.stat.descriptive.AbstractStorelessUnivariateStatistic
const AbstractUnivariateStatistic = @jimport org.hipparchus.stat.descriptive.AbstractUnivariateStatistic
const AggregatableStatistic = @jimport org.hipparchus.stat.descriptive.AggregatableStatistic
const Class = @jimport java.lang.Class
const Collection = @jimport java.util.Collection
const DoubleConsumer = @jimport java.util.function.DoubleConsumer
const Iterable = @jimport java.lang.Iterable
const JString = @jimport java.lang.String
const KthSelector = @jimport org.hipparchus.util.KthSelector
const List = @jimport java.util.List
const Max = @jimport org.hipparchus.stat.descriptive.rank.Max
const Median = @jimport org.hipparchus.stat.descriptive.rank.Median
const Min = @jimport org.hipparchus.stat.descriptive.rank.Min
const NaNStrategy = @jimport org.hipparchus.stat.ranking.NaNStrategy
const Object = @jimport java.lang.Object
const PSquarePercentile = @jimport org.hipparchus.stat.descriptive.rank.PSquarePercentile
const PSquarePercentile_PSquareMarkers = @jimport org.hipparchus.stat.descriptive.rank.PSquarePercentile$PSquareMarkers
const Percentile = @jimport org.hipparchus.stat.descriptive.rank.Percentile
const Percentile_EstimationType = @jimport org.hipparchus.stat.descriptive.rank.Percentile$EstimationType
const PivotingStrategy = @jimport org.hipparchus.util.PivotingStrategy
const RandomGenerator = @jimport org.hipparchus.random.RandomGenerator
const RandomPercentile = @jimport org.hipparchus.stat.descriptive.rank.RandomPercentile
const StorelessUnivariateStatistic = @jimport org.hipparchus.stat.descriptive.StorelessUnivariateStatistic
const UnivariateStatistic = @jimport org.hipparchus.stat.descriptive.UnivariateStatistic

export Max
export Median
export Min
export PSquarePercentile
export Percentile
export RandomPercentile
export accept
export aggregate
export and_then
export clear
export copy
export equals
export evaluate
export get_aggregate_n
export get_aggregate_quantile_rank
export get_aggregate_rank
export get_class
export get_data
export get_estimation_type
export get_kth_selector
export get_n
export get_na_n_strategy
export get_pivoting_strategy
export get_quantile
export get_quantile_rank
export get_rank
export get_result
export hash_code
export increment
export increment_all
export max_values_retained
export new_markers
export notify
export notify_all
export quantile
export reduce
export set_data
export set_quantile
export to_string
export wait
export with_estimation_type
export with_kth_selector
export with_na_n_strategy

include("max.jl")
include("median.jl")
include("min.jl")
include("p_square_percentile.jl")
include("percentile.jl")
include("random_percentile.jl")

end


using HPAT

#HPAT.DomainPass.set_debug_level(3)
#HPAT.DistributedPass.set_debug_level(3)

@acc hpat function cumsum_test1(n)
    A = ones(n)
    B = cumsum(A)
    return sum(B)
end

using Base.Test
@test_approx_eq cumsum_test1(5) 15.0


# ---
# title: 42. Trapping Rain Water
# id: problem42
# author: Tian Jun
# date: 2020-10-31
# difficulty: Hard
# categories: Array, Two Pointers, Stack
# link: <https://leetcode.com/problems/trapping-rain-water/description/>
# hidden: true
# ---
#
# Given `n` non-negative integers representing an elevation map where the width
# of each bar is `1`, compute how much water it can trap after raining.
#
#
#
# **Example 1:**
#
# ![](https://assets.leetcode.com/uploads/2018/10/22/rainwatertrap.png)
#
#
#
#     Input: height = [0,1,0,2,1,0,1,3,2,1,2,1]
#     Output: 6
#     Explanation: The above elevation map (black section) is represented by array [0,1,0,2,1,0,1,3,2,1,2,1]. In this case, 6 units of rain water (blue section) are being trapped.
#
#
# **Example 2:**
#
#
#
#     Input: height = [4,2,0,3,2,5]
#     Output: 9
#
#
#
#
# **Constraints:**
#
#   * `n == height.length`
#   * `0 <= n <= 3 * 104`
#   * `0 <= height[i] <= 105`
#
#
## @lc code=start
using LeetCode

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


# assert file to test polynomial implementation
using Compat
using Base.Test
using Polynomials

pNULL = Poly(Float32[])
p0 = Poly([0])
p1 = Poly([1,0,0,0,0,0,0,0,0,0,0,0,0,0])
p2 = Poly([1,1,0,0])
p3 = Poly([1,2,1,0,0,0,0])
p4 = Poly([1,3,3,1,0,0])
p5 = Poly([1,4,6,4,1,0,0,0,0,0,0,0,0,0,0,0,0,0])
pN = Poly([276,3,87,15,24,0])
pR = Poly([3//4, -2//1, 1//1])
X = Poly([0.0, 1.0])
p1000 = Poly(randn(1000))

@test length(pNULL) == 1
@test length(p1000-p1000) == 1
@test length(p1000^0) == 1
@test length(0*p1000) == 1
@test length(p1000) == 1000
sprint(show, p1000)
sprint(show, pNULL)

@test p3 == Poly([1,2,1])
@test pN*10 == Poly([2760, 30, 870, 150, 240])
@test pN/10 == Poly([27.6, 0.3, 8.7, 1.5, 2.4])
@test 10*pNULL + pN == pN
@test 10*p0 + pN == pN
@test p5 + 2*p1 == Poly([3,4,6,4,1])
@test 10*pNULL - pN == -pN
@test p0 - pN == -pN
@test p5 - 2*p1 == Poly([-1,4,6,4,1])
@test p2*p2*p2 == p4
@test p2^4 == p5
@test pNULL^3 == pNULL
@test pNULL*pNULL == pNULL

@test map(degree, [pNULL,p0,p1,p2,p3,p4,p5,pN,pR,p1000]) == [0,0,0,1,2,3,4,4,2,999]

@test polyval(pN, -.125) == 276.9609375
@test polyval(pNULL, 10) == 0
@test polyval(p0, -10) == 0
@test isa(polyval(p0, X), Poly)
@test isa(polyval(p1, X), Poly)
@test polyval(poly([1//2, 3//2]), 1//2) == 0//1
@test polyder(polyint(pN)) == pN
@test polyder(pR) == Poly([-2//1,2//1])
@test polyint(pNULL,1) == p1
@test polyint(Poly(Rational[1,2,3])) == Poly(Rational[0, 1, 1, 1])
@test polyder(p3) == Poly([2,2])
@test polyder(p1) == polyder(p0) == polyder(pNULL) == pNULL

if VERSION >= v"0.4"
    @test pN(-.125) == 276.9609375
    @test pN([0.1, 0.2, 0.3]) == polyval(pN, [0.1, 0.2, 0.3])
end

@test poly([-1,-1]) == p3
@test roots(p0)==roots(p1)==roots(pNULL)==[]
@test roots(p2) == [-1]
a_roots = copy(pN.a)
@test all(abs(sort(roots(poly(a_roots))) - sort(a_roots)) .< 1e6)
@test length(roots(p5)) == 4
@test roots(pNULL) == []
@test roots(pR) == [1//2, 3//2]

@test pNULL + 2 == p0 + 2 == 2 + p0 == Poly([2])
@test p2 - 2 == -2 + p2 == Poly([-1,1])
@test 2 - p2 == Poly([1,-1])

p0 = Poly([0])
p1 = Poly([1])
p2 = Poly([5, 6, -3, 2 ,4])
p3 = Poly([7, -3, 2, 6])
p4 = p2 * p3
@test divrem(p4, p2) == (p3, zero(p3))
@test p3%p2 == p3
@test all((abs((p2 ÷ p3 - Poly([1/9,2/3])).a)) .< eps())
@test divrem(p0,p1) == (p0,p0)
@test divrem(p1,p1) == (p1,p0)
@test divrem(p2,p2) == (p1,p0)
@test divrem(pR, pR) == (one(pR), zero(pR))
@test_throws DivideError p1 ÷ p0
@test_throws DivideError divrem(p0,p0)

#Tests for multivariable support
pX = Poly([1, 2, 3, 4, 5])
pS1 = Poly([1, 2, 3, 4, 5], "s")
pS2 = Poly([1, 2, 3, 4, 5], 's')
pS3 = Poly([1, 2, 3, 4, 5], :s)
@test pX != pS1
@test pS1 == pS2
@test pS1 == pS3
@test_throws ErrorException pS1 + pX
@test_throws ErrorException pS1 - pX
@test_throws ErrorException pS1 * pX
@test_throws ErrorException pS1 ÷ pX
@test_throws ErrorException pS1 % pX

#Testing copying.
pcpy1 = Poly([1,2,3,4,5], :y)
pcpy2 = copy(pcpy1)
@test pcpy1 == pcpy2

#Tests for Pade approximants

println("Test for the exponential function.")
a = Poly(1.//convert(Vector{BigInt},gamma(BigFloat(1):BigFloat(17))),"x")
PQexp = Pade(a,8,8)
@test isapprox(convert(Float64, padeval(PQexp,1.0)), exp(1.0))
@test isapprox(convert(Float64, padeval(PQexp,-1.0)), exp(-1.0))

println("Test for the sine function.")
b = Poly(convert(Vector{BigInt},sinpi((0:16)/2)).//convert(Vector{BigInt},gamma(BigFloat(1):BigFloat(17))),"x")
PQsin = Pade(b,8,7)
@test isapprox(convert(Float64, padeval(PQsin,1.0)), sin(1.0))
@test isapprox(convert(Float64, padeval(PQsin,-1.0)),sin(-1.0))

println("Test for the cosine function.")
c = Poly(convert(Vector{BigInt},sinpi((1:17)/2)).//convert(Vector{BigInt},gamma(BigFloat(1):BigFloat(17))),"x")
PQcos = Pade(c,8,8)
@test isapprox(convert(Float64, padeval(PQcos,1.0)), cos(1.0))
@test isapprox(convert(Float64, padeval(PQcos,-1.0)), cos(-1.0))

println("Test for the summation of a factorially divergent series.")
d = Poly(convert(Vector{BigInt},(-1).^(0:60).*gamma(BigFloat(1):BigFloat(61.0))).//1,"x")
PQexpint = Pade(d,30,30)
@compat println("The approximate sum of the divergent series is:  ", Float64(padeval(PQexpint,1.0)))
println("The approximate sum of the convergent series is: ",exp(1)*(-γ-sum([(-1).^k/k./gamma(k+1) for k=1:20])))
@test isapprox(convert(Float64, padeval(PQexpint,1.0)),
               exp(1)*(-γ-sum([(-1).^k/k./gamma(k+1) for k=1:20])))

## polyfit
xs = linspace(0, pi, 10)
ys = sin(xs)
p = polyfit(xs, ys)
p = polyfit(xs, ys, :t)
p = polyfit(xs, ys, 2)
@test maximum(abs(map(x->polyval(p, x), xs) - ys)) <= 0.03

## truncation
p1 = Poly([1,1]/10)
p2 = Poly([1,2]/10)
p3 = Poly([1,3]/10)
psum = p1 + p2 - p3
@test degree(psum) == 1         # will have wrong degree
@test degree(truncate(psum)) == 0 # the degree should be correct after truncation

@test truncate(Poly([2,1]),reltol=1/2,abstol=0) == Poly([2])
@test truncate(Poly([2,1]),reltol=1,abstol=0)   == Poly([0])
@test truncate(Poly([2,1]),reltol=0,abstol=1)   == Poly([2])

## setindex!
println("Test for setindex!()")
p1    = Poly([1,2,1])
p1[5] = 1
@test p1[5] == 1
@test p1 == Poly([1,2,1,0,0,1])

## elementwise operations #52
println("Test for element-wise operations")
p1  = Poly([1, 2])
p2  = Poly([3, 1.])
p   = [p1, p2]
q   = [3, p1]
@test isa(q,Vector{Poly{Int64}})
psum  = p+3
pprod = p*3
pmin  = p-3
@test isa(psum, Vector{Poly{Float64}})
@test isa(pprod,Vector{Poly{Float64}})
@test isa(pmin, Vector{Poly{Float64}})

## getindex with ranges #43
p1 = Poly([4,5,6])
@test all(p1[0:1] .== [4,5])
@test all(p1[0:end] .== [4,5,6])
p1[0:1] = [7,8]
@test all(p1[0:end] .== [7,8,6])


# ---
# title: 540. Single Element in a Sorted Array
# id: problem540
# author: Indigo
# date: 2021-06-23
# difficulty: Medium
# categories: Binary Search
# link: <https://leetcode.com/problems/single-element-in-a-sorted-array/description/>
# hidden: true
# ---
#
# You are given a sorted array consisting of only integers where every element
# appears exactly twice, except for one element which appears exactly once. Find
# this single element that appears only once.
#
# **Follow up:** Your solution should run in O(log n) time and O(1) space.
#
#
#
# **Example 1:**
#
#
#
#     Input: nums = [1,1,2,3,3,4,4,8,8]
#     Output: 2
#
#
# **Example 2:**
#
#
#
#     Input: nums = [3,3,7,7,10,11,11]
#     Output: 10
#
#
#
#
# **Constraints:**
#
#   * `1 <= nums.length <= 10^5`
#   * `0 <= nums[i] <= 10^5`
#
#
## @lc code=start
using LeetCode

function single_non_duplicate(nums::Vector{Int})
    i, j = 1, length(nums)
    while i < j
        mid = (i + j) >> 1
        if isodd(mid)
            nums[mid] == nums[mid + 1] ? (i = mid + 2) : (j = mid)
        else
            nums[mid] == nums[mid + 1] ? (j = mid - 1) : (i = mid + 1)
        end
    end
    return nums[i]
end
## @lc code=end


struct HS071 <: MOI.AbstractNLPEvaluator
    enable_hessian::Bool
end

# hs071
# min x1 * x4 * (x1 + x2 + x3) + x3
# st  x1 * x2 * x3 * x4 >= 25
#     x1^2 + x2^2 + x3^2 + x4^2 = 40
#     1 <= x1, x2, x3, x4 <= 5
# Start at (1,5,5,1)
# End at (1.000..., 4.743..., 3.821..., 1.379...)

function MOI.initialize(d::HS071, requested_features::Vector{Symbol})
    for feat in requested_features
        if !(feat in MOI.features_available(d))
            error("Unsupported feature $feat")
            # TODO: implement Jac-vec and Hess-vec products
            # for solvers that need them
        end
    end
end

function MOI.features_available(d::HS071)
    if d.enable_hessian
        return [:Grad, :Jac, :Hess]
    else
        return [:Grad, :Jac]
    end
end

MOI.eval_objective(d::HS071, x) = x[1] * x[4] * (x[1] + x[2] + x[3]) + x[3]

function MOI.eval_constraint(d::HS071, g, x)
    g[1] = x[1]   * x[2]   * x[3]   * x[4]
    g[2] = x[1]^2 + x[2]^2 + x[3]^2 + x[4]^2
end

function MOI.eval_objective_gradient(d::HS071, grad_f, x)
    grad_f[1] = x[1] * x[4] + x[4] * (x[1] + x[2] + x[3])
    grad_f[2] = x[1] * x[4]
    grad_f[3] = x[1] * x[4] + 1
    grad_f[4] = x[1] * (x[1] + x[2] + x[3])
end

function MOI.jacobian_structure(d::HS071)
    return Tuple{Int64,Int64}[(1,1), (1,2), (1,3), (1,4), (2,1), (2,2),
                              (2,3), (2,4)]
end
# lower triangle only
function MOI.hessian_lagrangian_structure(d::HS071)
    @assert d.enable_hessian
    return Tuple{Int64,Int64}[(1,1), (2,1), (2,2), (3,1), (3,2), (3,3),
                              (4,1), (4,2), (4,3), (4,4)]
end

function MOI.eval_constraint_jacobian(d::HS071, J, x)
    # Constraint (row) 1
    J[1] = x[2]*x[3]*x[4]  # 1,1
    J[2] = x[1]*x[3]*x[4]  # 1,2
    J[3] = x[1]*x[2]*x[4]  # 1,3
    J[4] = x[1]*x[2]*x[3]  # 1,4
    # Constraint (row) 2
    J[5] = 2*x[1]  # 2,1
    J[6] = 2*x[2]  # 2,2
    J[7] = 2*x[3]  # 2,3
    J[8] = 2*x[4]  # 2,4
end

function MOI.eval_hessian_lagrangian(d::HS071, H, x, σ, μ)
    @assert d.enable_hessian
    # Again, only lower left triangle
    # Objective
    H[1] = σ * (2*x[4])               # 1,1
    H[2] = σ * (  x[4])               # 2,1
    H[3] = 0                          # 2,2
    H[4] = σ * (  x[4])               # 3,1
    H[5] = 0                          # 3,2
    H[6] = 0                          # 3,3
    H[7] = σ* (2*x[1] + x[2] + x[3])  # 4,1
    H[8] = σ * (  x[1])               # 4,2
    H[9] = σ * (  x[1])               # 4,3
    H[10] = 0                         # 4,4

    # First constraint
    H[2] += μ[1] * (x[3] * x[4])  # 2,1
    H[4] += μ[1] * (x[2] * x[4])  # 3,1
    H[5] += μ[1] * (x[1] * x[4])  # 3,2
    H[7] += μ[1] * (x[2] * x[3])  # 4,1
    H[8] += μ[1] * (x[1] * x[3])  # 4,2
    H[9] += μ[1] * (x[1] * x[2])  # 4,3

    # Second constraint
    H[1]  += μ[2] * 2  # 1,1
    H[3]  += μ[2] * 2  # 2,2
    H[6]  += μ[2] * 2  # 3,3
    H[10] += μ[2] * 2  # 4,4

end

function hs071test_template(model::MOI.ModelLike, config::TestConfig, evaluator::HS071)
    atol = config.atol
    rtol = config.rtol

    @test MOI.supports(model, MOI.NLPBlock())
    @test MOI.supports_constraint(model, MOI.SingleVariable, MOI.LessThan{Float64})
    @test MOI.supports_constraint(model, MOI.SingleVariable, MOI.GreaterThan{Float64})
    @test MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex)

    MOI.empty!(model)
    @test MOI.is_empty(model)

    lb = [25.0, 40.0]
    ub = [Inf, 40.0]

    block_data = MOI.NLPBlockData(MOI.NLPBoundsPair.(lb, ub), evaluator, true)

    v = MOI.add_variables(model, 4)
    @test MOI.get(model, MOI.NumberOfVariables()) == 4

    l = [1.0,1.0,1.0,1.0]
    u = [5.0,5.0,5.0,5.0]
    start = [1,5,5,1]

    for i in 1:4
        MOI.add_constraint(model, MOI.SingleVariable(v[i]), MOI.LessThan(u[i]))
        MOI.add_constraint(model, MOI.SingleVariable(v[i]), MOI.GreaterThan(l[i]))
        MOI.set(model, MOI.VariablePrimalStart(), v[i], start[i])
    end

    MOI.set(model, MOI.NLPBlock(), block_data)
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)

    # TODO: config.query tests
    if config.solve
        MOI.optimize!(model)

        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status

        @test MOI.get(model, MOI.ResultCount()) >= 1

        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT

        @test MOI.get(model, MOI.ObjectiveValue()) ≈ 17.014017145179164 atol=atol rtol=rtol

        optimal_v = [1.0, 4.7429996418092970, 3.8211499817883077, 1.3794082897556983]

        @test MOI.get(model, MOI.VariablePrimal(), v) ≈ optimal_v atol=atol rtol=rtol

        # TODO: Duals? Maybe better to test on a convex instance.
    end
end

hs071_test(model, config) = hs071test_template(model, config, HS071(true))
hs071_no_hessian_test(model, config) = hs071test_template(model, config, HS071(false))

# Test for FEASIBILITY_SENSE.
# Find x satisfying x^2 == 1.
struct FeasibilitySenseEvaluator <: MOI.AbstractNLPEvaluator
    enable_hessian::Bool
end

function MOI.initialize(d::FeasibilitySenseEvaluator,
                         requested_features::Vector{Symbol})
    for feat in requested_features
        if !(feat in MOI.features_available(d))
            error("Unsupported feature $feat")
            # TODO: implement Jac-vec and Hess-vec products
            # for solvers that need them
        end
    end
end

function MOI.features_available(d::FeasibilitySenseEvaluator)
    if d.enable_hessian
        return [:Grad, :Jac, :Hess]
    else
        return [:Grad, :Jac]
    end
end

MOI.eval_objective(d::FeasibilitySenseEvaluator, x) = 0.0

function MOI.eval_constraint(d::FeasibilitySenseEvaluator, g, x)
    g[1] = x[1]^2
end

function MOI.eval_objective_gradient(d::FeasibilitySenseEvaluator, grad_f, x)
    grad_f[1] = 0.0
end

function MOI.jacobian_structure(d::FeasibilitySenseEvaluator)
    return Tuple{Int64,Int64}[(1,1)]
end

function MOI.hessian_lagrangian_structure(d::FeasibilitySenseEvaluator)
    @assert d.enable_hessian
    return Tuple{Int64,Int64}[(1,1)]
end

function MOI.eval_constraint_jacobian(d::FeasibilitySenseEvaluator, J, x)
    J[1] = 2x[1]
end

function MOI.eval_hessian_lagrangian(d::FeasibilitySenseEvaluator, H, x, σ, μ)
    @assert d.enable_hessian
    H[1] = 2μ[1] # 1,1
end

function feasibility_sense_test_template(model::MOI.ModelLike,
                                         config::TestConfig,
                                         set_has_objective::Bool,
                                         evaluator::FeasibilitySenseEvaluator)
    atol = config.atol
    rtol = config.rtol

    @test MOI.supports(model, MOI.NLPBlock())
    @test MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex)

    MOI.empty!(model)
    @test MOI.is_empty(model)

    lb = [1.0]
    ub = [1.0]

    block_data = MOI.NLPBlockData(MOI.NLPBoundsPair.(lb, ub), evaluator,
                                  set_has_objective)

    x = MOI.add_variable(model)
    @test MOI.get(model, MOI.NumberOfVariables()) == 1

    # Avoid starting at zero because it's a critial point.
    MOI.set(model, MOI.VariablePrimalStart(), x, 1.5)

    MOI.set(model, MOI.NLPBlock(), block_data)
    MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE)

    # TODO: config.query tests
    if config.solve
        MOI.optimize!(model)

        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status

        @test MOI.get(model, MOI.ResultCount()) >= 1

        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT

        @test MOI.get(model, MOI.ObjectiveValue()) ≈ 0.0 atol=atol rtol=rtol

        @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1.0 atol=atol rtol=rtol
    end
end

function feasibility_sense_with_objective_and_hessian_test(model, config)
    feasibility_sense_test_template(model, config, true,
                                    FeasibilitySenseEvaluator(true))
end

function feasibility_sense_with_objective_and_no_hessian_test(model, config)
    feasibility_sense_test_template(model, config, true,
                                    FeasibilitySenseEvaluator(false))
end

function feasibility_sense_with_no_objective_and_with_hessian_test(model, config)
    feasibility_sense_test_template(model, config, false,
                                    FeasibilitySenseEvaluator(true))
end

function feasibility_sense_with_no_objective_and_no_hessian_test(model, config)
    feasibility_sense_test_template(model, config, false,
                                    FeasibilitySenseEvaluator(false))
end

function nlp_objective_and_moi_objective_test(model::MOI.ModelLike,
                                              config::TestConfig)
    atol = config.atol
    rtol = config.rtol

    @test MOI.supports(model, MOI.NLPBlock())
    @test MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex)

    MOI.empty!(model)
    @test MOI.is_empty(model)

    lb = [1.0]
    ub = [2.0]

    block_data = MOI.NLPBlockData(MOI.NLPBoundsPair.(lb, ub),
                                  FeasibilitySenseEvaluator(true), true)

    x = MOI.add_variable(model)
    @test MOI.get(model, MOI.NumberOfVariables()) == 1

    # Avoid starting at zero because it's a critial point.
    MOI.set(model, MOI.VariablePrimalStart(), x, 1.5)

    MOI.set(model, MOI.NLPBlock(), block_data)
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    f_x =  MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0)
    # This objective function should be ignored.
    MOI.set(model, MOI.ObjectiveFunction{typeof(f_x)}(), f_x)

    # TODO: config.query tests
    if config.solve
        MOI.optimize!(model)

        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status

        @test MOI.get(model, MOI.ResultCount()) >= 1

        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT

        @test MOI.get(model, MOI.ObjectiveValue()) ≈ 0.0 atol=atol rtol=rtol

        @test 1.0 ≤ MOI.get(model, MOI.VariablePrimal(), x) ≤ 2.0
    end
end

const nlptests = Dict("hs071" => hs071_test,
                      "hs071_no_hessian" => hs071_no_hessian_test,
                      "feasibility_sense_with_objective_and_hessian" =>
                      feasibility_sense_with_objective_and_hessian_test,
                      "feasibility_sense_with_objective_and_no_hessian" =>
                      feasibility_sense_with_objective_and_no_hessian_test,
                      "feasibility_sense_with_no_objective_and_with_hessian" =>
                      feasibility_sense_with_no_objective_and_with_hessian_test,
                      "feasibility_sense_with_no_objective_and_no_hessian" =>
                      feasibility_sense_with_no_objective_and_no_hessian_test,
                      "nlp_objective_and_moi_objective" =>
                      nlp_objective_and_moi_objective_test
                      )

@moitestset nlp


mutable struct kNNRecommender
    # hyper parameters
    topK::Int
    shrink::Float64
    weighting::Union{Nothing, Symbol}
    # parameters
    rating_matrix::SparseMatrixCSC
    similarity_matrix::SparseMatrixCSC

    kNNRecommender(topK, shrink) = new(topK, shrink)
    kNNRecommender(topK, shrink, weighting) = new(topK, shrink, weighting)
end

function tfidf(rating_matrix::SparseMatrixCSC)
    U, I, R = findnz(rating_matrix)
    n_users, n_items = size(rating_matrix)

    bincount = zeros(n_users) # to avoid zero devision
    for u in U
        bincount[u] += 1
    end

    idf = log.(n_items ./ (bincount .+ 1e-6)) .+ 1

    for j in 1:length(U)
        R[j] = R[j] * idf[U[j]]
    end

    return sparse(U, I, R)
end

function compute_similarity_matrix(rating_matrix::SparseMatrixCSC, topK::Int, shrink::Float64)
    # (user, item)^T * (user, item) -> (item, item)
    # Return S[i, j] where j is full items, and i is related items at topK

    simJ = Int[]
    simI = Int[]
    simS = Float64[]

    U, I, R = findnz(rating_matrix)
    n_users, n_items = size(rating_matrix)

    norms = sqrt.(sum(rating_matrix.^2, dims=1))

    for j in 1:n_items
        Uj, Rj = findnz(rating_matrix[:, j])
        simj = zeros(n_items)
        for (u, ruj) in zip(Uj, Rj)
            Iu, Ri = findnz(rating_matrix[u, :])
            for (i, rui) in zip(Iu, Ri)
                s = rui * ruj
                s /= norms[j] * norms[i] + shrink + 1e-6
                simj[i] += s
            end
        end

        arg_sort_i = sortperm(simj, rev=true)[2:topK+1]
        append!(simI, arg_sort_i)
        append!(simS, simj[arg_sort_i])
        append!(simJ, fill(j, length(arg_sort_i)))
    end

    return sparse(simI, simJ, simS)
end

function compute_similarity_matrix(knn::kNNRecommender)
    rating_matrix = knn.rating_matrix

    if knn.weighting == :tfidf
        rating_matrix = tfidf(rating_matrix)
    end

    knn.similarity_matrix = compute_similarity_matrix(rating_matrix, knn.topK, knn.shrink)

    return nothing
end

function predict(knn::kNNRecommender, uidx::Int, topN::Int)
    preds = sortperm((knn.rating_matrix[uidx, :]' * knn.similarity_matrix)', rev=true)

    I, R  = findnz(knn.rating_matrix[uidx, :])
    filter!(p->!(p in I), preds)

    return preds[1:min(topN, length(preds))]
end

function predict(knn::kNNRecommender, uidxs::Array{Int64,1}, topN::Int)
    return map(uidx->predict(knn, uidx, topN), uidxs)
end

function evaluate_recall(knn::kNNRecommender, topN::Int, rating_matrix_test::SparseMatrixCSC)
    recalls = Float64[]

    for uidx in 1:size(knn.rating_matrix)[1]
        gts, = findnz(rating_matrix_test[uidx, :])
        if length(gts) == 0 continue end

        preds = predict(knn, uidx, topN)
        tps = intersect(Set(preds), Set(gts))

        push!(recalls, length(tps) / length(gts))
    end

    return recalls
end


module IntegralUtils

using LinearAlgebra
using Decimals

export parse_function, parse_num, Φ, transform, divergence, round_float

"""
    ∂(f::Function, var::Symbol, P₀::Array{T, 1}; Δ::Real = 1e-3)::Union{Real, Array{Real, 1}} where T <: Real

Compute a partial derivative of `f` with respect to `var` at a point `P₀`.

# Examples
```
julia> ∂((x, y, z) -> cos(x)sin(y)exp(z), :y, [π, 0, log(2)])
-1.9999999999996667

julia> ∂((x, y, z) -> cos(x)sin(y)exp(z), :y, [π, 0, log(2)]; Δ = 1e-9)
-2.0

julia> ∂((x, y, z) -> [x, y, z], :z, [1, 1, 1])
3-element Array{Float64,1}:
 0.0
 0.0
 0.9999999999177334

julia> ∂((u, v) -> [cos(u), sin(u), v], :u, [0, 1])
3-element Array{Float64,1}:
 -5.000444502911705e-7
  0.9999999999998334
  0.0
```
"""
function ∂(f::Function, var::Symbol, P₀::Array{T1, 1}; Δ::Real = 1e-6)::Union{Real, Array{T2, 1} where T2 <: Real} where T1 <: Real
    if length(P₀) == 3
        limits = Dict(sym=>vec for (sym, vec) in
                zip((:x, :y, :z), [Δ*Diagonal(ones((3, 3)))[3i-2:3i] for i in 1:3]))
    elseif length(P₀) == 2
        limits = Dict(sym=>vec for (sym, vec) in
                zip((:u, :v), [Δ*Diagonal(ones((2, 2)))[2i-1:2i] for i in 1:2]))
    end
    return (f((P₀ + limits[var])...) - f(P₀...)) / Δ
end

"""
    divergence(F::Function, P₀::Array{T, 1})::Real where T <: Real

Calculate the divergence of a vector field `F` at a point `P₀`.

# Examples
```
julia> divergence((x, y, z) -> [z * cos(x), z * sin(y), z - cos(x+y)], [π, π, 1])
4.998225722196992e-7

julia> divergence((x, y, z) -> [z * cos(x), z * sin(y), z - cos(x+y)], [0, 0, 1])
1.9999994998731165
```
"""
function divergence(F::Function, P₀::Array{T, 1})::Real where T <: Real
   return sum(hcat([∂(F, sym, P₀) for sym in (:x, :y, :z)]...)' .* Diagonal(ones(3, 3)))
end

"""
    normal(r::Function, P₀::Array{T, 1})::Array{Real, 1} where T <: Real

Compute a normal vector to the surface parametrized by `r` at point `P₀`.

# Examples
```
julia> normal((u, v) -> [cos(u)cos(v), sin(u)cos(v), sin(v)], [π/2, 0])
3-element Array{Real,1}:
 -5.000444502910872e-7
  0.9999999999174001
  5.000444502499502e-7

julia> normal((u, v) -> [cos(u), sin(u), v], [π/2, 1])
3-element Array{Real,1}:
 -5.000444502500335e-7
 0.9999999998353001
 0.0
```
"""
function normal(r::Function, P₀::Array{T, 1})::Array{Real, 1} where T <: Real
    rᵤ = ∂(r, :u, P₀)
    rᵥ = ∂(r, :v, P₀)
    return cross(rᵤ, rᵥ)
end

"""
    transform(F::Function, r::Function, P₀::Array{T, 1})::Real where T <: Real

Convert ``F(P₀)⋅dS`` to ``F(r(P₀))dudv``.

# Examples
```
julia> transform((x, y, z) -> [x, y, z], (u, v) -> [cos(u)cos(v), sin(u)cos(v), sin(v)], [π/2, 0])
0.9999999999174001

julia> transform((x, y, z) -> [-y, x, 0], (u, v) -> [(cos(u) + 2)cos(v), (cos(u) + 2)sin(v), sin(u)], [0, 0])
-4.500400052619784e-6
```
"""
function transform(F::Function, r::Function, P₀::Array{T, 1})::Real where T <: Real
   return dot(F(r(P₀...)...), normal(r, P₀))
end

function try_eval(f::Function, args::Float64...)::Float64
    try
        return f(args...)
    catch e
        return 0.0
    end
end

"""
    create_weights(X::Tuple{Real, Real}, ρ::Function, η::Function, ϕ::Function, ψ::Function, ξ::Int64, υ::Int64, ζ::Int64)::Array{Float64, 3}

Build a 3d matrix according to composite Simpson's rule for triple integrals.

# Examples
```
julia> create_weights((0, 1), (x, y) -> 0, (x, y) -> 1 - y - x, x -> 0, x -> 1 - x, 2, 2, 2)
3×3×3 Array{Float64,3}:
[:, :, 1] =
 1.0  1.0  0.0
 2.0  2.0  0.0
 0.0  0.0  0.0

[:, :, 2] =
 4.0  4.0  0.0
 8.0  8.0  0.0
 0.0  0.0  0.0

[:, :, 3] =
 1.0  1.0  0.0
 2.0  2.0  0.0
 0.0  0.0  0.0

 julia> create_weights((-1, 1), (x, y) -> sqrt(2-y^2-x^2), (x, y) -> 1, x -> 0, x -> sqrt(1-x^2), 4, 4, 2)
 5×5×3 Array{Float64,3}:
 [:, :, 1] =
  0.0  -1.11847  -0.828427  -1.11847  0.0
  0.0  -4.22673  -3.13553   -4.22673  0.0
  0.0  -1.73205  -1.2915    -1.73205  0.0
  0.0  -2.11231  -1.59166   -2.11231  0.0
  0.0   0.0       0.0        0.0      0.0

 [:, :, 2] =
  0.0   -4.4739    -3.31371   -4.4739   0.0
  0.0  -16.9069   -12.5421   -16.9069   0.0
  0.0   -6.9282    -5.16601   -6.9282   0.0
  0.0   -8.44925   -6.36665   -8.44925  0.0
  0.0    0.0        0.0        0.0      0.0

 [:, :, 3] =
  0.0  -1.11847  -0.828427  -1.11847  0.0
  0.0  -4.22673  -3.13553   -4.22673  0.0
  0.0  -1.73205  -1.2915    -1.73205  0.0
  0.0  -2.11231  -1.59166   -2.11231  0.0
  0.0   0.0       0.0        0.0      0.0
```
"""
function create_weights(X::Tuple{Real, Real}, ρ::Function, η::Function, ϕ::Function, ψ::Function,
    ξ::Int64, υ::Int64, ζ::Int64)::Array{Float64, 3}
    Xᵢ = LinRange(X... , ξ + 1)
    areas = reshape([try_eval((a, b) -> η(a, b) - ρ(a, b), x, y) * try_eval(a -> ψ(a) - ϕ(a), x) for x in Xᵢ for y in LinRange(ϕ(x), ψ(x), υ + 1)], (1, υ + 1, ξ + 1))
    areas[areas .< 0] .= 0.0
    return vcat([1], 4 * ones(ξ - 2) - repeat([0, 2], (ξ - 2) ÷ 2), [4, 1])  .*
            vcat([1], 4 * ones(υ - 2) - repeat([0, 2], (υ - 2) ÷ 2), [4, 1])' .*
            reshape(vcat([1], 4 * ones(ζ - 2) - repeat([0, 2], (ζ - 2) ÷ 2), [4, 1]), (1, 1, ζ + 1)) .* areas
end

"""
    create_weights(U::Tuple{Real, Real}, ϕ::Function, ψ::Function, μ::Int64, ν::Int64)::Array{Float64, 2}

Create a 2d matrix of weights according to composite simpson rule for double integrals.

# Examples
```
julia> create_weights((0, 1), x -> 0, x -> x, 6, 4)
7×5 Array{Float64,2}:
 0.0        0.0      0.0       0.0      0.0
 0.666667   2.66667  1.33333   2.66667  0.666667
 0.666667   2.66667  1.33333   2.66667  0.666667
 2.0        8.0      4.0       8.0      2.0
 1.33333    5.33333  2.66667   5.33333  1.33333
 3.33333   13.3333   6.66667  13.3333   3.33333
 1.0        4.0      2.0       4.0      1.0

julia> create_weights((0, π/4), x -> sin(x), x -> cos(x), 8, 4)
9×5 Array{Float64,2}:
 1.0           4.0          2.0           4.0          1.0
 3.58867      14.3547       7.17734      14.3547       3.58867
 1.57139       6.28556      3.14278       6.28556      1.57139
 2.66662      10.6665       5.33325      10.6665       2.66662
 1.08239       4.32957      2.16478       4.32957      1.08239
 1.6421        6.56839      3.2842        6.56839      1.6421
 0.551799      2.2072       1.1036        2.2072       0.551799
 0.554469      2.21787      1.10894       2.21787      0.554469
 1.11022e-16   4.44089e-16  2.22045e-16   4.44089e-16  1.11022e-16
```
"""
function create_weights(U::Tuple{Real, Real}, ϕ::Function, ψ::Function, μ::Int64, ν::Int64)::Array{Float64, 2}
    Uᵢ = LinRange(U..., μ + 1)
    lengths = ψ.(Uᵢ) - ϕ.(Uᵢ)
    return vcat([1], 4 * ones(μ - 2) - repeat([0, 2], (μ - 2) ÷ 2), [4, 1]) .*
            vcat([1], 4 * ones(ν - 2) - repeat([0, 2], (ν - 2) ÷ 2), [4, 1])' .* lengths
end

"""
    split_region(X::Tuple{Real, Real}, ρ::Function, η::Function, ϕ::Function, ψ::Function, ξ::Int64, υ::Int64, ζ::Int64)::Array{Array{Float64, 1}, 3}

Divide a 3d region Int64o points on space.

# Examples
```
julia> split_region((0, 1), (x, y) -> 0, (x, y) -> 1, x -> 0, x -> 1, 4, 2, 2)
5×3×3 Array{Array{Float64,1},3}:
[:, :, 1] =
 [0.0, 0.0, 0.0]  [0.0, 0.5, 1.0]   [0.25, 0.0, 0.5]
 [0.0, 0.0, 0.5]  [0.0, 1.0, 0.0]   [0.25, 0.0, 1.0]
 [0.0, 0.0, 1.0]  [0.0, 1.0, 0.5]   [0.25, 0.5, 0.0]
 [0.0, 0.5, 0.0]  [0.0, 1.0, 1.0]   [0.25, 0.5, 0.5]
 [0.0, 0.5, 0.5]  [0.25, 0.0, 0.0]  [0.25, 0.5, 1.0]

[:, :, 2] =
 [0.25, 1.0, 0.0]  [0.5, 0.0, 1.0]  [0.5, 1.0, 0.5]
 [0.25, 1.0, 0.5]  [0.5, 0.5, 0.0]  [0.5, 1.0, 1.0]
 [0.25, 1.0, 1.0]  [0.5, 0.5, 0.5]  [0.75, 0.0, 0.0]
 [0.5, 0.0, 0.0]   [0.5, 0.5, 1.0]  [0.75, 0.0, 0.5]
 [0.5, 0.0, 0.5]   [0.5, 1.0, 0.0]  [0.75, 0.0, 1.0]

[:, :, 3] =
 [0.75, 0.5, 0.0]  [0.75, 1.0, 1.0]  [1.0, 0.5, 0.5]
 [0.75, 0.5, 0.5]  [1.0, 0.0, 0.0]   [1.0, 0.5, 1.0]
 [0.75, 0.5, 1.0]  [1.0, 0.0, 0.5]   [1.0, 1.0, 0.0]
 [0.75, 1.0, 0.0]  [1.0, 0.0, 1.0]   [1.0, 1.0, 0.5]
 [0.75, 1.0, 0.5]  [1.0, 0.5, 0.0]   [1.0, 1.0, 1.0]

julia> split_region((-1, 1), (x, y) -> 0, (x, y) -> sqrt(2-x^2-y^2), x -> -sqrt(1-x^2), x -> sqrt(1-x^2), 4, 4, 2)
5×5×3 Array{Array{Float64,1},3}:
[:, :, 1] =
 [-1.0, 0.0, 0.0]  [-1.0, 0.0, 1.0]  [-1.0, 0.0, 0.5]  [-0.5, -0.866025, 0.0]    [-0.5, -0.433013, 1.25]
 [-1.0, 0.0, 0.5]  [-1.0, 0.0, 0.0]  [-1.0, 0.0, 1.0]  [-0.5, -0.866025, 0.5]    [-0.5, 0.0, 0.0]
 [-1.0, 0.0, 1.0]  [-1.0, 0.0, 0.5]  [-1.0, 0.0, 0.0]  [-0.5, -0.866025, 1.0]    [-0.5, 0.0, 0.661438]
 [-1.0, 0.0, 0.0]  [-1.0, 0.0, 1.0]  [-1.0, 0.0, 0.5]  [-0.5, -0.433013, 0.0]    [-0.5, 0.0, 1.32288]
 [-1.0, 0.0, 0.5]  [-1.0, 0.0, 0.0]  [-1.0, 0.0, 1.0]  [-0.5, -0.433013, 0.625]  [-0.5, 0.433013, 0.0]

[:, :, 2] =
 [-0.5, 0.433013, 0.625]  [0.0, -1.0, 0.0]       [0.0, -0.5, 1.32288]  [0.0, 0.5, 0.661438]  [0.5, -0.866025, 0.0]
 [-0.5, 0.433013, 1.25]   [0.0, -1.0, 0.5]       [0.0, 0.0, 0.0]       [0.0, 0.5, 1.32288]   [0.5, -0.866025, 0.5]
 [-0.5, 0.866025, 0.0]    [0.0, -1.0, 1.0]       [0.0, 0.0, 0.707107]  [0.0, 1.0, 0.0]       [0.5, -0.866025, 1.0]
 [-0.5, 0.866025, 0.5]    [0.0, -0.5, 0.0]       [0.0, 0.0, 1.41421]   [0.0, 1.0, 0.5]       [0.5, -0.433013, 0.0]
 [-0.5, 0.866025, 1.0]    [0.0, -0.5, 0.661438]  [0.0, 0.5, 0.0]       [0.0, 1.0, 1.0]       [0.5, -0.433013, 0.625]

[:, :, 3] =
 [0.5, -0.433013, 1.25]  [0.5, 0.433013, 0.625]  [1.0, 0.0, 0.0]  [1.0, 0.0, 1.0]  [1.0, 0.0, 0.5]
 [0.5, 0.0, 0.0]         [0.5, 0.433013, 1.25]   [1.0, 0.0, 0.5]  [1.0, 0.0, 0.0]  [1.0, 0.0, 1.0]
 [0.5, 0.0, 0.661438]    [0.5, 0.866025, 0.0]    [1.0, 0.0, 1.0]  [1.0, 0.0, 0.5]  [1.0, 0.0, 0.0]
 [0.5, 0.0, 1.32288]     [0.5, 0.866025, 0.5]    [1.0, 0.0, 0.0]  [1.0, 0.0, 1.0]  [1.0, 0.0, 0.5]
 [0.5, 0.433013, 0.0]    [0.5, 0.866025, 1.0]    [1.0, 0.0, 0.5]  [1.0, 0.0, 0.0]  [1.0, 0.0, 1.0]
```
"""
function split_region(X::Tuple{Real, Real}, ρ::Function, η::Function, ϕ::Function, ψ::Function,
    ξ::Int64, υ::Int64, ζ::Int64)::Array{Array{Float64, 1}, 3}
    Xᵢ = LinRange(X..., ξ + 1)
    return reshape([[x, y, z] for x in Xᵢ for y in LinRange(ϕ(x), ψ(x), υ + 1) for z in LinRange(try_eval(ρ, x, y), try_eval(η, x, y), ζ + 1)], :, υ + 1, ξ + 1)
end

"""
    split_region(Uᵢ::Tuple{Real, Real}, ϕ::Function, ψ::Function, μ::Int64, ν::Int64)::Array{Array{Float64, 1}, 2}

Divide a 2d region Int64o points on plane.

# Examples
```
julia> split_region((0, 1), x -> 0, x -> x, 6, 4)
7×5 Array{Array{Float64,1},2}:
 [0.0, 0.0]             [0.166667, 0.0833333]  [0.333333, 0.333333]  [0.666667, 0.166667]  [0.833333, 0.625]
 [0.0, 0.0]             [0.166667, 0.125]      [0.5, 0.0]            [0.666667, 0.333333]  [0.833333, 0.833333]
 [0.0, 0.0]             [0.166667, 0.166667]   [0.5, 0.125]          [0.666667, 0.5]       [1.0, 0.0]
 [0.0, 0.0]             [0.333333, 0.0]        [0.5, 0.25]           [0.666667, 0.666667]  [1.0, 0.25]
 [0.0, 0.0]             [0.333333, 0.0833333]  [0.5, 0.375]          [0.833333, 0.0]       [1.0, 0.5]
 [0.166667, 0.0]        [0.333333, 0.166667]   [0.5, 0.5]            [0.833333, 0.208333]  [1.0, 0.75]
 [0.166667, 0.0416667]  [0.333333, 0.25]       [0.666667, 0.0]       [0.833333, 0.416667]  [1.0, 1.0]

julia> split_region((0, 2), x -> -sqrt(4-x^2), x -> sqrt(4-x^2), 6, 6)
7×7 Array{Array{Float64,1},2}:
 [0.0, -2.0]       [0.333333, -1.97203]   [0.666667, -1.88562]   [1.0, -1.73205]  [1.33333, -1.49071]   [1.66667, -1.10554]   [2.0, 0.0]
 [0.0, -1.33333]   [0.333333, -1.31468]   [0.666667, -1.25708]   [1.0, -1.1547]   [1.33333, -0.993808]  [1.66667, -0.737028]  [2.0, 0.0]
 [0.0, -0.666667]  [0.333333, -0.657342]  [0.666667, -0.628539]  [1.0, -0.57735]  [1.33333, -0.496904]  [1.66667, -0.368514]  [2.0, 0.0]
 [0.0, 0.0]        [0.333333, 0.0]        [0.666667, 0.0]        [1.0, 0.0]       [1.33333, 0.0]        [1.66667, 0.0]        [2.0, 0.0]
 [0.0, 0.666667]   [0.333333, 0.657342]   [0.666667, 0.628539]   [1.0, 0.57735]   [1.33333, 0.496904]   [1.66667, 0.368514]   [2.0, 0.0]
 [0.0, 1.33333]    [0.333333, 1.31468]    [0.666667, 1.25708]    [1.0, 1.1547]    [1.33333, 0.993808]   [1.66667, 0.737028]   [2.0, 0.0]
 [0.0, 2.0]        [0.333333, 1.97203]    [0.666667, 1.88562]    [1.0, 1.73205]   [1.33333, 1.49071]    [1.66667, 1.10554]    [2.0, 0.0]
```
"""
function split_region(U::Tuple{Real, Real}, ϕ::Function, ψ::Function, μ::Int64, ν::Int64)::Array{Array{Float64, 1}, 2}
    Uᵢ = LinRange(U..., μ + 1)
    return reshape([[u, v] for u in Uᵢ for v in LinRange(ϕ(u), ψ(u), ν + 1)], μ + 1, :)
end

"""
    coeff(A::Tuple{Real, Real}, n::Int64)::Float64

Compute a coefficient for composite Simpson's rule with `n` midpoints and interval `A`.

# Examples
```
julia> coeff((0, 1), 2)
0.16666666666666666

julia> coeff((0, 4), 4)
0.3333333333333333
```
"""
coeff(A::Tuple{Real, Real}, n::Real)::Float64 = (A[2] - A[1]) / (3 * n)

"""
    ∯(F::Function, X::Tuple{Real, Real}, ρ::Function, η::Function, ϕ::Function, ψ::Function, ξ::Int64, υ::Int64, ζ::Int64)::Float64

Determine flux of vector field `F` through a 3d region using Gauss-Ostrogradski theorem and Simpson's rule for ``ξ⋅υ⋅ζ`` nodes.

# Examples
```
julia> ∯((x, y, z) -> [x, y, z], (0, 1), (x, y) -> 0, (x, y) -> 1, x -> 0, x -> 1, 2, 2, 2)
3.0000000000163776

julia> ∯((x, y, z) -> [x^2, y^2, z^2], (-1, 1), (x, y) -> 0, (x, y) -> x^2+y^2, x -> -1, x -> 1, 6, 4, 2)
2.5488967009810803
```
"""
function ∯(F::Function, X::Tuple{Real, Real}, ρ::Function, η::Function, ϕ::Function, ψ::Function, ξ::Int64, υ::Int64, ζ::Int64)::Float64

    weights = create_weights(X, ρ, η, ϕ, ψ, ξ, υ, ζ)
    points = split_region(X, ρ, η, ϕ, ψ, ξ, υ, ζ)
    return sum(divergence.(F, points) .* weights) * prod([coeff(interval, steps)
            for (interval, steps) in zip((X, (0, 1), (0, 1)), (ξ, υ, ζ))])
end

"""
    ∯(F::Function, r::Function, U::Tuple{Real, Real}, ϕ::Function, ψ::Function, μ::Int64, ν::Int64)::Float64

Determine flux of vector field `F` through a 2d region using Jacobian and Simpson's rule for ``μ⋅ν`` nodes.

# Examples
```
julia> ∯((x, y, z) -> [x, y, z], (u, v) -> [cos(u), sin(u), v], (0, 2π), u -> 0, u -> u, 2, 2)
19.739208804825942

julia> ∯((x, y, z) -> [x^2, y^2, z^2], (u, v) -> [u, v, 4-u-v], (0, 4), u -> 0, u -> 4-u, 8, 10)
64.00000000410644
```
"""
function ∯(F::Function, r::Function, U::Tuple{Real, Real}, ϕ::Function, ψ::Function, μ::Int64, ν::Int64)::Float64
    points = split_region(U, ϕ, ψ, μ, ν)
    weights = create_weights(U, ϕ, ψ, μ, ν)
    return sum(transform.(F, r, points) .* weights') * prod(coeff(interval, steps)
            for (interval, steps) in zip((U, (0, 1)), (μ, ν)))
end

"""
    draw_points(Xᵢ::LinRange{Float64}, Yᵢ::LinRange{Float64}, Zᵢ::LinRange{Float64}, N::Int64)::Array{Array{Float64, 1}, 1}

Choose randomly 3d points from a cuboid.

# Examples
```
julia> draw_points(LinRange(0, 1, 5), LinRange(0, 1, 5), LinRange(0, 1, 5), 11)
11-element Array{Array{Float64,1},1}:
 [0.75, 0.0, 0.5]
 [1.0, 0.25, 1.0]
 [1.0, 0.0, 1.0]
 [1.0, 0.5, 0.25]
 [0.5, 0.25, 1.0]
 [0.5, 0.0, 0.0]
 [0.5, 0.5, 0.75]
 [1.0, 0.75, 1.0]
 [0.5, 1.0, 0.0]
 [0.5, 1.0, 1.0]
 [0.75, 0.75, 1.0]

julia> draw_points(LinRange(-1, 1, 5), LinRange(0, π, 19), LinRange(2, 2, 2), 3)
3-element Array{Array{Float64,1},1}:
 [-0.5, 2.0943951023931953, 2.0]
 [0.0, 2.96705972839036, 2.0]
 [0.0, 2.443460952792061, 2.0]
```
"""
function draw_points(Xᵢ::LinRange{Float64}, Yᵢ::LinRange{Float64}, Zᵢ::LinRange{Float64}, N::Int64)::Array{Array{Float64, 1}, 1}
    return [[rand(Xᵢ), rand(Yᵢ), rand(Zᵢ)] for i in 1:N]
end

"""
    draw_points(Xᵢ::LinRange{Float64}, Yᵢ::LinRange{Float64}, N::Int64)::Array{Array{Float64, 1}, 1}

Choose randomly 2d points from a rectangle.

# Examples
```
julia> draw_points(LinRange(0, 1, 5), LinRange(0, 1, 5), 11)
11-element Array{Array{Float64,1},1}:
 [0.5, 1.0]
 [0.75, 0.75]
 [0.5, 0.75]
 [0.0, 0.25]
 [1.0, 1.0]
 [0.75, 0.25]
 [0.25, 0.25]
 [0.5, 0.75]
 [1.0, 0.5]
 [0.5, 1.0]
 [0.0, 0.25]

julia> draw_points(LinRange(-1, 1, 5), LinRange(2, 2, 19), 3)
3-element Array{Array{Float64,1},1}:
 [0.5, 2.0]
 [-1.0, 2.0]
 [-0.5, 2.0]
```
"""
function draw_points(Xᵢ::LinRange{Float64}, Yᵢ::LinRange{Float64}, N::Int64)::Array{Array{Float64, 1}, 1}
    return [[rand(Xᵢ), rand(Yᵢ)] for i in 1:N]
end

"""
    surface_part(F::Function, r::Function, S::Array{LinRange{Float64}, 1}, N::Int64, ϕ::Function, ψ::Function)::Float64

Compute flux over one of parts of a rectangle using Monte Carlo method.

# Examples
```
julia> surface_part((x, y, z) -> [x, y, z], (u, v) -> [u, v, 0], [LinRange(0, 1, 100), LinRange(0, 1, 100)], 1000, u -> 0, u -> u)
0.0

julia> surface_part((x, y, z) -> [x^2, y^2, z^2], (u, v) -> [u, v, u-v], [LinRange(0, 1, 100), LinRange(-1, 1, 100)], 100000, u -> -u, u -> u)
0.33893245180194237
```
"""
function surface_part(F::Function, r::Function, S::Array{LinRange{Float64}, 1}, N::Int64, ϕ::Function, ψ::Function)::Float64
    Uᵢ, Vᵢ = S
    points = draw_points(Uᵢ, Vᵢ, N)
    validate(P::Array{Float64, 1}) = ϕ(P[1]) <= P[2] <= ψ(P[1]) ? transform(F, r, P) : 0.0
    return sum(validate.(points)) * (max(Uᵢ...) - min(Uᵢ...)) * (max(Vᵢ...) - min(Vᵢ...)) / N
end

"""
    surface_part(F::Function, S::Array{LinRange{Float64}, 1}, N::Int64, ρ::Function, η::Function, ϕ::Function, ψ::Function)::Float64

Compute flux over one of parts of a cuboid using Monte Carlo method.

# Examples
```
julia> surface_part((x, y, z) -> [x, y, z], [LinRange(0, 1, 100), LinRange(0, 1, 100), LinRange(0, 1, 100)], 1000, (x, y) -> 0, (x, y) -> 1, x -> 0, x -> x)
1.4940000000104536

julia> surface_part((x, y, z) -> [x^2, y^2, z^2], [LinRange(0, 1, 100), LinRange(0, 1, 100), LinRange(-1, 1, 100)], 100000, (x, y) -> 0, (x, y) -> x+y, x -> -x, x -> x)
1.301682061621842
```
"""
function surface_part(F::Function, S::Array{LinRange{Float64}, 1}, N::Int64, ρ::Function, η::Function, ϕ::Function, ψ::Function)::Float64
    Xᵢ, Yᵢ, Zᵢ = S
    points = draw_points(Xᵢ, Yᵢ, Zᵢ, N)
    validate(P::Array{Float64, 1}) = ϕ(P[1]) <= P[2] <= ψ(P[1]) && ρ(P[1:2]...) <= P[3] <= η(P[1:2]...) ? divergence(F, P) : 0.0
    return sum(validate.(points)) * (max(Xᵢ...) - min(Xᵢ...)) * (max(Yᵢ...) - min(Yᵢ...)) * (max(Zᵢ...) - min(Zᵢ...)) / N
end

"""
    ∯(F::Function, r::Function, U::Tuple{Real, Real}, ϕ::Function, ψ::Function, N::Int64, parts::Float64)::Float64

Determine flux of vector field `F` through a 3d region using Gauss-Ostrogradski theorem and Monte Carlo method for `N` random points.

# Examples
```
julia> ∯((x, y, z) -> [x, y, z], (0, 1), (x, y) -> 0, (x, y) -> 1, x -> 0, x -> 1, 10000, 16.0)
2.999400020649781

julia> ∯((x, y, z) -> [x^2, y^2, z^2], (-1, 1), (x, y) -> 0, (x, y) -> x^2+y^2, x -> -1, x -> 1, 100, 256.0)
2.4227719867790514
```
"""
function ∯(F::Function, r::Function, U::Tuple{Real, Real}, ϕ::Function, ψ::Function, N::Int64, parts::Float64)::Float64
    x_part = Int64(parts / 2)
    y_part = Int64(parts / x_part)
    Uᵢ = LinRange(U..., x_part*N)
    Vᵢ = LinRange(min(ϕ.(Uᵢ)...), max(ψ.(Uᵢ)...), y_part*N)
    S = [[Uᵢ[N*i+1:(i+1)N], Vᵢ[N*j+1:(j+1)N]] for i in 0:(x_part-1), j in 0:(y_part-1)]
    return sum(surface_part.(F, r, S, N, ϕ, ψ))
end

"""
    ∯(F::Function, X::Tuple{Real, Real}, ρ::Function, η::Function, ϕ::Function, ψ::Function, N::Int64, parts::Float64)::Float64

Determine flux of vector field `F` through a 2d region using Jacobian and Monte Carlo method for `N` random points.

# Examples
```
julia> ∯((x, y, z) -> [x, y, z], (u, v) -> [cos(u), sin(u), v], (0, 2π), u -> 0, u -> u, 10000, 16.0)
19.776707787586332

julia> ∯((x, y, z) -> [x^2, y^2, z^2], (u, v) -> [u, v, 4-u-v], (0, 4), u -> 0, u -> 4-u, 100000, 32.0)
63.9916073475161
```
"""
function ∯(F::Function, X::Tuple{Real, Real}, ρ::Function, η::Function, ϕ::Function, ψ::Function, N::Int64, parts::Float64)::Float64
    x_part = y_part = Int64(parts / 4)
    z_part = Int64(parts / 2x_part)
    Xᵢ = LinRange(X..., x_part*N)
    Yᵢ = LinRange(min(ϕ.(Xᵢ)...), max(ψ.(Xᵢ)...), y_part*N)
    unzip(f, P) = try_eval(f, P...)
    Zᵢ = LinRange(min(unzip.(ρ, zip(Xᵢ, Yᵢ))...), max(unzip.(η, zip(Xᵢ, Yᵢ))...), z_part*N)
    S = [[Xᵢ[N*i+1:(i+1)N], Yᵢ[N*j+1:(j+1)N], Zᵢ[N*k+1:(k+1)N]] for i in 0:(x_part-1), j in 0:(y_part-1), k in 0:(z_part-1)]
    return sum(surface_part.(F, S, N, ρ, η, ϕ, ψ))
end

"""
    round_float(α::Float64, ϵ::Float64)::Union{Float64, Int64}

Round `α` to the same Real of digits as `ϵ` or nearest integer with `ϵ` accuracy.

# Examples
```
julia> round_float(π, 1e-12)
3.14159265359

julia> round_float(exp(1), 1e-3)
2.718

julia> round_float(1.999, 1e-3)
2
```
"""
round_float(α::Float64, ϵ::Float64)::Union{Int64, Float64} = abs(α - round(α)) < ϵ ? Int64(round(α)) : round(α, digits = abs(Decimal(ϵ).q))

"""
    arguments(args::Symbol...)

Parse arguments' symbols to a string.

# Examples
```
julia> arguments(:u, :v)
"(u, v)"

julia> arguments(:x, :y, :z)
"(x, y, z)"
```
"""
arguments(args::Symbol...)::String = '(' * String(args[1]) * reduce(*, ", " .* String.(args[2:end])) * ')'

"""
    parse_function(name::Symbol, body::String, args::Symbol...)::Function

Parse a body of a function and arguments' and function name's symbols to the actual function.

# Examples
```
julia> parse_function(:f, "cos(x)sin(y)+exp(z)", :x, :y, :z)
f (generic function with 1 method)

julia> parse_function(:g, "u^2-cos(exp(1/v))", :u, :v)
g (generic function with 1 method)
```
"""
parse_function(body::String, args::Symbol...)::Function = eval(Meta.parse(arguments(args...) * "->" * body))

"""
    parse_function(name::Symbol, body::String, args::Symbol)::Function

Parse a body of a function and argument and function name's symbols to the actual function.

# Examples
```
julia> parse_function(:f, "2cos(x)", :x)
f (generic function with 1 method)

julia> parse_function(:g, "u^2-7u", :u)
g (generic function with 1 method)
```
"""
parse_function(body::String, args::Symbol)::Function = eval(Meta.parse(string(args) * "->" * body))

"""
    parse_num(value::String)::Float64

Parse a Real or an expression from string to float.

# Examples
```
julia> parse_num("2pi")
6.283185307179586

julia> parse_num("2pi - 6/3 + 7.2")
11.483185307179586
```
"""
parse_num(value::String)::Float64 = eval(Meta.parse(value))

"""
    Φ(F::Function, X::Tuple{Real, Real}, ρ::Function, η::Function, ϕ::Function, ψ::Function; N::Int64 = 100, technique::String = "Simpson")::Union{Int64, Float64}

Determine a flux of a vector field `F` through a surface.

# Examples
```
julia> Φ((x, y, z) -> [x, y, z], (0, 1), (x, y) -> 0, (x, y) -> 1, x -> 0, x -> 1)
3

julia> Φ((x, y, z) -> [-y, x, 0], (-1, 1), (x, y) -> 0, (x, y) -> sqrt(2-y^2-x^2), x -> -sqrt(1-x^2), x -> sqrt(1-x^2); technique = "Monte Carlo", ϵ = 0.1)
0
```
"""
function Φ(F::Function, X::Tuple{Real, Real}, ρ::Function, η::Function, ϕ::Function, ψ::Function;
     N::Int64 = 100, technique::String = "Simpson")::Union{Int64, Float64}
    if technique in ("Simpson", "Monte Carlo")
        surface_integral(n) = technique == "Simpson" ? ∯(F, X, ρ, η, ϕ, ψ, 2n, 2n, 2n) : ∯(F, X, ρ, η, ϕ, ψ, 100n, 4.0n)
    else
        throw(ArgumentError("Invalid technique name."))
    end
    return abs(surface_integral(N))
end

"""
    Φ(F::Function, r::Function, U::Tuple{Real, Real}, ϕ::Function, ψ::Function; N::Int64 = 100, technique::String = "Simpson")::Union{Int64, Float64}

Determine a flux of a vector field `F` through a surface parametrized by `r`.

# Examples
```
julia> Φ((x, y, z) -> (x^2, y^2, z^2), (u, v) -> [u, v, 4-u-v], (0, 4), u -> 0, u -> 4-u)
64

julia> Φ((x, y, z) -> [x-y, y-z, z-x], (u, v) -> [(cos(u)+2)cos(v), (cos(u)+2)sin(v), sin(u)], (0, 2π), u -> 0, u -> 2π; technique = "Monte Carlo", ϵ = 0.01)
-118.25
```
"""
function Φ(F::Function, r::Function, U::Tuple{Real, Real}, ϕ::Function, ψ::Function;
     N::Int64 = 100, technique::String = "Simpson")::Union{Int64, Float64}
    if technique in ("Simpson", "Monte Carlo")
        surface_integral(n::Int64) = technique == "Simpson" ? ∯(F, r, U, ϕ, ψ, 2n, 2n) : ∯(F, r, U, ϕ, ψ, 100n, 4.0n)
    else
        throw(ArgumentError("Invalid technique name."))
    end
    return abs(surface_integral(N))
end

end


function cartesian_interval_test(lib::Polyhedra.Library)
    G = [-1  0
          0 -1
          1  1]
    F = [0, 0, 1]
    S = polyhedron(hrep(G, F), lib)
    U = polyhedron(convexhull([-2], [2]))
    @test Polyhedra.FullDim(U) == StaticArrays.Size{(1,)}()
    # Mixes `S` whose `FullDim` is `2` and `U`
    # whose `FullDim` has type `StaticArrays.Size`.
    poly = S * U
    @test Polyhedra.FullDim(poly) == 3
    h = HalfSpace([-1,  0,  0], 0) ∩
        HalfSpace([ 0, -1,  0], 0) ∩
        HalfSpace([ 1,  1,  0], 1) ∩
        HalfSpace([ 0,  0,  1], 2) ∩
        HalfSpace([ 0,  0, -1], 2)
    inequality_fulltest(poly, h)
    v = convexhull([0, 0, 2], [0, 1, 2], [1, 0, 2],
                   [0, 0, -2], [0, 1, -2], [1, 0, -2])
    generator_fulltest(poly, v)
end


# Test replay for loop

using Turing
using ForwardDiff: Dual

# Generate synthesised data
xs = rand(Normal(0.5, 1), 100)

# Define model
@model priorsinarray(xs) = begin
  priors = Vector{Real}(undef, 2)
  priors[1] ~ InverseGamma(2, 3)
  priors[2] ~ Normal(0, sqrt(priors[1]))
  for i = 1:length(xs)
    xs[i] ~ Normal(priors[2], sqrt(priors[1]))
  # for x in xs
    # x ~ Normal(priors[2], sqrt(priors[1]))
  end
  priors
end

# Sampling
chain = sample(priorsinarray(xs), HMC(10, 0.01, 10))


using ContinuumArrays, QuasiArrays, FillArrays, InfiniteArrays
import QuasiArrays: Inclusion, QuasiDiagonal

using Plots; pyplot();

S = LinearSpline(range(0,1; length=10))
xx = range(0,1; length=1000)

S = Jacobi(true,true)

P = Legendre()
X = QuasiDiagonal(Inclusion(-1..1))

@test X[-1:0.1:1,-1:0.1:1] == Diagonal(-1:0.1:1)

axes(X)
J = pinv(P)*X*P

J - I
Vcat(Hcat(1, Zeros(1,∞)), J)


"""
    sfspectrum(x::Number, A::T, ω::T, Γ::T[, φ::AbstractArray, χnr]) where {T<:AbstractArray}

Calculate the sum frequency spectrum according to

``I = \\left| \\chi_{NR} + \\sum_q \\frac{A_q}{x - ω_q - i\\Gamma_q} \\right|^2``

# Arguments
* `x` the frequency/wavenumber at which to evaluate the sfspectrum
* `A` oscillator strength
* `ω` resonances
* `Γ` damping coefficients

# Keyword Arguments

# Examples
```
x = range(2800, 3000, length=201)
A = [1, 1]
ω = [2880, 2930]
Γ = [8, 7]
```
In order to use the broadcast functionality we have to wrap the parameters in Refs:
```
y = sfspectrum.(x, Ref(A), Ref(ω), Ref(Γ))
```
Alternatively use array comprehensions:
```
y = [sfspectrum(_x, A, ω, Γ) for _x in x]
```
"""
function sfspectrum(x, A, ω, Γ, φ::T, χnr) where T<:AbstractVector
    y = χnr + 0.0im
    for i in eachindex(A)
        y += lorentzian(x, A[i], ω[i], Γ[i]) * exp(1im * φ[i])
    end
    abs2(y)
end

"""
If no phase information and non-resonant background is passed to the function, we set it to 0.
"""
function sfspectrum(x, A, ω, Γ)
    T = eltype(A)
    N = length(A)
    sfspectrum(x, A, ω, Γ, zeros(T,N), zero(T))
end

function sfspectrum!(y::Array{C}, x::T, A::T, ω::T, Γ::T, φ::T, χnr::C) where {T<:AbstractArray, C<:Complex}
    y .= χnr
    for j in eachindex(y)
        for i in eachindex(A)
            y[j] += lorentzian(x[j], A[i], ω[i], Γ[i]) * exp(1im * φ[i])
        end
        y[j] = abs2(y[j])
    end
    y
end

function sfspectrum!(y, x, A, ω, Γ)
    y .= zero(eltype(y))
    @inbounds Threads.@threads for j in eachindex(y)
        for i in eachindex(A)
            y[j] += lorentzian(x[j], A[i], ω[i], Γ[i])
        end
        y[j] = abs2(y[j])
    end
    y
end

"""
If just one phase is passed, we assume that this belongs to the non-resonant background.
"""
function sfspectrum(x, A, ω, Γ, φ::M, χnr) where M <: Number
    T = eltype(A)
    N = length(A)
    φ_array = zeros(T,N)
    χnr_complex = χnr * exp(1im * φ)
    sfspectrum(x, A, ω, Γ, φ_array, χnr_complex)
end

"""
If no phase information and but the non-resonant background is passed it's assumed that the relative phase of the resonances is +/- π, which can be controlled via the sign of `A`.
"""
function sfspectrum(x, A, ω, Γ, χnr)
    T = eltype(A)
    N = length(A)
    φ = zeros(T,N)
    sfspectrum(x, A, ω, Γ, φ, χnr)
end

"""
Documentation
"""
function lorentzian(x, A, ω, Γ)
    A / (x - ω + 1im * Γ)
end

"""
"""
function sfspectrum_voigt(x, A, ω, Γ, σ)
    T = eltype(A)
    N = length(A)
    sfspectrum_voigt(x, A, ω, Γ, σ, zeros(T,N), zero(T))
end

"""
"""
function sfspectrum_voigt(x::Number, A::T, ω::T, Γ::T, σ::T, φ::T, χnr::Number) where {T<:AbstractArray}
    y = χnr
    for i in eachindex(A)
        y += voigt(x, A[i], ω[i], Γ[i], σ[i])
    end
    abs2(y)
end

"""
"""
function gaussian(x,μ,σ)
	1 / (σ * √(2π)) * ℯ^(-(x-μ)^2 / 2σ^2)
end

"""
"""
function voigt(x, A, ω, Γ, σ)
    f = τ -> gaussian(τ, ω, σ) * lorentzian(x-τ, A, 0, Γ)
    r = 4σ # range of evaluated integral
	integral, err = quadgk(f, ω-r, ω+r)
	integral
end


using FastBEAST
using StaticArrays
using LinearAlgebra
using Printf

function OneoverRkernel(testpoint::SVector{3,T}, sourcepoint::SVector{3,T}) where T
    if isapprox(testpoint, sourcepoint, rtol=eps()*1e1)
        return 0.0
    else
        return 1.0 / (norm(testpoint - sourcepoint))
    end
end

function assembler(kernel, testpoints, sourcepoints)
    kernelmatrix = zeros(promote_type(eltype(testpoints[1]),eltype(sourcepoints[1])),
                length(testpoints), length(sourcepoints))

    for i = 1:length(testpoints)
        for j = 1:length(sourcepoints)
            kernelmatrix[i,j] = kernel(testpoints[i], sourcepoints[j])
        end
    end
    return kernelmatrix
end

function assembler(kernel, matrix, testpoints, sourcepoints)
    for i = 1:length(testpoints)
        for j = 1:length(sourcepoints)
            matrix[i,j] = kernel(testpoints[i], sourcepoints[j])
        end
    end
end

function aca_benchmark(N)
    spoints = [@SVector rand(3) for i = 1:N]
    tpoints = 0.1*[@SVector rand(3) for i = 1:N] + [1.0*SVector(1.0, 0.0, 0.0) for i = 1:N]

    @views OneoverRkernelassembler(matrix, tdata, sdata) = assembler(OneoverRkernel, matrix, tpoints[tdata], spoints[sdata])

    @time U, V = aca_compression(OneoverRkernelassembler, 1:N, 1:N; tol=1e-4, isdebug=false, maxrank=40)
    return size(U)
end


@testset "ridge-reg" begin
    rng = StableRNG(622161)
    n, p = 100, 5
    X = randn(rng, n, p)
    y = randn(rng, n)
    X1 = R.augment_X(X, true)

    λ = 0.3

    Xt = MLJBase.table(X)
    rr = RidgeRegressor(lambda=λ, penalize_intercept=true,
                        scale_penalty_with_samples = false)
    fr, = MLJBase.fit(rr, 1, Xt, y)
    ŷ = MLJBase.predict(rr, fr, Xt)

    θ = (X1'X1 + λ*I)\(X1'y)
    coefs = θ[1:end-1]
    intercept = θ[end]

    fp = MLJBase.fitted_params(rr, fr)
    @test last.(fp.coefs) ≈ coefs
    @test fp.intercept ≈ intercept
end

@testset "logistic" begin
    ((X, y, θ), (X1, y1, θ1)) = generate_binary(100, 5)

    λ = 0.7
    γ = 0.1

    Xt = MLJBase.table(X)
    yc = MLJBase.categorical(y1)

    lr = LogisticClassifier(lambda=λ, gamma=γ, scale_penalty_with_samples = false)
    fr, = MLJBase.fit(lr, 1, Xt, yc)

    ŷ = MLJBase.predict(lr, fr, Xt)
    ŷ = MLJBase.mode.(ŷ)

    mcr = MLJBase.misclassification_rate(ŷ, yc)
    @test mcr ≤ 0.2
end

@testset "multinomial" begin
    ((X, y, θ), (X1, y1, θ1)) = generate_multiclass(100,  5, 3)

    λ = 0.5
    γ = 0.2

    Xt = MLJBase.table(X)
    yc = MLJBase.categorical(y1)

    mc = MultinomialClassifier(lambda=λ, gamma=γ, fit_intercept=false,
                               scale_penalty_with_samples = false)
    fr, = MLJBase.fit(mc, 1, Xt, yc)

    mach = MLJBase.machine(mc, Xt, yc)
    MLJBase.fit!(mach)
    fp = MLJBase.fitted_params(mach)
    @test length(fp.coefs) == 5

    ŷ = MLJBase.predict(mc, fr, Xt)
    ŷ = MLJBase.mode.(ŷ)

    mcr = MLJBase.misclassification_rate(ŷ, yc)
    @test mcr ≤ 0.3
end

# see issue https://github.com/alan-turing-institute/MLJ.jl/issues/387
@testset "String-Symbol" begin
    model = LogisticClassifier(penalty="l1")
    @test model.penalty == "l1"
    gr = MLJLinearModels.glr(model, 2)
    @test gr isa GLR
    @test gr.penalty isa ScaledPenalty{L1Penalty}
end

# see issue #71
@testset "Logistic-m" begin
    X, y = MLJBase.make_blobs(centers=3)
    model = LogisticClassifier()
    mach = MLJBase.machine(model, X, y)
    MLJBase.fit!(mach)
    fp = MLJBase.fitted_params(mach)
    @test unique(fp.classes) == [1,2,3]
end


type SGNHTHMCState <: SamplerState
  x::Array{Float64}
  p::Array{Float64}
  zeta::Array{Float64,1}

  niters::Int64
  stepsize::Float64
  mass::Array{Float64,1}
  D::Array{Float64,1}
  Best::Array{Float64,1}
  function SGNHTHMCState(x::Array{Float64};stepsize = 0.001, p=:none, mass=[1.0],niters=10, D=[1.0], Best=[0.0], zeta=[0.0])
    if isa(mass,Number)
      mass = mass * ones(length(x))
    end
    if p == :none
      p = sqrt(mass).*randn(length(x))
    end
    new(x,p,zeta,niters,stepsize,mass,D,Best)
  end
end

function sample!(s::SGNHTHMCState, llik, sgrad)
  D = s.D
  niters = s.niters
  stepsize = s.stepsize
  Best = s.Best
  zeta = s.zeta
  m = s.mass

  for iter=1:s.niters

    p_grad = zeta.*s.p./ m
    n = sqrt(stepsize.*(2D.-stepsize.*Best)).*randn(length(s.x))
    s.p[:] += stepsize.*(sgrad(s.x)-p_grad) + n
    s.x[:] += stepsize.*s.p./m
    zeta[:] += stepsize.* (s.p'*(s.p./(length(s.x)*m.^2)) - mean(1./m))
  end
  s
end


using Bolt
using PyPlot
using Unitful, UnitfulAstro, NaturallyUnitful

par = CosmoParams()

# integrate saha to some transition redshift, we choose 1587.4
z_transition = 1587.4
x_transition = z2x(z_transition)
saha_z_grid = 1800:-10:z_transition
peebles_z_grid = z_transition:-10:1

early_time_Xₑ = Bolt.saha_Xₑ(par)
late_time_Xₑ = Bolt.peebles_Xₑ(
    par, early_time_Xₑ(x_transition), x_transition, 1.0)

clf()
plot(saha_z_grid, [early_time_Xₑ(z2x(z)) for z in saha_z_grid], "-", label="Saha")
plot(peebles_z_grid, [late_time_Xₑ(z2x(z)) for z in peebles_z_grid], "-", label="Peebles")
ylabel(raw"$X_e$")
xlim(1800,0)
ylim(2e-4, 2)
yscale("log")
xlabel(raw"$z$")
legend()
gcf()


using ChaosTools
using Test, StaticArrays
using OrdinaryDiffEq: Vern9
using DynamicalSystemsBase.Systems: lorenz

ν_thresh_lower, ν_thresh_upper = 0.1, 0.9
C_thresh_lower, C_thresh_upper = 0.1, 0.9

println("\nTesting Partially predictable chaos...")

@testset "Predictability Continuous" begin
    @testset "Lorenz map" begin
        @testset "Lorenz map - strongly chaotic" begin
            lz = lorenz(ρ=180.70)
            chaos_type, ν, C = predictability(lz; alg=Vern9(), maxiters=1e9)
            @test chaos_type == :SC
            @test ν < ν_thresh_lower
            @test C < C_thresh_lower
        end
        @testset "Lorenz map - PPC 1" begin
            lz = lorenz(ρ=180.78)
            chaos_type, ν, C = predictability(lz; alg=Vern9(), maxiters=1e9)
            @test chaos_type == :PPC
            @test ν < ν_thresh_lower
            @test C > C_thresh_upper
        end
        @testset "Lorenz map - PPC 2" begin
            lz = lorenz(ρ=180.95)
            chaos_type, ν, C = predictability(lz; alg=Vern9(), maxiters=1e9)
            @test chaos_type == :PPC
            @test ν < ν_thresh_lower
            @test C > C_thresh_upper
        end
        @testset "Lorenz map - laminar" begin
            lz = lorenz(ρ=181.10)
            chaos_type, ν, C = predictability(lz; T_max = 400, alg=Vern9(), maxiters=1e9)
            @test chaos_type == :REG
            @test ν > ν_thresh_upper
            @test C > C_thresh_upper
        end
    end
end

@testset "Predictability Discrete" begin
    @testset "Henon map" begin
        ds = Systems.henon()
        a_reg = 0.8; a_ppc = 1.11; a_reg2 = 1.0; a_cha = 1.4
        res = [:REG, :REG, :PPC, :SC]

        for (i, a) in enumerate((a_reg, a_reg2, a_ppc, a_cha))
            set_parameter!(ds, 1, a)
            chaos_type, ν, C = predictability(ds; T_max = 400000)
            @test chaos_type == res[i]
        end
    end
end


# Solve the Landau-Zener problem i ψ' = H(t) ψ, with H(t) = [t 1;1 -t]

using Test
using StaticArrays, LinearAlgebra
using OrdinaryDiffEq, DiffEqBase

H(t) = -im*(@SMatrix [t 1;1 -t])

fun(ψ,p,t) = H(t)*ψ
fun_inplace(dψ,ψ,p,t) = (dψ .= H(t)*ψ)

T = 0.1
tspan = (0,T)
explicit = [Midpoint(),RK4(),DP5(),Tsit5(),Vern7()]
implicit_autodiff = [ImplicitEuler(),Trapezoid(),Kvaerno3(),Rosenbrock23()]
implicit_noautodiff = [ImplicitEuler(autodiff=false),Trapezoid(autodiff=false),Kvaerno3(autodiff=false),Rosenbrock23(autodiff=false)]

@testset "Complex Tests on Explicit Methods" for alg in explicit
  for f in (fun, fun_inplace)
    ψ0 = [1.0+0.0im; 0.0]
    prob = ODEProblem(f,ψ0,(-T,T))
    sol = solve(prob,alg)
    @test norm(sol(T)) ≈ 1 atol=1e-2
  end
  ψ0 = @SArray [1.0+0.0im; 0.0]
  prob = ODEProblem(fun,ψ0,(-T,T))
  sol = solve(prob,alg)
  @test norm(sol(T)) ≈ 1 atol=1e-2
end

@testset "Complex Tests on Implicit Autodiff Methods" for alg in implicit_autodiff
  @test_broken begin
    for f in (fun, fun_inplace)
      ψ0 = [1.0+0.0im; 0.0]
      prob = ODEProblem(f,ψ0,(-T,T))
      sol = solve(prob,alg)
      @test norm(sol(T)) ≈ 1 atol=1e-2
    end
    ψ0 = @SArray [1.0+0.0im; 0.0]
    prob = ODEProblem(fun,ψ0,(-T,T))
    sol = solve(prob,alg)
    @test norm(sol(T)) ≈ 1 atol=1e-2
  end
end

@testset "Complex Tests on Implicit Finite Diff Methods" for alg in implicit_noautodiff
    ψ0 = [1.0+0.0im; 0.0]
    prob = ODEProblem(fun_inplace,ψ0,(-T,T))
    sol = solve(prob,alg)
    @test norm(sol(T)) ≈ 1 atol=1e-2
end

@testset "Complex Tests on Implicit Finite Diff Out-of-place Methods" begin
  for alg in implicit_noautodiff
    ψ0 = [1.0+0.0im; 0.0]
    prob = ODEProblem(fun,ψ0,(-T,T))
    sol = solve(prob,alg)
    @test norm(sol(T)) ≈ 1 atol=1e-2
  end
end

@testset "Complex Tests on Implicit Finite Diff Out-of-place Methods SArray" for alg in implicit_noautodiff
  @test_broken begin
    for alg in implicit_noautodiff
      ψ0 = @SArray [1.0+0.0im; 0.0]
      prob = ODEProblem(fun,ψ0,(-T,T))
      sol = solve(prob,alg)
      @test norm(sol(T)) ≈ 1 atol=1e-2
    end
  end
end