using Test

using AbstractAlgebra
using GroupRings
using SparseArrays

@testset "GroupRings" begin
   @testset "Constructors: SymmetricGroup" begin
      G = SymmetricGroup(3)

      @test isa(GroupRing(G), AbstractAlgebra.NCRing)
      @test isa(GroupRing(G), GroupRing)

      RG = GroupRing(G)
      @test isdefined(RG, :basis) == true
      @test length(RG.basis) == 6
      @test isdefined(RG, :basis_dict) == true
      @test isdefined(RG, :pm) == false

      @test isa(GroupRing(SymmetricGroup(6), rand(1:6, 6,6)), GroupRing)

      RG = GroupRing(G, cachedmul=true)
      @test isdefined(RG, :pm) == true
      @test RG.pm == zeros(Int, (6,6))

      @test isa(complete!(RG), GroupRing)
      @test all(RG.pm .> 0)
      @test RG.pm == GroupRings.initializepm!(GroupRing(G, cachedmul=false), fill=true).pm

      @test RG.basis_dict == GroupRings.reverse_dict(collect(G))

      @test isa(GroupRing(G, collect(G)), GroupRing)
      S = collect(G)
      pm = create_pm(S)
      @test isa(GroupRing(G, S), GroupRing)
      @test isa(GroupRing(G, S, pm), GroupRing)

      A = GroupRing(G, S)
      B = GroupRing(G, S, pm)

      @test RG == A
      @test RG == B
   end

   @testset "GroupRing constructors FreeGroup" begin
      using Groups
      F = FreeGroup(3)
      S = gens(F)
      append!(S, [inv(s) for s in S])

      basis, sizes = Groups.wlmetric_ball(S, one(F), radius=4)
      d = GroupRings.reverse_dict(basis)
      @test_throws KeyError create_pm(basis)
      pm = create_pm(basis, d, sizes[2])

      @test isa(GroupRing(F, basis, pm), GroupRing)
      @test isa(GroupRing(F, basis, d, pm), GroupRing)

      A = GroupRing(F, basis, pm)
      B = GroupRing(F, basis, d, pm)
      @test A == B

      RF = GroupRing(F, basis, cachedmul=true)
      @test count(!iszero, RF.pm) == 0

      GroupRings.complete!(RF)
      @test count(!iszero, RF.pm) == 45469

      g = zero(B)
      s = S[2]
      g[s] = 1
      @test g == B(s)
      @test g[s^2] == 0
      @test_throws KeyError g[s^10]
   end

   @testset "GroupRingElems constructors/basic manipulation" begin
      G = SymmetricGroup(3)
      RG = GroupRing(G, cachedmul=true)
      a = rand(6)
      @test isa(GroupRingElem(a, RG), GroupRingElem)
      @test isa(RG(a), GroupRingElem)

      @test all(isa(RG(g), GroupRingElem) for g in G)

      @test_throws String GroupRingElem([1,2,3], RG)
      @test isa(RG(G([2,3,1])), GroupRingElem)
      p = G([2,3,1])
      a = RG(p)
      @test length(a) == 1
      @test isa(a.coeffs, SparseVector)

      @test a.coeffs[5] == 1
      @test a[5] == 1
      @test a[p] == 1

      @test string(a) == "(1,2,3)"
      @test string(-a) == " - 1(1,2,3)"

      @test RG([0,0,0,0,1,0]) == a

      s = G([1,2,3])
      @test a[s] == 0
      a[s] = 2

      @test a.coeffs[1] == 2
      @test a[1] == 2
      @test a[s] == 2

      @test string(a) == "2() + (1,2,3)"
      @test string(-a) == " - 2() - 1(1,2,3)"

      @test length(a) == 2

      @testset "RSL(3,Z)" begin
         N = 3
         halfradius = 2
         M = MatrixAlgebra(zz, N)
         E(M, i,j) = (e_ij = one(M); e_ij[i,j] = 1; e_ij)
         S = [E(M, i,j) for i in 1:N for j in 1:N if i≠j]
         S = unique([S; inv.(S)])
         E_R, sizes = Groups.wlmetric_ball(S, radius=2*halfradius)
         E_rdict = GroupRings.reverse_dict(E_R)
         pm = GroupRings.create_pm(E_R, E_rdict, sizes[halfradius]; twisted=true);

         @test GroupRing(M, E_R, E_rdict, pm) isa GroupRing
      end
   end

   @testset "Arithmetic" begin
      G = SymmetricGroup(3)
      RG = GroupRing(G, cachedmul=true)
      a = RG(ones(Int, order(G)))

      @testset "scalar operators" begin

         @test isa(-a, GroupRingElem)
         @test (-a).coeffs == -(a.coeffs)

         @test isa(2*a, GroupRingElem)
         @test eltype(2*a) == typeof(2)
         @test (2*a).coeffs == 2 .*(a.coeffs)

         ww = "Scalar and coeffs are in different rings! Promoting result to Float64"

         @test isa(2.0*a, GroupRingElem)
         @test eltype(2.0*a) == typeof(2.0)
         @test (2.0*a).coeffs == 2.0.*(a.coeffs)

         @test (a/2).coeffs == a.coeffs./2
         b = a/2
         @test isa(b, GroupRingElem)
         @test eltype(b) == typeof(1/2)
         @test (b/2).coeffs == 0.25*(a.coeffs)

         @test isa(convert(Rational{Int}, a), GroupRingElem)
         @test eltype(convert(Rational{Int}, a)) == Rational{Int}
         @test convert(Rational{Int}, a).coeffs ==
            convert(Vector{Rational{Int}}, a.coeffs)

         b = convert(Rational{Int}, a)

         @test isa(b//4, GroupRingElem)
         @test eltype(b//4) == Rational{Int}

         @test isa(b//big(4), NCRingElem)
         @test eltype(b//(big(4)//1)) == Rational{BigInt}

         @test isa(a//1, GroupRingElem)
         @test eltype(a//1) == Rational{Int}
         @test (1.0a)//1 == (1.0a)

      end

      @testset "Additive structure" begin
         @test RG(ones(Int, order(G))) == sum(RG(g) for g in G)
         a = RG(ones(Int, order(G)))
         b = sum((-1)^parity(g)*RG(g) for g in G)
         @test 1/2*(a+b).coeffs == [1.0, 0.0, 1.0, 0.0, 1.0, 0.0]

         a = RG(1) + RG(perm"(2,3)") + RG(perm"(1,2,3)")
         b = RG(1) - RG(perm"(1,2)(3)") - RG(perm"(1,2,3)")

         @test a - b == RG(perm"(2,3)") + RG(perm"(1,2)(3)") + 2RG(perm"(1,2,3)")

         @test 1//2*2a == a
         @test a + 2a == (3//1)*a
         @test 2a - (1//1)*a == a
      end

      @testset "Multiplicative structure" begin
         for g in G, h in G
            a = RG(g)
            b = RG(h)
            @test a*b == RG(g*h)
            @test (a+b)*(a+b) == a*a + a*b + b*a + b*b
         end

         for g in G
            @test star(RG(g)) == RG(inv(g))
            @test (one(RG)-RG(g))*star(one(RG)-RG(g)) ==
               2*one(RG) - RG(g) - RG(inv(g))
            @test aug((one(RG)-RG(g))) == 0
         end

         a = RG(1) + RG(perm"(2,3)") + RG(perm"(1,2,3)")
         b = RG(1) - RG(perm"(1,2)(3)") - RG(perm"(1,2,3)")

         @test a*b == mul!(a,a,b)

         @test aug(a) == 3
         @test aug(b) == -1
         @test aug(a)*aug(b) == aug(a*b) == aug(b*a)

         z = sum((one(RG)-RG(g))*star(one(RG)-RG(g)) for g in G)
         @test aug(z) == 0

         @test supp(z) == parent(z).basis
         @test supp(RG(1) + RG(perm"(2,3)")) == [one(G), perm"(2,3)"]
         @test supp(a) == [perm"(3)", perm"(2,3)", perm"(1,2,3)"]

      end

      @testset "HPC multiplicative operations" begin

         G = SymmetricGroup(5)
         RG = GroupRing(G, cachedmul=true)
         RG2 = GroupRing(G, cachedmul=false)

         Z = zero(RG)
         W = zero(RG)

         for g in [rand(G) for _ in 1:30]
            X = RG(g)
            Y = -RG(inv(g))
            for i in 1:10
               X[rand(G)] += rand(1:3)
               Y[rand(G)] -= rand(1:3)
            end

            @test X*Y ==
                  RG2(X)*RG2(Y) ==
                  GroupRings.mul!(Z, X, Y)

            @test Z.coeffs == GroupRings.GRmul!(W.coeffs, X.coeffs, Y.coeffs, RG.pm) == W.coeffs
            @test (2*X*Y).coeffs ==
               GroupRings.fmac!(W.coeffs, X.coeffs, Y.coeffs, RG.pm) ==
               GroupRings.mul!(2, X*Y).coeffs
         end
      end
   end

   @testset "SumOfSquares in group rings" begin
      ∗ = star

      G = FreeGroup(["g", "h", "k", "l"])
      S = G.(G.gens)
      S = [S; inv.(S)]

      ID = one(G)
      RADIUS=3
      @time E_R, sizes = Groups.wlmetric_ball(S, ID, radius=2*RADIUS);
      @test sizes == [9, 65, 457, 3201, 22409, 156865]
      E_rdict = GroupRings.reverse_dict(E_R)
      pm = GroupRings.create_pm(E_R, E_rdict, sizes[RADIUS]; twisted=true);
      RG = GroupRing(G, E_R, E_rdict, pm)

      g = RG.basis[2]
      h = RG.basis[3]
      k = RG.basis[4]
      l = RG.basis[5]
      G = (1-RG(g))
      @test G^2 == 2 - RG(g) - ∗(RG(g))

      G = (1-RG(g))
      H = (1-RG(h))
      K = (1-RG(k))
      L = (1-RG(l))
      GH = (1-RG(g*h))
      KL = (1-RG(k*l))

      X = (2 - ∗(RG(g)) - RG(h))
      Y = (2 - ∗(RG(g*h)) - RG(k))

      @test -(2 - RG(g*h) - ∗(RG(g*h))) + 2G^2 + 2H^2 == X^2
      @test (2 - RG(g*h) - ∗(RG(g*h))) == GH^2
      @test -(2 - RG(g*h*k) - ∗(RG(g*h*k))) + 2GH^2 + 2K^2 == Y^2
      @test -(2 - RG(g*h*k) - ∗(RG(g*h*k))) +
         2(GH^2 - 2G^2 - 2H^2) +
         4G^2 + 4H^2 + 2K^2 ==
            Y^2

      @test GH^2 - 2G^2 - 2H^2 == - X^2
      @test -(2 - RG(g*h*k) - ∗(RG(g*h*k))) + 4G^2 + 4H^2 + 2K^2 == 2X^2 + Y^2

      @test GH^2 == 2G^2 + 2H^2 - (2 - ∗(RG(g)) - RG(h))^2
      @test KL^2 == 2K^2 + 2L^2 - (2 - ∗(RG(k)) - RG(l))^2

      @test -(2 - ∗(RG(g*h*k*l)) - RG(g*h*k*l)) + 2*GH^2 + 2*KL^2 ==
         (2 - ∗(RG(g*h)) - RG(k*l))^2

      @test -(2 - ∗(RG(g*h*k*l)) - RG(g*h*k*l)) +
         2(2G^2 + 2H^2 - (2 - ∗(RG(g)) - RG(h))^2) +
         2(2K^2 + 2L^2 - (2 - ∗(RG(k)) - RG(l))^2) ==
            (2 - ∗(RG(g*h)) - RG(k*l))^2

      @test -(2 - ∗(RG(g*h*k*l)) - RG(g*h*k*l)) +
         2(2G^2 + 2H^2) +
         2(2K^2 + 2L^2) ==
            (2 - ∗(RG(g*h)) - RG(k*l))^2 +
            2(2 - ∗(RG(g)) - RG(h))^2 +
            2(2 - ∗(RG(k)) - RG(l))^2

      @test -(2 - ∗(RG(g*h*k*l)) - RG(g*h*k*l)) +
         2(2 - ∗(RG(g*h*k)) - RG(g*h*k)) + 2L^2 ==
            (2 - ∗(RG(g*h*k)) - RG(l))^2

      @test 2 - ∗(RG(g*h*k)) - RG(g*h*k) ==
         2GH^2 + 2K^2 - (2 - ∗(RG(g*h)) - RG(k))^2

      @test -(2 - ∗(RG(g*h*k*l)) - RG(g*h*k*l)) +
         2(2GH^2 + 2K^2 - (2 - ∗(RG(g*h)) - RG(k))^2) + 2L^2 ==
            (2 - ∗(RG(g*h*k)) - RG(l))^2

      @test -(2 - ∗(RG(g*h*k*l)) - RG(g*h*k*l)) +
         2(2GH^2 + 2K^2) +  2L^2 ==
            (2 - ∗(RG(g*h*k)) - RG(l))^2 +
            2(2 - ∗(RG(g*h)) - RG(k))^2

      @test -(2 - ∗(RG(g*h*k*l)) - RG(g*h*k*l)) +
         8G^2 + 8H^2 + 4K^2 + 2L^2 ==
            (2 - ∗(RG(g*h*k)) - RG(l))^2 + 2(2 - ∗(RG(g*h)) - RG(k))^2 + 4(2 - ∗(RG(g)) - RG(h))^2

      @test -(2 - ∗(RG(g*h*k*l)) - RG(g*h*k*l)) +
         2GH^2 + 2KL^2 == (2 - ∗(RG(g*h)) - RG(k*l))^2

      @test -(2 - ∗(RG(g*h*k*l)) - RG(g*h*k*l)) + 2(2G^2 + 2H^2) + 2(2K^2 + 2L^2) ==
         (2 - ∗(RG(g*h)) - RG(k*l))^2 + 2(2 - ∗(RG(k)) - RG(l))^2 + 2(2 - ∗(RG(g)) - RG(h))^2
   end
end


module FanBeam

export para2fan, fan2para

using ..Monads
using ..CTImages: CTSinogram
using ..Geometry
using ..Interpolation: interpolate, AbstractInterp2DOrNone
using ..Utils: linspace, ORI, _wrap_angle
using IntervalSets

"""
    para2fan(
        sinog_para::AbstractMatrix{T},
        fbg::FanBeamGeometry;
        <keyword arguments>
    ) where {T<:Real} -> fbg, sinog_fan

Convert given sinogram `sinog_para` from parallel beam geometry
to fan beam projections.

The function returns a tuple where `fbg` is the new geometry
with fan beam projections parameters and `sinog_fan` is the
converted sinogram.

# Arguments
- `sinog_para`: the input sinogram.
- `pbg`: the geometry parameters.
- `D`: focal spot to ISO distance.
- `D′=nothing`: focal spot to detectors distance; if not specified
    and also `γ` is not specified, is defined so that the total
    fan angle is 1 radian.
- `γ=nothing`: total fan angle in degrees.
- `δ=1`: detectors spacing (cell size).
- `background=nothing`: background value to be used, defaults to 0.
- `interpolation`: interpolation strategy; it should be a function
    taking a matrix as input and returning a function of the indices to
    get the interpolated value. By default it is a bilinear interpolation.

See Also: [`fan2para`](@ref)
"""
function para2fan(
    sinog_para::AbstractMatrix{T},
    fbg::FanBeamGeometry;
    background::Optional{Real} = nothing,
    interpolation::Optional{Interp} = nothing,
) where {T <: Real, Interp <: AbstractInterp2DOrNone}
    nd, nϕ = num_det(fbg), num_proj(fbg)

    @assert (nd, nϕ) == size(sinog_para) "Sinogram size $(size(sinog_para)) should match geometry ($nd,$nϕ)"

    D::T = f2iso(fbg)
    Δβ::T = deg2rad(scan_angle(fbg)) # This should be 2π?
    βs = linspace(T, ORI(0..Δβ), nϕ)
    β₀::T = deg2rad(start_angle(fbg)) + 1
    δϕi::T = nϕ / 2π # This should be α / nϕ?
    γ::T = fan_angle(fbg)
    δγ::T = γ / nd
    x′max::T = D * sin(γ/2)
    δx′i::T = nd / 2x′max
    center = center_channel(fbg)
    γ₀::T = center * δγ
    γs = linspace(T, ORI(-γ₀..γ₀), nd)
    x′₀::T = center + 1
    nthalf = nϕ + 1//2

    interpolation = maybe(interpolate, interpolation)
    interp = interpolation(sinog_para)
    z::T = maybe(zero(T), background)
    sinog_fan = similar(sinog_para, nd, nϕ)
    fill!(sinog_fan, z)

    @inline function compute_value(x′::T, ϕ::T)
        if ϕ < 0
            ϕ += π
            x′ = -x′
        end
        if ϕ > 2π
            ϕ -= π
            x′ = -x′
        end
        x′ = x′ * δx′i + x′₀
        ϕ = mod2pi(ϕ) * δϕi + 1 # need +1 in order to be in 1:nϕ
        return x′ ∈ 1..nd && ϕ ∈ 1..nϕ+1 ?
            interp(x′, _wrap_angle(ϕ, nϕ, nthalf)) : z
    end

    Threads.@threads for iγ ∈ 1:nd
        @inbounds γ′ = γs[iγ]
        x′ = D * sin(γ′)
        @inbounds @simd for iβ ∈ 1:nϕ
            β = βs[iβ]
            ϕ = β - γ′
            sinog_fan[iγ, iβ] = compute_value(x′, ϕ)
        end
    end

    fbg, CTSinogram(sinog_fan)
end

function para2fan(
    sinog_para::AbstractMatrix{T},
    pbg::ParallelBeamGeometry;
    D::Real = 500,
    D′::Optional{Real} = nothing,
    D1::Optional{Real} = nothing,
    γ::Optional{Real} = nothing,
    gamma::Optional{Real} = nothing,
    δ::Optional{Real} = one(T),
    dx::Optional{Real} = one(T),
    kwargs...,
) where {T<:Real}
    D′ = maybe(D′, D1)
    γ = maybe(γ, gamma)
    δ = maybe(δ, dx)
    fbg = FanBeamGeometry(
        T,
        pbg.ct;
        pbg.nϕ,
        pbg.nd,
        pbg.rows,
        pbg.cols,
        D,
        D′,
        γ,
        δ,
        pbg.α,
        pbg.α₀,
        pbg.center,
    )
    para2fan(sinog_para, fbg; kwargs...)
end

para2fan(g::AbstractGeometry; kwargs...) = x -> para2fan(x, g; kwargs...)

"""
    fan2para(
        sinog_fan::AbstractMatrix{T},
        fbg::FanBeamGeometry;
        <keyword arguments>
    ) where {T<:Real} -> pbg, sinog_para

Convert given sinogram `sinog_fan` from fan beam geometry
to parallel beam projections.

It is assumed that the detectors are arranged on a circle arc,
not flat.

The function returns a tuple where `fbg` is the new geometry
with fan beam projections parameters and `sinog_fan` is the
converted sinogram.

# Arguments
- `sinog_para`: the input sinogram.
- `pbg`: the parallel geometry parameters.
- `background=nothing`: background value to be used, defaults to 0.
- `interpolation`: interpolation strategy; it should be a function
    taking a matrix as input and returning a function of the indices to
    get the interpolated value. By default it is a bilinear interpolation.

See Also: [`para2fan`](@ref)
"""
function fan2para(
    sinog_fan::AbstractMatrix{T},
    fbg::FanBeamGeometry{T,DefaultTomograph};
    background::Optional{Real} = nothing,
    interpolation::Optional{Interp} = nothing,
) where {T <: Real, Interp <: AbstractInterp2DOrNone}
    nd, nϕ = num_det(fbg), num_proj(fbg)

    @assert (nd, nϕ) == size(sinog_fan) "Sinogram size $(size(sinog_fan)) should match geometry ($nd,$nϕ)"

    pbg = ParallelBeamGeometry(
        T,
        fbg.ct;
        nϕ,
        nd,
        fbg.rows,
        fbg.cols,
        fbg.α,
        fbg.center,
    )

    D::T = f2iso(fbg)
    γ::T = fan_angle(fbg)
    Δϕ::T = deg2rad(scan_angle(fbg))
    ϕs = linspace(T, ORI(0..Δϕ), nϕ)
    δβi::T = nϕ / 2π
    β₀::T = deg2rad(start_angle(fbg))
    δγi::T = nd / γ
    x′max::T = D * sin(γ/2)
    δx′::T = 2x′max / nd
    center::T = center_channel(pbg)
    x′₀::T = center * δx′
    x′₁::T = (nd - center) * δx′
    xs = linspace(T, -x′₀..x′₁, nd) # support for custom center channel
    @assert (x′₀ / D) <= 1 "Fan beam parameters incompatible |x′₀/D| = $(x′₀ / D) which should be less than 1"
    γ₀::T = center + 1 # support for custom center channel.

    interpolation = maybe(interpolate, interpolation)
    interp = interpolation(sinog_fan)
    z::T = maybe(zero(T), background)
    sinog_para = similar(sinog_fan, nd, nϕ)
    fill!(sinog_para, z)
    nthalf = nϕ + 1//2

    @inline function compute_value(γ′::T, β::T)
        if β < 0
            # β += 2γ′ + π
            # γ′ = -γ′
            β += π - 2γ′ # We use by default the symmetry at π instead of 2π
            γ′ = -γ′
        end
        if β > 2π
            # β += 2γ′ - π
            # γ′ = -γ′
            β -= π + 2γ′
            γ′ = -γ′
        end
        γ′′ = γ′ * δγi + γ₀
        β = mod2pi(β) * δβi + 1 # need +1 in order to be in 1:nβ
        return γ′′ ∈ 1..nd && β ∈ 1..nϕ+1 ?
            interp(γ′′, _wrap_angle(β, nϕ, nthalf)) : z
    end

    Threads.@threads for ix ∈ 1:nd
        @inbounds x′ = xs[ix]
        γ′ = asin(x′ / D)
        @inbounds @simd for iϕ ∈ 1:nϕ
            ϕ = ϕs[iϕ]
            #β::T = ϕ - γ′ + β₀ # Default tomograph geometry requires '-'
            β = ϕ + γ′ + β₀ # It does not!
            sinog_para[ix, iϕ] = compute_value(γ′, β)
        end
    end

    pbg, CTSinogram(sinog_para)
end

fan2para(g::AbstractGeometry; kwargs...) = x -> fan2para(x, g; kwargs...)

end # module


export bissect_sec
function bissect_sec(h :: AbstractNLPModel;
                     t1 :: Float64 = h.meta.x0[1],
                     t2 :: Float64 = 100.0,
                     tol :: Float64 = 1e-7,
                     maxiter :: Int = 50,
                     verbose :: Bool = false)

    (length(h.meta.x0) > 1) && warn("Not a 1-D problem ")
  (tₐ, tᵦ) = trouve_intervalle(h, t1, 2.5)
  γ = 0.8; t = tᵦ; tₚ = tₐ; tqnp = tₐ
  hₖ = 0; hkm1 = 0; gkm1 = 0; hplus = 0
  iter = 0

  gₖ = grad(h, [t])[1]
  gkm1 = grad(h, [tqnp])[1]

  verbose &&
    @printf(" iter        tₚ        t         dN         gₖ      ")
  verbose &&
    @printf("gplus        \n")
  verbose &&
    @printf(" %7.2e %7.2e %7.2e  %7.2e  %7.2e  %7.2e\n",
                iter, tₚ, t, 0.0, gₖ,  0.0)

  while ((abs(gₖ) > tol) && (iter < maxiter)) || (iter == 0)
    s = t - tqnp; y = gₖ - gkm1
    dN = -gₖ * s / y

    if ((tₚ - t) * dN > 0.0) && (dN / (tₚ - t) < γ)
      tplus = t + dN
      #hplus = obj(h, tplus)
      gplus = grad(h, [tplus])[1]
      verbose && print_with_color(:green, "N")
    else
      tplus = (t + tₚ) / 2
      #hplus = obj(h,  tplus)
      gplus = grad(h, [tplus])[1]
      verbose && print_with_color(:green, "B")
    end

    if t > tₚ
      if gplus < 0.0
        tₚ = t; tqnp = t; t = tplus
      else
        tqnp = t; t = tplus
      end
    else
      if gplus > 0.0
        tₚ = t; tqnp = t; t = tplus
      else
        tqnp = t; t = tplus
      end
    end

    #mise à jour des valeurs
    hkm1 = hₖ; gkm1 = gₖ; hₖ = hplus; gₖ = gplus
    iter = iter + 1
    verbose &&
        @printf(" %7.2e %7.2e %7.2e  %7.2e  %7.2e  %7.2e\n",
                iter, tₚ, t, dN, gₖ, gplus)
  end
    topt = t
    status = :NotSolved
    (abs(gₖ) < tol) && (status = :Optimal)
    (iter >= maxiter) && (status = :Tired)
    tired = iter > maxiter
    optimal = abs(gₖ) < tol
    return (topt, obj(h, [topt])[1], norm(gₖ, Inf), iter, optimal, tired, status, h.counters.neval_obj, h.counters.neval_grad, h.counters.neval_hess)
end


# tracer components of the Redi rotation tensor

"""
    AbstractIsopycnalTensor

Abstract supertype for an isopycnal rotation model.
"""
abstract type AbstractIsopycnalTensor end

"""
    struct IsopycnalTensor{FT} <: AbstractIsopycnalTensor

A tensor that rotates a vector into the isopycnal plane using the local slopes
of the buoyancy field.

Slopes are computed via `slope_x = - ∂b/∂x / ∂b/∂z` and `slope_y = - ∂b/∂y / ∂b/∂z`,
with the negative sign to account for the stable stratification (`∂b/∂z < 0`).
Then, the components of the isopycnal rotation tensor are:

```
               ⎡     1 + slope_y²         - slope_x slope_y      slope_x ⎤
(1 + slope²)⁻¹ | - slope_x slope_y          1 + slope_x²         slope_y |
               ⎣       slope_x                 slope_y            slope² ⎦
```

where `slope² = slope_x² + slope_y²`.
"""
struct IsopycnalTensor{FT} <: AbstractIsopycnalTensor
    minimum_bz :: FT
end

"""
    struct SmallSlopeIsopycnalTensor{FT} <: AbstractIsopycnalTensor

A tensor that rotates a vector into the isopycnal plane using the local slopes
of the buoyancy field and employing the small-slope approximation, i.e., that
the horizontal isopycnal slopes, `slope_x` and `slope_y` are ``≪ 1``. Slopes are
computed via `slope_x = - ∂b/∂x / ∂b/∂z` and `slope_y = - ∂b/∂y / ∂b/∂z`, with
the negative sign to account for the stable stratification (`∂b/∂z < 0`). Then,
by utilizing the small-slope appoximation, the components of the isopycnal
rotation tensor are:

```
⎡   1            0         slope_x ⎤
|   0            1         slope_y |
⎣ slope_x      slope_y      slope² ⎦
```

where `slope² = slope_x² + slope_y²`.
"""
struct SmallSlopeIsopycnalTensor{FT} <: AbstractIsopycnalTensor
    minimum_bz :: FT
end

SmallSlopeIsopycnalTensor(; minimum_bz = 0) = SmallSlopeIsopycnalTensor(minimum_bz)

@inline function isopycnal_rotation_tensor_xz_fcc(i, j, k, grid::AbstractGrid{FT}, buoyancy, tracers, slope_model::SmallSlopeIsopycnalTensor) where FT
    bx = ∂x_b(i, j, k, grid, buoyancy, tracers)
    bz = ℑxzᶠᵃᶜ(i, j, k, grid, ∂zᶜᶜᶠ, buoyancy_perturbation, buoyancy.model, tracers)
    bz = max(bz, slope_model.minimum_bz)

    slope_x = - bx / bz

    return ifelse(bz == 0, zero(FT), slope_x)
end

@inline function isopycnal_rotation_tensor_xz_ccf(i, j, k, grid::AbstractGrid{FT}, buoyancy, tracers, slope_model::SmallSlopeIsopycnalTensor) where FT
    bx = ℑxzᶜᵃᶠ(i, j, k, grid, ∂xᶠᶜᶜ, buoyancy_perturbation, buoyancy.model, tracers)
    bz = ∂z_b(i, j, k, grid, buoyancy, tracers)
    bz = max(bz, slope_model.minimum_bz)

    slope_x = - bx / bz

    return ifelse(bz == 0, zero(FT), slope_x)
end

@inline function isopycnal_rotation_tensor_yz_cfc(i, j, k, grid::AbstractGrid{FT}, buoyancy, tracers, slope_model::SmallSlopeIsopycnalTensor) where FT
    by = ∂y_b(i, j, k, grid, buoyancy, tracers)
    bz = ℑyzᵃᶠᶜ(i, j, k, grid, ∂zᶜᶜᶠ, buoyancy_perturbation, buoyancy.model, tracers)
    bz = max(bz, slope_model.minimum_bz)

    slope_y = - by / bz

    return ifelse(bz == 0, zero(FT), slope_y)
end

@inline function isopycnal_rotation_tensor_yz_ccf(i, j, k, grid::AbstractGrid{FT}, buoyancy, tracers, slope_model::SmallSlopeIsopycnalTensor) where FT
    by = ℑyzᵃᶜᶠ(i, j, k, grid, ∂yᶜᶠᶜ, buoyancy_perturbation, buoyancy.model, tracers)
    bz = ∂z_b(i, j, k, grid, buoyancy, tracers)
    bz = max(bz, slope_model.minimum_bz)

    slope_y = - by / bz

    return ifelse(bz == 0, zero(FT), slope_y)
end

@inline function isopycnal_rotation_tensor_zz_ccf(i, j, k, grid::AbstractGrid{FT}, buoyancy, tracers, slope_model::SmallSlopeIsopycnalTensor) where FT
    bx = ℑxzᶜᵃᶠ(i, j, k, grid, ∂xᶠᶜᶜ, buoyancy_perturbation, buoyancy.model, tracers)
    by = ℑyzᵃᶜᶠ(i, j, k, grid, ∂yᶜᶠᶜ, buoyancy_perturbation, buoyancy.model, tracers)
    bz = ∂z_b(i, j, k, grid, buoyancy, tracers)
    bz = max(bz, slope_model.minimum_bz)

    slope_x = - bx / bz
    slope_y = - by / bz
    slope² = slope_x^2 + slope_y^2

    return ifelse(bz == 0, zero(FT), slope²)
end


"""
    rmkstruct(N, D; fast = false, atol::Real = 0, rtol::Real = atol>0 ? 0 : n*ϵ) -> (KRInfo, iz, nfp, ip)

Determine the Kronecker-structure and infinite pole-zero structure information of the rational matrix
`R(λ) := N(λ)./D(λ)` and return an `KRInfo` object, the multiplicities of infinite zeros in `iz`,
the number of finite poles in `nfp` and the multiplicities of infinite poles in `ip`.

The numerator `N(λ)` is a polynomial matrix of the form `N(λ) = N_1 + λ N_2 + ... + λ**k N_(k+1)`,
for which the coefficient matrices `N_i`, `i = 1, ..., k+1` are stored in the 3-dimensional matrix `N`,
where `N[:,:,i]` contains the `i`-th coefficient matrix `N_i` (multiplying `λ**(i-1)`).

The denominator `D(λ)` is a polynomial matrix of the form `D(λ) = D_1 + λ D_2 + ... + λ**l D_(l+1)`,
for which the coefficient matrices `D_i`, `i = 1, ..., l+1`, are stored in the 3-dimensional matrix `D`,
where `D[:,:,i]` contain the `i`-th coefficient matrix `D_i` (multiplying `λ**(i-1)`).

Alternatively, `N(λ)` and `D(λ)` can be specified as matrices of elements of the `Polynomial` type
provided by the [Polynomials](https://github.com/JuliaMath/Polynomials.jl) package.
In this case, no check is performed that `N(λ)` and `D(λ)` have the same indeterminates.

The information on the Kronecker-structure of the rational matrix `R(λ)` consists of
the right Kronecker indices `rki`, left Kronecker indices `lki`, the number of finite zeros `nf`
and the normal rank `nrank` and can be obtained from `KRInfo` as
`KRInfo.rki`, `KRInfo.lki`, `KRInfo.nf` and `KRInfo.nrank`, respectively.
For more details, see  [`KRInfo`](@ref). Additionally, `KRInfo.id` contains the
infinite elementary divisors of the underlying system matrix pencil `S(λ)` used as linearization.

The Kronecker-structure information of the rational matrix `R(λ)` is obtained from the
Kronecker-structure information of the underlying linearization using the results of [1] and [2].
The determination of the Kronecker-structure information is performed by building an irreducible
descriptor system realization `(A-λE,B,C,D)` with `A-λE` a regular pencil of order `n`,
satisfying `R(λ) = C*inv(λE-A)*B+D` and reducing the structured system matrix pencil
`S(λ) = [A-λE B; C D]` to an appropriate Kronecker-like form (KLF) which exhibits
the number of its finite eigenvalues (also the finite zeros of `R(λ)`), the multiplicities of the
infinite eigenvalues (in excess with one to the multiplicities of infinite zeros of `R(λ)`),
the left and right Kronecker indices (also of `R(λ)`) and the normal rank
(in excess with `n` to the normal rank of `R(λ)`).
The Kronecker-structure information and pole-zero stucture information on `R(λ)` are recovered from
the Kronecker-structure information of `S(λ)` and `A-λE`, respectively, using the results of [1] and [2].

The right Kronecker indices are provided in the integer vector `rki`.
The number of elements of `rki` is the dimension of the right nullspace of the rational matrix `R(λ)`
and their sum is the least degree of a right polynomial nullspace basis or the least McMillan degree
of a right rational nullspace basis.

The left Kronecker indices are provided in the integer vector `lki`.
The number of elements of `lki` is the dimension of the left nullspace of the rational matrix `R(λ)`
and their sum is the least degree of a left polynomial nullspace basis or the least McMillan degree
of a left rational nullspace basis.

The multiplicities of infinite eigenvalues of `S(λ)` are provided in the integer vector `id`,
where each `i`-th element `id[i]` is the order of an infinite elementary divisor
(i.e., the multiplicity of an infinite eigenvalue). To each `id[i] > 1` corresponds an infinite zero
of `R(λ)` of multiplicity `id[i]-1`. The multiplicities of the infinite zeros of `R(λ)` are returned in `iz`.

The poles of `R(λ)` are the zeros of the regular pencil `A-λE`. The finite eigenvalues of `A-λE` are the
finite poles of `R(λ)` and their number is returned in `nfp`. The infinite zeros of `A-λE` are the
infinite poles of `R(λ)` and their  multiplicities are returned in `ip`.

The irreducible descriptor system based linearization is built using the methods described in [3] in conjunction with
pencil manipulation algorithms of [4] and [5] to compute reduced order linearizations. These algorithms
employ rank determinations based on either the use of
rank revealing QR-decomposition with column pivoting, if `fast = true`, or the SVD-decomposition, if `fast = false`.
The rank decision based on the SVD-decomposition is generally more reliable, but the involved computational effort is higher.

The reductions to appropriate KLFs of `S(λ)` and `A-λE` are performed using orthogonal similarity transformations [5]
and involves rank decisions based  on rank revealing QR-decompositions with column pivoting, if `fast = true`,
or, the more reliable, SVD-decompositions, if `fast = false`.

The keyword arguments `atol`  and `rtol` specify the absolute and relative tolerances,  respectively, for the nonzero
coefficients of `N(λ)` and `D(λ)`.
The default relative tolerance is `n*ϵ`, where `n` is the size of the smallest dimension of `N(λ)`, and `ϵ` is the
machine epsilon of the element type of coefficients of `N(λ)`.

[1] G. Verghese, B. Levy, and T. Kailath, Generalized state-space systems, IEEE Trans. Automat. Control,
26:811-831 (1981).

[2] G. Verghese, Comments on ‘Properties of the system matrix of a generalized state-space system’,
Int. J. Control, Vol.31(5) (1980) 1007–1009.

[3] A. Varga, On computing the Kronecker structure of polynomial and rational matrices using Julia, 2020,
[arXiv:2006.06825](https://arxiv.org/pdf/2006.06825).

[4] P. Van Dooreen, The generalized eigenstructure problem in linear system theory,
IEEE Transactions on Automatic Control, vol. AC-26, pp. 111-129, 1981.

[5] A. Varga, Solving Fault Diagnosis Problems - Linear Synthesis Techniques, Springer Verlag, 2017.
"""
function rmkstruct(N::AbstractArray{T1,3}, D::AbstractArray{T2,3}; fast::Bool = false, atol::Real = zero(real(T1)),
                   rtol::Real = (min(size(N)...)*eps(real(float(one(T1)))))*iszero(atol)) where {T1,T2}
    A, E, B, C, D, blkdims = rm2ls(N, D; contr = true, obs = true, noseig = false,
                                   fast = fast, atol = atol, rtol = rtol)
    info = spkstruct(A, E, B, C, D; fast = fast, atol1 = atol, atol2 = atol, rtol = rtol)
    iz  = info.id .- 1
    iz = iz[iz .> 0]
    n = size(A,1)
    info.nrank -= n
    nfp = blkdims[1]
    if n > nfp
       ii = nfp+1:n
       infop = pkstruct(view(A,ii,ii),view(E,ii,ii), fast = fast, atol1 = atol, atol2 = atol, rtol = rtol)
       ip = infop.id .- 1
       ip = ip[ip .> 0]
    else
      ip = Int[]
    end
    return info, iz, nfp, ip
end
function rmkstruct(P::AbstractArray{T,3}; kwargs...) where {T}
   info, iz, ip = pmkstruct(P; kwargs...)
   return info, iz, 0, ip
end
function rmkstruct(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
   D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
   return rmkstruct(poly2pm(N),poly2pm(D); kwargs...)
end
# function rmkstruct(N::Union{AbstractVecOrMat{<:Polynomial{T1,X}},Polynomial{T1,X},Number,AbstractVecOrMat{<:Number}},
#    D::Union{AbstractVecOrMat{<:Polynomial{T2,X}},Polynomial{T2,X},Number,AbstractVecOrMat{<:Number}}; kwargs...)  where {T1<:Number,T2<:Number,X}
#    return rmkstruct(poly2pm(N),poly2pm(D); kwargs...)
# end
# function rmkstruct(N::Union{AbstractVecOrMat{Polynomial{T1,X}}, AbstractVecOrMat{<:Polynomial}, Polynomial{T1,X},Polynomial,Number,AbstractVecOrMat{<:Number}},
#    D::Union{AbstractVecOrMat{Polynomial{T2,X}}, AbstractVecOrMat{<:Polynomial}, Polynomial{T2,X},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)  where {T1<:Number,T2<:Number,X}
#    return rmkstruct(poly2pm(N),poly2pm(D); kwargs...)
# end
function rmkstruct(P::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
   info, iz, ip = pmkstruct(poly2pm(P); kwargs...)
   return info, iz, 0, ip
end
function rmkstruct(N::AbstractArray{T,3},
                   D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...) where T
   return rmkstruct(N,poly2pm(D); kwargs...)
end
function rmkstruct(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                   D::AbstractArray{T,3}; kwargs...) where T
   return rmkstruct(poly2pm(N),D; kwargs...)
end
"""
    rmzeros(N, D; fast = false, atol::Real = 0, rtol::Real = atol>0 ? 0 : n*ϵ) -> (val, iz, KRInfo)

Return the finite and infinite zeros of the rational matrix `R(λ) := N(λ)./D(λ)` in `val`,
the multiplicities of infinite zeros in `iz` and the information on the Kronecker-structure of
the rational matrix `R(λ)` in the `KRInfo` object.

The information on the Kronecker-structure of the rational matrix `R(λ)` consists of
the right Kronecker indices `rki`, left Kronecker indices `lki`, the number of finite zeros `nf`
and the normal rank `nrank` and can be obtained from `KRInfo` as
`KRInfo.rki`, `KRInfo.lki`, `KRInfo.nf` and `KRInfo.nrank`, respectively.
For more details, see  [`KRInfo`](@ref). Additionally, `KRInfo.id` contains the
infinite elementary divisors of the underlying linearization as a structured system matrix pencil.

The Kronecker-structure information of the rational matrix `R(λ)` is obtained from the
Kronecker-structure information of the underlying linearization using the results of [1] and [2].
The determination of the Kronecker-structure information is performed by building an irreducible
descriptor system realization `(A-λE,B,C,D)` with `A-λE` a regular pencil of order `n`,
satisfying `R(λ) = C*inv(λE-A)*B+D` and reducing the structured system matrix pencil
`S(λ) = [A-λE B; C D]` to an appropriate Kronecker-like form (KLF) which exhibits
the number of its finite eigenvalues (also the finite zeros of `R(λ)`), the multiplicities of the
infinite eigenvalues (in excess with one to the multiplicities of infinite zeros of `R(λ)`),
the left and right Kronecker indices (also of `R(λ)`) and the normal rank
(in excess with `n` to the normal rank of `R(λ)`).

The numerator `N(λ)` is a polynomial matrix of the form `N(λ) = N_1 + λ N_2 + ... + λ**k N_(k+1)`,
for which the coefficient matrices `N_i`, `i = 1, ..., k+1` are stored in the 3-dimensional matrix `N`,
where `N[:,:,i]` contains the `i`-th coefficient matrix `N_i` (multiplying `λ**(i-1)`).

The denominator `D(λ)` is a polynomial matrix of the form `D(λ) = D_1 + λ D_2 + ... + λ**l D_(l+1)`,
for which the coefficient matrices `D_i`, `i = 1, ..., l+1`, are stored in the 3-dimensional matrix `D`,
where `D[:,:,i]` contain the `i`-th coefficient matrix `D_i` (multiplying `λ**(i-1)`).

Alternatively, `N(λ)` and `D(λ)` can be specified as matrices of elements of the `Polynomial` type
provided by the [Polynomials](https://github.com/JuliaMath/Polynomials.jl) package.
In this case, no check is performed that `N(λ)` and `D(λ)` have the same indeterminates.

The irreducible descriptor system based linearization is built using the methods described in [3] in conjunction with
pencil manipulation algorithms of [4] and [5] to compute reduced order linearizations. These algorithms
employ rank determinations based on either the use of
rank revealing QR-decomposition with column pivoting, if `fast = true`, or the SVD-decomposition, if `fast = false`.
The rank decision based on the SVD-decomposition is generally more reliable, but the involved computational effort is higher.

The reduction to the appropriate KLF of `S(λ)` is performed using orthogonal similarity transformations [5]
and involves rank decisions based on rank revealing QR-decompositions with column pivoting, if `fast = true`,
or, the more reliable, SVD-decompositions, if `fast = false`.

The keyword arguments `atol`  and `rtol` specify the absolute and relative tolerances,  respectively, for the nonzero
coefficients of `N(λ)` and `D(λ)`.
The default relative tolerance is `n*ϵ`, where `n` is the size of the smallest dimension of `N(λ)`, and `ϵ` is the
machine epsilon of the element type of coefficients of `N(λ)`.

[1] G. Verghese, B. Levy, and T. Kailath, Generalized state-space systems, IEEE Trans. Automat. Control,
26:811-831 (1981).

[2] G. Verghese, Comments on ‘Properties of the system matrix of a generalized state-space system’,
Int. J. Control, Vol.31(5) (1980) 1007–1009.

[3] A. Varga, On computing the Kronecker structure of polynomial and rational matrices using Julia, 2020,
[arXiv:2006.06825](https://arxiv.org/pdf/2006.06825).

[4] P. Van Dooreen, The generalized eigenstructure problem in linear system theory,
IEEE Transactions on Automatic Control, vol. AC-26, pp. 111-129, 1981.

[5] A. Varga, Solving Fault Diagnosis Problems - Linear Synthesis Techniques, Springer Verlag, 2017.
"""
function rmzeros(N::AbstractArray{T1,3}, D::AbstractArray{T2,3};
                 fast = false, atol::Real = zero(real(T1)),
                 rtol::Real = (min(size(N)...)*eps(real(float(one(T1)))))*iszero(atol)) where {T1,T2}
    A, E, B, C, D, blkdims = rm2ls(N, D; contr = true, obs = true, noseig = false,
                                   fast = fast, atol = atol, rtol = rtol)
    val, iz, info = spzeros(A, E, B, C, D; fast = fast, atol1 = atol, atol2 = atol, rtol = rtol)
    info.nrank -= size(A,1)
    return val, iz, info
end
function rmzeros(N::AbstractArray{T,3}; kwargs...) where {T}
   return pmzeros2(N; kwargs...)
end
function rmzeros(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                 D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
   return rmzeros(poly2pm(N),poly2pm(D); kwargs...)
end
function rmzeros(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
   return pmzeros2(poly2pm(N); kwargs...)
end
function rmzeros(N::AbstractArray{T,3},
                 D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...) where T
   return rmzeros(N,poly2pm(D); kwargs...)
end
function rmzeros(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                 D::AbstractArray{T,3}; kwargs...) where T
   return rmzeros(poly2pm(N),D; kwargs...)
end
"""
    rmpoles(N, D; fast = false, atol::Real = 0, rtol::Real = atol>0 ? 0 : n*ϵ) -> (val, ip, id)

Return the finite and infinite poles of the rational matrix `R(λ) := N(λ)./D(λ)`  in `val`,
the multiplicities of infinite poles in `ip` and the infinite elementary divisors of the pole pencil
of the underlying irreducible descriptor system based linearization of `R(λ)` in `id`.

The information on the finite and infinite poles of the rational matrix `R(λ)` is obtained from the
Kronecker-structure information of the underlying linearization using the results of [1] and [2].
The determination of the Kronecker-structure information is performed by building an irreducible
descriptor system realization `(A-λE,B,C,D)` with `A-λE` a regular pencil of order `n`,
satisfying `R(λ) = C*inv(λE-A)*B+D` and reducing the regular pencil `A-λE`
to an appropriate Kronecker-like form (KLF) which exhibits
the its finite eigenvalues (also the finite poles of `R(λ)`) and the multiplicities of the
infinite eigenvalues (in excess with one to the multiplicities of infinite poles of `R(λ)`).
The multiplicities of the infinite zeros of `A-λE` and of infinite poles of `R(λ)` are the same [2]
and are returned in `ip`. The number of infinite poles in `val` is equal to the sum of multiplicites in `ip`.
The multiplicities of the infinite eigenvalues of `A-λE` are returned in `id`.

The numerator `N(λ)` is a polynomial matrix of the form `N(λ) = N_1 + λ N_2 + ... + λ**k N_(k+1)`,
for which the coefficient matrices `N_i`, `i = 1, ..., k+1` are stored in the 3-dimensional matrix `N`,
where `N[:,:,i]` contains the `i`-th coefficient matrix `N_i` (multiplying `λ**(i-1)`).

The denominator `D(λ)` is a polynomial matrix of the form `D(λ) = D_1 + λ D_2 + ... + λ**l D_(l+1)`,
for which the coefficient matrices `D_i`, `i = 1, ..., l+1`, are stored in the 3-dimensional matrix `D`,
where `D[:,:,i]` contain the `i`-th coefficient matrix `D_i` (multiplying `λ**(i-1)`).

Alternatively, `N(λ)` and `D(λ)` can be specified as matrices of elements of the `Polynomial` type
provided by the [Polynomials](https://github.com/JuliaMath/Polynomials.jl) package.
In this case, no check is performed that `N(λ)` and `D(λ)` have the same indeterminates.

The irreducible descriptor system based linearization is built using the methods described in [3] in conjunction with
pencil manipulation algorithms of [4] and [5] to compute reduced order linearizations. These algorithms
employ rank determinations based on either the use of
rank revealing QR-decomposition with column pivoting, if `fast = true`, or the SVD-decomposition, if `fast = false`.
The rank decision based on the SVD-decomposition is generally more reliable, but the involved computational effort is higher.

The reduction of `A-λE` to the KLF is performed using orthonal similarity transformations and involves rank decisions
based on rank revealing QR-decompositions with column pivoting,
if `fast = true`, or, the more reliable, SVD-decompositions, if `fast = false`. For efficiency purposes, the reduction is only
partially performed, without accumulating the performed orthogonal transformations.

The keyword arguments `atol`  and `rtol` specify the absolute and relative tolerances for the nonzero
coefficients of `N(λ)` and `D(λ)`,  respectively.
The default relative tolerance is `n*ϵ`, where `n` is the size of the smallest dimension of `N(λ)`, and `ϵ` is the
machine epsilon of the element type of coefficients of `N(λ)`.

[1] G. Verghese, B. Levy, and T. Kailath, Generalized state-space systems, IEEE Trans. Automat. Control,
26:811-831 (1981).

[2] G. Verghese, Comments on ‘Properties of the system matrix of a generalized state-space system’,
Int. J. Control, Vol.31(5) (1980) 1007–1009.

[3] A. Varga, On computing the Kronecker structure of polynomial and rational matrices using Julia, 2020,
[arXiv:2006.06825](https://arxiv.org/pdf/2006.06825).

[4] P. Van Dooreen, The generalized eigenstructure problem in linear system theory,
IEEE Transactions on Automatic Control, vol. AC-26, pp. 111-129, 1981.

[5] A. Varga, Solving Fault Diagnosis Problems - Linear Synthesis Techniques, Springer Verlag, 2017.
"""
function rmpoles(N::AbstractArray{T1,3}, D::AbstractArray{T2,3};
                 fast = false, atol::Real = zero(real(T1)),
                 rtol::Real = (min(size(N)...)*eps(real(float(one(T1)))))*iszero(atol)) where {T1,T2}
   A, E, _, _, _, blkdims = rm2ls(N, D; contr = true, obs = true, noseig = false,
                                  fast = fast, atol = atol, rtol = rtol)
   n = size(A,1)
   nfp = blkdims[1]
   if nfp < n
      ii = nfp+1:n
      infop = pkstruct(view(A,ii,ii),view(E,ii,ii), fast = fast, atol1 = atol, atol2 = atol, rtol = rtol)
      ip = infop.id .- 1
      ip = ip[ip .> 0]
      id = infop.id
   else
      ip = Int[]
      id = Int[]
   end
   return [eigvalsnosort!(view(A,1:nfp,1:nfp)); Inf*ones(eltype(A),sum(ip))], ip, id
end
function rmpoles(N::AbstractArray{T,3}; kwargs...) where T
   return pmpoles2(N; kwargs...)
end
function rmpoles(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                 D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
   return rmpoles(poly2pm(N),poly2pm(D); kwargs...)
end
function rmpoles(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
   return pmpoles2(poly2pm(N); kwargs...)
end
function rmpoles(N::AbstractArray{T,3},
                 D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...) where T
   return rmpoles(N,poly2pm(D); kwargs...)
end
function rmpoles(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                 D::AbstractArray{T,3}; kwargs...) where T
   return rmpoles(poly2pm(N),D; kwargs...)
end
"""
    rmzeros1(N, D; fast = false, atol::Real = 0, rtol::Real = atol>0 ? 0 : n*ϵ) -> (val, iz, KRInfo)

Return the finite and infinite zeros of the rational matrix `R(λ) := N(λ)./D(λ)` in `val`,
the multiplicities of infinite zeros in `iz` and the information on the Kronecker-structure of
the rational matrix `R(λ)` in the `KRInfo` object.

The information on the Kronecker-structure of the rational matrix `R(λ)` consists of
the right Kronecker indices `rki`, left Kronecker indices `lki`, the number of finite zeros `nf`
and the normal rank `nrank` and can be obtained from `KRInfo` as
`KRInfo.rki`, `KRInfo.lki`, `KRInfo.nf` and `KRInfo.nrank`, respectively.
For more details, see  [`KRInfo`](@ref). Additionally, `KRInfo.id` contains the
infinite elementary divisors of the underlying pencil based linearization.

The Kronecker-structure information of the rational matrix `R(λ)` is obtained from the
Kronecker-structure information of the underlying pencil based linearization using the results of [1] and [2].
The determination of the Kronecker-structure information is performed by building a strongly minimal
pencil based linearization `(A-λE,B-λF,C-λG,D-λH)` with `A-λE` a regular pencil of order `n`,
satisfying `R(λ) = (C-λG)*inv(λE-A)*(B-λF)+D-λH` and reducing the structured system matrix pencil
`S(λ) = [A-λE B-λF; C-λG D-λH]` to an appropriate Kronecker-like form (KLF) which exhibits
the number of its finite eigenvalues (also the finite zeros of `R(λ)`), the multiplicities of the
infinite eigenvalues (in excess with one to the multiplicities of infinite zeros of `R(λ)`),
the left and right Kronecker indices (also of `R(λ)`) and the normal rank
(in excess with `n` to the normal rank of `R(λ)`).

The numerator `N(λ)` is a polynomial matrix of the form `N(λ) = N_1 + λ N_2 + ... + λ**k N_(k+1)`,
for which the coefficient matrices `N_i`, `i = 1, ..., k+1` are stored in the 3-dimensional matrix `N`,
where `N[:,:,i]` contains the `i`-th coefficient matrix `N_i` (multiplying `λ**(i-1)`).

The denominator `D(λ)` is a polynomial matrix of the form `D(λ) = D_1 + λ D_2 + ... + λ**l D_(l+1)`,
for which the coefficient matrices `D_i`, `i = 1, ..., l+1`, are stored in the 3-dimensional matrix `D`,
where `D[:,:,i]` contain the `i`-th coefficient matrix `D_i` (multiplying `λ**(i-1)`).

Alternatively, `N(λ)` and `D(λ)` can be specified as matrices of elements of the `Polynomial` type
provided by the [Polynomials](https://github.com/JuliaMath/Polynomials.jl) package.
In this case, no check is performed that `N(λ)` and `D(λ)` have the same indeterminates.

The strongly minimal pencil based linearization is built using the methods described in [3] in conjunction with
pencil manipulation algorithms of [4] to compute reduced order linearizations. These algorithms
employ rank determinations based on either the use of
rank revealing QR-decomposition with column pivoting, if `fast = true`, or the SVD-decomposition, if `fast = false`.
The rank decision based on the SVD-decomposition is generally more reliable, but the involved computational effort is higher.

The reduction to the appropriate KLF of `S(λ)` is performed using orthogonal similarity transformations [5]
and involves rank decisions based on rank revealing QR-decompositions with column pivoting, if `fast = true`,
or, the more reliable, SVD-decompositions, if `fast = false`.

The keyword arguments `atol`  and `rtol` specify the absolute and relative tolerances,  respectively, for the nonzero
coefficients of `N(λ)` and `D(λ)`.
The default relative tolerance is `n*ϵ`, where `n` is the size of the smallest dimension of `N(λ)`, and `ϵ` is the
machine epsilon of the element type of coefficients of `N(λ)`.

[1] G. Verghese, B. Levy, and T. Kailath, Generalized state-space systems, IEEE Trans. Automat. Control,
26:811-831 (1981).

[2] G. Verghese, Comments on ‘Properties of the system matrix of a generalized state-space system’,
Int. J. Control, Vol.31(5) (1980) 1007–1009.

[3] A. Varga, On computing the Kronecker structure of polynomial and rational matrices using Julia, 2020,
[arXiv:2006.06825](https://arxiv.org/pdf/2006.06825).

[4] F.M. Dopico, M.C. Quintana and P. Van Dooren, Linear system matrices of rational transfer functions, to appear in "Realization and Model Reduction of Dynamical Systems",
A Festschrift to honor the 70th birthday of Thanos Antoulas", Springer-Verlag. [arXiv:1903.05016](https://arxiv.org/pdf/1903.05016.pdf)

[5] A. Varga, Solving Fault Diagnosis Problems - Linear Synthesis Techniques, Springer Verlag, 2017.
"""
function rmzeros1(N::AbstractArray{T1,3}, D::AbstractArray{T2,3};
                  fast = false, atol::Real = zero(real(T1)),
                   rtol::Real = (min(size(N)...)*eps(real(float(one(T1)))))*iszero(atol)) where {T1,T2}
   A, E, B, F, C, G, D, H, blkdims = rm2lps(N, D; obs = true, contr = true, fast = fast,
                                            atol = atol, rtol = rtol)
   val, iz, info = pzeros([A B; C D], [E F; G H]; fast = fast, atol1 = atol, atol2 = atol, rtol = rtol)
   info.nrank -= size(A,1)
   return val, iz, info
end
function rmzeros1(N::AbstractArray{T,3}; kwargs...) where {T}
   return pmzeros1(N; kwargs...)
end
function rmzeros1(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                 D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
   return rmzeros1(poly2pm(N),poly2pm(D); kwargs...)
end
function rmzeros1(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
   return pmzeros1(poly2pm(N); kwargs...)
end
function rmzeros1(N::AbstractArray{T,3},
                 D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...) where T
   return rmzeros1(N,poly2pm(D); kwargs...)
end
function rmzeros1(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                 D::AbstractArray{T,3}; kwargs...) where T
   return rmzeros1(poly2pm(N),D; kwargs...)
end
"""
    rmpoles1(N, D;  fast = false, atol::Real = 0, rtol::Real = atol>0 ? 0 : n*ϵ) -> (val, ip, id)

Return the finite and infinite poles of the rational matrix `R(λ) := N(λ)./D(λ)`  in `val`,
the multiplicities of infinite poles in `ip` and the infinite elementary divisors of the pole pencil
of the underlying strongly irreducible pencil based linearization of  `R(λ)` in `id`.

The information on the finite and infinite poles of the rational matrix `R(λ)` is obtained from the
pole-structure information of the underlying pencil based linearization using the results of [1] and [2].
The determination of the pole-structure information is performed by building a strongly minimal
pencil based linearization `(A-λE,B-λF,C-λG,D-λH)` with `A-λE` a regular pencil of order `n`,
satisfying `R(λ) = (C-λG)*inv(λE-A)*(B-λF)+D-λH` and reducing the structured system matrix pencil
`Sp(λ) = [A-λE -λF; -λG -λH]` to an appropriate Kronecker-like form (KLF) which exhibits
the number of its finite eigenvalues (also the finite poles of `R(λ)`) and the multiplicities of the
infinite eigenvalues (in excess with one to the multiplicities of infinite poles of `R(λ)`).
The multiplicities of the infinite zeros of `Sp(λ)` and of infinite poles of `R(λ)` are the same [2]
and are returned in `ip`. The number of infinite poles in `val` is equal to the sum of multiplicites in `ip`.
The multiplicities of the infinite eigenvalues of `Sp(λ)` are returned in `id`.

The numerator `N(λ)` is a polynomial matrix of the form `N(λ) = N_1 + λ N_2 + ... + λ**k N_(k+1)`,
for which the coefficient matrices `N_i`, `i = 1, ..., k+1` are stored in the 3-dimensional matrix `N`,
where `N[:,:,i]` contains the `i`-th coefficient matrix `N_i` (multiplying `λ**(i-1)`).

The denominator `D(λ)` is a polynomial matrix of the form `D(λ) = D_1 + λ D_2 + ... + λ**l D_(l+1)`,
for which the coefficient matrices `D_i`, `i = 1, ..., l+1`, are stored in the 3-dimensional matrix `D`,
where `D[:,:,i]` contain the `i`-th coefficient matrix `D_i` (multiplying `λ**(i-1)`).

Alternatively, `N(λ)` and `D(λ)` can be specified as matrices of elements of the `Polynomial` type
provided by the [Polynomials](https://github.com/JuliaMath/Polynomials.jl) package.
In this case, no check is performed that `N(λ)` and `D(λ)` have the same indeterminates.

The strongly minimal pencil based linearization is built using the methods described in [3] in conjunction with
pencil manipulation algorithms of [4] to compute reduced order linearizations. These algorithms
employ rank determinations based on either the use of
rank revealing QR-decomposition with column pivoting, if `fast = true`, or the SVD-decomposition, if `fast = false`.
The rank decision based on the SVD-decomposition is generally more reliable, but the involved computational effort is higher.

The reduction of `Sp(λ)` to the KLF is performed using orthonal similarity transformations [5] and involves rank decisions
based on rank revealing QR-decompositions with column pivoting,
if `fast = true`, or, the more reliable, SVD-decompositions, if `fast = false`. For efficiency purposes, the reduction is only
partially performed, without accumulating the performed orthogonal transformations.

The keyword arguments `atol`  and `rtol` specify the absolute and relative tolerances,  respectively, for the nonzero
coefficients of `N(λ)` and `D(λ)`.
The default relative tolerance is `n*ϵ`, where `n` is the size of the smallest dimension of `N(λ)`, and `ϵ` is the
machine epsilon of the element type of coefficients of `N(λ)`.

[1] G. Verghese, B. Levy, and T. Kailath, Generalized state-space systems, IEEE Trans. Automat. Control,
26:811-831 (1981).

[2] G. Verghese, Comments on ‘Properties of the system matrix of a generalized state-space system’,
Int. J. Control, Vol.31(5) (1980) 1007–1009.

[3] A. Varga, On computing the Kronecker structure of polynomial and rational matrices using Julia, 2020,
[arXiv:2006.06825](https://arxiv.org/pdf/2006.06825).

[4] F.M. Dopico, M.C. Quintana and P. Van Dooren, Linear system matrices of rational transfer functions, to appear in "Realization and Model Reduction of Dynamical Systems",
A Festschrift to honor the 70th birthday of Thanos Antoulas", Springer-Verlag. [arXiv:1903.05016](https://arxiv.org/pdf/1903.05016.pdf)

[5] A. Varga, Solving Fault Diagnosis Problems - Linear Synthesis Techniques, Springer Verlag, 2017.
"""
function rmpoles1(N::AbstractArray{T1,3}, D::AbstractArray{T2,3};
                  fast = false, atol::Real = zero(real(T1)),
                  rtol::Real = (min(size(N)...)*eps(real(float(one(T1)))))*iszero(atol)) where {T1,T2}
   A, E, _, F, _, G, _, H, blkdims = rm2lps(N,D; obs = true, contr = true, fast = fast, atol = atol, rtol = rtol)
   n = size(A,1)
   nfp = blkdims[1]
   p, m = size(H)
   ii = nfp+1:n
   ni = n-nfp
   T = promote_type(T1,T2)
   M = [A[ii,ii] zeros(T,ni,p+m); zeros(T,p,ni+m) I; zeros(T,m,ni) I zeros(T,m,p)]
   N = [E[ii,ii] F[ii,:] zeros(T,ni,p); G[:,ii] H zeros(T,p,p); zeros(T,m,ni+m+p)]
   infop = pkstruct(M, N, fast = fast, atol1 = atol, atol2 = atol, rtol = rtol)
   ip = infop.id .- 1
   ip = ip[ip .> 0]
   id = infop.id
   return [eigvalsnosort!(view(A,1:nfp,1:nfp)); Inf*ones(eltype(A),sum(ip))], ip, id
   # p, m = size(D)
   # n = size(A,1)
   # T = eltype(A)
   # M = [A zeros(T,n,p+m); zeros(T,p,n+m) I; zeros(T,m,n) I zeros(T,m,p)]
   # N = [E F zeros(T,n,p); G H zeros(T,p,p); zeros(T,m,n+m+p)]
   # val, iz, info = pzeros(M, N; fast = fast, atol1 = atol, atol2 = atol, rtol = rtol)
   # return val, iz, info.id
end
function rmpoles1(N::AbstractArray{T,3}; kwargs...) where T
   return pmpoles1(N; kwargs...)
end
function rmpoles1(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                 D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...) where {T1,T2}
   return rmpoles1(poly2pm(N),poly2pm(D); kwargs...)
end
function rmpoles1(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...) where {T}
   return pmpoles1(poly2pm(N); kwargs...)
end
function rmpoles1(N::AbstractArray{T,3},
                 D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...) where T
   return rmpoles1(N,poly2pm(D); kwargs...)
end
function rmpoles1(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                 D::AbstractArray{T,3}; kwargs...) where T
   return rmpoles1(poly2pm(N),D; kwargs...)
end
"""
       rmrank(N, D; fastrank = true, atol = 0, rtol = atol > 0 ? 0 : n*ϵ)

Determine the normal rank of the rational matrix `R(λ) := N(λ)./D(λ)`.

If `fastrank = true`, the rank is evaluated by counting how many singular values of `R(γ)` have magnitude greater
than `max(atol, rtol*σ₁)`, where `σ₁` is the largest singular value of `R(γ)` and `γ` is a randomly generated
complex value of magnitude equal to one.
If `fastrank = false`, first a structured linearization of `R(λ)` is built in the form of a descriptor system matrix
`S(λ) = [A-λE B; C D]` with `A-λE` an order `n` regular subpencil, and then its normal rank `nr` is determined.
The normal rank of `R(λ)` is `nr - n`.

The numerator `N(λ)` is a polynomial matrix of the form `N(λ) = N_1 + λ N_2 + ... + λ**k N_(k+1)`,
for which the coefficient matrices `N_i`, `i = 1, ..., k+1` are stored in the 3-dimensional matrix `N`,
where `N[:,:,i]` contains the `i`-th coefficient matrix `N_i` (multiplying `λ**(i-1)`).

The denominator `D(λ)` is a polynomial matrix of the form `D(λ) = D_1 + λ D_2 + ... + λ**l D_(l+1)`,
for which the coefficient matrices `D_i`, `i = 1, ..., l+1`, are stored in the 3-dimensional matrix `D`,
where `D[:,:,i]` contain the `i`-th coefficient matrix `D_i` (multiplying `λ**(i-1)`).

Alternatively, `N(λ)` and `D(λ)` can be specified as matrices of elements of the `Polynomial` type
provided by the [Polynomials](https://github.com/JuliaMath/Polynomials.jl) package.
In this case, no check is performed that `N(λ)` and `D(λ)` have the same indeterminates.

The irreducible descriptor system based linearization is built using the methods described in [1] in conjunction with
pencil manipulation algorithms of [2] and [3] to compute reduced order linearizations. These algorithms
employ rank determinations based on either the use of
rank revealing QR-decomposition with column pivoting, if `fast = true`, or the SVD-decomposition, if `fast = false`.
The rank decision based on the SVD-decomposition is generally more reliable, but the involved computational effort is higher.

The reduction to the appropriate KLF of `S(λ)` is performed using orthogonal similarity transformations [3]
and involves rank decisions based on rank revealing SVD-decompositions.

The keyword arguments `atol`  and `rtol` specify the absolute and relative tolerances for the nonzero
coefficients of `N(λ)` and `D(λ)`,  respectively.
The default relative tolerance is `n*ϵ`, where `n` is the size of the smallest dimension of `N(λ)`, and `ϵ` is the
machine epsilon of the element type of coefficients of `N(λ)`.

[1] A. Varga, On computing the Kronecker structure of polynomial and rational matrices using Julia, 2020,
[arXiv:2006.06825](https://arxiv.org/pdf/2006.06825).

[2] P. Van Dooreen, The generalized eigenstructure problem in linear system theory,
IEEE Transactions on Automatic Control, vol. AC-26, pp. 111-129, 1981.

[3] A. Varga, Solving Fault Diagnosis Problems - Linear Synthesis Techniques, Springer Verlag, 2017.
"""
function rmrank(N::AbstractArray{T1,3}, D::AbstractArray{T2,3}; fastrank::Bool = true, atol::Real = zero(real(T1)),
                rtol::Real = (min(size(N)...)*eps(real(float(one(T1)))))*iszero(atol)) where {T1,T2}
   if fastrank
       return rank(rmeval(N,D,exp(rand()*im)), atol = atol, rtol = rtol)
   else
      A, E, B, C, D, blkdims = rm2ls(N, D; fast = false, atol = atol, rtol = rtol)
      r = sprank(A, E, B, C, D, fastrank = false, atol1 = atol, atol2 = atol, rtol = rtol)
      return r-size(A,1)
   end
end
function rmrank(N::AbstractArray{T,3}; kwargs...) where T
    pmrank(N; kwargs...)
end
function rmrank(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
   return rmrank(poly2pm(N),poly2pm(D); kwargs...)
end
function rmrank(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
    pmrank(poly2pm(N); kwargs...)
end
function rmrank(N::AbstractArray{T,3},
                D::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...) where T
   return rmrank(N,poly2pm(D); kwargs...)
end
function rmrank(N::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                D::AbstractArray{T,3}; kwargs...) where T
   return rmrank(poly2pm(N),D; kwargs...)
end


struct PointCollection{T}
    OriginalPoints::Vector{T}
    EnclosingRectangle::Rectangle{T}
    ComputationRectangle::Rectangle{VoronoiDelaunay.Point2D}
    TransformedPoints::Vector{IndexablePoint2D}
    CornerNeighbors::Dict{Int64, Vector{Int64}}
end

function PointCollection(points, rect)
    corner_neighbors = corner_nearest_neighbor(points, rect)

    rect_width = right(rect) - left(rect)
    rect_height = upper(rect) - lower(rect)

    # Scale height and width such that the maximum is 1
    m = max(rect_width, rect_height)
    scaled_width = rect_width/m
    scaled_height = rect_height/m

    # Keep aspect ratio of original rectangle in the computation rectangle
    computation_rect = Rectangle(
        VoronoiDelaunay.Point2D(1.5 - 1/6*scaled_width, 1.5 - 1/6*scaled_height),
        VoronoiDelaunay.Point2D(1.5 + 1/6*scaled_width, 1.5 + 1/6*scaled_height)
    )

    transformed_points = map_to_computation_rectangle(points, rect, computation_rect)

    PointCollection(points, rect, computation_rect, transformed_points, corner_neighbors)
end

function raw_tesselation(pc::PointCollection)
    n_points = length(pc.OriginalPoints)

    generators = VoronoiDelaunay.DelaunayTessellation2D{IndexablePoint2D}(n_points)
    # Note that the elements of pc.TransformedPoints are reordered by VoronoiDelaunay
    push!(generators, pc.TransformedPoints)

    voronoi_cells = Dict(1:n_points .=> [Vector{VoronoiDelaunay.Point2D}(undef, 0) for _ in 1:n_points])

    for edge in VoronoiDelaunay.voronoiedges(generators)
        l = clip(edge, pc.ComputationRectangle)
        if isnothing(l)
            continue
        end

        generator_a = VoronoiDelaunay.getgena(edge) |> getindex
        generator_b = VoronoiDelaunay.getgenb(edge) |> getindex

        a = VoronoiDelaunay.geta(l)
        b = VoronoiDelaunay.getb(l)

        push!(voronoi_cells[generator_a], a)
        push!(voronoi_cells[generator_a], b)

        push!(voronoi_cells[generator_b], a)
        push!(voronoi_cells[generator_b], b)
    end

    voronoi_cells
end

struct Tessellation{T}
    Generators::Vector{T}
    EnclosingRectangle::Rectangle{T}
    Cells::Vector{Vector{T}}
end

Base.eltype(::Tessellation{T}) where T = T

function voronoicells(pc::PointCollection{T}) where T
    rt = raw_tesselation(pc)

    computation_corners = corners(pc.ComputationRectangle)
    for (corner_index, corner) in enumerate(computation_corners)
        corner_neighbors = pc.CornerNeighbors[corner_index]
        for neighbor_index in corner_neighbors
            push!(rt[neighbor_index], corner)
        end
    end

    n_cells = length(rt)
    cells = [Vector{T}(undef, 0) for _ in 1:n_cells]
    for n in 1:n_cells
        cell_corners = unique(rt[n])

        unsorted_cell_corners = map_rectangle(
            cell_corners, pc.ComputationRectangle, pc.EnclosingRectangle
        )
        cells[n] = sort(unsorted_cell_corners)
    end

    Tessellation(pc.OriginalPoints, pc.EnclosingRectangle, cells)
end

"""
    voronoicells(points, rect) -> Tessellation

Compute the Voronoi cells with the vector of generators `points` in the rectangle `rect`.
"""
function voronoicells(points, rect)
    pc = PointCollection(points, rect)
    voronoicells(pc)
end

function voronoicells(x::Vector, y::Vector, rect)
    n = length(x)
    if n != length(y)
        throw(ArgumentError("x and y must have equal length"))
    end

    points = [GeometryBasics.Point2(x[i], y[i]) for i in 1:n]
    voronoicells(points, rect)
end


using NeuralQuantum, QuantumOpticsBase, Statistics
using Test

N = 4
g = 0.7
V = 2.0

hilb = HomogeneousSpin(N)

H = LocalOperator(hilb)
for i=1:N
    global H += g/2.0 * sigmax(hilb, i)
    global H  += V/4.0 * sigmaz(hilb, i) * sigmaz(hilb, mod(i, N)+1)
end

Sx = LocalOperator(hilb)
Sy = LocalOperator(hilb)
Sz = LocalOperator(hilb)
for i=1:N
    global Sx += sigmax(hilb, i)/N
    global Sy += sigmay(hilb, i)/N
    global Sz += sigmaz(hilb, i)/N
end

prob  = NeuralQuantum.HamiltonianGSEnergyProblem(H)

net  = RBM( N, 2)

sampl = MetropolisSampler(LocalRule(), 300, N, burn=30)
#sampl = ExactSampler(1000)
algo  = SR(ϵ=(0.001), use_iterative=true)
algo  = Gradient()

is = BatchedSampler(net, sampl, prob, algo; batch_sz=32)
res, grad = sample!(is)

# exact value
Hmat = Matrix(H)
psi  = ket(net, hilb)

E_ex = psi'*Hmat*psi / (psi'psi)

# precise sampling
sampl_ex = ExactSampler(1000)
algo     = SR()
is = BatchedSampler(net, sampl_ex, prob, algo; batch_sz=32)

res, grad = sample!(is)

@test abs(res.mean - E_ex) < res.error

#


using Primes

factortodict(d, n) = (d[minimum(collect(keys(factor(n))))] = n)

# Numbers are from from the Perl 6 example.
numbers = [64921987050997300559,  70251412046988563035,  71774104902986066597,
           83448083465633593921,  84209429893632345702,  87001033462961102237,
           87762379890959854011,  89538854889623608177,  98421229882942378967,
           259826672618677756753, 262872058330672763871, 267440136898665274575,
           278352769033314050117, 281398154745309057242, 292057004737291582187]

mins = Dict()

Base.@sync(
    Threads.@threads for n in numbers
        factortodict(mins, n)
    end
)

answer = maximum(keys(mins))
println("The number that has the largest minimum prime factor is $(mins[answer]), with a smallest factor of $answer")


@testset "kbase simple" begin
    R, (x, y) = PolynomialRing(QQ, ["x", "y"], ordering=:degrevlex)

    @test Groebner.kbase([x, y]) == [R(1)]
    @test Groebner.kbase([x + y, y]) == [R(1)]
    @test Groebner.kbase([-2x + 8, 10y + 1]) == [R(1)]

    @test Groebner.kbase([x^2 + 1, y^2 - x - 1]) == [R(1), y, x, x*y]
    @test Groebner.kbase([8x^5 + 1, y - 1]) == [R(1), x, x^2, x^3, x^4]
    @test Groebner.kbase([x^2, y^2, x*y]) == [R(1), y, x]
    @test Groebner.kbase([x^3, y^3, x*y]) == [R(1), y, x, y^2, x^2]
    @test length(Groebner.kbase([8x^77 + 1, y^34 - 1])) == 77*34

    R, (x, y) = PolynomialRing(QQ, ["x", "y"], ordering=:lex)
    @test Groebner.kbase([x, y]) == [R(1)]
    @test Groebner.kbase([x + y, y]) == [R(1)]
    @test Groebner.kbase([-2x + 8, 10y + 1]) == [R(1)]

    @test Groebner.kbase([x^2 + 1, y^2 - 1]) == [R(1), y, x, x*y]
    @test Groebner.kbase([8x^5 + 1, y - 1]) == [R(1), x, x^2, x^3, x^4]
    @test Groebner.kbase([x^2, y^2, x*y]) == [R(1), y, x]
    @test Groebner.kbase([x^3, y^3, x*y]) == [R(1), y, y^2, x, x^2]
    @test length(Groebner.kbase([8x^77 + 1, y^34 - 1])) == 77*34

    R, (x, y) = PolynomialRing(QQ, ["x", "y"], ordering=:deglex)

    @test Groebner.kbase([x, y]) == [R(1)]
    @test Groebner.kbase([x + y, y]) == [R(1)]
    @test Groebner.kbase([-2x + 8, 10y + 1]) == [R(1)]

    @test Groebner.kbase([x^2 + 1, y^2 - x - 1]) == [R(1), y, x, x*y]
    @test Groebner.kbase([8x^5 + 1, y - 1]) == [R(1), x, x^2, x^3, x^4]
    @test Groebner.kbase([x^2, y^2, x*y]) == [R(1), y, x]
    @test Groebner.kbase([x^3, y^3, x*y]) == [R(1), y, x, y^2, x^2]
    @test length(Groebner.kbase([8x^77 + 1, y^34 - 1])) == 77*34
end

@testset "kbase big" begin
    root4 = Groebner.rootn(4)
    gb = Groebner.groebner(root4)
    R = parent(first(root4))
    (x1,x2,x3,x4) = gens(R)
    @test Groebner.kbase(gb) == [R(1), x4, x4^2, x4^3, x3, x3*x4, x3*x4^2,
                                 x3*x4^3, x3^2, x3^2*x4, x3^2*x4^2, x3^2*x4^3,
                                 x2, x2*x4, x2*x4^2, x2*x4^3, x2*x3, x2*x3*x4,
                                 x2*x3*x4^2, x2*x3*x4^3, x2*x3^2, x2*x3^2*x4,
                                 x2*x3^2*x4^2, x2*x3^2*x4^3]

     root6 = Groebner.rootn(6)
     gb = Groebner.groebner(root6)
     @test length(Groebner.kbase(gb)) == 720
     root6 = Groebner.rootn(6)
     gb = Groebner.groebner(root6, ordering=:deglex)
     @test length(Groebner.kbase(gb)) == 720
     root6 = Groebner.rootn(6)
     gb = Groebner.groebner(root6, ordering=:degrevlex)
     @test length(Groebner.kbase(gb)) == 720

     noon2 = Groebner.change_ordering(Groebner.noonn(2), :degrevlex)
     R = parent(first(noon2))
     (x1,x2) = gens(R)
     gb = Groebner.groebner(noon2, ordering=:degrevlex)
     @test Groebner.kbase(gb) == [R(1), x2, x1, x2^2, x1*x2]

     noon3 = Groebner.noonn(3)
     R = parent(first(noon3))
     gb = Groebner.groebner(noon3, ordering=:degrevlex)
     @test length(Groebner.kbase(gb)) == 21

     noon3 = Groebner.noonn(7)
     R = parent(first(noon3))
     gb = Groebner.groebner(noon3, ordering=:degrevlex)
     @test length(Groebner.kbase(gb)) == 2173
end


using LinearAlgebra, MAT, SparseArrays
using Statistics, HNSW, Distances, MatrixMarket
using Graphs, SimpleWeightedGraphs
using Arpack, Clustering

function undirectedEdges(idxs,dists) #O(nk)
    num_node = length(idxs) #each node
    k = length(idxs[1]) #nearest neighbors
    edges_dict = Dict{Tuple{Int,Int},Float64}()
    #edges = Vector{Tuple{Int,Int,Float64}}(undef,num_node*(k-1))
    idxs = convert(Array{Array{Int}},idxs) #idxs to integers
    for i = 1:num_node
        for j = 2:k
            if(dists[i][1]!=0.0)
                @warn "Node $i could not find itself: ef_construcion, M or ef in HNSW construction may be too low."
                a = min(i,idxs[i][j-1])
                b = max(i,idxs[i][j-1])
                edges_dict[(a,b)] = dists[i][j-1]
            else
                a = min(idxs[i][1],idxs[i][j])
                b = max(idxs[i][1],idxs[i][j])
                edges_dict[(a,b)] = dists[i][j]
            end
        end
    end
    sort(collect(edges_dict), by = x -> x[1])
end

function getLaplacian(G)
    A = adjacency_matrix(G)
    D = sparse(Diagonal(vec(sum(A;dims = 2))))
    L = D-A
    return L
end

function FiedlerAddEdges(G,fea,samplePerc,chosenPercEachIter,sig,embedTol)

    num_eigs_f = 5
    num_node,dim = size(fea)

    #=
    @info "laplacian_matrix"
    @time begin
    L = laplacian_matrix(G)
    end
    =#
    #@info "laplacian_matrix"
    #@time begin
    L = getLaplacian(G)
    #end

    L = L + (1/(sig^2))*sparse(LinearAlgebra.I,L.m,L.n)
    #mmwrite(raw"C:\Users\jacec\Desktop\code\code\JuliaL.mtx",L)
    #L = mmread(raw"C:\Users\jacec\Documents\MATLAB\GRASPEL_Code\code\JuliaL.mtx")
    vals, vecs = eigs(L; nev = num_eigs_f, which = :SM, tol = 1e-6)
    #@info vals
    #@info vecs[1,:]
    for i in 2:num_eigs_f
        vecs[:,i] = vecs[:,i]/sqrt(vals[i])
    end
    #vecs[:,2:num_eigs_f] = vecs[:,2:num_eigs_f].*[sqrt(i) for i in vals[2:num_eigs_f]]
    #@info vecs[1,:]

    #Fiedler Vector
    l2 = vecs[:,2]

    #Graph node sorting
    I = sortperm(l2)
    #@info I

    top = I[1:Int64(floor(num_node*samplePerc))]
    bot = I[num_node-Int64(floor(num_node*samplePerc)+1):num_node]
    chosenSet = zeros(Float64,Int64(ceil(num_node*chosenPercEachIter)),5)
    minDelta, location = findmin(chosenSet[:,3])

    #sample candidate edges

    for i in 1:length(chosenSet)*1000
        a_idx = rand(top);
        b_idx = rand(bot);
        #always set p to have the bigger node index
        p = max(a_idx, b_idx)
        q = min(a_idx, b_idx)
        #e_pq = zeros(num_node,1)       unsure what these were for
        #e_pq[p] = 1
        #e_pq[q] = -1
        distX = pairwise(SqEuclidean(),reshape(fea[p,:],:,1),reshape(fea[q,:],:,1))[1]
        weight = 1/distX
        distZ = pairwise(SqEuclidean(),reshape(vecs[p,:],:,1),reshape(vecs[q,:],:,1))[1]*dim
        delta_fiedler = distZ*weight
        #check if edge has been selected
        flagP = p in chosenSet[:,1]
        flagQ = q in chosenSet[:,2]
        if delta_fiedler > minDelta && !(flagP || flagQ) && delta_fiedler > embedTol
            chosenSet[location,1] = p
            chosenSet[location,2] = q
            chosenSet[location,3] = delta_fiedler
            chosenSet[location,4] = weight*1 #scale weight by up to 10
            gradient = (1-1/delta_fiedler)*distZ
            chosenSet[location,5] = gradient #gradient
            minDelta, location = findmin(chosenSet[:,3])
            #@info "P:$p Q:$q distX:$distX weight:$weight
            #distZ:$distZ delta:$delta_fiedler gradient:$gradient"
        end
    end

    #alternative cost formula
    if false
        lambda = diag(vals)
        cost = log(prod(lambda[2:num_eigs]))-trace(L*fea*transpose(fea))/dim
    else
        cost = maximum(chosenSet[:,5])
    end
    variationRatio = mean(chosenSet[:,3])

    if minimum(chosenSet[:,3]) == 0
        return G, variationRatio, cost
    end

    for k in 1:length(chosenSet[:,1])
        add_edge!(G,chosenSet[k,1],chosenSet[k,2],chosenSet[k,3])
    end

    return G, variationRatio, cost
end

#FiedlerConstruct
function FiedlerConstruct(fea, samplePerc, edge_iter, sig, maxIterations, k, tol, num_node, dim)
    varRatio = Array{Float64}(undef,maxIterations)
    costVec = Array{Float64}(undef,maxIterations)

    #Generate Initial KNN

    #Intialize HNSW struct
    feaVec = [vec(fea[i,:]) for i = 1:num_node] #input should be a vector of vectors
    @info "$k NN HNSW:..."
    @time begin
        #hnsw = HierarchicalNSW(feaVec; efConstruction=100, M=45, ef=90)
        hnsw = HierarchicalNSW(feaVec; efConstruction=100, M=45, ef=100)
        #Add all data points into the graph
        #Optionally pass a subset of the indices in data to partially construct the graph
        add_to_graph!(hnsw)

        # Find k+1 (approximate) nearest neighbors for each of the queries; first result is usually itself
        # results (# edges) are slightly different each time
        idxs, dists = knn_search(hnsw, feaVec, k+1)

        e = undirectedEdges(idxs,dists)
        #Normalize edge weights by w = 1/dist^2
        we = [Pair(e[i][1],1/(e[i][2]^2)) for i = 1:length(e)]
        #Store as sparse graph, ~14000-14005 nodes for USPS dataset
        G = SimpleWeightedGraph(num_node)
        for i in we
            add_edge!(G,i[1][1],i[1][2],i[2])
        end

    end
    @info "Processed Graph" G

    #MatrixMarket.mmwrite("JuliaL.mtx",L)

    @info "GRASPEL Iterations..."
    @time begin
        numIter = 0
        for i in 1:maxIterations
            numIter = i
            G, varR, cost = FiedlerAddEdges(G,fea,samplePerc,edge_iter,sig,tol)

            varRatio[i] = varR
            costVec[i] = cost

            if varR < tol
                break
            end

        end
    end
    return G, varRatio[1:numIter], costVec[1:numIter], numIter
end


"""
    FunctionTransform(f)

Take a function or object `f` as an argument which is going to act on each vector individually.
Make sure that `f` is supposed to act on a vector.
For example replace `f(x)=sin(x)` by `f(x)=sin.(x)`
```
    f(x) = abs.(x)
    tr = FunctionTransform(f)
```
"""
struct FunctionTransform{F} <: Transform
    f::F
end

(t::FunctionTransform)(x) = t.f(x)

_map(t::FunctionTransform, x::AbstractVector{<:Real}) = map(t.f, x)
_map(t::FunctionTransform, x::ColVecs) = ColVecs(mapslices(t.f, x.X; dims=1))
_map(t::FunctionTransform, x::RowVecs) = RowVecs(mapslices(t.f, x.X; dims=2))

duplicate(t::FunctionTransform,f) = FunctionTransform(f)

Base.show(io::IO, t::FunctionTransform) = print(io, "Function Transform: ", t.f)


module Acor

using FFTW
using Statistics

""" Returns an estimate of the autocorrelation function of the given
series.  """
function acf(xs::Array{Float64, 1})
    n = size(xs, 1)
    N = 1
    while N < 2*n
        N = N << 1
    end

    mu = mean(xs)

    ys = zeros(N)
    for i in 1:n
        ys[i] = xs[i] - mu
    end

    ys_tilde = rfft(ys)
    ac = irfft(abs2.(ys_tilde), N)

    ac[1:n]/ac[1]
end

""" Returns an estimate of the autocorrelation length of the given
series, or `Inf` if this is not possible to accurately estimate.  """
function acl(xs::Array{Float64, 1})
    ac = acf(xs)

    n = size(ac, 1)

    cumac = 2.0.*cumsum(ac) .- 1.0
    for i in 1:div(n,2)
        if cumac[i] < i/5
            return cumac[i]
        end
    end

    return Inf
end

""" Returns an array giving the estimated ACL for each parameter,
treating `xs` as the output of an ensemble MCMC (shape `(ndim,
nwalkers, nsteps)`).  """
function acl(xs::Array{Float64, 3})
    ndim = size(xs, 1)
    nwalk = size(xs, 2)
    nstep = size(xs, 3)

    acls = zeros(ndim)
    means = zeros(nstep)

    for i in 1:ndim
        for k in 1:nstep
            means[k] = 0.0
            for j in 1:nwalk
                means[k] += xs[i,j,k]
            end
            means[k] /= nwalk
        end
        acls[i] = acl(means)
    end

    acls
end

""" Returns an array of shape `(ndim, nwalkers*nsteps)` where suitable
for thinning to generate independent samples.

This involves both a flattening and a dimension-permutation, so that
successive samples from the same ensemble don't sit adjacent to each
other in memory."""
function flatten(pts)
    p = permutedims(pts, (1, 3, 2))
    reshape(p, (size(p,1), size(p,2)*size(p,3)))
end

""" Thins an ensemble by the maximum autocorrelation length of the
    parameters in the ensemble. """
function thin(pts)
    l = maximum(acl(pts))

    if l == Inf
        warn("Cannot thin because ACL is infinite.")
        zeros(size(pts, 1), size(pts, 2), 0)
    else
        n = convert(Int, ceil(l))
        pts[:,:,1:n:end]
    end
end

""" Returns an estimate of the Gelman-Rubin R statistic for each
parameter, treating each walker as an independent simulation.  The R
statistic estimates the factor by which the variance of the estimated
distribution can be reduced by running the simulation longer."""
function gelman_rubin_rs(ps)
    ndim, nwalkers, nsteps = size(ps)

    rs = zeros(ndim)
    means = zeros(ndim, nwalkers)
    vars = zeros(ndim, nwalkers)

    for i in 1:ndim
        for j in 1:nwalkers
            for k in 1:nsteps
                means[i,j] += ps[i,j,k]
            end
            means[i,j] /= nsteps

            for k in 1:nsteps
                x = ps[i,j,k] - means[i,j]
                vars[i,j] += x*x
            end
            vars[i,j] /= nsteps
        end

        Bon = var(means[i,:])
        W = mean(vars[i,:])

        s2 = (nsteps-1.0)/nsteps*W + Bon

        rs[i] = s2/W
    end

    rs
end

""" An estimate of the AIC (Akaike Information Criterion) from the
MCMC samples of the log-likelihood.  The AIC is `-2.0*max(lnps) +
nparams`.  The estimate assumes Gaussianity of the likelihood
function; under this assumption the mean of the log-likelihood is
`nparams/2.0` below the maximum and the variance of the log-likelihood
is `nparams/2.0`.  So the estimate of the AIC is

    waic = -2.0*mean(lnps) + 2.0*var(lnps)

This estimate is related to (or equal to?) the WAIC described by XXXX
(Gelman ref.).
"""
waic(lnps) = -2.0*mean(lnps) + 2.0*var(lnps)

"""Returns an array of acceptance rates for each walker from the given
chain. """
function acceptance_rate(ps)
    nw = size(ps, 2)
    nt = size(ps, 3)

    ar = zeros(nw)

    for j in 1:nw
        acc = 0
        for k in 1:nt-1
            if ps[:,j,k] == ps[:,j,k+1]
                # Pass
            else
                acc += 1
            end
        end
        ar[j] = float(acc)/float(nt)
    end

    ar
end

""" Returns the acceptance rate given that the chain `ps` has been
thinned by `thin`."""
function acceptance_rate(ps, thin)
    ar = acceptance_rate(ps)

    1 - exp(log1p(-ar)/thin)
end

end


import MKL_jll
using Test
using IntelVectorMath

include("common.jl")
include("real.jl")
include("complex.jl")


""" Test the Syntax module.

The unit tests are sparse because many of the Doctrine tests are really just
tests of the Syntax module.
"""
module TestSyntax

using Test
using Catlab

# Monoid
########

""" Theory of monoids.
"""
@signature Monoid(Elem) begin
  Elem::TYPE
  munit()::Elem
  mtimes(x::Elem,y::Elem)::Elem
end

""" Syntax for the theory of monoids.
"""
@syntax FreeMonoid Monoid

Elem(mod::Module, args...) = Elem(mod.Elem, args...)

@test isa(FreeMonoid, Module)
@test occursin("theory of monoids", string(Docs.doc(FreeMonoid)))
@test sort(names(FreeMonoid)) == sort([:FreeMonoid, :Elem])

x, y, z = Elem(FreeMonoid,:x), Elem(FreeMonoid,:y), Elem(FreeMonoid,:z)
@test isa(mtimes(x,y), FreeMonoid.Elem)
@test isa(munit(FreeMonoid.Elem), FreeMonoid.Elem)
@test mtimes(mtimes(x,y),z) != mtimes(x,mtimes(y,z))

# Test equality
@test x == Elem(FreeMonoid,:x)
@test x != y
@test Elem(FreeMonoid,"X") == Elem(FreeMonoid,"X")
@test Elem(FreeMonoid,"X") != Elem(FreeMonoid,"Y")

# Test hash
@test hash(x) == hash(x)
@test hash(x) != hash(y)
@test hash(mtimes(x,y)) == hash(mtimes(x,y))
@test hash(mtimes(x,y)) != hash(mtimes(x,z))

@syntax FreeMonoidAssoc Monoid begin
  mtimes(x::Elem, y::Elem) = associate(new(x,y))
end

x, y, z = [ Elem(FreeMonoidAssoc,sym) for sym in [:x,:y,:z] ]
e = munit(FreeMonoidAssoc.Elem)
@test mtimes(mtimes(x,y),z) == mtimes(x,mtimes(y,z))
@test mtimes(e,x) != x && mtimes(x,e) != x

@syntax FreeMonoidAssocUnit Monoid begin
  mtimes(x::Elem, y::Elem) = associate_unit(new(x,y), munit)
end

x, y, z = [ Elem(FreeMonoidAssocUnit,sym) for sym in [:x,:y,:z] ]
e = munit(FreeMonoidAssocUnit.Elem)
@test mtimes(mtimes(x,y),z) == mtimes(x,mtimes(y,z))
@test mtimes(e,x) == x && mtimes(x,e) == x

abstract type MonoidExpr{T} <: GATExpr{T} end
@syntax FreeMonoidTyped(MonoidExpr) Monoid

x = Elem(FreeMonoidTyped.Elem, :x)
@test FreeMonoidTyped.Elem <: MonoidExpr
@test isa(x, FreeMonoidTyped.Elem) && isa(x, MonoidExpr)

@signature Monoid(Elem) => MonoidNumeric(Elem) begin
  elem_int(x::Int)::Elem
end
@syntax FreeMonoidNumeric MonoidNumeric

x = elem_int(FreeMonoidNumeric.Elem, 1)
@test isa(x, FreeMonoidNumeric.Elem)
@test first(x) == 1

""" A monoid with two distinguished elements.
"""
@signature Monoid(Elem) => MonoidTwo(Elem) begin
  one()::Elem
  two()::Elem
end
""" The free monoid on two generators.
"""
@syntax FreeMonoidTwo MonoidTwo begin
  Elem(::Type{Elem}, value) = error("No extra generators allowed!")
end

x, y = one(FreeMonoidTwo.Elem), two(FreeMonoidTwo.Elem)
@test all(isa(expr, FreeMonoidTwo.Elem) for expr in [x, y, mtimes(x,y)])
@test_throws ErrorException Elem(FreeMonoidTwo, :x)

# Category
##########

@signature Category(Ob,Hom) begin
  Ob::TYPE
  Hom(dom::Ob, codom::Ob)::TYPE

  id(X::Ob)::Hom(X,X)
  compose(f::Hom(X,Y), g::Hom(Y,Z))::Hom(X,Z) ⊣ (X::Ob, Y::Ob, Z::Ob)
end

@syntax FreeCategory Category begin
  compose(f::Hom, g::Hom) = associate(new(f,g))
end

@test isa(FreeCategory, Module)
@test sort(names(FreeCategory)) == sort([:FreeCategory, :Ob, :Hom])

X, Y, Z, W = [ Ob(FreeCategory.Ob, sym) for sym in [:X, :Y, :Z, :W] ]
f, g, h = Hom(:f, X, Y), Hom(:g, Y, Z), Hom(:h, Z, W)
@test isa(X, FreeCategory.Ob) && isa(f, FreeCategory.Hom)
@test_throws MethodError FreeCategory.Hom(:f)
@test dom(f) == X
@test codom(f) == Y

@test isa(id(X), FreeCategory.Hom)
@test dom(id(X)) == X
@test codom(id(X)) == X

@test isa(compose(f,g), FreeCategory.Hom)
@test dom(compose(f,g)) == X
@test codom(compose(f,g)) == Z
@test isa(compose(f,f), FreeCategory.Hom) # Doesn't check domains.
@test compose(compose(f,g),h) == compose(f,compose(g,h))

@syntax FreeCategoryStrict Category begin
  compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
end

X, Y = Ob(FreeCategoryStrict.Ob, :X), Ob(FreeCategoryStrict.Ob, :Y)
f, g = Hom(:f, X, Y), Hom(:g, Y, X)

@test isa(compose(f,g), FreeCategoryStrict.Hom)
@test_throws SyntaxDomainError compose(f,f)

# Functor
#########

@instance Monoid(String) begin
  munit(::Type{String}) = ""
  mtimes(x::String, y::String) = string(x,y)
end

F(expr; kw...) = functor((String,), expr; kw...)

x, y, z = Elem(FreeMonoid,:x), Elem(FreeMonoid,:y), Elem(FreeMonoid,:z)
gens = Dict(x => "x", y => "y", z => "z")
@test F(mtimes(x,mtimes(y,z)); generators=gens) == "xyz"
@test F(mtimes(x,munit(FreeMonoid.Elem)); generators=gens) == "x"

terms = Dict(:Elem => (x) -> string(first(x)))
@test F(mtimes(x,mtimes(y,z)); terms=terms) == "xyz"
@test F(mtimes(x,munit(FreeMonoid.Elem)); terms=terms) == "x"

# Serialization
###############

# To JSON
X, Y, Z = [ Ob(FreeCategory.Ob, sym) for sym in [:X, :Y, :Z] ]
f = Hom(:f, X, Y)
g = Hom(:g, Y, Z)
@test to_json_sexpr(X) == ["Ob", "X"]
@test to_json_sexpr(f) == ["Hom", "f", ["Ob", "X"], ["Ob", "Y"]]
@test to_json_sexpr(compose(f,g)) == [
  "compose",
  ["Hom", "f", ["Ob", "X"], ["Ob", "Y"]],
  ["Hom", "g", ["Ob", "Y"], ["Ob", "Z"]],
]
@test to_json_sexpr(f, by_reference=x->true) == "f"
@test to_json_sexpr(compose(f,g), by_reference=x->true) == ["compose","f","g"]

# From JSON
@test parse_json_sexpr(FreeMonoid, [:Elem, "x"]) == Elem(FreeMonoid, :x)
@test parse_json_sexpr(FreeMonoid, [:munit]) == munit(FreeMonoid.Elem)
@test parse_json_sexpr(FreeCategory, ["Ob", "X"]) == X
@test parse_json_sexpr(FreeCategory, ["Ob", "X"]; symbols=false) ==
  Ob(FreeCategory.Ob, "X")
@test parse_json_sexpr(FreeCategory, ["Hom", "f", ["Ob", "X"], ["Ob", "Y"]]) == f
@test parse_json_sexpr(FreeCategory, ["Hom", "f", ["Ob", "X"], ["Ob", "Y"]]; symbols=false) ==
  Hom("f", Ob(FreeCategory.Ob, "X"), Ob(FreeCategory.Ob, "Y"))

# Round trip
@test parse_json_sexpr(FreeCategory, to_json_sexpr(compose(f,g))) == compose(f,g)
@test parse_json_sexpr(FreeCategory, to_json_sexpr(id(X))) == id(X)

end


@testset "indexing" begin
    L32 = @inferred(range(SInt32(1), stop=SInt32(4), length=SInt(4)))
    L64 = @inferred(range(SInt64(1), stop=SInt64(4), length=SInt(4)))
    @test @inferred(L32[1]) == 1.0 && @inferred(L64[1]) == 1.0
    @test L32[2] == 2 && L64[2] == 2
    @test L32[3] == 3 && L64[3] == 3
    @test L32[4] == 4 && L64[4] == 4

    @test @inferred(range(SVal(1.0), stop=SVal(2.0), length=SVal(2)))[1] == 1.0
    @test @inferred(range(SVal(1.0f0), stop=SVal(2.0f0), length=SVal(2)))[1] == 1.0f0
    @test @inferred(range(SVal(Float16(1.0)), stop=SVal(Float16(2.0)), length=SVal(2)))[1] == Float16(1.0)

    let r = SInt(5):SInt(-1):SInt(1)
        @test r[1]==5
        @test r[2]==4
        @test r[3]==3
        @test r[4]==2
        @test r[5]==1
    end
    # FIXME @test @inferred(range(SVal(0.1), step=SVal(0.1), stop=SVal(0.3))[2]) == 0.2
    @test @inferred(range(SVal(0.1f0), step=SVal(0.1f0), stop=SVal(0.3f0))[2]) == 0.2f0

    @test @inferred(SVal(1):SVal(5))[1:4] == 1:SVal(1):4
    #FIXME @test @inferred(SVal(1.0):SVal(5))[1:4] == 1.0:4
    @test (SVal(2):SVal(6))[1:4] == 2:5
    @test (SOne:SVal(6))[2:5] == 2:5
    @test (SOne:SVal(6))[2:2:5] == 2:2:4
    @test (SOne:SVal(2):SVal(13))[2:6] == 3:2:11
    @test (SOne:SVal(2):SVal(13))[2:3:7] == 3:6:13

    #@test isempty(srange(1:4)[5:4])
    #@test_throws BoundsError range(1:10)[8:-1:-2]

    let r = typemax(SInt)-SVal(5):typemax(SInt)-SOne
        @test_throws BoundsError r[7]
    end
    @testset "indexing range with empty range (#4309)" begin
        @test (SVal(3):SVal(6))[5:4] == 7:6
        @test_throws BoundsError (SVal(3):SVal(6))[5:5]
        @test_throws BoundsError (SVal(3):SVal(6))[5]
        @test (SZero:SVal(2):SVal(10))[7:6] == 12:2:10
        @test_throws BoundsError (SZero:SVal(2):SVal(10))[7:7]
    end
end

@testset "intersect" begin
    @test intersect(SOne:SVal(5), SVal(2):SVal(3)) == 2:3
    @test intersect(SVal(-3):SVal(5), SVal(2):SVal(8)) == 2:5
    @test intersect(SVal(-8):SVal(-3), SVal(-8):SVal(-3)) == -8:-3
    @test intersect(SOne:SVal(5), SVal(5):SVal(13)) == 5:5
    @test isempty(intersect(SVal(-8):SVal(-3), SVal(-2):SVal(2)))
    @test isempty(intersect(SVal(-3):SVal(7), SVal(2):SVal(1)))
    @test intersect(SOne:SVal(11), SVal(-2):SVal(3):SVal(15)) == 1:3:10
    @test intersect(SOne:SVal(11), SVal(-2):SVal(2):SVal(15)) == 2:2:10
    @test intersect(SOne:SVal(11), SVal(-2):SVal(1):SVal(15)) == 1:11
    @test intersect(SOne:SVal(11), SVal(15):SVal(-1):SVal(-2)) == 1:11
    @test intersect(SOne:SVal(11), SVal(15):SVal(-4):SVal(-2)) == 3:4:11
    @test intersect(SVal(-20):SVal(-5), SVal(-10):SVal(3):SVal(-2)) == -10:3:-7
    @test isempty(intersect(SVal(-5):SVal(5), SVal(-6):SVal(13):SVal(20)))
    @test isempty(intersect(SOne:SVal(11), SVal(15):SVal(4):SVal(-2)))
    @test isempty(intersect(SVal(11):SOne, SVal(15):SVal(-4):SVal(-2)))
    #@test intersect(-5:5, 1+0*(1:3)) == 1:1
    #@test isempty(intersect(-5:5, 6+0*(1:3)))
    @test intersect(SVal(-15):SVal(4):SVal(7), SVal(-10):SVal(-2)) == SVal(-7):SVal(4):SVal(-3)
    @test intersect(SVal(13):SVal(-2):SVal(1), SVal(-2):SVal(8)) == (SVal(7):SVal(-2):SVal(1))
    @test isempty(intersect(SVal(13):SVal(2):SOne, SVal(-2):SVal(8)))
    @test isempty(intersect(SVal(13):SVal(-2):SOne, SVal(8):SVal(-2)))
    #@test intersect(5+0*(1:4), 2:8) == 5+0*(1:4)
    #@test isempty(intersect(5+0*(1:4), -7:3))
    @test intersect(SZero:SVal(3):SVal(24), SZero:SVal(4):SVal(24)) == 0:12:24
    @test intersect(SZero:SVal(4):SVal(24), SZero:SVal(3):SVal(24)) == 0:12:24
    @test intersect(SZero:SVal(3):SVal(24), SVal(24):SVal(-4):SZero) == 0:12:24
    @test intersect(SVal(24):SVal(-3):SZero, SZero:SVal(4):SVal(24)) == 24:-12:0
    @test intersect(SVal(24):SVal(-3):SZero, SVal(24):SVal(-4):SZero) == 24:-12:0
    @test intersect(SVal(1):SVal(3):SVal(24), SZero:SVal(4):SVal(24)) == 4:12:16
    @test intersect(SZero:SVal(6):SVal(24), SZero:SVal(4):SVal(24)) == 0:12:24
    @test isempty(intersect(SOne:SVal(6):SVal(2400), SZero:SVal(4):SVal(2400)))
    @test intersect(SVal(-51):SVal(5):SVal(100), SVal(-33):SVal(7):SVal(125)) == -26:35:79
    @test intersect(SVal(-51):SVal(5):SVal(100), SVal(-32):SVal(7):SVal(125)) == -11:35:94
    #@test intersect(0:6:24, 6+0*(0:4:24)) == 6:6:6
    #@test intersect(12+0*(0:6:24), 0:4:24) == AbstractRange(12, 0, 5)
    #@test isempty(intersect(6+0*(0:6:24), 0:4:24))
    @test intersect(SVal(-10):SVal(3):SVal(24), SVal(-10):SVal(3):SVal(24)) == -10:3:23
    @test isempty(intersect(SVal(-11):SVal(3):SVal(24), SVal(-10):SVal(3):SVal(24)))

    #= TODO: this overflows when finding length, which happens in base too
    @test intersect(srange(typemin(Int):2:typemax(Int)), srange(1:10)) == srange(2:2:10)
    @test intersect(srange(1:10), srange(typemin(Int):2:typemax(Int))) == srange(2:2:10)

    @test intersect(reverse(srange(typemin(Int):2:typemax(Int))),srange(typemin(Int):2:typemax(Int))) == reverse(srange(typemin(Int):2:typemax(Int)))
    @test intersect(srange(typemin(Int):2:typemax(Int)), reverse(srange(typemin(Int):2:typemax(Int)))) == srange(typemin(Int):2:typemax(Int))
    =#

    @test intersect(SOne:SInt(2), SVal(3)) == 3:2
    @test intersect(SOne:SVal(2), SOne:SVal(5), SVal(3):SVal(7), SVal(4):SVal(6)) == 4:3

    @test intersect(SOne:SVal(3), SVal(2)) == intersect(SVal(2), SOne:SVal(3)) == 2:2
    @test intersect(SVal(1.0):SVal(3.0), SVal(2)) == intersect(SVal(2), SVal(1.0):SVal(3.0)) == [2.0]
end

@testset "findall(::Base.Fix2{typeof(in)}, ::Array)" begin
    @test findall(in(SVal(3):SVal(20)), [5.2, 3.3]) == findall(in(Vector(SInt(3):SInt(20))), [5.2, 3.3])

    let span = SVal(5):SVal(20),
        r = SVal(-7):SVal(3):SVal(42)
        @test findall(in(span), r) == 5:10
        r = SVal(15):SVal(-2):SVal(-38)
        @test findall(in(span), r) == 1:6
    end
end

@testset "findfirst" begin
    @test findfirst(isequal(7), SOne:SVal(2):SVal(10)) == 4
    @test findfirst(==(7), SOne:SVal(2):SVal(10)) == 4
    @test findfirst(==(10), SOne:SVal(2):SVal(10)) == nothing
    @test findfirst(==(11), SOne:SVal(2):SVal(10)) == nothing
end

@testset "reverse" begin
    @test reverse(reverse(SOne:SVal(10))) == 1:10
    @test reverse(reverse(typemin(SInt):typemax(SInt))) == typemin(Int):typemax(Int)
    @test reverse(reverse(typemin(SInt):SVal(2):typemax(SInt))) == typemin(Int):2:typemax(Int)
end

@testset "sort/sort!/partialsort" begin
    @test sort(SOne:SVal(2)) == 1:2
    @test sort!(SOne:SVal(2)) == 1:2
    @test sort(SOne:SVal(10), rev=true) == 10:-1:1
    @test sort(SVal(-3):SVal(3), by=abs) == [0,-1,1,-2,2,-3,3]
    @test partialsort(SOne:SVal(10), 4) == 4
end

@testset "length" begin
    @test length(SVal(.1):SVal(.1):SVal(.3)) == 3
    @test length(SVal(1.1):SVal(1.1):SVal(3.3)) == 3
    @test length(SVal(1.1):SVal(1.3):SVal(3)) == 2
    @test length(SVal(1):SVal(1):SVal(1.8)) == 1
    @test length(SVal(1):SVal(.2):SVal(2)) == 6
    @test length(SVal(1.):SVal(.2):SVal(2.)) == 6
    @test length(SVal(2):SVal(-.2):SVal(1)) == 6
    @test length(SVal(2.):SVal(-.2):SVal(1.)) == 6
    @test length(SVal(2):SVal(.2):(1)) == 0
    @test length(SVal(2.):SVal(.2):SVal(1.)) == 0

    @test length(SOne:SZero) == 0
    @test length(SVal(0.0):SVal(-0.5)) == 0
    @test length(SOne:SVal(2):SZero) == 0
# FIXME    @test length(srange(Char(0):Char(0x001fffff))) == 2097152
# FIXME    @test length(srange(typemax(SUInt64)//one(SUInt64):1:typemax(SUInt64)//one(SUInt64))) == 1
end

@testset "in" begin
    @test 0 in SUInt(0):SInt(100):typemax(SUInt)
    @test last(SVal(UInt(0)):SVal(100):SVal(typemax(UInt))) in SVal(UInt(0)):SVal(100):SVal(typemax(UInt))
    @test -9223372036854775790 in SVal(-9223372036854775790):SVal(100):SVal(9223372036854775710)
    @test -9223372036854775690 in SVal(-9223372036854775790):SVal(100):SVal(9223372036854775710)
    @test -90 in SVal(-9223372036854775790):SVal(100):SVal(9223372036854775710)
    @test 10 in SVal(-9223372036854775790):SVal(100):SVal(9223372036854775710)
    @test 110 in SVal(-9223372036854775790):SVal(100):SVal(9223372036854775710)
    @test 9223372036854775610 in SVal(-9223372036854775790):SVal(100):SVal(9223372036854775710)
    @test 9223372036854775710 in SVal(-9223372036854775790):SVal(100):SVal(9223372036854775710)

    @test !(SVal(3.5) in SOne:SVal(5))
    @test (3 in SOne:SVal(5))
    @test (3 in SVal(5):SVal(-1):SOne)
    #@test (3 in 3+0*(1:5))
    #@test !(4 in 3+0*(1:5))

    let r = SVal(0.0):SVal(0.01):SVal(1.0)
        @test (r[30] in r)
    end
    let r = (-SInt(4)*SInt64(maxintfloat(Int === Int32 ? Float32 : Float64))):SInt(5)
        @test (3 in r)
        @test (3.0 in r)
    end

    @test !(1 in SOne:SZero)
    @test !(1.0 in SVal(1.0):SVal(0.0))
end

# TODO: reimplement all below with SRational and SComplex
@testset "in() works across types, including non-numeric types (#21728)" begin
    @test 1//1 in SOne:SVal(3)
    @test 1//1 in SVal(1.0):SVal(3.0)
    @test !(5//1 in SOne:SVal(3))
    @test !(5//1 in SVal(1.0):SVal(3.0))

    @test Complex(1, 0) in SOne:SVal(3)
    @test Complex(1, 0) in SVal(1.0):SVal(3.0)
    @test Complex(1.0, 0.0) in SOne:SVal(3)
    @test Complex(1.0, 0.0) in SVal(1.0):SVal(3.0)
    @test !(Complex(1, 1) in SOne:SVal(3))
    @test !(Complex(1, 1) in SVal(1.0):SVal(3.0))
    @test !(Complex(1.0, 1.0) in SOne:SVal(3))
    @test !(Complex(1.0, 1.0) in SVal(1.0):SVal(3.0))
    @test !(π in SOne:SVal(3))
    @test !(π in SVal(1.0):SVal(3.0))
    @test !("a" in SOne:SVal(3))
    @test !("a" in SVal(1.0):SVal(3.0))

# FIXME   @test !(1 in srange(Date(2017, 01, 01):Dates.Day(1):Date(2017, 01, 05)))
# FIXME   @test !(Complex(1, 0) in srange(Date(2017, 01, 01):Dates.Day(1):Date(2017, 01, 05)))
# FIXME   @test !(π in srange(Date(2017, 01, 01):Dates.Day(1):Date(2017, 01, 05)))
# FIXME   @test !("a" in srange(Date(2017, 01, 01):Dates.Day(1):Date(2017, 01, 05)))
end