# =========================================================================== #
# Compliant julia 1.x

# Using the following packages
using JuMP, GLPK
using LinearAlgebra

include("loadSPP.jl")
include("setSPP.jl")
include("getfname.jl")

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

# Setting the data
fname = "Desktop/solveSPP/Data/didactic.dat"  # path for a standard config on macOS
cost, matrix = loadSPP(fname)

# Proceeding to the optimization
solverSelected = GLPK.Optimizer
ip, ip_x = setSPP(solverSelected, cost, matrix)
println("Solving..."); optimize!(ip)

# Displaying the results
println("z  = ", objective_value(ip))
print("x  = "); println(value.(ip_x))

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

# Collecting the names of instances to solve
target = "Desktop/solveSPP/Data"            # path for a standard config on macOS
fnames = getfname(target)


#=
Functions:
  contract_edge(e,x,k)
  contract_edge_k_1(e,x)
  contract(A,x,m)
  contract_k_1(A,x)
  contract_k_1!(A(type needs to be determined),x,k,y)
  contract_multi(A,vs)
  contract_edge_k_1!(indices,val,x,res)
#------------------------------------------------------------------------------
						           Contraction
------------------------------------------------------------------------------=#

"""-----------------------------------------------------------------------------
    contract_edge(e,x,k)

This function takes in an edge of a super symmetric tensor and computes the
resulting edges which result from contracting the the edge along k modes with
the vector x.

Input:
------
* e -(Tuple(Array{Int,1},Float)):

    A list of indices paired with an edge value. Note that the list of indices
    corresponds to multiple sets of indices because we consider all.
    permutations.
* x -(Array{Float,1})

    The vector to contract with.
* k - (Int):

    A positive integer which corresponds to the number of modes to contract
    along, must be greater than 0, and less than or equal to the cardinality
    of the edge.

Output:
-------
* condensed_dict - (Dict{Array{Int,1},AbstractFloat}

    The hyper edges in the lower order tensor which are formed by contracting
    the vector along the hyperedge e.
-----------------------------------------------------------------------------"""
function contract_edge(e::Tuple{Array{Int,1},M},x::Array{N,1},k::Int) where {N <: AbstractFloat, M<:AbstractFloat}
    order = length(e)

    (indices,val) = e
    condensed_dict = Dict{Array{Int,1},N}()
    visited_sub_indices = Dict{Array{Int,1},Dict{Array{Int,1},N}}()

    for i in 1:length(indices)
        sub_edge = deleteat!(copy(indices),i)
        if !haskey(visited_sub_indices,sub_edge)
            if k == 1
                condensed_dict[sub_edge] = val*x[indices[i]]
            else
                visited_sub_indices[sub_edge] = contract_edge((sub_edge,val*x[indices[i]]),x,k-1)
            end
        end
    end

    if k != 1
        for (_,sub_dict) in visited_sub_indices
            reduce_dictionaries!(condensed_dict,sub_dict)
        end
    end

    return condensed_dict
end

"""-----------------------------------------------------------------------------
    contract_edge_k_1(e,x)

  This function takes in an edge of a super symmetric tensor and computes the
resulting edges which result from contracting the the edge along k-1 modes with
the vector x, where k is the order of the hyper edge.

Input:
------
* e -(Tuple(Array{Int,1},AbstractFloat)):

    a list of sorted indices paired with an edge value. Note that the list of
    indices corresponds to multiple sets of indices because we consider all
    permutations.
* x -(Array{AbstractFloat,1})

    The vector of the same dimenionality of the tensor, to contract with.

Output:
-------
* contraction_vals - (Array{Tuple{Array{Int,1},AbstractFloat}})

    The hyper edges in the lower order tensor which are formed by contracting
    the vector along the hyperedge e.
-----------------------------------------------------------------------------"""
function contract_edge_k_1(e::Tuple{Array{Int,1},N},x::Array{N,1}) where N <: AbstractFloat
    (indices,val) = e
    order = length(indices)

    visited_sub_indices = Set{Array{Int,1}}()
    contraction_vals = Array{Tuple{Array{Int,1},N}}(undef,1)

    for i in 1:order
        sub_edge = deleteat!(copy(indices),i)
        if !in(sub_edge,visited_sub_indices)#haskey(scaling_factors,sub_edge)
            scaling = multiplicity_factor(sub_edge)
            push!(visited_sub_indices,sub_edge)
            push!(contraction_vals,([indices[i]],scaling*val*prod(x[sub_edge])))
        end
    end
    return contraction_vals
end

"""-----------------------------------------------------------------------------
    contract(A,x,m)

  This function contracts the tensor along m modes. Note that when the tensor is
dense this function uses Base.Cartesian, and thus in order to generate the loops
with a variable used a trick which instantiates empty arrays of length 0, and
passes them to another function which can pull the orders out to generate the
loops.

Input:
------
* A -(SSSTensor or Array{AbstractFloat,k}):

    The tensor to contract.
* x - (Array{AbstractFloat,1}):

    A vector of numbers to contract with.
* m - (Int)

    The number of modes to contract A with x along.

Output:
-------
* y - (SSSTensor or CSC Matrix or Array{Float64,k-m}):

    The output vector of Ax^m. THe output will be sparse if the input tensor is
    sparse, and dense otherwise. When the output is second order, and A is
    sparse, then the output will be a sparse matrix.
-----------------------------------------------------------------------------"""
function contract(A::SSSTensor, x::Array{N,1},m::Int) where {N <: AbstractFloat}
    @assert length(x) == A.cubical_dimension
    k = order(A)
    @assert 0 < m <= k

    new_edges = Dict{Array{Int,1},N}()
    #compute contractions
    for edge in A.edges
        new_e = contract_edge(Tuple(edge),x,m)
        reduce_dictionaries!(new_edges,new_e)
    end

    if k == m
        for (_,v) in new_edges
            return v
        end
    elseif k - m == 1
        y = zeros(length(x))
        for (e,v) in new_edges
            y[e[1]] = v
        end
        return y
    elseif k - m == 2
	  index = 0
	  nnzs = length(new_edges)
	  I = zeros(Int64,2*nnzs)
	  J = zeros(Int64,2*nnzs)
	  V = zeros(N,2*nnzs)

	  for (e,val) in new_edges
	     i,j = e
		 if i == j
		   index += 1
		   I[index] = i
		   J[index] = j
		   V[index] = val
	     else
		   index += 2
 	       I[index-1] = i
		   J[index-1] = j
		   I[index] = j
		   J[index] = i
		   V[index-1] = val
		   V[index] = val
		 end
	  end
	  return sparse(I[1:index],J[1:index],V[1:index],A.cubical_dimension,A.cubical_dimension)
	else
        return SSSTensor(new_edges,A.cubical_dimension)
    end
end

#Dense Case
function contract(A::Array{N,k}, x::Array{M,1},m::Int64) where {M <: AbstractFloat,N <: AbstractFloat,k}

    return dense_contract(A,x,zeros(Int,repeat([0],m)...),
	                      zeros(Int,repeat([0],k-m)...))
end

@generated function dense_contract(A::Array{N,k}, x::Array{M,1},
                                   B::Array{Int,m}, C::Array{Int,p}) where
								   {M<:AbstractFloat,N<:AbstractFloat,k,m,p}
    quote
        n = size(A)[1]
        @assert n == length(x)
        @assert $k >= m

        y = zeros(N,repeat([n],$k - $m)...)

        @nloops $k i A begin
            xs = prod(x[collect(@ntuple $m j-> i_{j+$p})])
            (@nref $p y i) += xs*(@nref $k A i)
        end
		if $k == m
	  		for val in y
	    		return val
	  		end
		else
          return y
        end
      end
end

"""-----------------------------------------------------------------------------
    contract_k_1(A,x)

This function contracts the tensor along k-1 modes to produce a vector. This
will produce the same result as contract(A,x,k-1), but runs in a much faster
time.

Inputs
------
* A -(SSSTensor):

    The tensor to contract.
* x - (Array{AbstractFloat,1}):

    A vector of numbers to contract with.
Outputs
-------
* y - (Array{AbstractFloat,1}):

    The output vector of Ax^{k-1}.
-----------------------------------------------------------------------------"""
function contract_k_1(A::Ten, x::Array{N,1}) where {N <: AbstractFloat,Ten <: AbstractSSTen}
    @assert length(x) == A.cubical_dimension

    new_edges = Array{Tuple{Array{Int,1},N}}(undef,0)
    y = zeros(A.cubical_dimension)

    #compute contractions
    for edge in A.edges
        contracted_edges = contract_edge_k_1(Tuple(edge),x)
        push!(new_edges,contracted_edges...)
    end
    #reduce edges and copy into new vector
    edge_dict = reduce_edges(new_edges)

    for (i,v) in edge_dict
        y[i[1]] = v
    end
    return y
end

function contract_k_1!(A::Ten, x::Array{N,1},y::Array{N,1}) where {N <: AbstractFloat,Ten <: AbstractSSTen}
    @assert length(x) == A.cubical_dimension

    new_edges = Array{Tuple{Array{Int,1},N}}(undef,0)
	@inbounds for i in 1:A.cubical_dimension
		y[i] = zero(N)
	end

    #compute contractions
    for edge in A.edges
        contracted_edges = contract_edge_k_1(Tuple(edge),x)
        push!(new_edges,contracted_edges...)
    end
    #reduce edges and copy into new vector
    edge_dict = reduce_edges(new_edges)

    for (i,v) in edge_dict
        y[i[1]] = v
    end
    return y
end

"""-----------------------------------------------------------------------------
    contract_multi(A,vs)

  This function computes the result of contracting the tensor A by the columns
of the array Vs.

-----------------------------------------------------------------------------"""
function contract_multi(A::SSSTensor, Vs::Array{N,2}) where N <: AbstractFloat
  k = order(A)
  n,m = size(Vs)
  @assert m <= k
  @assert n == A.cubical_dimension

  i = 1
  while true
    if k - i >= 2
      global A_sub = contract(A,Vs[:,i],1)
    elseif k - i == 1
	  global A_sub = A_sub*Vs[:,i]
    elseif k - i == 0
	  global A_sub = dot(A_sub,Vs[:,i])
    end
	i += 1

	if i > m #no more vectors
	  return A_sub
    end
  end
end

#TODO: make this take in an arbitrary number of vectors.
function contract(A::SSSTensor,v::Array{N,1},u::Array{N,1}) where N <: AbstractFloat
  return contract_multi(A,hcat(v,u))
end

"""-----------------------------------------------------------------------------
    contract_edge_k_1!(indices,val,x,res)

  This function computes the value of contracting one hyper edge with an input
vector x to all but 1 mode, and saves the result in the appropriate locations
in res.

Inputs:
-------
* indices - (Array{Int,2}):

    2D array with all the indices of the hyper edges in the tensor. Note that
    the number of rows is not necesarily equal to the non-zeros in the tensors.
    The indices of each row is assumed to be sorted in increasing order.
* nnz_val -  (N <: AbstractFloat):

    The non-zero associated with the hyper edges.
* x - (Array{N <: AbstractFloat, 1}):

    The vector to contract the hyper edge with.
* res_val - (Array{N <:AbstractFloat, 1}):

    The location to update the solution to. res is assumed to be initialized and
    so the values are simply added to the entries of y, this is done because
    this function is considered a helper function to the contract function o
    verloaded for the dense array representation of symmetric tensors.
-----------------------------------------------------------------------------"""
@inline function contract_edge_k_1!(edge::Tuple{Array{Int,1},N},
                                    x::Array{N,1},res::Array{N,1},
									sub_edge::Array{Int,1},
									multiplicities::Dict{Int,Int},
									final_counts::Array{Int,1},
									ord::Int)where {M,T, N <: AbstractFloat}
	(indices, nnz_val) = edge
    #ord = length(indices)
	prev_index = -1
	#sub_edge = Array{Int,1}(undef,ord - 1)

	for j = 1:ord
		#only compute contraction once per index
		if prev_index != indices[j]

			#form sub_edge
			i = 1
			for k in Base.Iterators.flatten((1:j-1,j+1:ord))
				sub_edge[i] = indices[k]
				i += 1
			end

			val = nnz_val * multiplicity_factor(sub_edge,multiplicities,final_counts)
			for k in sub_edge
				val *= x[k]
			end
			res[indices[j]] += val
		end
		prev_index = indices[j]
	end

end

"""-----------------------------------------------------------------------------
    contract_k_1(A,x,k)

  Contracts the vector x with the tensor to all but one mode

-----------------------------------------------------------------------------"""
function contract_k_1(A::COOTen,x::Array{N,1}) where {N <: AbstractFloat}
	@assert A.cubical_dimension == length(x)

	y = zeros(A.cubical_dimension) #resulting vector

	if A.order == 3
		#need a guarantee of unique indices in the hyper edges.
		tri_contract!(A,x,y)
	else
		#preallocate helper functions
		sub_edge = Array{Int,1}(undef,A.order - 1)
		multiplicities = Dict{Int,Int}()
		final_counts = zeros(Int,A.order)

		for edge in A
			contract_edge_k_1!(edge,x,y,sub_edge,multiplicities,final_counts,A.order)
		end

	end
	return y
end

"""-----------------------------------------------------------------------------
    contract_k_1(A,x,k)

  Contracts the vector x with the tensor to all but one mode

-----------------------------------------------------------------------------"""
function contract_k_1!(A::COOTen,x::Array{N,1},y::Array{N,1}) where {N <: AbstractFloat}
	@assert A.cubical_dimension == length(x)

	for i in 1:A.cubical_dimension
		y[i] = zero(N)
	end

	if A.order == 3
		tri_contract!(A,x,y)
	else

		#preallocate helper functions
		sub_edge = Array{Int,1}(undef,A.order - 1)
		multiplicities = Dict{Int,Int}()
		final_counts = zeros(Int,A.order)

		for edge in A
			contract_edge_k_1!(edge,x,y,sub_edge,multiplicities,final_counts,A.order)
		end
	end

end

"""-----------------------------------------------------------------------------
    tri_contract!(A,x,y)

    Special case of contract_k_1!, when the input tensor is one of unique
  triangles then when we write out the contraction operation and loop over the
  rows of COOTen.indices.

  Inputs:
  -------
  A - (COOTen sparse tensor):
    The tensor to contract.

  x - (Array{AbstractFloat,1}):
    The vector to contract the tensor with

  y - (Array{AbstractFloat,1}):
    The place to store the output results, assumed to be zeroed.

-----------------------------------------------------------------------------"""
function tri_contract!(A::COOTen,x::Array{N,1},y::Array{N,1}) where {N <: AbstractFloat}

	@assert A.order == 3
	#y = zeros(Float64,A.cubical_dimension)

	for i in 1:A.unique_nnz
		ci = A.indices[i,1]
        cj = A.indices[i,2]
        ck = A.indices[i,3]
		y[ci] += 2*x[cj]*x[ck]*A.vals[i]
        y[cj] += 2*x[ci]*x[ck]*A.vals[i]
        y[ck] += 2*x[cj]*x[ci]*A.vals[i]
	end
end

"""-----------------------------------------------------------------------------
    inner_product(A::COOTen,B::COOTen)

    Computes the inner product between two COOTensors. currently assuming all
    indices in each row are unique

-----------------------------------------------------------------------------"""
function inner_product(A::COOTen,B::COOTen)

	@assert A.cubical_dimension == B.cubical_dimension
	@assert A.order == B.order

	multiplicity_factor = factorial(A.order )
	inner_product_val = 0.0

	A_head = 1
	B_head = 1
	while A_head <= A.unique_nnz && B_head <= B.unique_nnz
		if A.indices[A_head,:] == B.indices[B_head,:]
			println(A_head, A.vals[A_head])
			inner_product_val += A.vals[A_head]*B.vals[B_head]*multiplicity_factor
			A_head += 1
			B_head += 1
		elseif A.indices[A_head,:] < B.indices[B_head,:]
			A_head += 1
		else
			B_head += 1
		end
	end
	return inner_product_val
end


#Exponential Loss

using LaTeXStrings, LossFunctions, Measures, Latexify

Plots.plot(ExpLoss(), c=:olive, leg=:topright, linewidth=2, annotation=(1, 1.5, L"L\left( a \right) = e^{-a}", 18))


struct LinkedList
    next :: Vector{Int32}
end

function run!(list :: LinkedList, currentcup :: Int, n :: Int)
    wrap(n) = mod1(n, length(list.next))
    for _ in 1:n
        pickedcup1 = list.next[currentcup]
        pickedcup2 = list.next[pickedcup1]
        pickedcup3 = list.next[pickedcup2]
        afterpickedup = list.next[pickedcup3]
        list.next[currentcup] = afterpickedup

        targetcup = wrap(currentcup - 1)
        while targetcup in (pickedcup1, pickedcup2, pickedcup3)
            targetcup = wrap(targetcup - 1)
        end
        aftertargetcup = list.next[targetcup]

        list.next[targetcup] = pickedcup1
        list.next[pickedcup3] = aftertargetcup
        currentcup = afterpickedup
    end
end

function cuporder(start, list, size)
    n = list.next[start]
    s = ""
    for _ in 2:size
        s = join([s, repr(n)])
        n = list.next[n]
    end
    return parse(Int64, s)
end

function cycle(numbers, cycles; part2=false, N=10^6)
    list = LinkedList(Int32[])
    if part2
        sizehint!(list.next, N)
    end
    indexof(n) = findfirst(i -> i == mod1(n, length(numbers)), numbers)
    for n in 1:length(numbers)
        push!(list.next, numbers[mod1(indexof(n) + 1, length(numbers))])
    end
    if part2
        for n in (length(numbers)+1):N
            push!(list.next, n+1)
        end
        list.next[N] = first(numbers)
        list.next[last(numbers)] = length(numbers)+1
    end
    run!(list, first(numbers), cycles)
    if part2
        return Int64(list.next[1]) * list.next[list.next[1]]
    else
        return cuporder(1, list, length(numbers))
    end
end

# test = [3,8,9,1,2,5,4,6,7] # 10:92658374 100:67384529; part2:149245887792
input = [5,3,8,9,1,4,7,6,2]

println("P1: ", cycle(input, 100))
println("P2: ", cycle(input, 10^7; part2 = true))


using PumasTutorials, NCA, NCAUtilities, CSV, DataFrames, StatsPlots, Statistics, Unitful

data = DataFrame(CSV.File(joinpath(dirname(pathof(PumasTutorials)), "..", "tutorials", "nca", "NCA_tutorial.csv")))
first(data, 6)

single_dose_data  = filter(x -> x.TIME <= 24 , data)

timeu = u"hr"
concu = u"mg/L"
amtu  = u"mg";

pop        = read_nca(  single_dose_data,
                        id    = :ID,
                        time  = :TIME,
                        observations  = :CONC,
                        amt   = :AMT,
                        route = :ROUTE,
                        llq   = 0.001concu,
                        timeu = timeu,
                        concu = concu,
                        amtu  = amtu
                      )

NCAUtilities.observations_vs_time(pop[1])

report0     = run_nca(pop, sigdigits=3)

vz        = NCA.vz(pop, sigdigits=3)  # Volume of Distribution/F, in this case since the drug is given orally
cl        = NCA.cl(pop, sigdigits=3)  # Clearance/F, in this case since the drug is given orally
lambdaz   = NCA.lambdaz(pop, threshold=3, sigdigits=3)  # Terminal Elimination Rate Constant, threshold=3 specifies the max no. of time point used for calculation
lambdaz_1 = NCA.lambdaz(pop, slopetimes=[8,12,16].*timeu, sigdigits=3) # slopetimes in this case specifies the exact time point you want for the calculation
thalf     = NCA.thalf(pop[4], sigdigits=3) # Half-life calculation for 4th individual
cmax_d    = NCA.cmax(pop, normalize=true, sigdigits=3) # Dose Normalized Cmax
mrt       = NCA.mrt(pop, sigdigits=3) # Mean residence time
aumc      = NCA.aumc(pop, method=:linlog, sigdigits=3) # AUMC calculation, using :linlog method
rename!(lambdaz_1, Dict(:lambdaz => "lambdaz_specf")) #since we have two lambdaz calculation rename one column to merge in a dataframe
df_1      = innerjoin(vz,cl,lambdaz, lambdaz_1,cmax_d,mrt,aumc, on=[:id], makeunique=true)

auc0_12   = NCA.auc(pop, interval=(0,12).*timeu, method=:linuplogdown, sigdigits=3) #various other methods are :linear, :linlog
auc12_24  = NCA.auc(pop, interval=(12,24).*timeu, method=:linuplogdown, sigdigits=3)
final     = innerjoin(report0.reportdf, auc0_12, auc12_24, on = [:id], makeunique=true)

nca_output_single = select(final, :doseamt, :kel, :half_life, :cmax, :tmax, :auclast, :aucinf_obs, :vz_f_obs, :cl_f_obs, :aumcinf_obs, :auc0_12, :auc12_24)

Stat_report = describe(nca_output_single)

multiple_dose_data           = filter(x -> x.TIME > 24 , data)

pop1       = read_nca( multiple_dose_data,
                       id    = :ID,
                       time  = :TIME,
                       observations  = :CONC,
                       amt   = :AMT,
                       route = :ROUTE,
                       ii    = :II,        # please specify II for Multiple-dose NCA
                       llq   = 0.001concu,
                       timeu = timeu,
                       concu = concu,
                       amtu  = amtu)
report1    = run_nca(pop1, sigdigits=3)

Stat_report1        = summarize(report1.reportdf, parameters = [:doseamt, :kel, :half_life, :tmax, :cmax, :auclast, :auc_tau_obs, :vz_f_obs, :cl_f_obs, :aumcinf_obs, :tau, :cavgss])

CSV.write("NCA_single_dose.csv", final)      # Single-NCA final report
CSV.write("NCA_multiple_dose.csv", report1.reportdf)  # Multiple-NCA final report


@testset "CIR approx smoothing tests" begin

    Random.seed!(1)

    δ = 3.
    γ = 2.5
    σ = 4.
    Nobs = 2
    dt = 0.011
    Nsteps = 10
    λ = 1.

    α = δ/2
    β = γ/σ^2

    function rec_rcCIR(Dts, x, δ, γ, σ)
        x_new = DualOptimalFiltering.rCIR(1, Dts[1], x[end], δ, γ, σ)
        if length(Dts) == 1
            return Float64[x; x_new]
        else
            return Float64[x; rec_rcCIR(Dts[2:end], x_new, δ, γ, σ)]
        end
    end

    function generate_CIR_trajectory2(times, x0, δ, γ, σ)
        θ1 = δ*σ^2
        θ2 = 2*γ
        θ3 = 2*σ
        Dts = diff(times)
        return rec_rcCIR(Dts, [x0], δ, γ, σ)
    end

    time_grid = [k*dt for k in 0:(Nsteps-1)]
    X = generate_CIR_trajectory2(time_grid, 3, δ*1.2, γ/1.2, σ*0.7)
    Y = map(λ -> rand(Poisson(λ), Nobs), X);
    data = zip(time_grid, Y) |> Dict;

    @test_nowarn DualOptimalFiltering.smooth_CIR_keep_fixed_number(δ, γ, σ, λ, data, 10; silence = false)

    # @test_nowarn DualOptimalFiltering.smooth_CIR_keep_above_threshold(δ, γ, σ, λ, data, 0.01, 10^(-20); silence = false)

    @test_nowarn DualOptimalFiltering.smooth_CIR_keep_fixed_fraction(δ, γ, σ, λ, data, 0.9; silence = false)

end


"""
    rm2lspm(NUM, DEN; contr = false, obs = false, atol = 0, rtol) -> (A, B, C, D, blkdims)

Construct a representation of the rational matrix `R(λ) := NUM(λ)./DEN(λ)` as the sum of its strictly proper part,
realized as a structured linearization

              | A-λI | B |
              |------|---|
              |  C   | 0 |

and its polynomial part `D(λ)`. The resulting representation satisfies

     R(λ) = C*inv(λI-A)*B + D(λ).

The linearization `(A-λI,B,C,0)` of the strictly proper part is in general non-minimal.
A controllable realization is constructed if `contr = true`, while an observable realization results if `obs = true`.
The matrix `A` results block diagonal and the vector `blkdims` contains the sizes of the diagonal blocks of `A`,
such that the size of the `i`-th diagonal block is provided in `blkdims[i]`. See [1] for details.

`NUM(λ)` is a polynomial matrix of the form `NUM(λ) = 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 `NUM`,
where `NUM[:,:,i]` contains the `i`-th coefficient matrix `N_i` (multiplying `λ**(i-1)`).

`DEN(λ)` is a polynomial matrix of the form `DEN(λ) = 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 `DEN`,
where `DEN[:,:,i]` contain the `i`-th coefficient matrix `D_i` (multiplying `λ**(i-1)`).

Alternatively, `NUM(λ)` and `DEN(λ)` 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 polynomial part `D(λ)` is contained in `D`, which is a 2-dimensional array if `D(λ)` has degree 0
or a 3-dimensional array if `D(λ)` has degree  greater than 0. In the latter case `D[:,:,i]` contains the `i`-th
coefficient matrix multiplying `λ**(i-1)`.

The keyword arguments `atol` and `rtol`, specify the absolute and relative tolerances for the
nonzero coefficients of `DEN(λ)`, respectively.

[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).
"""
function rm2lspm(NUM::AbstractArray{T1,3},DEN::AbstractArray{T2,3}; contr::Bool = false, obs::Bool = false,
                 atol::Real = 0, rtol::Real = Base.rtoldefault(float(real(T1)))) where {T1,T2}
    p, m, knum = size(NUM)
    p1, m1, kden = size(DEN)
    (p,m) == (p1, m1) || error("Numerator and denominator polynomial matrices must have the same size")

    degQ1 = 0
    degD1 = 0
    n = 0
    for j = 1:m
        for i = 1:p
            n1 = poldeg1(NUM[i,j,1:knum])
            d1 = poldeg1(DEN[i,j,1:kden])
            d1 == 0 && error("DivideError: zero denominator polynomial")
            n1 < d1 || (degQ1 = max(degQ1,n1-d1+1))
            degD1 = max(degD1,d1)
            n += d1-1
        end
    end
    T = eltype(one(T1)/one(T2))
    degQ1 ≤ 1 ? D = zeros(T,p,m) : D = zeros(T,p,m,max(degQ1,1))
    A = zeros(T,n,n)
    B = zeros(T,n,m)
    C = zeros(T,p,n)
    ONE = one(T)
    contr && obs && (m >= p ? obs = false : contr = false)
    if contr
        k = 1
        nb = 0
        blkdims = zeros(Int,m)
        for j = 1:m
            d1 = poldeg1(DEN[1,j,1:kden])
            d1 == 1 ? mc = [one(T)] : (mc = DEN[1,j,1:d1])
            for i = 2:p
                d1 = poldeg1(DEN[i,j,1:kden])
                d1 > 1 && ( mc = pollcm(mc, DEN[i,j,1:d1], atol = atol, rtol = rtol) )
            end
            d1 = poldeg1(mc)
            n1 = d1-1
            i2 = k+n1-1
            if n1 > 0
                nb += 1
                blkdims[nb] = n1
                mc = mc/mc[d1]
                A[k, k:i2] = -mc[n1:-1:1]
                for ii = k:i2-1
                    A[ii+1, ii] = ONE
                end
                B[k,j] = ONE
                for i = 1:p
                    q, r = poldivrem(NUM[i,j,1:knum],DEN[i,j,1:kden])
                    nq1 = poldeg1(q)
                    nr1 = poldeg1(r)
                    degQ1 ≤ 1 ? D[i,j] = q[1] : D[i,j,1:nq1] = copy_oftype(q[1:nq1],T)
                    if nr1 > 0
                        pc = poldiv(conv(mc,r),DEN[i,j,1:kden])
                        npc = length(pc)
                        C[i,i2-npc+1:i2] = pc[npc:-1:1]
                    end
                end
                k += n1
            else
                for i = 1:p
                    nq1 = poldeg1(NUM[i,j,1:knum])
                    if nq1 > 0
                       q = NUM[i,j,1:nq1]/DEN[i,j,1]
                       degQ1 ≤ 1 ? D[i,j] = q[1] : D[i,j,1:nq1] = copy_oftype(q[1:nq1],T)
                    end
                end
            end
        end
        n = k-1
        return A[1:n,1:n], B[1:n,:], C[:,1:n], D, blkdims[1:nb]
    elseif obs
        k = 1
        nb = 0
        blkdims = zeros(Int,p)
        for i = 1:p
            d1 = poldeg1(DEN[i,1,1:kden])
            d1 == 1 ? mc = [one(T)] : (mc = DEN[i,1,1:d1])
            for j = 2:m
                d1 = poldeg1(DEN[i,j,1:kden])
                d1 > 1 && ( mc = pollcm(mc, DEN[i,j,1:d1], atol = atol, rtol = rtol) )
            end
            d1 = poldeg1(mc)
            n1 = d1-1
            i2 = k+n1-1
            if n1 > 0
                nb += 1
                blkdims[nb] = n1
                mc = mc/mc[d1]
                A[k:i2,i2] = -mc[1:n1]
                for ii = k:i2-1
                    A[ii+1,ii] = ONE
                end
                C[i,i2] = ONE
                for j = 1:m
                    q, r = poldivrem(NUM[i,j,1:knum],DEN[i,j,1:kden])
                    nq1 = poldeg1(q)
                    nr1 = poldeg1(r)
                    degQ1 ≤ 1 ? D[i,j] = q[1] : D[i,j,1:nq1] = copy_oftype(q[1:nq1],T)
                    if nr1 > 0
                        pc = poldiv(conv(mc,r),DEN[i,j,1:kden])
                        npc = length(pc)
                        B[k:k+npc-1,j] = pc[1:npc]
                    end
                end
                k += n1
            else
                for j = 1:m
                    nq1 = poldeg1(NUM[i,j,1:knum])
                    if nq1 > 0
                       q = NUM[i,j,1:nq1]/DEN[i,j,1]
                       degQ1 ≤ 1 ? D[i,j] = q[1] : D[i,j,1:nq1] = copy_oftype(q[1:nq1],T)
                    end
                end
            end
        end
        n = k-1
        return A[1:n,1:n], B[1:n,:], C[:,1:n], D, blkdims[1:nb]
    else
        k = 1
        blkdims = zeros(Int,m*p)
        nb = 0
        for j = 1:m
            for i = 1:p
                q, r = poldivrem(NUM[i,j,1:knum],DEN[i,j,1:kden])
                nq1 = poldeg1(q)
                nr1 = poldeg1(r)
                degQ1 ≤ 1 ? D[i,j] = q[1] : D[i,j,1:nq1] = copy_oftype(q[1:nq1],T)
                d1 = poldeg1(DEN[i,j,1:kden])
                n1 = d1-1
                i2 = k+n1-1
                if n1 > 0
                    nb += 1
                    blkdims[nb] = n1
                    s = -DEN[i,j,d1]
                    A[k, k:i2] = DEN[i,j,n1:-1:1]/s
                    for ii = k:i2-1
                        A[ii+1, ii] = ONE
                    end
                    B[k,j] = ONE
                    C[i,i2-nr1+1:i2] = r[nr1:-1:1]/(-s)
                    k += n1
                end
            end
        end
        return A, B, C, D, blkdims[1:nb]
    end
end
function rm2lspm(NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                 DEN::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}};kwargs...)
    return rm2lspm(poly2pm(NUM),poly2pm(DEN);kwargs...)
end
function rm2lspm(NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}};kwargs...)
   return rm2lspm(poly2pm(NUM);kwargs...)
end
function rm2lspm(NUM::AbstractArray{T,3}; kwargs...) where T
    sys = rm2lspm(NUM,ones(T,size(NUM)[1:2]...,1); kwargs...)
    return sys[1:4]..., [0]
end

"""
    rmeval(NUM,DEN,val) -> Rval

Evaluate the rational matrix `R(λ) := NUM(λ)./DEN(λ)` for `λ = val`.

`NUM(λ)` is a polynomial matrix of the form `NUM(λ) = 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)`).

`DEN(λ)` is a polynomial matrix of the form `DEN(λ) = 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, `NUM(λ)` and `DEN(λ)` 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.
"""
function rmeval(NUM::AbstractArray{T1,3},DEN::AbstractArray{T2,3},val::Number) where {T1,T2}
    return pmeval(NUM,val) ./ pmeval(DEN,val)
end
function rmeval(NUM::AbstractArray{T,3},val::Number) where {T}
    return pmeval(NUM,val)
end
rmeval(NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
       DEN::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},val::Number) =
       rmeval(poly2pm(NUM),poly2pm(DEN),val::Number)
rmeval(NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},val::Number) =
       pmeval(poly2pm(NUM),val::Number)
"""
      rm2ls(NUM, DEN; contr = false, obs = false, noseig = false, minimal = false,
            fast = true, atol = 0, rtol) -> (A, E, B, C, D, blkdims)

Build a structured linearization as a system matrix `S(λ)` of the form

               | A-λE | B |
        S(λ) = |------|---|
               |  C   | D |

for the rational matrix `R(λ) = NUM(λ) ./ DEN(λ)`, such that `S(λ)` preserves a part of the Kronecker structure of `R(λ)`.
The regular pencil `A-λE` has the block diagonal form

             | Af-λI |   0   |
      A-λE = |-------|-------|
             |  0    | I-λEi |

and the dimensions of the diagonal blocks `Af` and `Ei` are provided in `blkdims[1]` and `blkdims[2]`, respectively.

`NUM(λ)` is a polynomial matrix of the form `NUM(λ) = 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 `NUM`,
where `NUM[:,:,i]` contains the `i`-th coefficient matrix `N_i` (multiplying `λ**(i-1)`).

`DEN(λ)` is a polynomial matrix of the form `DEN(λ) = 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 `DEN`,
where `DEN[:,:,i]` contain the `i`-th coefficient matrix `D_i` (multiplying `λ**(i-1)`).

Alternatively, `NUM(λ)` and `DEN(λ)` 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.

If `n` is the order of `A-λE`, then the computed linearization satisfies:

(1) `A-λE` is regular and `R(λ) = C*inv(λE-A)*B+D`;

(2) `rank[B A-λE] = n` (controllability) if `minimal = true` or `contr = true`, in which case
the right Kronecker structure is preserved;

(3) `rank[A-λE; C] = n` (observability)  if `minimal = true` or `obs = true`, in which case
the left Kronecker structure is preserved;

(4) `A-λE` has no non-dynamic modes if `minimal = true` or `noseig = true`.

If conditions (1)-(4) are satisfied, the linearization is called `minimal` and the resulting order `n`
is the least achievable order. If conditions (1)-(3) are satisfied, the linearization is called `irreducible`
and the resulting order `n` is the least achievable order using orthogonal similarity transformations.
For an irreducible linearization `S(λ)` preserves the pole-zero structure (finite and infinite) and the
left and right Kronecker structures of `R(λ)`.

The descriptor system based linearization is built using the methods described in [1] in conjunction with
pencil manipulation algorithms [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 keyword arguments `atol` and `rtol`, specify the absolute and relative tolerances, respectively, for the
nonzero coefficients of `NUM(λ)` and `DEN(λ)`.

[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 rm2ls(NUM::AbstractArray{T1,3},DEN::AbstractArray{T2,3}; minimal::Bool = false, contr::Bool = false, obs::Bool = false, noseig::Bool = false,
                fast::Bool = true, atol::Real = zero(real(T1)),
                rtol::Real = (min(size(NUM)...)*eps(real(float(one(T1)))))*iszero(atol)) where {T1,T2}
    Ap, Bp, Cp, D = rm2lspm(NUM, DEN, contr = contr, obs = obs, atol = atol, rtol = rtol)
    minimal && (contr = true; obs = true; noseig = true;)
    if ndims(D) == 2
        A, B, C  = lsminreal(Ap, Bp, Cp, contr = contr, obs = obs, fast = fast, atol = atol, rtol = rtol)
        E = I
        nf = size(A,1)
        ni = 0
    else
        T = typeof(one(T1)/one(T2))
        Ai, Ei, Bi, Ci, Di  = pm2ls(D, minimal = minimal, contr = contr, obs = obs, fast = fast, atol = atol, rtol = rtol)
        Af, Ef, Bf, Cf, D  = lsminreal2(Ap,I,Bp,Cp,Di,contr = contr, obs = obs, fast = fast, atol1 = atol, atol2 = atol, rtol = rtol)
        nf = size(Af,1)
        ni = size(Ai,1)
        A = [Af zeros(T,nf,ni); zeros(T,ni,nf) Ai]
        E = [Ef zeros(T,nf,ni); zeros(T,ni,nf) Ei]
        B = [Bf; Bi]
        C = [Cf Ci]
    end
    return A, E, B, C, D, [nf,ni]
end
rm2ls(NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
      DEN::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...) =
       rm2ls(poly2pm(NUM),poly2pm(DEN); kwargs...)
function rm2ls(NUM::AbstractArray{T,3}; kwargs...) where T
    sys = pm2ls(NUM; kwargs...)
    return sys..., [0;size(sys[1],1)]
end
function rm2ls(NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
    sys = pm2ls(poly2pm(NUM); kwargs...)
    return sys..., [0;size(sys[1],1)]
end
"""
    ls2rm(A, E, B, C, D; fast = true, atol1 = 0, atol2 = 0, gaintol = 0, rtol = min(atol1,atol2) > 0 ? 0 : n*ϵ, val) -> (NUM, DEN)

Build the rational matrix `R(λ) = C*inv(λE-A)*B+D := NUM(λ) ./ DEN(λ)` corresponding to its structured linearization

            | A-λE | B |
     S(λ) = |------|---|
            |  C   | D |

by explicitly determining for each rational entry of `R(λ)`, its numerator coefficients from its finite zeros and gain, and
its denominator coefficients from its finite poles (see [1]).

The keyword arguments `atol1` and `atol2` specify the absolute tolerances for the elements of `A`, `B`, `C`, `D`, and,
respectively, of `E`, and `rtol` specifies the relative tolerances for the nonzero elements of `A`, `B`, `C`, `D` and `E`.
The default relative tolerance is `(n+1)*ϵ`, where `n` is the dimension of `A`, and `ϵ` is the
machine epsilon of the element type of coefficients of `A`.

The keyword argument `gaintol` specifies the threshold for the magnitude of the nonzero elements of the gain matrix
`C*inv(γE-A)*B+D`, where `γ = val` if `val` is a number or `γ` is a randomly chosen complex value of unit magnitude,
if `val = missing`. Generally, `val` should not be a zero of any of entries of `R`.

`NUM(λ)` is a polynomial matrix of the form `NUM(λ) = 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 `NUM`,
where `NUM[:,:,i]` contains the `i`-th coefficient matrix `N_i` (multiplying `λ**(i-1)`).

`DEN(λ)` is a polynomial matrix of the form `DEN(λ) = 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 `DEN`,
where `DEN[:,:,i]` contain the `i`-th coefficient matrix `D_i` (multiplying `λ**(i-1)`).

[1] A. Varga Computation of transfer function matrices of generalized state-space models.
    Int. J. Control, 50:2543–2561, 1989.
"""
function ls2rm(A::AbstractMatrix, E::Union{AbstractMatrix,UniformScaling{Bool}}, B::AbstractMatrix, C::AbstractMatrix, D::AbstractMatrix;
                fast::Bool = true, atol1::Real = zero(real(eltype(A))), atol2::Real = zero(real(eltype(E))),
                gaintol::Real = zero(real(eltype(A))), val::Union{Number,Missing} = missing,
                rtol::Real = ((size(A,1)+1)*eps(real(float(one(eltype(A))))))*iszero(min(atol1,atol2)))
   emat = (typeof(E) <: AbstractMatrix)
   eident = !emat || isequal(E,I)
   n = LinearAlgebra.checksquare(A)
   emat && (n,n) != size(E) && throw(DimensionMismatch("A and E must have the same dimensions"))
   p, m = size(D)
   (n,m) == size(B) || throw(DimensionMismatch("A, B and D must have compatible dimensions"))
   (p,n) == size(C) || throw(DimensionMismatch("A, C and D must have compatible dimensions"))

   T = promote_type(eltype(A),eltype(E), eltype(B),eltype(C),eltype(D))
   T <: BlasFloat || (T = promote_type(Float64,T))
   compl = T <: Complex

   ismissing(val) && (val = exp(rand()*im))
   Rval = lseval(A, E, B, C, D, val)
   unimodular = emat && isunimodular(A, E, atol1 = atol1, atol2 = atol2, rtol = rtol)
   NUM = zeros(T,p,m,n+1)
   if unimodular
      DEN = ones(T,p,m,1)
   else
      DEN = zeros(T,p,m,n+1)
      DEN[:,:,1] = ones(T,p,m)
   end
   for i = 1:p
       Ao, Eo, Bo, Co, Do = lsminreal2(A, E, B, C[i:i,:], D[i:i,:], contr = false, infinite = false, noseig = false,
                                       atol1 = atol1, atol2 = atol2, rtol = rtol)
       for j = 1:m
           if abs(Rval[i,j]) > gaintol
              A1, E1, B1, C1, D1 = lsminreal2(Ao, Eo, Bo[:,j:j], Co, Do[:,j:j], obs = false, infinite = false, noseig = false,
                                              atol1 = atol1, atol2 = atol2, rtol = rtol)
              zer, iz, _ = spzeros(A1, E1, B1, C1, D1; fast = fast, atol1 = atol1, atol2 = atol2, rtol = rtol)
              cz, zval = polcoeffval(zer[1:(length(zer)-sum(iz))],val)
              if unimodular
                 NUM[i,j,1:length(cz)] = compl ? cz*(Rval[i,j]/zval) : real(cz*(Rval[i,j]/zval))
              else
                 pol, ip, _ = pzeros(A1, E1; fast = fast, atol1 = atol1, atol2 = atol2, rtol = rtol)
                 cp, pval = polcoeffval(pol[1:(length(pol)-sum(ip))],val)
                 NUM[i,j,1:length(cz)] = compl ? cz*(Rval[i,j]*pval/zval) : real(cz*(Rval[i,j]*pval/zval))
                 DEN[i,j,1:length(cp)] = compl ? cp : real(cp)
              end
            end
       end
   end
   return NUM[:,:,1:pmdeg(NUM)+1], DEN[:,:,1:pmdeg(DEN)+1]
end
ls2rm(A::AbstractMatrix, B::AbstractMatrix, C::AbstractMatrix, D::AbstractMatrix; kwargs...) =
      ls2rm(A::AbstractMatrix, I, B::AbstractMatrix, C::AbstractMatrix, D::AbstractMatrix; kwargs...)
"""
     rm2lps(NUM, DEN; contr = false, obs = false) -> (A, E, B, F, C, G, D, H, blkdims)

Build a structured pencil based linearization as a system matrix `S(λ)` of the form

            | A-λE | B-λF |
     S(λ) = |------|------|
            | C-λG | D-λH |

for the rational matrix `R(λ) = NUM(λ) ./ DEN(λ)` such that `S(λ)` preserves a part of the Kronecker structure of `R(λ)`.
The regular pencil `A-λE` has the block diagonal form

             | Af-λI |   0    |
      A-λE = |-------|--------|
             |  0    | Ai-λEi |

and the dimensions of the diagonal blocks `Af` and `Ai` are provided in `blkdims[1]` and `blkdims[2]`, respectively.

`NUM(λ)` is a polynomial matrix of the form `NUM(λ) = 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 `NUM`,
where `NUM[:,:,i]` contains the `i`-th coefficient matrix `N_i` (multiplying `λ**(i-1)`).

`DEN(λ)` is a polynomial matrix of the form `DEN(λ) = 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 `DEN`,
where `DEN[:,:,i]` contain the `i`-th coefficient matrix `D_i` (multiplying `λ**(i-1)`).

Alternatively, `NUM(λ)` and `DEN(λ)` 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.

If `n` is the order of the matrix `A`, then the computed linearization satisfies:

(1) `A-λE` is a `n x n` regular pencil;

(2) `R(λ) = (C-λG)*inv(λE-A)*(B-λF)+D-λH`;

(3) `rank[B-λF A-λE] = n` for any finite and infinite `λ` (strong controllability) if `contr = true`, in which case
the right Kronecker structure is preserved;

(4) `rank[A-λE; C-λG] = n` for any finite and infinite `λ` (strong observability)  if `obs = true`, in which case
the left Kronecker structure is preserved.

If conditions (1)-(4) are satisfied, the linearization is called `strongly minimal`, the resulting order `n`
is the least achievable order and `S(λ)` preserves the pole-zero structure (finite and infinite) and the
left and right Kronecker structures of `R(λ)`.

The pencil based linearization is built using the methods described in [1] in conjunction with
pencil manipulation algorithms [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 keyword arguments `atol` and `rtol`, specify the absolute and relative tolerances for the
nonzero coefficients of `NUM(λ)` and `DEN(λ)`, respectively.

[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] F.M. Dopico, M.C. Quintana and P. Van Dooren, Linear system matrices of rational transfer functions,
in "Realization and Model Reduction of Dynamical Systems, A Festschrift to honor the 70th birthday of Thanos Antoulas",
Eds. C. Beattie, P. Benner, M. Embree, S. Gugercin and S. Lefteriu, Springer-Verlag, 2020.
[arXiv:1903.05016](https://arxiv.org/pdf/1903.05016.pdf)
"""
function rm2lps(NUM::AbstractArray{T1,3},DEN::AbstractArray{T2,3}; minimal::Bool = false, contr::Bool = false, obs::Bool = false, noseig::Bool = false,
                fast::Bool = true, atol::Real = zero(real(T1)),
                rtol::Real = (min(size(NUM)...)*eps(real(float(one(T1)))))*iszero(atol)) where {T1,T2}
    T = typeof(one(T1)/one(T2))
    Ap, Bp, Cp, D = rm2lspm(NUM,DEN)
    minimal && (contr = true; obs = true; noseig = true;)
    if ndims(D) == 2
        A, B, C, _  = lsminreal(Ap, Bp, Cp, contr = contr, obs = obs, fast = fast, atol = atol, rtol = rtol)
        F = zeros(T,size(B)...)
        G = zeros(T,size(C)...)
        H = zeros(T,size(D)...)
        E = Matrix{T}(I,size(A)...)
        nf = size(A,1)
        ni = 0
    else
        Ai, Ei, Bi, Fi, Ci, Gi, D, H  = lpsminreal(pm2lps(D)...; contr = contr, obs = obs,
                                                    fast = fast, atol1 = atol, atol2 = atol, rtol = rtol)
        Ap, Bp, Cp, _  = lsminreal(Ap, Bp, Cp, contr = contr, obs = obs, fast = fast, atol = atol,  rtol = rtol)
        nf = size(Ap,1)
        ni = size(Ai,1)
        A = [Ap zeros(T,nf,ni); zeros(T,ni,nf) Ai]
        E = [I zeros(T,nf,ni); zeros(T,ni,nf) Ei]
        B = [Bp; Bi]
        F = [zeros(T,size(Bp)...); Fi]
        C = [Cp Ci]
        G = [zeros(T,size(Cp)...) Gi]
    end
    return A, E, B, F, C, G, D, H, [nf, ni]
end
rm2lps(NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
       DEN::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...) =
       rm2lps(poly2pm(NUM),poly2pm(DEN); kwargs...)
function rm2lps(NUM::AbstractArray{T,3}; kwargs...) where T
    sys = pm2lps(NUM; kwargs...)
    return sys..., [0;size(sys[1],1)]
end
function rm2lps(NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
    sys = pm2lps(poly2pm(NUM); kwargs...)
    return sys..., [0;size(sys[1],1)]
end
"""
    lps2rm(A, E, B, F, C, G, D, H; fast = true, atol1 = 0, atol2 = 0, gaintol = 0, rtol = min(atol1,atol2) > 0 ? 0 : n*ϵ, val) -> (NUM, DEN)

Build the rational matrix `R(λ) = (C-λG)*inv(λE-A)*(B-λF)+D-λH := NUM(λ) ./ DEN(λ)` corresponding to its structured linearization

            | A-λE | B-λF |
     S(λ) = |------|------|
            | C-λG | D-λH |

by explicitly determining for each rational entry of `R(λ)`, its numerator coefficients from its finite zeros and gain, and
its denominator coefficients from its finite poles. An extension of the approach of [1] is employed, relying on the procedures
of [2] to compute strongly irreducible linearizations.

The keyword arguments `atol1` and `atol2` specify the absolute tolerances for the elements of `A`, `B`, `C`, `D`, and of
`E`, `F`, `G`, `H`, respectively,  and `rtol` specifies the relative tolerances for the nonzero elements of
`A`, `B`, `C`, `D`, `E`, F`, `G`, `H`.
The default relative tolerance is `(n+2)*ϵ`, where `n` is the size of the size dimension of `A`, and `ϵ` is the
machine epsilon of the element type of coefficients of `A`.

The keyword argument `gaintol` specifies the threshold for the magnitude of the nonzero elements of the gain matrix
`C*inv(γE-A)*B+D`, where `γ = val` if `val` is a number or `γ` is a randomly chosen complex value of unit magnitude,
if `val = missing`. Generally, `val` should not be a root of any of entries of `P`.

[1] A. Varga Computation of transfer function matrices of generalized state-space models.
    Int. J. Control, 50:2543–2561, 1989.

[2] F.M. Dopico, M.C. Quintana and P. Van Dooren, Linear system matrices of rational transfer functions,
in "Realization and Model Reduction of Dynamical Systems, A Festschrift to honor the 70th birthday of Thanos Antoulas",
Eds. C. Beattie, P. Benner, M. Embree, S. Gugercin and S. Lefteriu, Springer-Verlag, 2020.
[arXiv:1903.05016](https://arxiv.org/pdf/1903.05016.pdf)
"""
function lps2rm(A::AbstractMatrix, E::AbstractMatrix, B::AbstractMatrix, F::AbstractMatrix,
                C::AbstractMatrix, G::AbstractMatrix, D::AbstractMatrix, H::AbstractMatrix;
                fast::Bool = true, atol1::Real = zero(real(eltype(A))), atol2::Real = zero(real(eltype(E))),
                gaintol::Real = zero(real(eltype(A))), val::Union{Number,Missing} = missing,
                rtol::Real = ((size(A,1)+2)*eps(real(float(one(eltype(A))))))*iszero(min(atol1,atol2)))
   n = LinearAlgebra.checksquare(A)
   (n,n) == size(E) || throw(DimensionMismatch("A and E must have the same dimensions"))
   p, m = size(D)
   (n,m) == size(B) || throw(DimensionMismatch("A, B and D must have compatible dimensions"))
   (p,n) == size(C) || throw(DimensionMismatch("A, C and D must have compatible dimensions"))
   (n,m) == size(F) || throw(DimensionMismatch("B and F must have the same dimensions"))
   (p,n) == size(G) || throw(DimensionMismatch("C and G must have the same dimensions"))
   (p,m) == size(H) || throw(DimensionMismatch("D and H must have the same dimensions"))
   T = promote_type(eltype(A), eltype(B), eltype(C), eltype(D), eltype(E), eltype(F), eltype(G), eltype(H))
   T <: BlasFloat || (T = promote_type(Float64,T))

   compl = T <: Complex
   ismissing(val) && (val = exp(rand()*im))
   Rval = lpseval(A, E, B, F, C, G, D, H, val)
   unimodular = isunimodular(A, E, atol1 = atol1, atol2 = atol2, rtol = rtol)
   NUM = zeros(T,p,m,n+2)
   if unimodular
      DEN = ones(T,p,m,1)
   else
      DEN = zeros(T,p,m,n+2)
      DEN[:,:,1] = ones(T,p,m)
   end
   for i = 1:p
       Ao, Eo, Bo, Fo, Co, Go, Do, Ho, Vo, Wo = lpsminreal(A, E, B, F, C[i:i,:], G[i:i,:], D[i:i,:], H[i:i,:],
                                                           contr = false, atol1 = atol1, atol2 = atol2, rtol = rtol)
       for j = 1:m
           A1, E1, B1, F1, C1, G1, D1, H1, _ = lpsminreal(Ao, Eo, (Bo/Wo)[:,j:j], (Fo/Wo)[:,j:j], Co, Go, (Do/Wo)[:,j:j], (Ho/Wo)[:,j:j],
                                           obs = false, atol1 = atol1, atol2 = atol2, rtol = rtol)
           if abs(Rval[i,j]) > gaintol
              zer, iz, _ = pzeros([A1 B1; C1 D1],[E1 F1; G1 H1]; fast = fast, atol1 = atol1, atol2 = atol2, rtol = rtol)
              cz, zval = polcoeffval(zer[1:(length(zer)-sum(iz))],val)
              if unimodular
                 NUM[i,j,1:length(cz)] = compl ? cz*(Rval[i,j]/zval) : real(cz*(Rval[i,j]/zval))
              else
                 n1 = size(A1,1)
                 M1 = [A1 zeros(T,n1,2); zeros(T,1,n1+1) one(T); zeros(T,1,n1) one(T)  zero(T)]
                 N1 = [E1 F1 zeros(T,n1,1); G1 H1 zero(T); zeros(T,1,n1+2)]
                 pol, ip, _ = pzeros(M1,N1; fast = fast, atol1 = atol1, atol2 = atol2, rtol = rtol)
                 cp, pval = polcoeffval(pol[1:(length(pol)-sum(ip))],val)
                 NUM[i,j,1:length(cz)] = compl ? cz*(Rval[i,j]*pval/zval) : real(cz*(Rval[i,j]*pval/zval))
                 DEN[i,j,1:length(cp)] = compl ? cp : real(cp)
              end
           end
       end
   end
   return NUM[:,:,1:pmdeg(NUM)+1], DEN[:,:,1:pmdeg(DEN)+1]
end
"""
     lpmfd2ls(DEN, NUM; fast = true, contr = false, obs = false, minimal = false, atol = 0, rtol) -> (A, E, B, C, D)

Build a structured linearization as a system matrix `S(λ)` of the form

            | A-λE | B |
     S(λ) = |------|---|
            |  C   | D |

of the left polynomial matrix fractional description `R(λ) = inv(DEN(λ))*NUM(λ)`,
such that `R(λ) = C*inv(λE-A)*B+D`. The resulting linearization `S(λ)` preserves a part,
if `minimal = false`, or the complete Kronecker structure, if `minimal = true`, of `R(λ)`.
In the latter case, the order `n` of `A-λE` is the least possible one.

`DEN(λ)` and `NUM(λ)` can be specified as polynomial matrices of the form `X(λ) = X_1 + λ X_2 + ... + λ**k X_(k+1)`,
for `X = DEN` or `X = NUM`, for which the coefficient matrices `X_i`, `i = 1, ..., k+1`, are stored in
the 3-dimensional matrices `X`, where `X[:,:,i]` contains the `i`-th coefficient matrix `X_i` (multiplying `λ**(i-1)`).

`DEN(λ)` and `NUM(λ)` can also be specified as matrices, vectors or scalars 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 computed structured linearization satisfies:

(1) `A-λE` is regular;

(2) `rank[B A-λE] = n` (controllability) if `minimal = true` or `contr = true`, in which case
the right Kronecker structure is preserved;

(3) `rank[A-λE; C] = n` (observability)  if `minimal = true` or `obs = true`, in which case
the left Kronecker structure is preserved;

(4) `S(λ)` has no simple infinite eigenvalues if `minimal = true`.

If conditions (1)-(4) are satisfied, the linearization is called `minimal` and the resulting order `n`
is the least achievable order. If conditions (1)-(3) are satisfied, the linearization is called `irreducible`
and the resulting order `n` is the least achievable order using orthogonal similarity transformations.
For an irreducible linearization `S(λ)` preserves the pole-zero structure (finite and infinite) and the
left and right Kronecker structures of `R(λ)`.

The keyword arguments `atol` and `rtol`, specify, respectively, the absolute and relative tolerance for the
nonzero coefficients of the matrices `DEN(λ)` and `NUM(λ)`. The default relative tolerance is `nt*ϵ`,
where `nt` is the size of the square matrix `DEN(λ)` and `ϵ` is the machine epsilon of the element type of its coefficients.

The structured linearization is built using the methods described in [1].

[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).
"""
function lpmfd2ls(DEN::Union{AbstractArray{T1,3},AbstractArray{T1,2}},NUM::Union{AbstractArray{T2,3},AbstractArray{T2,2}};
                  contr::Bool = false, obs::Bool = false, minimal::Bool = false,
                  fast::Bool = true, atol::Real = zero(real(T1)),
                  rtol::Real = size(DEN,1)*eps(real(float(one(T1))))*iszero(atol)) where {T1, T2}
    p = size(NUM,1)
    m = size(NUM,2)
    p1 =  size(DEN,1)
    p1 == size(DEN,2) || throw(DimensionMismatch("DEN(λ) must be a square polynomial matrix"))
    p == p1 || throw(DimensionMismatch("DEN(λ) and NUM(λ) must have the same number of rows"))

    return spm2ls(DEN, NUM, Matrix{T1}(I,p,p), zeros(T1,p,m), contr = contr, obs = obs, minimal = minimal,
                  fast = fast, atol = atol, rtol = rtol)
end
function lpmfd2ls(DEN::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                  NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
    return lpmfd2ls(poly2pm(DEN),poly2pm(NUM); kwargs...)
end
"""
     rpmfd2ls(DEN, NUM; fast = true, contr = false, obs = false, minimal = false, atol = 0, rtol) -> (A, E, B, C, D)

Build a structured linearization as a system matrix `S(λ)` of the form

            | A-λE | B |
     S(λ) = |------|---|
            |  C   | D |

of the right polynomial matrix fractional description `R(λ) = NUM(λ)*inv(DEN(λ))`,
such that `R(λ) = C*inv(λE-A)*B+D`. The resulting linearization `S(λ)` preserves a part,
if `minimal = false`, or the complete Kronecker structure, if `minimal = true`, of `R(λ)`.
In the latter case, the order `n` of `A-λE` is the least possible one.

`DEN(λ)` and `NUM(λ)` can be specified as polynomial matrices of the form `X(λ) = X_1 + λ X_2 + ... + λ**k X_(k+1)`,
for `X = DEN` or `X = NUM`, for which the coefficient matrices `X_i`, `i = 1, ..., k+1`, are stored in
the 3-dimensional matrices `X`, where `X[:,:,i]` contains the `i`-th coefficient matrix `X_i` (multiplying `λ**(i-1)`).

`DEN(λ)` and `NUM(λ)` can also be specified as matrices, vectors or scalars 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 computed structured linearization satisfies:

(1) `A-λE` is regular;

(2) `rank[B A-λE] = n` (controllability) if `minimal = true` or `contr = true`, in which case
the right Kronecker structure is preserved;

(3) `rank[A-λE; C] = n` (observability)  if `minimal = true` or `obs = true`, in which case
the left Kronecker structure is preserved;

(4) `S(λ)` has no simple infinite eigenvalues if `minimal = true`.

If conditions (1)-(4) are satisfied, the linearization is called `minimal` and the resulting order `n`
is the least achievable order. If conditions (1)-(3) are satisfied, the linearization is called `irreducible`
and the resulting order `n` is the least achievable order using orthogonal similarity transformations.
For an irreducible linearization `S(λ)` preserves the pole-zero structure (finite and infinite) and the
left and right Kronecker structures of `R(λ)`.

The keyword arguments `atol` and `rtol`, specify, respectively, the absolute and relative tolerance for the
nonzero coefficients of the matrices `DEN(λ)` and `NUM(λ)`. The default relative tolerance is `nt*ϵ`,
where `nt` is the size of the square matrix `DEN(λ)` and `ϵ` is the machine epsilon of the element type of its coefficients.

The structured linearization is built using the methods described in [1].

[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).
"""
function rpmfd2ls(DEN::Union{AbstractArray{T1,3},AbstractArray{T1,2}},NUM::Union{AbstractArray{T2,3},AbstractArray{T2,2}};
                  contr::Bool = false, obs::Bool = false, minimal::Bool = false,
                  fast::Bool = true, atol::Real = zero(real(T1)),
                  rtol::Real = size(DEN,1)*eps(real(float(one(T1))))*iszero(atol)) where {T1, T2}
    p = size(NUM,1)
    m = size(NUM,2)
    m1 =  size(DEN,1)
    m1 == size(DEN,2) || throw(DimensionMismatch("DEN(λ) must be a square polynomial matrix"))
    m == m1 || throw(DimensionMismatch("DEN(λ) and NUM(λ) must have the same number of rows"))

    return spm2ls(DEN, Matrix{T1}(I,m,m), NUM, zeros(T1,p,m), contr = contr, obs = obs, minimal = minimal,
                  fast = fast, atol = atol, rtol = rtol)
end
function rpmfd2ls(DEN::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                  NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
    return rpmfd2ls(poly2pm(DEN),poly2pm(NUM); kwargs...)
end
"""
     lpmfd2lps(DEN, NUM; fast = true, contr = false, obs = false, minimal = false, atol = 0, rtol) -> (A, E, B, F, C, G, D, H)

Build a structured linearization as a system matrix `S(λ)` of the form

            | A-λE | B-λF |
     S(λ) = |------|------|
            | C-λG | D-λH |

of the left polynomial matrix fractional description `R(λ) = inv(DEN(λ))*NUM(λ)`,
such that `R(λ) = (C-λG)*inv(λE-A)*(B-λF)+D-λH`. The resulting linearization `S(λ)` preserves a part,
if `minimal = false`, or the complete Kronecker structure, if `minimal = true`, of `R(λ)`.
In the latter case, the order `n` of `A-λE` is the least possible one.

`DEN(λ)` and `NUM(λ)` can be specified as polynomial matrices of the form `X(λ) = X_1 + λ X_2 + ... + λ**k X_(k+1)`,
for `X = DEN` or `X = NUM`, for which the coefficient matrices `X_i`, `i = 1, ..., k+1`, are stored in
the 3-dimensional matrices `X`, where `X[:,:,i]` contains the `i`-th coefficient matrix `X_i` (multiplying `λ**(i-1)`).

`DEN(λ)` and `NUM(λ)` can also be specified as matrices, vectors or scalars 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 computed structured linearization satisfies:

(1) `A-λE` is regular;

(2) `rank[B-λF A-λE] = n` (strong controllability) if `minimal = true` or `contr = true`, in which case
the right Kronecker structure is preserved;

(3) `rank[A-λE; C-λG] = n` (strong observability)  if `minimal = true` or `obs = true`, in which case
the left Kronecker structure is preserved.

If conditions (1)-(3) are satisfied, the linearization is called `strongly minimal`, the resulting order `n`
is the least achievable order and `S(λ)` preserves the pole-zero structure (finite and infinite) and the
left and right Kronecker structures of `R(λ)`.

The keyword arguments `atol` and `rtol`, specify, respectively, the absolute and relative tolerance for the
nonzero coefficients of the matrices `DEN(λ)` and `NUM(λ)`. The default relative tolerance is `nt*ϵ`,
where `nt` is the size of the square matrix `DEN(λ)` and `ϵ` is the machine epsilon of the element type of its coefficients.

The structured linearization is built using the methods described in [1].

[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).
"""
function lpmfd2lps(DEN::Union{AbstractArray{T1,3},AbstractArray{T1,2}},NUM::Union{AbstractArray{T2,3},AbstractArray{T2,2}};
                  contr::Bool = false, obs::Bool = false, minimal::Bool = false,
                  fast::Bool = true, atol::Real = zero(real(T1)),
                  rtol::Real = size(DEN,1)*eps(real(float(one(T1))))*iszero(atol)) where {T1, T2}
    p = size(NUM,1)
    m = size(NUM,2)
    p1 =  size(DEN,1)
    p1 == size(DEN,2) || throw(DimensionMismatch("DEN(λ) must be a square polynomial matrix"))
    p == p1 || throw(DimensionMismatch("DEN(λ) and NUM(λ) must have the same number of rows"))

    return spm2lps(DEN, NUM, Matrix{T1}(I,p,p), zeros(T1,p,m), contr = contr, obs = obs, minimal = minimal,
                  fast = fast, atol = atol, rtol = rtol)
end
function lpmfd2lps(DEN::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                   NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
    return lpmfd2lps(poly2pm(DEN),poly2pm(NUM); kwargs...)
end
"""
     rpmfd2lps(DEN, NUM; fast = true, contr = false, obs = false, minimal = false, atol = 0, rtol) -> (A, E, B, F, C, G, D, H)

Build a structured linearization as a system matrix `S(λ)` of the form

            | A-λE | B-λF |
     S(λ) = |------|------|
            | C-λG | D-λH |

of the right polynomial matrix fractional description `R(λ) = NUM(λ)*inv(DEN(λ))`,
such that `R(λ) = (C-λG)*inv(λE-A)*(B-λF)+D-λH`. The resulting linearization `S(λ)` preserves a part,
if `minimal = false`, or the complete Kronecker structure, if `minimal = true`, of `R(λ)`.
In the latter case, the order `n` of `A-λE` is the least possible one.

`DEN(λ)` and `NUM(λ)` can be specified as polynomial matrices of the form `X(λ) = X_1 + λ X_2 + ... + λ**k X_(k+1)`,
for `X = DEN` or `X = NUM`, for which the coefficient matrices `X_i`, `i = 1, ..., k+1`, are stored in
the 3-dimensional matrices `X`, where `X[:,:,i]` contains the `i`-th coefficient matrix `X_i` (multiplying `λ**(i-1)`).

`DEN(λ)` and `NUM(λ)` can also be specified as matrices, vectors or scalars 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 computed structured linearization satisfies:

(1) `A-λE` is regular;

(2) `rank[B-λF A-λE] = n` (strong controllability) if `minimal = true` or `contr = true`, in which case
the right Kronecker structure is preserved;

(3) `rank[A-λE; C-λG] = n` (strong observability)  if `minimal = true` or `obs = true`, in which case
the left Kronecker structure is preserved.

If conditions (1)-(3) are satisfied, the linearization is called `strongly minimal`, the resulting order `n`
is the least achievable order and `S(λ)` preserves the pole-zero structure (finite and infinite) and the
left and right Kronecker structures of `R(λ)`.

The keyword arguments `atol` and `rtol`, specify, respectively, the absolute and relative tolerance for the
nonzero coefficients of the matrices `DEN(λ)` and `NUM(λ)`. The default relative tolerance is `nt*ϵ`,
where `nt` is the size of the square matrix `DEN(λ)` and `ϵ` is the machine epsilon of the element type of its coefficients.

The structured linearization is built using the methods described in [1].

[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).
"""
function rpmfd2lps(DEN::Union{AbstractArray{T1,3},AbstractArray{T1,2}},NUM::Union{AbstractArray{T2,3},AbstractArray{T2,2}};
                  contr::Bool = false, obs::Bool = false, minimal::Bool = false,
                  fast::Bool = true, atol::Real = zero(real(T1)),
                  rtol::Real = size(DEN,1)*eps(real(float(one(T1))))*iszero(atol)) where {T1, T2}
    p = size(NUM,1)
    m = size(NUM,2)
    m1 =  size(DEN,1)
    m1 == size(DEN,2) || throw(DimensionMismatch("DEN(λ) must be a square polynomial matrix"))
    m == m1 || throw(DimensionMismatch("DEN(λ) and NUM(λ) must have the same number of rows"))

    return spm2lps(DEN, Matrix{T1}(I,m,m), NUM, zeros(T1,p,m), contr = contr, obs = obs, minimal = minimal,
                  fast = fast, atol = atol, rtol = rtol)
end
function rpmfd2lps(DEN::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}},
                   NUM::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
    return rpmfd2lps(poly2pm(DEN),poly2pm(NUM); kwargs...)
end
"""
     pminv2ls(P; fast = true, contr = false, obs = false, minimal = false, atol = 0, rtol) -> (A, E, B, C, D)

Build a structured linearization as a system matrix `S(λ)` of the form

            | A-λE | B |
     S(λ) = |------|---|
            |  C   | D |

of the inverse `R(λ) = inv(P(λ))`, such that `R(λ) = C*inv(λE-A)*B+D`.
The resulting linearization `S(λ)` preserves a part, if `minimal = false`, or
the complete Kronecker structure, if `minimal = true`, of `R(λ)`.
In the latter case, the order `n` of `A-λE` is the least possible one.

`P(λ)` can be specified as a polynomial matrix of the form `P(λ) = P_1 + λ P_2 + ... + λ**k P_(k+1)`,
for which the coefficient matrices `P_i`, `i = 1, ..., k+1`, are stored in
the 3-dimensional matrix `P`, where `P[:,:,i]` contains the `i`-th coefficient matrix `P_i` (multiplying `λ**(i-1)`).

`P(λ)` can also be specified as a matrix or scalar of elements of the `Polynomial` type
provided by the [Polynomials](https://github.com/JuliaMath/Polynomials.jl) package.

The computed structured linearization satisfies:

(1) `A-λE` is regular;

(2) `rank[B A-λE] = n` (controllability) if `minimal = true` or `contr = true`, in which case
the right Kronecker structure is preserved;

(3) `rank[A-λE; C] = n` (observability)  if `minimal = true` or `obs = true`, in which case
the left Kronecker structure is preserved;

(4) `S(λ)` has no simple infinite eigenvalues if `minimal = true`.

If conditions (1)-(4) are satisfied, the linearization is called `minimal` and the resulting order `n`
is the least achievable order. If conditions (1)-(3) are satisfied, the linearization is called `irreducible`
and the resulting order `n` is the least achievable order using orthogonal similarity transformations.
For an irreducible linearization `S(λ)` preserves the pole-zero structure (finite and infinite) and the
left and right Kronecker structures of `R(λ)`.

The keyword arguments `atol` and `rtol`, specify, respectively, the absolute and relative tolerance for the
nonzero coefficients of the matrix `P(λ)`. The default relative tolerance is `nt*ϵ`,
where `nt` is the size of the square matrix `P(λ)` and `ϵ` is the machine epsilon of the element type of its coefficients.

The structured linearization is built using the methods described in [1].

[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).
"""
function pminv2ls(P::Union{AbstractArray{T1,3},AbstractArray{T1,2}};
                  contr::Bool = false, obs::Bool = false, minimal::Bool = false,
                  fast::Bool = true, atol::Real = zero(real(T1)),
                  rtol::Real = size(P,1)*eps(real(float(one(T1))))*iszero(atol)) where {T1}
    n = size(P,1)
    n == size(P,2) || throw(DimensionMismatch("P(λ) must be a square polynomial matrix"))

    return spm2ls(P, Matrix{T1}(I,n,n), Matrix{T1}(I,n,n), zeros(T1,n,n), contr = contr, obs = obs, minimal = minimal,
                  fast = fast, atol = atol, rtol = rtol)
end
function pminv2ls(P::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
    return pminv2ls(poly2pm(P); kwargs...)
end
"""
     pminv2lps(P; fast = true, contr = false, obs = false, minimal = false, atol = 0, rtol) -> (A, E, B, F, C, G, D, H)

Build a structured linearization as a system matrix `S(λ)` of the form

            | A-λE | B-λF |
     S(λ) = |------|------|
            | C-λG | D-λH |

of the inverse `R(λ) = inv(P(λ))`, such that `R(λ) = (C-λG)*inv(λE-A)*(B-λF)+D-λH`.
The resulting linearization `S(λ)` preserves a part, if `minimal = false`, or
the complete Kronecker structure, if `minimal = true`, of `R(λ)`.
In the latter case, the order `n` of `A-λE` is the least possible one.

`P(λ)` can be specified as a polynomial matrix of the form `P(λ) = P_1 + λ P_2 + ... + λ**k P_(k+1)`,
for which the coefficient matrices `P_i`, `i = 1, ..., k+1`, are stored in
the 3-dimensional matrix `P`, where `P[:,:,i]` contains the `i`-th coefficient matrix `P_i` (multiplying `λ**(i-1)`).

`P(λ)` can also be specified as a matrix or scalar of elements of the `Polynomial` type
provided by the [Polynomials](https://github.com/JuliaMath/Polynomials.jl) package.

The computed structured linearization satisfies:

(1) `A-λE` is regular;

(2) `rank[B-λF A-λE] = n` (strong controllability) if `minimal = true` or `contr = true`, in which case
the right Kronecker structure is preserved;

(3) `rank[A-λE; C-λG] = n` (strong observability)  if `minimal = true` or `obs = true`, in which case
the left Kronecker structure is preserved.

If conditions (1)-(3) are satisfied, the linearization is called `strongly minimal`, the resulting order `n`
is the least achievable order and `S(λ)` preserves the pole-zero structure (finite and infinite) and the
left and right Kronecker structures of `R(λ)`.

The keyword arguments `atol` and `rtol`, specify, respectively, the absolute and relative tolerance for the
nonzero coefficients of the matrix `P(λ)`. The default relative tolerance is `nt*ϵ`,
where `nt` is the size of the square matrix `P(λ)` and `ϵ` is the machine epsilon of the element type of its coefficients.

The structured linearization is built using the methods described in [1].

[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).
"""
function pminv2lps(P::Union{AbstractArray{T1,3},AbstractArray{T1,2}};
                  contr::Bool = false, obs::Bool = false, minimal::Bool = false,
                  fast::Bool = true, atol::Real = zero(real(T1)),
                  rtol::Real = size(P,1)*eps(real(float(one(T1))))*iszero(atol)) where {T1}
    n = size(P,1)
    n == size(P,2) || throw(DimensionMismatch("P(λ) must be a square polynomial matrix"))

    return spm2lps(P, Matrix{T1}(I,n,n), Matrix{T1}(I,n,n), zeros(T1,n,n), contr = contr, obs = obs, minimal = minimal,
                  fast = fast, atol = atol, rtol = rtol)
end
function pminv2lps(P::Union{AbstractVecOrMat{<:Polynomial},Polynomial,Number,AbstractVecOrMat{<:Number}}; kwargs...)
    return pminv2lps(poly2pm(P); kwargs...)
end


#
# test LowRankMatrix
#
using ApproxFun, SingularIntegralEquations, Base.Test

L = lrzeros(Complex{Float64},10,20)
@test size(L) == (10,20)

@test full(L) == zeros(Complex{Float64},10,20)

@test_approx_eq full(lrones(10,10)) ones(10,10)

@test rank(lreye(10,10)) == 10

L = LowRankMatrix(1./collect(1:10),1./collect(1:10.0))

x = collect(linspace(-1,1,10))

@test norm(L*x-full(L)*x) < 200eps()

@test isa(L*L',LowRankMatrix)

A = rand(10,10)

@test isa(A*L,LowRankMatrix)
@test isa(L*A,LowRankMatrix)

@test isa(A+L,Matrix)
@test isa(L⊕A,LowRankMatrix)

@test rank(L+L) == 2rank(L⊕L)
# @test rank(L-L) == 2rank(L⊖L)
@test rank(2L+1+full(L)) ≥ rank(L⊕full(L))


"""
    resample(p::Particles, essthresh, rs)

Resamples the particle object `p` if the ess is under `essthresh`. The
resampling algorithm `rs` is for example a multinomial resampling.
"""
function resample(p::Particles, essthresh::Float=Inf,
                  rs::Function=multinomialresampling, M::Int=0
                  )::Tuple{Particles,Float}
    ess = 1.0/sum(p.w.^2)
    N   = length(p)
    M   = M>0 ? M : N
    (M != N || ess < essthresh * N) ? (rs(p, M), ess) : (p, ess)
end

"""
    multinomialresampling(p::Particles)

Multinomial resampling of a particles object `p`.
"""
function multinomialresampling(p::Particles, M::Int=0)::Particles
    N    = length(p)
    M    = (M>0)? M : N
    ni   = rand(Multinomial(M, p.w))
    mask = [j for i in 1:N for j in ones(Int,ni[i])*i]
    Particles(p.x[mask], ones(M)/M)
end


@testset "LinearCombination" begin
    @testset "scalar 1d" begin
        @testset for TC in rnc((Int,Float64))
            C = rand(TC,3)

            p = @inferred(LinearCombination(C, Monomials(3)))
            @test ndims(p) == ndims(typeof(p)) == 1

            @test @inferred(p(1)) ≈ sum(C)
            @test @inferred(p([1,2])) ≈ [sum(C), sum(C.*2 .^ (0:2))]
        end
    end

    @testset "scalar 2d" begin
        @testset for TC in rnc((Int,Float64))
            C = rand(TC,2,2)

            p = @inferred(LinearCombination(C, Monomials.((2,2))))
            @test @inferred(LinearCombination(C, Monomials(2))) == p
            @test ndims(p) == ndims(typeof(p)) == 2

            @test @inferred(p(1,1)) ≈ sum(C)
            @test @inferred(p(1,1:2)) ≈ [1,1]'*C*[1 1; 1 2]
            @test @inferred(p(1:2,1:2)) ≈ [1 1; 1 2]'*C*[1 1; 1 2]
        end
    end

    @testset "matrix 1d" begin
        C = rand(5)
        p = LinearCombination(C,Monomials(length(C)))
        x = LinRange(-1,1,11)
        M = Diagonal(collect(x))
        @test diag(@inferred(p(M))) == p.(x)
    end

    @testset "matrix times vector 1d" begin
        C = rand(5)
        p = LinearCombination(C,Monomials(length(C)))
        x = LinRange(-1,1,11)
        M = Diagonal(collect(x))
        v = one.(x)
        @test @inferred(((M,v)->@evaluate p(M) * v)(M,v)) == p.(x)
    end
end


using Distributions, StatsBase

function cross_entropy(f, P; k=1, m=50, m_extra=20, m_elite=m, base_dist=Uniform(-1,1))
    for _=1:k
        # Sample from the current distribution
        samples = clamp.(rand(P, m), -1f0, 1f0)
        dim = size(samples, 1)

        # Add some noise to avoid the distribution collapsing entirely
        # samples .+= rand(Uniform(-1f-3, 1f-3), size(samples)...)

        # Add some extra samples from an exploration policy to account for dramatic shifts from previous iterations
        samples = hcat(samples, rand(base_dist, dim, m_extra))

        vals = [f(samples[:,i]) for i=1:size(samples,2)]
        weights = softmax(-vals)

        order = sortperm(vals)
        elite_samples = Float64.(samples[:, order[1:m_elite]])
        try
            P = fit(MvNormal, elite_samples, weights[order[1:m_elite]])
        catch
            println("weights: ", weights)
            @error "Error in fitting MVNormal distribution"
        end
    end
    P
end

function mcmc(f, P; Q=(sz...)->0.01*randn(sz...), k=1, m=size(P,2), m_extra=20, base_dist = Uniform(-1,1))
    dim, Npart=size(P)
    @assert Npart == m
    for _=1:k
        # Perturb particles and add some additional ones
        # P = Float32.(clamp.(P .+ Q(dim, Npart), -1, 1))
        P = hcat(P, rand(base_dist, dim, m_extra))

        # compute the particle values
        vals = [f(P[:,i]) for i=1:size(P,2)]
        weights = exp.(-vals)

        # resample
        samples = sample(1:Npart+m_extra, Weights(weights), Npart)
        P = P[:, samples]
    end
    P
end

best_estimate(d::MvNormal) = d.μ
uncertainty(d::MvNormal) = det(d.Σ)
best_estimate(P::Array) = mean(P, dims=2)
uncertainty(P::Array) = mean(std(P, dims=2))

# # Demo the inference
# target_dist = MvNormal([-0.5,-0.5], [0.1, 0.1])
# foo(z) = -logpdf(target_dist, z)
# x = -2:0.01:2
# y = x
#
# ## Particle filter
#
# contour(x, y, (x,y)->exp(logpdf(target_dist, [x,y])))
# P = randn(2,100) # Initial particle set
# scatter!(P[1,:], P[2,:], label="iteration 0")
# P = mcmc(foo, P)
# scatter!(P[1,:], P[2,:], label="iteration 1")
# P = mcmc(foo, P)
# scatter!(P[1,:], P[2,:], label="iteration 2")
#
# println("best estimate: ", best_estimate(P))
#
# ## cross entropy
# p1 = contour(x, y, (x,y)->exp(logpdf(target_dist, [x,y])), cbar=false)
# P = MvNormal([0,0], Diagonal([1.,1.]))
# p2 = contour(x, y, (x,y)->exp(logpdf(P, [x,y])), seriescolor=:blues)
# P = cross_entropy(foo, P)
# p3 = contour(x, y, (x,y)->exp(logpdf(P, [x,y])), seriescolor=:blues)
# P = cross_entropy(foo, P)
# p4 = contour(x, y, (x,y)->exp(logpdf(P, [x,y])), seriescolor=:blues)
#
# plot(p1, p2, p3, p4)
#
# println("best estimate: ", best_estimate(P))


module LaplBEM

using LinearAlgebra: dot, norm, cross
using Statistics: mean
using FinEtools
using FinEtools.IntegRuleModule: TriRule, GaussRule
import FinEtools.MatrixUtilityModule: locjac!
using Base.Threads

@inline function _distance(dx, dy, dz)::Float64
    return sqrt(dx^2 + dy^2 + dz^2)
end

@inline function _G(r)
	# 3-D free-field Green's function for the Laplace equation
	# x    is the position vector of the observation point
	# x0    is the position vector of the source (same size as x0)
	#         g = 1/(4*pi)/(norm(x-x0));
	return 0.079577471545948/(r);
end

"""
    Greens(k, xsource, xfield)

Compute.
"""
@inline function Greens(xsource, xfield)
    dx, dy, dz = xfield[1]-xsource[1], xfield[2]-xsource[2], xfield[3]-xsource[3]
    return _G(_distance(dx, dy, dz))
end

function _Gn(r, nxx0)#(x,x0,n)
	#
	# Normal derivative of the 3-D free-field Green's function for the Laplace equation
	#
	# x    is the position vector of the observation point (field point)
	# x0    is the position vector of the source (same size as x0)
	#         r=(x-x0);
	#         g = 1/(4*pi)*((n*r')/(norm(r)^3));
	return 0.079577471545948*((nxx0)/(r^3));
end

"""
    dGreensdn(xsource, xfield, n)

We are assuming the normal to point INTO THE FLUID!
"""
@inline function dGreensdn(xsource, xfield, n) # Normal derivative
    dx, dy, dz = xfield[1]-xsource[1], xfield[2]-xsource[2], xfield[3]-xsource[3]
    r = _distance(dx, dy, dz);
    return  _Gn(r, (n[1]*dx + n[2]*dy + n[3]*dz))
end

"""
    trinml!(n, x)

Compute triangle normal (NOT a unit normal, it has a length of twice the area
of the triangle).
"""
function trinml!(n, x)
    a1, a2, a3 = x[2, 1] - x[1, 1], x[2, 2] - x[1, 2], x[2, 3] - x[1, 3]
    b1, b2, b3 = x[3, 1] - x[1, 1], x[3, 2] - x[1, 2], x[3, 3] - x[1, 3]
    n[1] = a2*b3-a3*b2
    n[2] = a3*b1-a1*b3
    n[3] = a1*b2-a2*b1
    return n
end

"""
    triunitnml!(n, x)

Compute the UNIT triangle normal.
"""
function triunitnml!(n, x)
    n = trinml!(n, x)
    n .= n ./ norm(n)
    return n
end

"""
    tricntr!(c, x)

Compute triangle centroid.
"""
function tricntr!(c, x)
    d = length(c)
    for i in 1:d
        c[i] = 0.0
        for j in 1:size(x, 1)
            c[i] += x[j, i]
        end
        c[i] *= (1.0/d)
    end
    return c
end

"""
    centroidareanormal!(c, n, x)

Compute triangle centroid and normal and its area.
"""
function centroidareanormal!(c, n, x)
    n = trinml!(n, x)
    twicearea = norm(n)
    n *= (1.0 / twicearea) # Unit normal
    c = tricntr!(c, x)
    return c, twicearea/2.0, n
end

"""
    surfnml!(n, J)

Compute the surface normal.
A lot of effort has been put into eliminating all allocations, because this function is called in the innermost loop.
"""
function surfnml!(n, J)
   a1, a2, a3 = J[1, 1], J[2, 1], J[3, 1]
   b1, b2, b3 = J[1, 2], J[2, 2], J[3, 2]
   n[1] = a2*b3-a3*b2
   n[2] = a3*b1-a1*b3
   n[3] = a1*b2-a2*b1
   return n
end

function _bfundata(fes, ir)
    Ns = FFltMat[];
    gradNparams = FFltMat[];
    for j in 1:ir.npts
        push!(Ns, bfun(fes,vec(ir.param_coords[j,:])));
        push!(gradNparams, bfundpar(fes,vec(ir.param_coords[j,:])));
    end
    return Ns, gradNparams
end

"""
    _eint(f::T, econn, ex, xsource, n, p, w, Ns, gradNparams, xfield, J, nfield)::Complex{Float64} where {T}

Compute the element integral.
We go to a lot of trouble here to eliminate all sources of allocations. This function is called
for each entry of the Helmholtz matrices.
"""
function _eint(f::T, econn, ex, xsource, n, p, w, Ns, gradNparams, xfield, J, nfield)::Complex{Float64} where {T}
    result = 0.0  + 0.0im
    for j in 1:length(w)
        locjac!(xfield, J, ex, Ns[j], gradNparams[j])
        Jac = norm(surfnml!(nfield, J))
        result += f(xsource, xfield, n) * (w[j] * Jac)
    end
    return result::Complex{Float64}
end

function _allelxs(xyz, conn)
    elxs = fill(Matrix{Float64}(undef, size(conn, 2), 3), size(conn, 1))
    for i in 1:size(conn, 1)
        elxs[i] = xyz[view(conn, i, :), :]
    end
    return elxs
end

"""
    allelxs(xyz, conn)

Construct array of all surface panel coordinates.
"""
function allelxs(xyz, conn)
    return _allelxs(xyz, conn)
end

function _allnormals(elxs)
    normals = fill(Vector{Float64}(undef, 3), length(elxs))
    cnormal = fill(0.0, 3)
    for i in 1:length(elxs)
        normals[i] = deepcopy(triunitnml!(cnormal, elxs[i]))
    end
    return normals
end

"""
    allnormals(elxs)

Construct array of all surface panel normals.
"""
function allnormals(elxs)
    return _allnormals(elxs)
end

function _doublelayer!(A, firstrowofset, rxyz, xyz, conn, elxs, solidangle, ir::TriRule)
	nrows = size(rxyz, 1)
    neqn = size(conn, 1)
    @assert size(A, 1) == nrows "Size of matrix needs to match number of row points"
    @assert size(A, 2) == neqn "Size of matrix needs to match number of surface panels"
    econn = (1,2,3)
    dummyfes = FESetT3(reshape(collect(econn), 1, 3))
    p, w = ir.param_coords, vec(ir.weights)
    Ns, gradNparams = _bfundata(dummyfes, ir)
    f = (xsource, xfield, n) -> (-1.0) * dGreensdn(xsource, xfield, n)
    cnormal = fill(0.0, 3)
    xfield = fill(zero(FFlt), 1, 3);
    J = fill(zero(FFlt), 3, 2);
    nfield = fill(zero(FFlt), 1, 3);
    normals = _allnormals(elxs)
    for r in 1:nrows
        rx = view(rxyz, r, :)
        for c in 1:neqn
            if r+firstrowofset != c
                A[r, c] += _eint(f, econn, elxs[c], rx, normals[c], p, w, Ns, gradNparams, xfield, J, nfield)
            else
                A[r, c] += solidangle
            end
        end
    end
    return A
end

function _singlelayer!(B, firstrowofset, rxyz, xyz, conn, elxs, iroffdiagonal::TriRule, irdiagonal::TriRule)
	nrows = size(rxyz, 1)
    neqn = size(conn, 1)
    @assert size(B, 1) == nrows "Size of matrix needs to match number of row points"
    @assert size(B, 2) == neqn "Size of matrix needs to match number of surface panels"
    econn = (1,2,3)
    dummyfes = FESetT3(reshape(collect(econn), 1, 3))
    poff, woff = iroffdiagonal.param_coords, vec(iroffdiagonal.weights)
    Nsoff, gradNparamsoff = _bfundata(dummyfes, iroffdiagonal)
    pdiagonal, wdiagonal = irdiagonal.param_coords, vec(irdiagonal.weights)
    Nsdiagonal, gradNparamsdiagonal = _bfundata(dummyfes, irdiagonal)
    f = (xsource, xfield, n) -> (-1.0) * Greens(xsource, xfield)
    ex = fill(0.0, 3, 3)
    xfield = fill(zero(FFlt), 1, 3);
    J = fill(zero(FFlt), 3, 2);
    nfield = fill(zero(FFlt), 1, 3);
    normals = _allnormals(elxs)
    ccentroid = fill(0.0, 3)
    for r in 1:nrows
        rx = view(rxyz, r, :)
        for c in 1:neqn
            if r+firstrowofset != c
                B[r, c] += _eint(f, econn, elxs[c], rx, normals[c], poff, woff, Nsoff, gradNparamsoff, xfield, J, nfield)
            else
                ex[3,:] .= tricntr!(ccentroid, elxs[c])
                ex[1,:] .= elxs[c][1,:]; ex[2,:] .= elxs[c][2,:];
                B[r, c] += _eint(f, econn, ex, rx, normals[c], pdiagonal, wdiagonal, Nsdiagonal, gradNparamsdiagonal, xfield, J, nfield)
                ex[1,:] .= elxs[c][2,:]; ex[2,:] .= elxs[c][3,:];
                B[r, c] += _eint(f, econn, ex, rx, normals[c], pdiagonal, wdiagonal, Nsdiagonal, gradNparamsdiagonal, xfield, J, nfield)
                ex[1,:] .= elxs[c][3,:]; ex[2,:] .= elxs[c][1,:];
                B[r, c] += _eint(f, econn, ex, rx, normals[c], pdiagonal, wdiagonal, Nsdiagonal, gradNparamsdiagonal, xfield, J, nfield)
            end
        end
    end
    return B
end

"""
    doublelayer!(A, xyz, conn, ir::TriRule)

Compute the double-layer matrix contribution.

Arguments
- `A` = matrix
- `xyz` = array of coordinates of the nodes
- `conn` = array of connectivities of the surface elements
- `ir` = integration rule

The contribution is _added_to the matrix `A`. This matrix is also returned for convenience.
"""
function doublelayer!(A, xyz, conn, ir::TriRule)
	neqn = size(conn, 1)
    rxyz = fill(0.0, neqn, 3)
    rcentroid = fill(0.0, 3)
    elxs = _allelxs(xyz, conn)
    for r in 1:neqn
        rxyz[r, :] .= tricntr!(rcentroid, elxs[r])
    end
    return _doublelayer!(A, 0, rxyz, xyz, conn, elxs, -0.5, ir)
end

"""
    singlelayer!(B, k, xyz, conn, iroffdiagonal::TriRule, irdiagonal::TriRule)

Compute the single-layer matrix contribution.

Arguments
- `B` = complex matrix
- `xyz` = array of coordinates of the nodes
- `conn` = array of connectivities of the surface elements
- `iroffdiagonal` = integration rule for the off-diagonal terms
- `irdiagonal` = integration rule for the diagonal terms

The contribution is _added_to the matrix `B`. This matrix is also returned for convenience.
"""
function singlelayer!(B, xyz, conn, iroffdiagonal::TriRule, irdiagonal::TriRule)
	neqn = size(conn, 1)
    rxyz = fill(0.0, neqn, 3)
    rcentroid = fill(0.0, 3)
    elxs = _allelxs(xyz, conn)
    for r in 1:neqn
        rxyz[r, :] .= tricntr!(rcentroid, elxs[r])
    end
    return _singlelayer!(B, 0, rxyz, xyz, conn, elxs, iroffdiagonal, irdiagonal)
end

# function _doublelayercolumns!(A, firstrowofset, columnrange, k, rxyz, xyz, conn, elxs, normals, solidangle, ir::TriRule)
# 	nrows = size(rxyz, 1)
#     neqn = size(conn, 1)
#     econn = (1,2,3)
#     dummyfes = FESetT3(reshape(collect(econn), 1, 3))
#     p, w = ir.param_coords, vec(ir.weights)
#     Ns, gradNparams = _bfundata(dummyfes, ir)
#     f = (xsource, xfield, n) -> dGreensdn(k, xsource, xfield, n)
#     cnormal = fill(0.0, 3)
#     xfield = fill(zero(FFlt), 1, 3);
#     J = fill(zero(FFlt), 3, 2);
#     nfield = fill(zero(FFlt), 1, 3);
#     for r = 1:nrows
#     	rx = view(rxyz, r, :)
#         for c in columnrange
#             if r+firstrowofset != c
#                 A[r, c] += (-1.0) * _eint(f, econn, elxs[c], rx, normals[c], p, w, Ns, gradNparams, xfield, J, nfield)
#             else
#                 A[r, c] += solidangle
#             end
#         end
#     end
#     return A
# end

"""
    tdoublelayer!(A, k, xyz, conn, ir::TriRule)

Compute the double-layer matrix contribution.  Threaded version.

- `A` = complex matrix
- `k` = wave number
- `xyz` = array of coordinates of the nodes
- `conn` = array of connectivities of the surface elements
- `ir` = integration rule

As `doublelayer(A, k, xyz, conn, ir::TriRule)`, but the calculation is carried
out in parallel using all available workers.
"""
# function tdoublelayer!(A, k, xyz, conn, ir::TriRule)
#     neqn = size(conn, 1)
#     rxyz = fill(0.0, neqn, 3)
#     rcentroid = fill(0.0, 3)
#     elxs = _allelxs(xyz, conn)
#     for r = 1:neqn
#     	rxyz[r, :] .= tricntr!(rcentroid, elxs[r])
#     end
#     normals = _allnormals(elxs)
#     firstrowofset = 0
#     nrows = size(rxyz, 1)
#     nth = Base.Threads.nthreads()
#     _tcolumnchunk = Int(ceil(neqn/nth))
#     c = 0
#     while c < neqn
#     	tasks = []
#     	for t in 1:nth
#     		columnrange = c+1:min(neqn, c+1+_tcolumnchunk)
#     		push!(tasks, Threads.@spawn _doublelayercolumns!(A, firstrowofset, columnrange, k, rxyz, xyz, conn, elxs, normals, 0.5, ir))
#     		c = maximum(columnrange)
#     		if c >= neqn
#     			break
#     		end
#     	end
#     	Threads.wait.(tasks);
#     end
#     return A
# end

"""
    tdoublelayer!(A, k, xyz, conn, ir::TriRule)

Compute the double-layer matrix contribution.  Threaded version.

As `doublelayer(A, k, xyz, conn, ir::TriRule)`, but the calculation is carried
out in parallel using all available threads.

Don't forget to set the number of threads with
`export JULIA_NUM_THREADS=4`.
"""
# function tdoublelayer!(A, k, chiefxyz, xyz, conn, ir::TriRule)
# 	neqn = size(conn, 1)
# 	elxs = _allelxs(xyz, conn)
# 	normals = _allnormals(elxs)
# 	firstrowofset = neqn
# 	nrows = size(chiefxyz, 1)
# 	nth = Base.Threads.nthreads()
# 	_tcolumnchunk = Int(ceil(neqn/nth))
# 	c = 0
# 	while c < neqn
# 		tasks = []
# 		for t in 1:nth
# 			columnrange = c+1:min(neqn, c+1+_tcolumnchunk)
# 			push!(tasks, Threads.@spawn _doublelayercolumns!(A, firstrowofset, columnrange, k, chiefxyz, xyz, conn, elxs, normals, 0.0, ir))
# 			c = maximum(columnrange)
# 			if c >= neqn
# 				break
# 			end
# 		end
# 		Threads.wait.(tasks);
# 	end
# 	return A
# end

# function _singlelayercolumns!(B, firstrowofset, columnrange, k, rxyz, xyz, conn, elxs, normals, iroffdiagonal::TriRule, irdiagonal::TriRule)
#     nrows = size(rxyz, 1)
#     neqn = size(conn, 1)
#     econn = (1,2,3)
#     dummyfes = FESetT3(reshape(collect(econn), 1, 3))
#     poff, woff = iroffdiagonal.param_coords, vec(iroffdiagonal.weights)
#     Nsoff, gradNparamsoff = _bfundata(dummyfes, iroffdiagonal)
#     pdiagonal, wdiagonal = irdiagonal.param_coords, vec(irdiagonal.weights)
#     Nsdiagonal, gradNparamsdiagonal = _bfundata(dummyfes, irdiagonal)
#     f = (xsource, xfield, n) -> Greens(k, xsource, xfield)
#     ex = fill(0.0, 3, 3)
#     xfield = fill(zero(FFlt), 1, 3);
#     J = fill(zero(FFlt), 3, 2);
#     nfield = fill(zero(FFlt), 1, 3);
#     ccentroid = fill(0.0, 3)
#     for r = 1:nrows
#         rx = view(rxyz, r, :)
#         for c = columnrange
#             if r+firstrowofset != c
#                 B[r, c] += _eint(f, econn, elxs[c], rx, normals[c], poff, woff, Nsoff, gradNparamsoff, xfield, J, nfield)
#             else
#                 ex[3,:] .= tricntr!(ccentroid, elxs[c])
#                 ex[1,:] .= elxs[c][1,:]; ex[2,:] .= elxs[c][2,:];
#                 B[r, c] += _eint(f, econn, ex, rx, normals[c], pdiagonal, wdiagonal, Nsdiagonal, gradNparamsdiagonal, xfield, J, nfield)
#                 ex[1,:] .= elxs[c][2,:]; ex[2,:] .= elxs[c][3,:];
#                 B[r, c] += _eint(f, econn, ex, rx, normals[c], pdiagonal, wdiagonal, Nsdiagonal, gradNparamsdiagonal, xfield, J, nfield)
#                 ex[1,:] .= elxs[c][3,:]; ex[2,:] .= elxs[c][1,:];
#                 B[r, c] += _eint(f, econn, ex, rx, normals[c], pdiagonal, wdiagonal, Nsdiagonal, gradNparamsdiagonal, xfield, J, nfield)
#             end
#         end
#     end
#     return B
# end

"""
    tsinglelayer!(B, k, xyz, conn, iroffdiagonal::TriRule, irdiagonal::TriRule)

Compute the single-layer matrix contribution.  Threaded version.

As `singlelayer!(B, k, xyz, conn, iroffdiagonal::TriRule,
irdiagonal::TriRule)`, but the calculation is carried out in parallel using
all available threads.
"""
# function tsinglelayer!(B, k, xyz, conn, iroffdiagonal::TriRule, irdiagonal::TriRule)
# 	neqn = size(conn, 1)
# 	elxs = _allelxs(xyz, conn)
# 	normals = _allnormals(elxs)
# 	rxyz = fill(0.0, neqn, 3)
# 	rcentroid = fill(0.0, 3)
# 	for r = 1:neqn
# 		rxyz[r, :] .= tricntr!(rcentroid, elxs[r])
# 	end
# 	nrows = size(rxyz, 1)
# 	firstrowofset = 0
# 	nth = Base.Threads.nthreads()
# 	_tcolumnchunk = Int(ceil(neqn/nth))
# 	c = 0
# 	while c < neqn
# 		tasks = []
# 		for t in 1:nth
# 			columnrange = c+1:min(neqn, c+1+_tcolumnchunk)
# 			push!(tasks, Threads.@spawn _singlelayercolumns!(B, firstrowofset, columnrange, k, rxyz, xyz, conn, elxs, normals, iroffdiagonal, irdiagonal))
# 			c = maximum(columnrange)
# 			if c >= neqn
# 				break
# 			end
# 		end
# 		Threads.wait.(tasks);
# 	end
# 	return B
# end

"""
    tsinglelayer!(B, k, xyz, conn, iroffdiagonal::TriRule, irdiagonal::TriRule)

Compute the single-layer matrix contribution. Threaded version.

As `singlelayer!(B, k, xyz, conn, iroffdiagonal::TriRule,
irdiagonal::TriRule)`, but the calculation is carried out in parallel using
all available threads.

Don't forget to set the number of threads with
`export JULIA_NUM_THREADS=4`.
"""
# function tsinglelayer!(B, k, chiefxyz, xyz, conn, iroffdiagonal::TriRule, irdiagonal::TriRule)
#     neqn = size(conn, 1)
#     elxs = _allelxs(xyz, conn)
#     normals = _allnormals(elxs)
#     nrows = size(chiefxyz, 1)
#     firstrowofset = neqn
#     nth = Base.Threads.nthreads()
#     _tcolumnchunk = Int(ceil(neqn/nth))
#     c = 0
#     while c < neqn
#     	tasks = []
#     	for t in 1:nth
#     		columnrange = c+1:min(neqn, c+1+_tcolumnchunk)
#     		push!(tasks, Threads.@spawn _singlelayercolumns!(B, firstrowofset, columnrange, k, chiefxyz, xyz, conn, elxs, normals, iroffdiagonal, irdiagonal))
#     		c = maximum(columnrange)
#     		if c >= neqn
#     			break
#     		end
#     	end
#     	Threads.wait.(tasks);
#     end
#     return B
# end

end


using Hexagons
using Test

convert(HexagonOffsetOddR, HexagonAxial(2, 4))

x, y = center(HexagonAxial(2, 3))
@test isapprox(x,7.06217782649107)
@test isapprox(y,5.5)

h = cube_round(23.5, 4.67)

collect(vertices(HexagonAxial(2, 3)))