module C

using Base.Test

# Error free transformations, as described in
# [LaLo05] "Solving Triangular Systems More Accurately and Efficiently", Ph. Langlois and N. Louvet
#
# Notation:
#
#    ⊕ ≡ floating point add
#    ⊖ ≡ floating point subtract
#    ⊗ ≡ floating point multiply
#    ⊘ ≡ floating point divide

function twosum{T<:AbstractFloat}(a::T, b::T)
    x = a+b
    z = x-a
    y = (a-(x-z)) + (b-z)
    (x,y)
end

# Split a floating point number into high and low bits
function split(a::Float64)
    z = a*134217729 #(2^27 + 1)
    x = z - (z-a)
    y = a - x
    (x,y)
end

function fast_split(a::Float64)
    ah = Float64(Float32(a))
    ah, a - ah
end

twoproduct(a,b) = twoproduct(promote(a,b)...)
function twoproduct{T}(a::T,b::T)
    x = a*b
    ah,al = split(a)
    bh,bl = split(b)
    y = al*bl - (((x - ah*bh) - al*bh) - ah*bl)
    (x,y)
end

approx_twoproduct(a,b) = approx_twoproduct(promote(a,b)...)
function approx_twoproduct{T}(a::T,b::T)
    x = a*b
    ah,al = fast_split(a)
    bh,bl = fast_split(b)
    y = al*bl - (((x - ah*bh) - al*bh) - ah*bl)
    (x,y)
end

function approxtwodiv(a,b)
    x = a/b
    v,w = twoproduct(x,b)
    y = (a-v-w)/b
    (x,y)
end

@testset "twosum" begin
    for i=1:1000
        a,b = rand(2)
        x,y = twosum(a,b)

        @test abs(y) < eps(x)
        @test big(a)+big(b) == big(x)+big(y)
    end
end

@testset "twoproduct" begin
    for i=1:1000
        a,b = rand(2)
        x,y = twoproduct(a,b)

        @test abs(y) < eps(x)
        @test big(a)*big(b) == big(x)+big(y)
    end
end

@testset "approxtwodiv" begin
    for i=1:1000
        a,b = rand(2)
        x,y = approxtwodiv(a,b)

        @test abs(y) < eps(x)
        @test abs(big(a)/big(b) - (big(x)+big(y))) < max(eps(a),eps(b))
    end
end

# Want correctly rounded version of
#
#   a * (N-i)/(N-1) + b * (i-1)/(N-1)
#
# Rearrange:
#
# (Ah + Al) * (N-i) + (Bh + Bl) * (i-1)
#
#   R ≡ c + (Mh + Ml) * i
#
#   We have:
#
#       abs(Ml) < eps(Mh) ⇒ abs(Ml⊗i) < 2*eps(Mh⊗i)
#       ... FIXME
#
#   xh,xl = twoproduct(Mh, i)
#   y = xl + Ml*i
#   zh,zl = twosum(c, xh)
#
#

immutable LinSpace{T} <: Range{T}
    a::T
    b::T
    c::T
    Mh::T
    Ml::T
    N::Int
end

macro myshow(ex)
    s = string(ex)
    quote
        println($s * " = " * string(map(x->@sprintf("%a",x), $(esc(ex)))))
    end
end

function LinSpace{T<:AbstractFloat}(a::T,b::T,N::Integer)
    M = (big(b) - big(a)) / (N-1)
    Mh = Float64(M)
    Ml = Float64(M - Mh)
    #@myshow Mh,Ml
    #=
    if abs(a) < abs(b)
        a,b = b,a
    end
    =#
    LinSpace(a,b, a, Mh, Ml, N)
end

LinSpace{T}(a::T,b::T,N) = LinSpace(promote(float(a),float(b))..., N)

Base.size(L::LinSpace) = (L.N,)
Base.linearindexing(L::LinSpace) = Base.LinearFast()

function Base.getindex(L::LinSpace, i::Int)
    # xh+xl = Mh*i
    # yh+yl = Ml*i
    # zh+zl ≈ (Mh+Ml)*(i-1)
    # rh+rl = c + zh
    xh,xl = twoproduct(L.Mh, i-1)
    yh,yl = twoproduct(L.Ml, i-1)
    zh,zl = xh, xl + yh
    rh,rl = twosum(L.c, zh)

    rh + (rl + zl)
    #=
    xl += L.Ml*i
    zh,zl = twosum(L.c, xh)
    zl += xl
    zh + zl
    =#
end

Base.step(L::LinSpace) = L.Mh

function bigindex(L::LinSpace, i::Int)
    big(L.a)*(big(L.N-i)/(L.N-1)) + big(L.b)*(big(i-1)/(L.N-1))
end

function verify(a, b, N, L)
    for i= 1:N
        x = bigindex(L, i)
        d = L[i] - Float64(x)
        if d != 0
            # @show (x - Float64(x))/eps(Float64(x))
            return L, i
        end
    end
    return nothing
end

badly_rounded_cases = []
for i=1:1000
    a = rand()
    b = rand()
    N = rand(1:10000)
    ret = verify(a,b,N, LinSpace(a,b,N))
    if ret !== nothing
        println("(LinSpace($a, $b, $N), $(ret[2]))")
        push!(badly_rounded_cases, ret)
    end
end

L,i = badly_rounded_cases[1]
@myshow bigindex(L,i)
@myshow L[i] - bigindex(L,i)

#=
pf(x) = @printf("%a\n", x)

# Float64 - 53 bits significand
# Float32 - 24 bits significand

a = 0x1.1111111111111p0
b = 0x1.1111111111111p0

af = Float64(Float32(a))
bf = Float64(Float32(b))
ar = a - af
br = b - bf

m  = af*bf # exact
mr = af*ar + bf*br + ar*br # approximate, but with extra bits

#pf(m)
#pf(mr)

@show BigFloat(a) * BigFloat(b)
@show BigFloat(m) + BigFloat(mr)
=#

end

L = C.L


# This is a comment.

function fizzbuzz(i)
    if (i % 3 == 0 && i % 5 == 0)
        return "FizzBuzz"
    elseif (i % 3 == 0)
        return "Fizz"
    elseif (i % 5 == 0)
        return "Buzz"
    else
        return i
    end
end

for i = 1:100
    println(fizzbuzz(i))
end


using Base.BLAS
using DataFrames
using StatsBase

immutable StackedNet{T<:FloatingPoint}
	layers::Vector{Layer{T}}
	error::ASCIIString
	error_function!::Function
	error_function_prime!::Function

	function StackedNet(units::Vector{Units}; error::ASCIIString="squared_error")
		if length(units) < 2
			return error("StackedNet units specification is too short.")
		end
		if minimum([unit.n for unit in units]) <= 0
			return error("Invalid number of units in StackedNet units specification.")
		end
		# Iterate over sequential paits of units and construct the required layers.
		layers = Array(Layer{T}, length(units)-1)
		for u = 1:length(units)-1
			units1, units2 = units[u], units[u + 1]
			layers[u] = Layer{T}(units1.n, units2.n, units2.activation)
		end
		# Set error function.
		error, error_function!, error_function_prime! = error_function_selector(error)
		# Create and return the object.
		new(layers, error, error_function!, error_function_prime!)
	end
end

# Returns the patternwise error on a specific pattern.
function error!{T<:FloatingPoint}(DN::StackedNet{T}, X::Matrix{T}, Y::Matrix{T}, p::Int)
	forward!(DN, X, p)
	L = DN.layers[end]
	y = sub(Y, :, p)
	DN.error_function!(L.ACT, y, L.E)
	sum(L.E)
end

# Returns the patternwise error on a set of patterns.
function error!{T<:FloatingPoint}(DN::StackedNet{T}, X::Matrix{T}, Y::Matrix{T})
	@inbounds begin
		E::T = 0.0
		for p = 1:size(X, 2)
			E += error!(DN, X, Y, p)
		end
		E = E / size(X, 2)
	end
	E
end

# Forward propagate a pattern through a StackedNet.
function forward!{T<:FloatingPoint}(DN::StackedNet{T}, X::Matrix{T}, p::Int)
	@inbounds begin
		x = sub(X, :, p)
		forward!(DN.layers[1], x)
		# forward!(DN.layers[1], X, p)
		for l = 2:length(DN.layers)
			forward!(DN.layers[l], DN.layers[l - 1].ACT)
		end
	end
end

# Returns output activations in a StackedNet for a set of input patterns.
function forward!{T<:FloatingPoint}(DN::StackedNet{T}, X::Matrix{T})
	@inbounds begin
		no = DN.layers[end].no
		np = size(X, 2)
		YH = zeros(T, (no, np))
		for p = 1:np
			forward!(DN, X, p)
			for o = 1:no
				YH[o, p] = DN.layers[end].ACT[o]
			end
		end
	end
	YH
end

# Returns output activations in a StackedNet for a set of input patterns.
function forward!{T<:FloatingPoint}(DN::StackedNet{T}, X::Matrix{T}, YH::Matrix{T})
	@inbounds begin
		for p = 1:np
			forward!(DN, X, p)
			for o = 1:no
				YH[o, p] = DN.layers[end].ACT[o]
			end
		end
	end
end

# Numerically checks the analytic gradient of a StackedNet and returns a data frame of results.
function gradient_check{T<:FloatingPoint}(DN::StackedNet{T}, X::Matrix{T}, Y::Matrix{T}; step=1e-8, abs_tolerance=1e-5, rel_tolerance=1e-3)
	# Initialize data frame.
	df = DataFrame(layer=Int64[], parameter=Int64[], analytic_grad=T[], numerical_grad=T[], absolute_error=T[], ok=Bool[])
	# Iterate over layers, and parameters within each layer.
	for l = 1:length(DN.layers)
		for (M, GM) in ((DN.layers[l].W, DN.layers[l].GW), (DN.layers[l].B, DN.layers[l].GB))
			for i = 1:length(M)
				current_parameter_value = M[i]
				# Set the gradient information.
				gradient_reset!(DN)
				gradient_update!(DN, X, Y)
				# Get the analytic gradient (scale it to reflect patternwise gradient).
				analytic_gradient = GM[i] / size(X, 2)
				# Get the numerical gradient.
				M[i] = current_parameter_value - step
				EN = error!(DN, X, Y)
				M[i] = current_parameter_value + step
				EP = error!(DN, X, Y)
				numerical_gradient = (EP - EN) / (2.0 * step)
				M[i] = current_parameter_value
				# Compare gradient values.
				# if (analytic_gradient == 0.0) & (numerical_gradient == 0.0)
				# 	error = 0.0
				# else
				# 	error = abs(analytic_gradient - numerical_gradient) / (abs(analytic_gradient) + abs(numerical_gradient))
				# end
				error = abs(analytic_gradient - numerical_gradient)
				ok = error <= abs_tolerance + (rel_tolerance * abs(numerical_gradient)) ? true : false
				push!(df, (l, i, analytic_gradient, numerical_gradient, error, ok))
			end
		end
	end
	df
end

# Finds the maximum absolute gradient value.
function gradient_maxabs{T<:FloatingPoint}(DN::StackedNet{T})
	@inbounds begin
		gmax::T = -Inf
		for l = 1:length(DN.layers)
			for M in (DN.layers[l].GW, DN.layers[l].GB)
				thismaxabs = maximum(abs(M))
				if thismaxabs > gmax
					gmax = thismaxabs
				end
			end
		end
	end
	gmax
end

# Zeros out all the gradient information in a StackedNet.
function gradient_reset!{T<:FloatingPoint}(DN::StackedNet{T})
	@inbounds begin
		for l = 1:length(DN.layers)
			fill!(DN.layers[l].GW, 0.0)
			fill!(DN.layers[l].GB, 0.0)
		end
	end
end

# Increment the gradient information (GW and GB) on each layer based on a single input-output pattern.
function gradient_update!{T<:FloatingPoint}(DN::StackedNet{T}, X::Matrix{T}, Y::Matrix{T}, p::Int)
	@inbounds begin
		# Forward propagate the input pattern through the network.
		forward!(DN, X, p)
		# Walk backward through the stacked network.
		nl = length(DN.layers)
		for l = nl:-1:1
			# Set references to layers.
			Lup = l == nl ? nothing : DN.layers[l + 1]
			L = DN.layers[l]
			Ldn = l == 1 ? nothing : DN.layers[l - 1]
			# Backpropagation.
			if l == nl
				y = sub(Y, :, p)
				DN.error_function_prime!(L.ACT, y, L.DE_DYH)
				backward!(L, L.DE_DYH)
			else
				backward!(L, Lup.DELTAS)
			end
			# Increment gradient information.
			axpy!(L.no, 1.0, L.DE_DNET, 1, L.GB, 1)
			if l == 1
				x = sub(X, :, p)
				ger!(1.0, X[:, p], L.DE_DNET, L.GW)
			else
				ger!(1.0, Ldn.ACT, L.DE_DNET, L.GW)
			end
		end
	end
end

# Increment the gradient information (GW and GB) on each layer based on a set of input-output pairs.
function gradient_update!{T<:FloatingPoint}(DN::StackedNet{T}, X::Matrix{T}, Y::Matrix{T})
	@inbounds begin
		for p = 1:size(X, 2)
			gradient_update!(DN, X, Y, p)
		end
	end
end

# Update the parameters of a StackedNet based on the accumulated gradient information.
function parameters_update!{T<:FloatingPoint}(DN::StackedNet{T}, learning_rate::T; reset_gradient::Bool=true)
	@inbounds begin
		#lr_use = get_learning_rate(DN, lr)
		for l = 1:length(DN.layers)
			L = DN.layers[l]
			for i = 1:length(L.B)
				L.B[i] -= learning_rate * sign(L.GB[i])
			end
			for i = 1:length(L.W)
				L.W[i] -= learning_rate * sign(L.GW[i])
			end
			# for i = 1:length(L.GB)
			# 	L.UB[i] = 0.9 * L.UB[i] + 0.1 * abs(L.GB[i])
			# 	if L.UB[i] > 0.0
			# 		L.B[i] -= (lr_use / L.UB[i]) * L.GB[i]
			# 	else
			# 		L.B[i] -= lr_use * L.GB[i]
			# 	end
			# end
			# for i = 1:length(L.GW)
			# 	L.UW[i] = 0.9 * L.UW[i] + 0.1 * abs(L.GW[i])
			# 	if L.UW[i] > 0.0
			# 		L.W[i] -= (lr_use / L.UW[i]) * L.GW[i]
			# 	else
			# 		L.W[i] -= lr_use * L.GW[i]
			# 	end
			# end
			# axpy!(length(L.W), -lr, L.GW, 1, L.W, 1)
			# axpy!(length(L.B), -lr, L.GB, 1, L.B, 1)
			if reset_gradient
				fill!(L.GW, 0.0)
				fill!(L.GB, 0.0)
			end
		end
	end
end

function get_learning_rate{T<:FloatingPoint}(DN::StackedNet{T}, max_parameter_change::T)
	gmax = gradient_maxabs(DN)
	max_parameter_change / gmax
end

function train_sgd!{T<:FloatingPoint}(DN::StackedNet{T}, X_training::Matrix{T}, Y_training::Matrix{T}; X_testing::Union(Nothing, Matrix{T})=nothing, Y_testing::Union(Nothing, Matrix{T})=nothing, custom_error::Union(Nothing, Function)=nothing, iterations::Int=1000, iterations_report::Int=100, learning_rate::T=1e-2, minibatch_size::Int=100, minibatch_replace::Bool=true)
	@inbounds begin
		num_patterns::Int64 = size(X_training, 2)
		# Minibatch size cannot be larger than number of patterns when sampling without replacement.
		if (minibatch_size > num_patterns) && !minibatch_replace
			minibatch_size = num_patterns
		end
		# Reserve space for minibatch integers.
		minibatch_ints = zeros(Int, minibatch_size)
		minibatch_domain = 1:num_patterns
		# Create results dataframe if reporting is required.
		if custom_error == nothing
			df = DataFrame(iteration=Int64[], error_training=T[], error_testing=T[])
		else
			df = DataFrame(iteration=Int64[], error_training=T[], error_testing=T[], custom_error_training=T[], custom_error_testing=T[])
		end
		# Reserve space for output predictions if custom error function specified.
		if custom_error != nothing
			YH_training = zeros(T, (DN.layers[end].no, size(X_training, 2)))
			YH_testing = zeros(T, (DN.layers[end].no, size(X_testing, 2)))
		end
		# Perform the required number of iterations of learning.
		gradient_reset!(DN)
		for iteration = 1:iterations
			# Increment the gradient information based on the minibatch.
			sort!(sample!(minibatch_domain, minibatch_ints, replace=minibatch_replace))
			for minibatch_int in minibatch_ints
				gradient_update!(DN, X_training, Y_training, minibatch_int)
			end
			# Update the parameters based on the gradient information.
			parameters_update!(DN, learning_rate, reset_gradient=true)
			# Decide whether to record performance.
			if (iteration % iterations_report) == 0
				# Construct row to add to dataframe.
				if custom_error == nothing
					row = (iteration, error!(DN, X_training, Y_training), error!(DN, X_testing, Y_testing))
				else
					forward!(DN, X_training, YH_training)
					forward!(DN, X_testing, YH_testing)
					row = (iteration, error!(DN, X_training, Y_training), error!(DN, X_testing, Y_testing), custom_error(YH_training, Y_training), custom_error(YH_testing, Y_testing))
				end
				push!(df, row)
				println("Iteration $iteration. Training error $(df[end, :error_training]). Testing error $(df[end, :error_testing]).")
			end
		end
	end
	df
end


julia> a = [1,2,3]
3-element Array{Int64,1}: ...
julia> a = []
0-element Array{Any,1}: ...
julia> a = Int64[]
0-element Array{Int64,1}: ...
julia> a = Array{Float32, 1}()
0-element Array{Float32, 1}: ...
julia> a = Vector{Int64}()
0-element Array{Int64,1}: ...
julia> a = zeros(Int64, 3)
3-element Array{Int64, 1): ...
julia> a = ones(Int8, 3)
3-element Array{Int8,1}: ...
a = Array{Int64, 1}(undef,3)
3-element Array{Int64,1}: ...
julia> a = fill("a", 3)
3-element Array{String,1}: ...
julia> a = rand(3)
3-element Array{Float64,1}: ...


"""
    r = RationalTransferFunction(num::AbstractPolynomial, den::AbstractPolynomial, Ts:Real)

Construct a rational transfer function model `r` from its numerator and denominator polynomials
`num` and `den`, respectively, and sampling time `Ts`.

If `r::RationalTransferFunction{T,λ,P <: Polynomial(T,λ),Ts}` is a rational transfer function system model
object defined as `r(λ) = num(λ)/den(λ)`, where  `num(λ)` and `den(λ)` are polynomials
with coefficients in `T` and with the indeterminate `λ`, and `Ts` is the sampling time, then:

`r.num` is the numerator polynomial `num(λ)`;

`r.den` is the denominator polynomial `den(λ)`.

The sampling time `Ts` can have the following values:

  - `Ts = 0` for a continuous-time system and
             `λ = s` is the complex variable in the Laplace transform;

  - `Ts > 0` or `Ts = -1` for a discrete-time system and
             `λ = z` is the complex variable in the `Z`-transform;
             `Ts = -1` indicates a discrete-time system with an unspecified sampling time.

The sampling time can be obtained as `r.Ts`.
The symbol (or _variable_) used for the indeterminate `λ` is the common symbol used for the
indeterminates of the polynomials `num(λ)` and `den(λ)` and can be obtained as `r.var`.
The roots of the numerator polynomial `num(λ)` (also called _zeros_ of `r(λ)`)
can be obtained as `r.zeros`, while the roots of the denominator polynomial `den(λ)`
(also called _poles_ of `r(λ)`) can be obtained as `r.poles`.
The ratio of the leading polynomial coefficients of `num(λ)` and `den(λ)`
(also called _gain_ of `r(λ)`) can be obtained as `r.gain`.
"""
struct RationalTransferFunction{T,X,P<:AbstractPolynomial{T,X},Ts} <: AbstractRationalFunction{T,X,P}
    ## Define RationalTransferFunction as a subtype of AbstractRationalFunction
    num::P
    den::P
    function RationalTransferFunction{T,X,P,Ts}(num::P, den::P) where{T, X, P<:AbstractPolynomial{T,X}, Ts}
        check_den(den)
        new{T,X,P,Ts}(num,den)
    end
    function RationalTransferFunction{T,X,P,Ts}(num::P, den::P, ts::Union{Real,Nothing}) where {T, X, P<:AbstractPolynomial{T,X}, Ts}
        check_den(den)
        check_Ts(Ts,ts)
        new{T,X,P,Ts}(num,den)
    end
    # can promote constants to polynomials if S <: T
    function  RationalTransferFunction{T,X,P,Ts}(num::S, den::P, ts::Union{Real,Nothing}) where {S, T, X, P<:AbstractPolynomial{T,X}, Ts}
        check_den(den)
        check_Ts(Ts,ts)
        new{T,X,P,Ts}(convert(P,num),den)
    end
    function  RationalTransferFunction{T,X,P,Ts}(num::P, den::S, ts::Union{Real,Nothing}) where {S, T, X, P<:AbstractPolynomial{T,X}, Ts}
        check_den(den)
        check_Ts(Ts,ts)
        new{T,X,P,Ts}(num, convert(P,den))
    end
    function RationalTransferFunction{T,X,P}(num::P, den::P, Ts::Union{Real,Nothing}) where {T,X,P<:AbstractPolynomial{T,X}}
        check_den(den)
        Ts′ = standardize_Ts(Ts)
        #new{T,X,P,Val(Ts′)}(num,den)
        new{T,X,P,Ts′}(num,den)
    end
end

#Base.eltype(sys::RationalTransferFunction) = eltype(sys.num)
_eltype(R::RationalTransferFunction) = eltype(R.num)
_eltype(R::VecOrMat{<:RationalTransferFunction}) = length(R) == 0 ? eltype(eltype(Polynomials.coeffs.(numpoly.(R)))) : eltype(eltype(R[1]))
_eltype(R::VecOrMat{<:Polynomial}) = length(R) == 0 ? eltype(eltype(Polynomials.coeffs.(R))) : eltype(eltype(R[1]))

#(pq::RationalTransferFunction)(x) = Polynomials.eval_rationalfunction(x, pq)
(pq::RationalTransferFunction)(x) = pq.num(x)/pq.den(x)

function Base.getproperty(F::RationalTransferFunction{T,X,P,Ts}, d::Symbol)  where {T,X,P,Ts}
    if d === :Ts
        return Ts
    elseif d === :var
        return X
    elseif d === :zeros
        return roots(getfield(F, :num))
    elseif d === :poles
        return roots(getfield(F, :den))
    elseif d === :gain
        return last(getfield(F, :num)) / last(getfield(F, :den))
    else
        getfield(F, d)
    end
end
Base.propertynames(F::RationalTransferFunction) =
    (:Ts, :var, :zeros, :poles, :gain, fieldnames(typeof(F))...)
poles(F::RationalTransferFunction) = F.poles
poles(F::Polynomial{T}) where T = zeros(T,0)
gain(F::RationalTransferFunction) = F.gain
gain(F::Polynomial) = last(F.coeffs)
gpole(F::RationalTransferFunction) = [F.poles; Inf*ones(Int,max(0,degree(F.num)-degree(F.den)))]
gzero(F::RationalTransferFunction) = [F.zeros; Inf*ones(Int,max(0,degree(F.den)-degree(F.num)))]

function Base.convert(::Type{PQ}, pq::RationalTransferFunction) where {PQ <:RationalFunction}
    p,q = pq
    p//q
end

# alternate constructors
function RationalTransferFunction(p′::P, q′::Q, Ts::Union{Real,Nothing}) where {T, X, P<:AbstractPolynomial{T,X},
                                                                                S,  Q<:AbstractPolynomial{S,X}}
    p, q = promote(p′, q′)
    RationalTransferFunction{eltype(p),X,typeof(p)}(p,q,Ts)
end
function RationalTransferFunction(p′::P, q′::S, Ts::Union{Real,Nothing}) where {S, T, X, P<:AbstractPolynomial{T,X}}
    p, q = promote(p′, q′)
    RationalTransferFunction{eltype(p),X,typeof(p)}(p,q,Ts)
 end
 function RationalTransferFunction(p′::S, q′::P, Ts::Union{Real,Nothing}) where {S, T, X, P<:AbstractPolynomial{T,X}}
    p, q = promote(p′, q′)
    RationalTransferFunction{eltype(q),X,typeof(q)}(p,q,Ts)
 end

function Polynomials.rational_function(::Type{PQ}, p::P, q::Q) where {PQ <:RationalTransferFunction,
                                                          T, X, P<:AbstractPolynomial{T,X},
                                                          S,    Q<:AbstractPolynomial{S,X}}
    RationalTransferFunction(promote(p,q)..., sampling_time(PQ))
end

## helpers for constructors
# standardize Ts or throw error
function standardize_Ts(Ts)
    isnothing(Ts) || Ts >= 0 || Ts == -1 ||
        throw(ArgumentError("Ts must be either a positive number, 0 (continuous system), or -1 (unspecified)"))
    Ts′ = isnothing(Ts) ? Ts : Float64(Ts)
end
function check_Ts(Ts, ts)
    # ValT(Ts) == promote_Ts(ValT(Ts), ts) || throw(ArgumentError("sampling times have mismatch"))
    ValT(Val(Ts)) == promote_Ts(ValT(Val(Ts)), ts) || throw(ArgumentError("sampling times have mismatch"))
end
function check_den(den)
    iszero(den) && throw(ArgumentError("Cannot create a rational function with zero denominator"))
end

ValT(::Val{T}) where {T} = T
# sampling_time(pq::RationalTransferFunction{T,X,P,Ts}) where {T,X,P,Ts} = ValT(Ts)
# sampling_time(::Type{𝑷}) where {T,X,P,Ts, 𝑷<:RationalTransferFunction{T,X,P,Ts}} = ValT(Ts)
sampling_time(pq::RationalTransferFunction{T,X,P,Ts}) where {T,X,P,Ts} = ValT(Val(Ts))
sampling_time(::Type{𝑷}) where {T,X,P,Ts, 𝑷<:RationalTransferFunction{T,X,P,Ts}} = ValT(Val(Ts))
sampling_time(::Type{<:Number}) = nothing
sampling_time(p::Number) = nothing

"""
    r = rtf(num, den; Ts = rts, var = rvar )

Create the rational transfer function `r(λ) = num(λ)/den(λ)` with the polynomials `num(λ)` and `den(λ)`,
sampling time `rts` and variable name `rvar`, representing
the transfer function of a single-input single-output system of the form

    Y(λ) = r(λ) U(λ),

where `U(λ)` and `Y(λ)` are the Laplace or `Z` transformed input `u(t)` and output `y(t)`, respectively,
and `λ = s`, the complex variable in the Laplace transform, if `rts = 0`, or  `λ = z`,
the complex variable in the `Z` transform, if `rts ≠ 0`.
Both `num` and `den` can be real or complex numbers as well.

The resulting `r` is such that `r.Ts = rts` (default `rts = 0`) and `r.var = rvar`.
The default value of `rvar` is `rvar = Polynomials.indeterminate(num)` if `num` is a polynomial,
`rvar = Polynomials.indeterminate(den)` if `num` is a number and `den` is a polynomial, and
`rvar = :s` if both `num` and `den` are numbers.
"""
function rtf(num::Polynomial, den::Polynomial; Ts::Real = 0,
             var::Symbol = promote_var(num,den))
    T = promote_type(eltype(num),eltype(den))
    return RationalTransferFunction{T,var,Polynomial{T,var},Float64(Ts)}(Polynomial{T,var}(num.coeffs), Polynomial{T,var}(den.coeffs))
end
function rtf(num::Polynomial, den::Number; Ts::Real = 0,  var::Symbol = Polynomials.indeterminate(num))
    T = promote_type(eltype(num),eltype(den))
    return RationalTransferFunction{T,var,Polynomial{T,var},Float64(Ts)}(Polynomial{T,var}(num.coeffs), Polynomial{T,var}(den))
end
function rtf(num::Number, den::Polynomial; Ts::Real = 0,  var::Symbol = Polynomials.indeterminate(den))
    T = promote_type(eltype(den),eltype(num))
    return RationalTransferFunction{T,var,Polynomial{T,var},Float64(Ts)}(Polynomial{T,var}(num),Polynomial{T,var}(den.coeffs))
end
function rtf(num::Number, den::Number; Ts::Real = 0, var::Symbol = :s)
    T = promote_type(eltype(num),eltype(den))
    return RationalTransferFunction{T,var,Polynomial{T,var},Float64(Ts)}(Polynomial{T,var}(num),Polynomial{T,var}(den))
end

# display sys
Base.print(io::IO, sys::RationalTransferFunction) = show(io, sys)
Base.show(io::IO, sys::RationalTransferFunction) = show(io, MIME("text/plain"), sys)

function Base.show(io::IO, mime::MIME{Symbol("text/plain")}, sys::RationalTransferFunction)
    summary(io, sys); println(io)
    p, q = Polynomials.pqs(sys)
    if order(sys) == 0
        printpoly(io, p)
        println(io, "\n\nStatic gain.")
        return
    end
    if q == one(q)
        printpoly(io, p, descending_powers=true)
    else
        count(p.coeffs .!= 0) <= 1 ? printpoly(io, p, descending_powers=true) :
             (print(io,"("); printpoly(io, p, descending_powers=true); print(io, ")") )
        print(io, " // ")
        count(q.coeffs .!= 0) <= 1 ? printpoly(io, q, descending_powers=true) :
             (print(io,"("); printpoly(io, q, descending_powers=true); print(io, ")") )
    end
    sys.Ts < 0 && println(io, "\n\nSample time: unspecified.")
    sys.Ts > 0 && println(io, "\n\nSample time: $(sys.Ts) second(s).")
    iszero(sys.Ts) ? println(io, "\n\nContinuous-time rational transfer function model.") :
                     println(io, "Discrete-time rational transfer function model.")
end

function show_rp(io::IO, sys::RationalTransferFunction)
    p,q = Polynomials.pqs(sys)
    if order(sys) == 0
        printpoly(io, p, descending_powers=true)
        return
    end
    if q == one(q)
        printpoly(io, p, descending_powers=true)
    else
        count(p.coeffs .!= 0) <= 1 ? printpoly(io, p, descending_powers=true) :
             (print(io,"("); printpoly(io, p, descending_powers=true); print(io, ")") )
        print(io, " // ")
        count(q.coeffs .!= 0) <= 1 ? printpoly(io, q, descending_powers=true) :
             (print(io,"("); printpoly(io, q, descending_powers=true); print(io, ")") )
    end
end

Base.print(io::IO, R::VecOrMat{<:RationalTransferFunction}) = show(io, R)
Base.show(io::IO, R::VecOrMat{<:RationalTransferFunction}) = show(io, MIME("text/plain"), R)

function Base.show(io::IO, mime::MIME{Symbol("text/plain")}, R::VecOrMat{<:RationalTransferFunction})
    summary(io, R); println(io)
    p, m = size(R,1), size(R,2)
    for j=1:m
         for i=1:p
            println(io, "From input $j to output $i")
            show_rp(io, R[i,j])
            print(io, "\n\n")
        end
    end
    Ts = sampling_time(R[1,1])
    Ts < 0 && println(io, "Sample time: unspecified.")
    Ts > 0 && println(io, "Sample time: $Ts second(s).")
    iszero(Ts) ? println(io, "Continuous-time rational transfer function matrix model.") :
                 println(io, "Discrete-time rational transfer function matrix model.")
end

function promote_var(num::Polynomial, den::Polynomial)
    m = length(num)
    n = length(den)
    if m > 1 &&  n > 1
       var = Polynomials.indeterminate(num)
       var != Polynomials.indeterminate(den) && error("The numerator and denominator polynomials must have the same variable")
    elseif m > 1
       var = Polynomials.indeterminate(num)
    elseif  n > 1
       var = Polynomials.indeterminate(den)
    else
       var = :s   # use the default variable for continuous-time systems
    end
    return var
end

"""
    r = rtf(f; Ts = rts, var = rvar)

Create the rational transfer function `r(λ) = f(λ)` with sampling time `rts` and variable name `rvar`
such that:

(1) if `f(λ)` is a rational transfer function, then `r.Ts = rts` (default `rts = f.Ts`) and `r.var = rvar` (default `rvar = f.var`);

(2) if `f(λ)` is a rational function, then `r.Ts = rts` (default `rts = 0`) and `r.var = rvar` (default `rvar = Polynomials.indeterminate(f.num)`);

(3) if `f(λ)` is a polynomial, then `r.Ts = rts` (default `rts = 0`) and `r.var = rvar` (default `rvar = Polynomials.indeterminate(f)`);

(4) if `f(λ)` is a ral or complex number, then `r.Ts = rts` (default `rts = 0`) and `r.var = rvar` (default `rvar = :s`);
"""
function rtf(f::RationalTransferFunction; Ts::Real = f.Ts, var::Symbol = f.var)
    T = eltype(eltype(f))
    return RationalTransferFunction{T,var,Polynomial{T,var},Float64(Ts)}(Polynomial{T,var}(f.num.coeffs), Polynomial{T,var}(f.den.coeffs))
end
function rtf(f::AbstractRationalFunction; Ts::Real = 0, var::Symbol = Polynomials.indeterminate(f.num))
    T = eltype(eltype(f))
    return RationalTransferFunction{T,var,Polynomial{T,var},Float64(Ts)}(Polynomial{T,var}(f.num.coeffs), Polynomial{T,var}(f.den.coeffs))
end
function rtf(p::P; Ts::Real = 0, var::Symbol = Polynomials.indeterminate(p)) where {T,X,P<:AbstractPolynomial{T,X}}
    #T = eltype(p)
    #return RationalTransferFunction{T}(Polynomial{T}(p.coeffs,var), Polynomial{T}(one(T),var), Ts)
    #return RationalTransferFunction{T,var}(Polynomial{T,var}(p.coeffs), Polynomial{T,var}(one(T)), Ts)
    return RationalTransferFunction{T,var,Polynomial{T,var},Float64(Ts)}(Polynomial{T,var}(p.coeffs), Polynomial{T,var}(one(T)))
end
function rtf(num::Number; Ts::Real = 0, var::Symbol = :s)
    T = eltype(num)
    return RationalTransferFunction{T,var,Polynomial{T,var},Float64(Ts)}(Polynomial{T,var}(num), Polynomial{T,var}(one(T)))
end

"""
    r = rtf(var; Ts = rts)
    r = rtf('s') or r = rtf('z'; Ts = rts)
    r = rtf("s") or r = rtf("z"; Ts = rts)
    r = rtf(:s) or r = rtf(:z; Ts = rts)

Create the rational transfer function `r(λ) = λ`, such that `r.var` and `r.Ts` are set as follows:

    (1) `r.var = :s` and `r.Ts = 0` if `var = 's'`, or `var = "s"` or `var = :s` ;

    (2) `r.var = :z` and `r.Ts = rts` if `var = 'z'`, or `var = "z"` or `var = :z`;

    (3) `r.var = var` and `r.Ts = rts` (default `rts = 0.`) otherwise.
"""
function rtf(::Type{T}, var::Union{AbstractString,Char,Symbol}; Ts::Union{Real,Missing} = missing) where T
    if var == "s" || var == 's' || var === :s
       ismissing(Ts) && (Ts = 0)
       Ts == 0. ? (return rtf(Polynomial{T}([0,1],:s),Ts=Ts)) : error("Ts must be zero for a continuous-time system")
    elseif var == "z" || var == 'z' || var === :z
       ismissing(Ts) && (Ts = -1)
       (Ts == -1 || Ts > 0) ? (return rtf(Polynomial{T}([0,1],:z),Ts = Ts)) : error("Ts must be either a positive number or -1 (unspecified)")
    else
       ismissing(Ts) && (Ts = 0.)
       Ts >= 0 || Ts == -1 || error("Ts must be either a positive number, 0 (continuous system), or -1 (unspecified)")
       return rtf(Polynomial{T}([0,1],var),Ts = Ts)
    end
end
rtf(var::Union{AbstractString,Char,Symbol}; Ts::Union{Real,Missing} = missing) = rtf(Int, var; Ts = Ts)
function Base.:/(p::AbstractPolynomial,q::AbstractPolynomial)
    RationalFunction(p,q)
end

"""
    r = rtf(z, p, k; Ts = rts, var = rvar)

Create from the roots (zeros) `z`, poles `p`, gain `k`, sampling time `rts` and variable name `rvar`
the rational transfer function `r(λ) = k*num(λ)/den(λ)`, where `num(λ)` and `den(λ)` are monic polynomials
with roots equal `z` and `p`, respectively, and such that `r.Ts = rts` (default `rts = 0`)
 and `r.var = rvar` (default `rvar = :s` if `Ts = 0` or `rvar = :z` if `Ts ≠ 0`).
"""
function rtf(zer::Vector{<:Number}, pol::Vector{<:Number}, k::Number; Ts::Real = 0,
             var::Symbol = (Ts == 0 ? :s : :z) )
    Ts >= 0. || Ts == -1. || error("Ts must be either a positive number, 0 (continuous system), or -1 (unspecified)")
    T = promote_type(eltype(zer),eltype(pol),eltype(k))
    reald = false   # check data correspond to real numerator and denominator
    if eltype(k) <: Real
       tempc = zer[imag.(zer) .> 0]
       if isempty(tempc)
          realz = true
       else
          tempc1 = conj(zer[imag.(zer) .< 0])
          realz = isequal(tempc[sortperm(real(tempc))],tempc1[sortperm(real(tempc1))])
       end
       if realz
          tempc = pol[imag.(pol) .> 0]
          if isempty(tempc)
             realp = true
          else
             tempc1 = conj(pol[imag.(pol) .< 0])
             realp = isequal(tempc[sortperm(real(tempc))],tempc1[sortperm(real(tempc1))])
          end
          reald = realp
       end
    end
    num = reald ? real(k*fromroots(zer)) :  k*fromroots(zer)
    den = reald ? real(fromroots(pol)) :  k*fromroots(pol)
    return RationalTransferFunction{T,var,Polynomial{T,var},Float64(Ts)}(Polynomial{T,var}(num.coeffs), Polynomial{T,var}(den.coeffs))
end

## ----
function Base.convert(::Type{PQ}, p::R) where {T,X,P,Ts,PQ <: RationalTransferFunction{T,X,P,Ts}, R<:AbstractRationalFunction}
    PQ(Polynomial(T.(p.num.coeffs),X), Polynomial(T.(p.den.coeffs),X), sampling_time(PQ))
end

function Base.convert(::Type{PQ}, p::P1) where {T,X,P,Ts,PQ <: RationalTransferFunction{T,X,P,Ts}, P1<:AbstractPolynomial}
    #PQ(p, one(p), sampling_time(PQ))
    PQ(Polynomial(T.(p.coeffs),X), Polynomial(one(T),X), sampling_time(PQ))
end
function Base.convert(::Type{PQ}, p::Number) where {PQ <: RationalTransferFunction}
    PQ(p, one(eltype(PQ)), sampling_time(PQ))
end

function promote_Ts(p,q)
    Ts1,Ts2 = sampling_time.((p,q))
    promote_Ts(Ts1, Ts2)
end

function promote_Ts(Ts1::Union{Real,Nothing}, Ts2::Union{Real,Nothing})
    isnothing(Ts1) && (return Ts2)
    isnothing(Ts2) && (return Ts1)
    Ts1 == Ts2 && (return Ts1)
    Ts1 == -1 && (Ts2 > 0 ? (return Ts2) : error("Sampling time mismatch"))
    Ts2 == -1 && (Ts1 > 0 ? (return Ts1) : error("Sampling time mismatch"))
    error("Sampling time mismatch")
end

function Base.promote_rule(::Type{PQ}, ::Type{PQ′}) where {T,X,P,Ts,PQ <: RationalTransferFunction{T,X,P,Ts},
                                                           T′,X′,P′,Ts′,PQ′ <: RationalTransferFunction{T′,X′,P′,Ts′}}
    S = promote_type(T,T′)
    Polynomials.assert_same_variable(X,X′)
    Y = X
    Q = promote_type(P, P′)
    ts = promote_Ts(PQ, PQ′)
    #RationalTransferFunction{S,Y,Q,Val(ts)}
    RationalTransferFunction{S,Y,Q,ts}
end
function Base.promote_rule(::Type{PQ}, ::Type{PQ′}) where {T,X,P,Ts,PQ <: RationalTransferFunction{T,X,P,Ts},
                                                           T′,X′,P′,PQ′ <: RationalFunction{T′,X′,P′}}
    S = promote_type(T,T′)
    Polynomials.assert_same_variable(X,X′)
    Y = X
    Q = promote_type(P, P′)
    ts = sampling_time(PQ)
     #RationalTransferFunction{S,Y,Q,Val(ts)}
    RationalTransferFunction{S,Y,Q,ts}
end

function Base.promote_rule(::Type{PQ}, ::Type{P′}) where {T,X,P,Ts,PQ <: RationalTransferFunction{T,X,P,Ts},
                                                          T′,X′,P′ <: Polynomial{T′,X′}}
    S = promote_type(T,T′)
    Polynomials.assert_same_variable(X,X′)
    Y = X
    Q = promote_type(P, P′)
    ts = sampling_time(PQ)
    #RationalTransferFunction{S,Y,Q,Val(ts)}
    RationalTransferFunction{S,Y,Q,ts}
end

#Base.promote_rule(::Type{PQ}, ::Type{P}) where {PQ <: RationalTransferFunction, P<:AbstractPolynomial} = PQ
Base.promote_rule(::Type{PQ}, ::Type{P}) where {PQ <: RationalTransferFunction, P<:Number} = PQ

Base.zero(::Type{PQ}) where {T,X,P,Ts,PQ <: RationalTransferFunction{T,X,P,Ts}}  = RationalTransferFunction{T,X,P,Ts}(zero(P), one(P))
Base.zero(f::RationalTransferFunction) = zero(typeof(f))
Base.one(::Type{PQ}) where {T,X,P,Ts,PQ <: RationalTransferFunction{T,X,P,Ts}}  = RationalTransferFunction{T,X,P,Ts}(one(P), one(P))
Base.one(f::RationalTransferFunction) = one(typeof(f))

"""
     zpk(r) -> (z, p, k)

Compute the roots (zeros) `z`, poles `p` and gain `k` of the rational transfer function `r(λ)`.
"""
function zpk(F::RationalTransferFunction)
    return F.zeros, F.poles, F.gain
end
function zpk(F::Polynomial{T}) where T
    return roots(F), zeros(T,0), gain(F)
end

"""
     rt = adjoint(r)

Compute the adjoint `rt(λ)` of the rational transfer function `r(λ)` such that for
`r(λ) = num(λ)/den(λ)` we have:

    (1) `rt(λ) = conj(num(-λ))/conj(num(-λ))`, if `r.Ts = 0`;

    (2) `rt(λ) = conj(num(1/λ))/conj(num(1/λ))`, if `r.Ts = -1` or `r.Ts > 0`.
"""
function Base.adjoint(f::RationalTransferFunction)
    if f.Ts == 0
       p1 = copy(conj(f.num.coeffs))
       i1 = 2:2:length(p1)
       p1[i1] = -p1[i1]
       p2 = copy(conj(f.den.coeffs))
       i2 = 2:2:length(p2)
       p2[i2] = -p2[i2]
       return RationalTransferFunction(Polynomial(p1,Polynomials.indeterminate(f.num)), Polynomial(p2,Polynomials.indeterminate(f.den)), f.Ts)
    else
       m = degree(f.num)+1
       n = degree(f.den)+1
       p1 = reverse(conj(f.num.coeffs[1:m]))
       p2 = reverse(conj(f.den.coeffs[1:n]))
       if m >= n
          return RationalTransferFunction(Polynomial(p1,Polynomials.indeterminate(f.num)), Polynomial([zeros(eltype(p2),m-n); p2],Polynomials.indeterminate(f.den)), f.Ts)
       else
          return RationalTransferFunction(Polynomial([zeros(eltype(p1),n-m); p1],Polynomials.indeterminate(f.num)), Polynomial(p2,Polynomials.indeterminate(f.den)), f.Ts)
       end
    end
end
transpose(f::RationalTransferFunction) = f

denpoly(f::RationalTransferFunction) = f.den
numpoly(f::RationalTransferFunction) = f.num
denpoly(f::Number) = one(Polynomial{eltype(f)})
numpoly(f::Number) = Polynomial{eltype(f)}(f)

"""
    n = order(r)

Determine the order `n` of a rational transfer function `r` as the maximum of degrees of its
numerator and denominator polynomials (`n` is also known as the _McMillan degree_ of `r`).
"""
function order(r::RationalTransferFunction)
    max(degree(r.num),degree(r.den))
end

# operations
function Base.:+(p::R, q::R) where {T,X,P,Ts,R <: RationalTransferFunction{T,X,P,Ts}}
    p0,p1 = pqs(p)
    q0,q1 = pqs(q)
    Polynomials.rational_function(R, p0*q1 + p1*q0, p1*q1)
end
Base.:+(p::Number, q::RationalTransferFunction) = q + p
function Base.:+(p::R,  q::Number) where {T,X,Q,Ts,R <: RationalTransferFunction{T,X,Q,Ts}}
    Tx = promote_type(T,eltype(q))
    p0,p1 = pqs(p)
    #p + q*one(p)
    Polynomials.rational_function(RationalTransferFunction{Tx,X,Polynomial{Tx,X},Ts}, p0 + p1*q, p1)
end
Base.:+(p::Polynomial, q::RationalTransferFunction) = q + p
#Base.:+(p::RationalTransferFunction, q::RationalTransferFunction) = +(promote(p,q)...)
function Base.:+(p::R, q::P) where {T,X,Q,Ts,R <: RationalTransferFunction{T,X,Q,Ts}, T1, P <: Polynomial{T1,X}}
    Tx = promote_type(T,T1)
    p0,p1 = pqs(p)
    Polynomials.rational_function(RationalTransferFunction{Tx,X,Polynomial{Tx,X},Ts}, p0 + p1*q, p1)
end

function Base.:-(p::R, q::R) where {T,X,P,Ts,R <: RationalTransferFunction{T,X,P,Ts}}
    p0,p1 = pqs(p)
    q0,q1 = pqs(q)
    Polynomials.rational_function(R, p0*q1 - p1*q0, p1*q1)
end

function Base.:*(p::R, q::R) where {T,X,P,Ts,R <: RationalTransferFunction{T,X,P,Ts}}
    p0,p1 = pqs(p)
    q0,q1 = pqs(q)
    Polynomials.rational_function(R, p0*q0, p1*q1)
end

# equality
import Base: ==
function ==(p::RationalTransferFunction{T,X,P,T1}, q::RationalTransferFunction{S,Y,Q,T2}) where {T,X,P,T1,S,Y,Q,T2}
    isconstant(p.num) && isconstant(p.den) && isconstant(q.num) && isconstant(q.den) && p(0) == q(0) && return true
    (X == Y && T1 == T2) || return false
    p₀, p₁ = pqs(p)
    q₀, q₁ = pqs(q)
    p₀ * q₁ == q₀ * p₁ || return false
end
function ==(p::RationalTransferFunction{T,X,P,T1}, q::Polynomial{S,Y}) where {T,X,P,T1,S,Y}
    isconstant(p.num) && isconstant(p.den) && isconstant(q) && p(0) == q(0) && return true
    X == Y || return false
    p₀, p₁ = pqs(p)
    p₀ == p₁ * q || return false
end
==(p::Polynomial{S,Y}, q::RationalTransferFunction{T,X,P,T1}) where {S,Y,T,X,P,T1} = ==(q,p)

function Base.isapprox(pq₁::PQ₁, pq₂::PQ₂,
                       rtol::Real = sqrt(eps(float(real(promote_type(T,S))))),
                       atol::Real = zero(real(promote_type(T,S)))) where
                                {T,X,P,T1,PQ₁<:RationalTransferFunction{T,X,P,T1},
                                 S,Y,Q,T2,PQ₂<:RationalTransferFunction{S,Y,Q,T2}}

    p₁,q₁ = pqs(pq₁)
    p₂,q₂ = pqs(pq₂)

    return X == Y && T1 == T2 && isapprox(p₁*q₂, q₁*p₂; rtol=rtol, atol=atol)
end
function Base.isapprox(pq₁::PQ₁, q₂::Q₂,
                       rtol::Real = sqrt(eps(float(real(promote_type(T,S))))),
                       atol::Real = zero(real(promote_type(T,S)))) where
                        {T,X,P,T1,PQ₁<:RationalTransferFunction{T,X,P,T1}, S,Y,Q₂<:Polynomial{S,Y}}

    p₁,q₁ = pqs(pq₁)
    return X == Y && isapprox(p₁, q₁*q₂; rtol=rtol, atol=atol)
end
Base.isapprox(q₁::Q₁, pq₂::PQ₂,
              rtol::Real = sqrt(eps(float(real(promote_type(T,S))))),
              atol::Real = zero(real(promote_type(T,S)))) where
              {T,X,P,T2,PQ₂<:RationalTransferFunction{T,X,P,T2},S,Y,Q₁<:Polynomial{S,Y}} = isapprox(pq₂,q₁)
Base.isapprox(q₁::Number, pq₂::PQ₂,
              rtol::Real = sqrt(eps(float(real(promote_type(T,eltype(q₁)))))),
              atol::Real = zero(real(promote_type(T,eltype(q₁))))) where
              {T,X,P,T2,PQ₂<:RationalTransferFunction{T,X,P,T2}} = isapprox(pq₂,q₁*one(pq₂))
Base.isapprox(pq₂::PQ₂, q₁::Number,
              rtol::Real = sqrt(eps(float(real(promote_type(T,eltype(q₁)))))),
              atol::Real = zero(real(promote_type(T,eltype(q₁))))) where
              {T,X,P,T2,PQ₂<:RationalTransferFunction{T,X,P,T2}} = isapprox(pq₂,q₁*one(pq₂))

## various functions
"""
     normalize(r)

Normalize the rational transfer function `r(λ)` to have a monic denominator polynomial.
"""
function normalize(f::RationalTransferFunction)
    k = last(f.den.coeffs)
    rtf(f.num/k,f.den/k,Ts = f.Ts)
end
"""
     simplify(r; atol = 0, rtol = atol)

Simplify the rational transfer function `r(λ)` by cancellation of common divisors of numerator and denominator.
The keyword arguments `atol` and `rtol` are the absolute and relative tolerances for the nonzero
numerator and denominator coefficients.
"""
function simplify(r::RationalTransferFunction; atol::Real = 0, rtol::Real=10*eps(float(real(_eltype(r)))))
    m = degree(r.num)
    (m == 0 || degree(r.den) == 0) && (return r)
    pnum = r.num.coeffs
    pden = r.den.coeffs
    d, u, w,  = MatrixPencils.polgcdvw(pnum, pden, atol = atol, rtol = rtol, maxnit = 3)
    length(u) > m && (return r) # no cancelation, keep original r
    knum = pnum[end]/d[end]/u[end]
    kden = pden[end]/d[end]/w[end]
    return rtf(Polynomial(u*knum,Polynomials.indeterminate(r.num)), Polynomial(w*kden,Polynomials.indeterminate(r.den)), Ts = r.Ts)
end
"""
    Rval = evalfr(R,val)

Evaluate the rational transfer function matrix `R(λ)` for `λ = val`.
"""
function evalfr(R::VecOrMat{<:RationalTransferFunction}, val::Number)
    return pmeval(numpoly.(R),val) ./ pmeval(denpoly.(R),val)
end
"""
    Rval = evalfr(R; fval = 0)

Evaluate the rational transfer function matrix `R(λ)` for `λ = val`, where `val = im*fval`
for a continuous-time system or `val = exp(im*fval*abs(Ts))` for a discrete-time system,
with `Ts` the system sampling time.
"""
function evalfr(R::VecOrMat{<:RationalTransferFunction}; fval::Number = 0)
    length(R) > 0 ? Ts = R[1].Ts : Ts = 0
    val = Ts == 0 ? im*abs(fval) : exp(im*abs(fval*Ts))
    return pmeval(numpoly.(R),val) ./ pmeval(denpoly.(R),val)
end
dcgain(R::VecOrMat{<:RationalTransferFunction}) = _eltype(R) <: Complex ? evalfr(R) : real(evalfr(R))
"""
    rval = evalfr(r,val)

Evaluate the rational transfer function  `r(λ)` for `λ = val`.
"""
function evalfr(r::RationalTransferFunction,val::Number)
    return r.num(val) ./ r.den(val)
end
"""
    rval = evalfr(r; fval = 0)

Evaluate the rational transfer function  `r(λ)` for `λ = val`, where `val = im*fval`
for a continuous-time system or `val = exp(im*fval*Ts)` for a discrete-time system,
with `Ts` the system sampling time.
"""
function evalfr(r::RationalTransferFunction; fval::Number = 0)
    Ts = r.Ts
    val = Ts == 0 ? im*abs(fval) : exp(im*abs(fval*Ts))
    return r.num(val) ./ r.den(val)
end
dcgain(r::RationalTransferFunction) = _eltype(r) <: Complex ? evalfr(r) : real(evalfr(r))


using OrdinaryDiffEq, Sundials, DiffEqDevTools, Plots, ODEInterfaceDiffEq, ODE, LSODA
using Random
Random.seed!(123)
gr()
# 2D Linear ODE
function f(du,u,p,t)
  @inbounds for i in eachindex(u)
    du[i] = 1.01*u[i]
  end
end
function f_analytic(u₀,p,t)
  u₀*exp(1.01*t)
end
tspan = (0.0,10.0)
prob = ODEProblem(ODEFunction(f,analytic=f_analytic),rand(100,100),tspan)

abstols = 1.0 ./ 10.0 .^ (3:13)
reltols = 1.0 ./ 10.0 .^ (0:10);

setups = [Dict(:alg=>DP5())
          Dict(:alg=>ode45())
          Dict(:alg=>dopri5())
          Dict(:alg=>ARKODE(Sundials.Explicit(),etable=Sundials.DORMAND_PRINCE_7_4_5))
          Dict(:alg=>Tsit5())]
solnames = ["OrdinaryDiffEq";"ODE";"ODEInterface";"Sundials ARKODE";"OrdinaryDiffEq Tsit5"]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;names=solnames,save_everystep=false,numruns=100)
plot(wp)

setups = [Dict(:alg=>DP5(),:dense=>false)
          Dict(:alg=>ode45(),:dense=>false)
          Dict(:alg=>dopri5()) # dense=false by default: no nonlinear interpolation
          Dict(:alg=>ARKODE(Sundials.Explicit(),etable=Sundials.DORMAND_PRINCE_7_4_5),:dense=>false)
          Dict(:alg=>Tsit5(),:dense=>false)]
solnames = ["OrdinaryDiffEq";"ODE";"ODEInterface";"Sundials ARKODE";"OrdinaryDiffEq Tsit5"]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;names=solnames,numruns=100)
plot(wp)

setups = [Dict(:alg=>DP5())
          Dict(:alg=>ode45())
          Dict(:alg=>dopri5())
          Dict(:alg=>ARKODE(Sundials.Explicit(),etable=Sundials.DORMAND_PRINCE_7_4_5))
          Dict(:alg=>Tsit5())]
solnames = ["OrdinaryDiffEq";"ODE";"ODEInterface";"Sundials ARKODE";"OrdinaryDiffEq Tsit5"]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;names=solnames,numruns=100)
plot(wp)

setups = [Dict(:alg=>DP5())
          Dict(:alg=>BS3())
          Dict(:alg=>BS5())
          Dict(:alg=>Tsit5())]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;save_everystep=false,numruns=100)
plot(wp)

setups = [Dict(:alg=>DP5())
          Dict(:alg=>Vern6())
          Dict(:alg=>TanYam7())
          Dict(:alg=>Vern7())
          Dict(:alg=>Vern8())
          Dict(:alg=>DP8())
          Dict(:alg=>Vern9())]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;save_everystep=false,numruns=100)
plot(wp)

setups = [Dict(:alg=>DP5())
          Dict(:alg=>Vern7())
          Dict(:alg=>dop853())
          Dict(:alg=>ode78())
          Dict(:alg=>odex())
          Dict(:alg=>lsoda())
          Dict(:alg=>ddeabm())
          Dict(:alg=>ARKODE(Sundials.Explicit(),order=8))
          Dict(:alg=>CVODE_Adams())]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;save_everystep=false,numruns=100)
plot(wp)

setups = [Dict(:alg=>DP5())
          #Dict(:alg=>ode45())
          Dict(:alg=>Tsit5())]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;error_estimate=:L2,dense_errors=true,numruns=100)
plot(wp)

setups = [Dict(:alg=>DP5())
          Dict(:alg=>Vern7())
          #Dict(:alg=>ode78())
          ]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;error_estimate=:L2,dense_errors=true,numruns=100)
plot(wp)

abstols = 1.0 ./ 10.0 .^ (3:13)
reltols = 1.0 ./ 10.0 .^ (0:10);
dts = [1,1/2,1/4,1/10,1/20,1/40,1/60,1/80,1/100,1/140,1/240]
setups = [Dict(:alg=>DP5())
          Dict(:alg=>ode45())
          Dict(:alg=>dopri5())
          Dict(:alg=>RK4(),:dts=>dts)
          Dict(:alg=>Tsit5())]
solnames = ["DifferentialEquations";"ODE";"ODEInterface";"DifferentialEquations RK4";"DifferentialEquations Tsit5"]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;names=solnames,
                      save_everystep=false,verbose=false,numruns=100)
plot(wp)

setups = [Dict(:alg=>Tsit5())
          Dict(:alg=>Vern9())
          Dict(:alg=>VCABM())
          Dict(:alg=>AitkenNeville(min_order=1, max_order=9, init_order=4, threading=true))
          Dict(:alg=>ExtrapolationMidpointDeuflhard(min_order=1, max_order=9, init_order=4, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, threading=true))]
solnames = ["Tsit5","Vern9","VCABM","AitkenNeville","Midpoint Deuflhard","Midpoint Hairer Wanner"]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;names=solnames,
                      save_everystep=false,verbose=false,numruns=100)
plot(wp)

setups = [Dict(:alg=>ExtrapolationMidpointDeuflhard(min_order=1, max_order=9, init_order=9, threading=false))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, threading=false))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, sequence = :romberg, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, sequence = :bulirsch, threading=true))]
solnames = ["Deuflhard","No threads","standard","Romberg","Bulirsch"]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;names=solnames,
                      save_everystep=false,verbose=false,numruns=100)
plot(wp)

setups = [Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=10, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=5, max_order=11, init_order=10, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=15, init_order=10, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=5, max_order=7, init_order=6, threading=true))]
solnames = ["1","2","3","4","5"]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;names=solnames,
                      save_everystep=false,verbose=false,numruns=100)
plot(wp)

using DiffEqBenchmarks
DiffEqBenchmarks.bench_footer(WEAVE_ARGS[:folder],WEAVE_ARGS[:file])


using Plots
using DifferentialEquations
plotlyjs()

# Rossler space equation
function rossler(t::Float64, x::Vector{Float64}, dx::Vector{Float64})
    dx[1] = -x[2] - x[3]
    dx[2] = x[1] + 0.2*x[2]
    dx[3] = 0.2 + x[3]*(x[1]-5.7)
    return dx
end

# Simulate Rossler
time_span = (0.0, 1000.0)
x0 = [1.0, 1.0, 2.0]
prob = ODEProblem(rossler, x0, time_span)
sol = solve(prob, RK4(), reltol=1e-8)

plot(sol, vars=(1, 2, 3))