# Filter types and conversions

abstract type FilterCoefficients{Domain} end

Base.convert(::Type{T}, f::FilterCoefficients) where {T<:FilterCoefficients} = T(f)

#
# Zero-pole gain form
#

"""
    ZeroPoleGain(z, p, k)

Filter representation in terms of zeros `z`, poles `p`, and
gain `k`:
```math
H(x) = k\\frac{(x - \\verb!z[1]!) \\ldots (x - \\verb!z[m]!)}{(x - \\verb!p[1]!) \\ldots (x - \\verb!p[n]!)}
```
"""
struct ZeroPoleGain{Domain,Z<:Number,P<:Number,K<:Number} <: FilterCoefficients{Domain}
    z::Vector{Z}
    p::Vector{P}
    k::K
end

ZeroPoleGain(f::FilterCoefficients{D}) where {D} = ZeroPoleGain{D}(f)
ZeroPoleGain(z::Vector, p::Vector, k) = ZeroPoleGain{:z}(z, p, k)
ZeroPoleGain{D,Z,P,K}(f::ZeroPoleGain) where {D,Z,P,K} = ZeroPoleGain{D,Z,P,K}(f.z, f.p, f.k)
ZeroPoleGain{D}(f::ZeroPoleGain{D,Z,P,K}) where {D,Z,P,K} = ZeroPoleGain{D,Z,P,K}(f)
ZeroPoleGain{D}(z::Vector{Z}, p::Vector{P}, k::K) where {D,Z<:Number,P<:Number,K<:Number} =
    ZeroPoleGain{D,Z,P,K}(z, p, k)

Base.promote_rule(::Type{ZeroPoleGain{D,Z1,P1,K1}}, ::Type{ZeroPoleGain{D,Z2,P2,K2}}) where {D,Z1,P1,K1,Z2,P2,K2} =
    ZeroPoleGain{D,promote_type(Z1,Z2),promote_type(P1,P2),promote_type(K1,K2)}

*(f::ZeroPoleGain{D}, g::Number) where {D} = ZeroPoleGain{D}(f.z, f.p, f.k*g)
*(g::Number, f::ZeroPoleGain{D}) where {D} = ZeroPoleGain{D}(f.z, f.p, f.k*g)
*(f1::ZeroPoleGain{D}, f2::ZeroPoleGain{D}) where {D} =
    ZeroPoleGain{D}([f1.z; f2.z], [f1.p; f2.p], f1.k*f2.k)
*(f1::ZeroPoleGain{D}, fs::ZeroPoleGain{D}...) where {D} =
    ZeroPoleGain{D}(vcat(f1.z, map(f -> f.z, fs)...), vcat(f1.p, map(f -> f.p, fs)...),
        f1.k*prod(f.k for f in fs))

Base.inv(f::ZeroPoleGain{D}) where {D} = ZeroPoleGain{D}(f.p, f.z, inv(f.k))

function Base.:^(f::ZeroPoleGain{D}, e::Integer) where {D}
    if e < 0
        return inv(f^-e)
    else
        return ZeroPoleGain{D}(repeat(f.z, e), repeat(f.p, e), f.k^e)
    end
end

#
# Transfer function form
#

function shiftpoly(p::LaurentPolynomial, i)
    if i > 0
        return p * LaurentPolynomial([one(eltype(p))], 1, indeterminate(p))^i
    elseif i < 0
        return p * LaurentPolynomial([one(eltype(p))], -1, indeterminate(p))^-i
    end
    return p
end

struct PolynomialRatio{Domain,T<:Number} <: FilterCoefficients{Domain}
    b::LaurentPolynomial{T,Domain}
    a::LaurentPolynomial{T,Domain}

    function PolynomialRatio{:z,Ti}(b::LaurentPolynomial, a::LaurentPolynomial) where {Ti<:Number}
        i = max(lastindex(a), lastindex(b))
        b = shiftpoly(b, -i)
        a = shiftpoly(a, -i)
        if !isone(a[0])
            if iszero(a[0])
                throw(ArgumentError("filter must have non-zero leading denominator coefficient"))
            end
            b /= a[0]
            a /= a[0]
        end
        return new{:z,Ti}(b, a)
    end
    function PolynomialRatio{:s,Ti}(b::LaurentPolynomial, a::LaurentPolynomial) where {Ti<:Number}
        if iszero(a)
            throw(ArgumentError("filter must have non-zero denominator"))
        end
        i = min(firstindex(a), firstindex(b))
        b = shiftpoly(b, -i)
        a = shiftpoly(a, -i)
        return new{:s,Ti}(b, a)
    end
end
PolynomialRatio(f::FilterCoefficients{D}) where {D} = PolynomialRatio{D}(f)
"""
    PolynomialRatio(b, a)

Filter representation in terms of the coefficients of the numerator
`b` and denominator `a` of the transfer function:
```math
H(s) = \\frac{\\verb!b[1]! s^{m-1} + \\ldots + \\verb!b[m]!}{\\verb!a[1]! s^{n-1} + \\ldots + \\verb!a[n]!}
```
or equivalently:
```math
H(z) = \\frac{\\verb!b[1]! + \\ldots + \\verb!b[n]! z^{-n+1}}{\\verb!a[1]! + \\ldots + \\verb!a[n]! z^{-n+1}}
```
`b` and `a` may be specified as `Polynomial` objects or
vectors ordered from highest power to lowest.
"""
PolynomialRatio(b, a) = PolynomialRatio{:z}(b, a)
function PolynomialRatio{:z}(b::LaurentPolynomial{T1}, a::LaurentPolynomial{T2}) where {T1,T2}
    return PolynomialRatio{:z,typeof(one(T1)/one(T2))}(b, a)
end
function PolynomialRatio{:s}(b::LaurentPolynomial{T1}, a::LaurentPolynomial{T2}) where {T1,T2}
    return PolynomialRatio{:s,promote_type(T1, T2)}(b, a)
end

# The DSP convention for Laplace domain is highest power first. The Polynomials.jl
# convention is lowest power first.
_polyprep(D::Symbol, x, T...) = LaurentPolynomial{T...}(reverse(x), D === :z ? -length(x)+1 : 0, D)
PolynomialRatio{D,T}(b::Union{Number,Vector{<:Number}}, a::Union{Number,Vector{<:Number}}) where {D,T} =
    PolynomialRatio{D,T}(_polyprep(D, b, T), _polyprep(D, a, T))
PolynomialRatio{D}(b::Union{Number,Vector{<:Number}}, a::Union{Number,Vector{<:Number}}) where {D} =
    PolynomialRatio{D}(_polyprep(D, b), _polyprep(D, a))

PolynomialRatio{D,T}(f::PolynomialRatio{D}) where {D,T} = PolynomialRatio{D,T}(f.b, f.a)
PolynomialRatio{D}(f::PolynomialRatio{D,T}) where {D,T} = PolynomialRatio{D,T}(f)

Base.promote_rule(::Type{PolynomialRatio{D,T}}, ::Type{PolynomialRatio{D,S}}) where {D,T,S} = PolynomialRatio{D,promote_type(T,S)}

function PolynomialRatio{D,T}(f::ZeroPoleGain{D}) where {D,T<:Real}
    b = convert(LaurentPolynomial{T}, real(f.k * fromroots(f.z)))
    a = convert(LaurentPolynomial{T}, real(fromroots(f.p)))
    return PolynomialRatio{D,T}(b, a)
end
PolynomialRatio{D}(f::ZeroPoleGain{D,Z,P,K}) where {D,Z,P,K} =
    PolynomialRatio{D,promote_type(real(Z),real(P),K)}(f)

ZeroPoleGain{D,Z,P,K}(f::PolynomialRatio{D}) where {D,Z,P,K} =
    ZeroPoleGain{D,Z,P,K}(ZeroPoleGain{D}(f))
function ZeroPoleGain{D}(f::PolynomialRatio{D,T}) where {D,T}
    i = -min(firstindex(f.a), firstindex(f.b), 0)
    z = roots(shiftpoly(f.b, i))
    p = roots(shiftpoly(f.a, i))
    return ZeroPoleGain{D}(z, p, f.b[end]/f.a[end])
end

*(f::PolynomialRatio{D}, g::Number) where {D} = PolynomialRatio{D}(g*f.b, f.a)
*(g::Number, f::PolynomialRatio{D}) where {D} = PolynomialRatio{D}(g*f.b, f.a)
*(f1::PolynomialRatio{D}, f2::PolynomialRatio{D}) where {D} =
    PolynomialRatio{D}(f1.b*f2.b, f1.a*f2.a)
*(f1::PolynomialRatio{D}, fs::PolynomialRatio{D}...) where {D} =
    PolynomialRatio{D}(f1.b*prod(f.b for f in fs), f1.a*prod(f.a for f in fs))

Base.inv(f::PolynomialRatio{D}) where {D} = begin
    PolynomialRatio{D}(f.a, f.b)
end

function Base.:^(f::PolynomialRatio{D,T}, e::Integer) where {D,T}
    if e < 0
        return PolynomialRatio{D}(f.a^-e, f.b^-e)
    else
        return PolynomialRatio{D}(f.b^e, f.a^e)
    end
end

"""
    coefb(f)

Coefficients of the numerator of a PolynomialRatio object, highest power
first, i.e., the `b` passed to `filt()`
"""
coefb(f::PolynomialRatio{:s}) = reverse([zeros(firstindex(f.b)); coeffs(f.b)])
coefb(f::PolynomialRatio{:z}) = reverse([coeffs(f.b); zeros(-lastindex(f.b))])
coefb(f::FilterCoefficients) = coefb(PolynomialRatio(f))

"""
    coefa(f)

Coefficients of the denominator of a PolynomialRatio object, highest power
first, i.e., the `a` passed to `filt()`
"""
coefa(f::PolynomialRatio{:s}) = reverse([zeros(firstindex(f.a)); coeffs(f.a)])
coefa(f::PolynomialRatio{:z}) = reverse([coeffs(f.a); zeros(-lastindex(f.a))])
coefa(f::FilterCoefficients) = coefa(PolynomialRatio(f))

#
# Biquad filter in transfer function form
# A separate immutable to improve efficiency of filtering using SecondOrderSections
#
"""
    Biquad(b0, b1, b2, a1, a2)

Filter representation in terms of the transfer function of a single
second-order section given by:
```math
H(s) = \\frac{\\verb!b0! s^2+\\verb!b1! s+\\verb!b2!}{s^2+\\verb!a1! s + \\verb!a2!}
```
or equivalently:
```math
H(z) = \\frac{\\verb!b0!+\\verb!b1! z^{-1}+\\verb!b2! z^{-2}}{1+\\verb!a1! z^{-1} + \\verb!a2! z^{-2}}
```
"""
struct Biquad{Domain,T<:Number} <: FilterCoefficients{Domain}
    b0::T
    b1::T
    b2::T
    a1::T
    a2::T
end
Biquad(f::FilterCoefficients{D}) where {D} = Biquad{D}(f)
Biquad(args...) = Biquad{:z}(args...)
Biquad{D}(b0::T, b1::T, b2::T, a1::T, a2::T) where {D,T} =
    Biquad{D,T}(b0, b1, b2, a1, a2)
Biquad{D}(b0::T, b1::T, b2::T, a0::T, a1::T, a2::T, g::Number=1) where {D,T} =
    (x = g*b0/a0; Biquad{D,typeof(x)}(x, g*b1/a0, g*b2/a0, a1/a0, a2/a0))

Biquad{D,T}(f::Biquad{D}) where {D,T} = Biquad{D,T}(f.b0, f.b1, f.b2, f.a1, f.a2)

Base.promote_rule(::Type{Biquad{D,T}}, ::Type{Biquad{D,S}}) where {D,T,S} = Biquad{D,promote_type(T,S)}

ZeroPoleGain{D,Z,P,K}(f::Biquad{D}) where {D,Z,P,K} = ZeroPoleGain{D,Z,P,K}(PolynomialRatio{D}(f))
ZeroPoleGain{D}(f::Biquad) where {D} = ZeroPoleGain{D}(convert(PolynomialRatio{D}, f))

function PolynomialRatio{D,T}(f::Biquad{D}) where {D,T}
    b = T[f.b0, f.b1, f.b2]
    a = T[one(T), f.a1, f.a2]
    PolynomialRatio{D,T}(b, a)
end
PolynomialRatio{D}(f::Biquad{D,T}) where {D,T} = PolynomialRatio{D,T}(f)

function Biquad{D,T}(f::PolynomialRatio{D}) where {D,T}
    a, b = f.a, f.b
    lastidx = max(lastindex(b), lastindex(a))

    if lastidx - min(firstindex(b), firstindex(a)) >= 3
        throw(ArgumentError("cannot convert a filter of length > 3 to Biquad"))
    end
    if !isone(a[lastidx])
        throw(ArgumentError("leading denominator coefficient of a Biquad must be one"))
    end
    Biquad{D,T}(b[lastidx], b[lastidx-1], b[lastidx-2], a[lastidx-1], a[lastidx-2])
end
Biquad{D}(f::PolynomialRatio{D,T}) where {D,T} = Biquad{D,T}(f)

Biquad{D,T}(f::ZeroPoleGain{D}) where {D,T} = Biquad{D,T}(convert(PolynomialRatio, f))
Biquad{D}(f::ZeroPoleGain{D}) where {D} = Biquad{D}(convert(PolynomialRatio, f))

*(f::Biquad{D}, g::Number) where {D} = Biquad{D}(f.b0*g, f.b1*g, f.b2*g, f.a1, f.a2)
*(g::Number, f::Biquad{D}) where {D} = Biquad{D}(f.b0*g, f.b1*g, f.b2*g, f.a1, f.a2)

Base.inv(f::Biquad{D,T}) where {D,T} = Biquad{D}(one(T), f.a1, f.a2, f.b0, f.b1, f.b2)

#
# Second-order sections (array of biquads)
#
"""
    SecondOrderSections(biquads, gain)

Filter representation in terms of a cascade of second-order
sections and gain. `biquads` must be specified as a vector of
`Biquads`.
"""
struct SecondOrderSections{Domain,T,G} <: FilterCoefficients{Domain}
    biquads::Vector{Biquad{Domain,T}}
    g::G
end
SecondOrderSections(f::FilterCoefficients{D}) where {D} = SecondOrderSections{D}(f)
SecondOrderSections{D}(biquads::Vector{Biquad{D,T}}, g::G) where {D,T,G} =
    SecondOrderSections{D,T,G}(biquads, g)

Base.promote_rule(::Type{SecondOrderSections{D,T1,G1}}, ::Type{SecondOrderSections{D,T2,G2}}) where {D,T1,G1,T2,G2} =
    SecondOrderSections{D,promote_type(T1,T2),promote_type(G1,G2)}

SecondOrderSections{D,T,G}(f::SecondOrderSections) where {D,T,G} =
    SecondOrderSections{D,T,G}(f.biquads, f.g)
SecondOrderSections{D}(f::SecondOrderSections{D,T,G}) where {D,T,G} = SecondOrderSections{D,T,G}(f)

function ZeroPoleGain{D,Z,P,K}(f::SecondOrderSections{D}) where {D,Z,P,K}
    z = Z[]
    p = P[]
    k = f.g
    for biquad in f.biquads
        biquadzpk = ZeroPoleGain{D}(biquad)
        append!(z, biquadzpk.z)
        append!(p, biquadzpk.p)
        k *= biquadzpk.k
    end
    ZeroPoleGain{D,Z,P,K}(z, p, k)
end
ZeroPoleGain{D}(f::SecondOrderSections{D,T,G}) where {D,T,G} =
    ZeroPoleGain{D,complex(T),complex(T),G}(f)

function Biquad{D,T}(f::SecondOrderSections{D}) where {D,T}
    if length(f.biquads) != 1
        throw(ArgumentError("only a single second order section may be converted to a biquad"))
    end
    Biquad{D,T}(f.biquads[1]*f.g)
end
Biquad{D}(f::SecondOrderSections{D,T,G}) where {D,T,G} = Biquad{D,promote_type(T,G)}(f)

PolynomialRatio{D,T}(f::SecondOrderSections{D}) where {D,T} = PolynomialRatio{D,T}(ZeroPoleGain(f))
PolynomialRatio{D}(f::SecondOrderSections{D}) where {D} = PolynomialRatio{D}(ZeroPoleGain(f))

# Group each pole in p with its closest zero in z
# Remove paired poles from p and z
function groupzp(z, p)
    n = min(length(z), length(p))
    groupedz = similar(z, n)
    i = 1
    while i <= n
        closest_zero_idx = 0
        closest_zero_val = Inf
        for j = 1:length(z)
            val = abs(z[j] - p[i])
            if val < closest_zero_val
                closest_zero_idx = j
                closest_zero_val = val
            end
        end
        groupedz[i] = splice!(z, closest_zero_idx)
        if !isreal(groupedz[i])
            i += 1
            groupedz[i] = splice!(z, closest_zero_idx)
        end
        i += 1
    end
    ret = (groupedz, p[1:n])
    splice!(p, 1:n)
    ret
end

# Sort zeros or poles lexicographically (so that poles are adjacent to
# their conjugates). Handle repeated values. Split real and complex
# values into separate vectors. Ensure that each value has a conjugate.
function split_real_complex(x::Vector{T}) where T
    # Get counts and store in a Dict
    d = Dict{T,Int}()
    for v in x
        # needs to be in normal form since 0.0 !== -0.0
        tonormal(x) = x == 0 ? abs(x) : x
        vn = complex(tonormal(real(v)), tonormal(imag(v)))
        d[vn] = get(d, vn, 0)+1
    end

    c = T[]
    r = typeof(real(zero(T)))[]
    for k in keys(d)
        if imag(k) != 0
            if !haskey(d, conj(k)) || d[k] != d[conj(k)]
                # No match for conjugate
                return (c, r, false)
            elseif imag(k) > 0
                # Add key and its conjugate
                for n = 1:d[k]
                    push!(c, k, conj(k))
                end
            end
        else
            for n = 1:d[k]
                push!(r, k)
            end
        end
    end
    return (c, r, true)
end

# Convert a filter to second-order sections
# The returned sections are in ZPK form
function SecondOrderSections{D,T,G}(f::ZeroPoleGain{D,Z,P}) where {D,T,G,Z,P}
    z = f.z
    p = f.p
    nz = length(z)
    n = length(p)
    nz > n && throw(ArgumentError("ZeroPoleGain must not have more zeros than poles"))

    # Split real and complex poles
    (complexz, realz, matched) = split_real_complex(z)
    matched || throw(ArgumentError("complex zeros could not be matched to their conjugates"))
    (complexp, realp, matched) = split_real_complex(p)
    matched || throw(ArgumentError("complex poles could not be matched to their conjugates"))

    # Sort poles according to distance to unit circle (nearest first)
    sort!(complexp, by=x->abs(abs(x) - 1))
    sort!(realp, by=x->abs(abs(x) - 1))

    # Group complex poles with closest complex zeros
    z1, p1 = groupzp(complexz, complexp)
    # Group real poles with remaining complex zeros
    z2, p2 = groupzp(complexz, realp)
    # Group remaining complex poles with closest real zeros
    z3, p3 = groupzp(realz, complexp)
    # Group remaining real poles with closest real zeros
    z4, p4 = groupzp(realz, realp)

    # All zeros are now paired with a pole, but not all poles are
    # necessarily paired with a zero
    @assert isempty(complexz)
    @assert isempty(realz)
    groupedz = [z1; z2; z3; z4]::Vector{Z}
    groupedp = [p1; p2; p3; p4; complexp; realp]::Vector{P}
    @assert length(groupedz) == nz
    @assert length(groupedp) == n

    # Allocate memory for biquads
    biquads = Vector{Biquad{D,T}}(undef, (n >> 1)+(n & 1))

    # Build second-order sections in reverse
    # First do complete pairs
    npairs = length(groupedp) >> 1
    odd = isodd(n)
    for i = 1:npairs
        pairidx = 2*(npairs-i)
        biquads[odd+i] = convert(Biquad, ZeroPoleGain{D}(groupedz[pairidx+1:min(pairidx+2, length(groupedz))],
                                                         groupedp[pairidx+1:pairidx+2], one(T)))
    end

    if odd
        # Now do remaining pole and (maybe) zero
        biquads[1] = convert(Biquad, ZeroPoleGain{D}(groupedz[length(groupedp):end],
                                                     [groupedp[end]], one(T)))
    end

    SecondOrderSections{D,T,G}(biquads, f.k)
end
SecondOrderSections{D}(f::ZeroPoleGain{D,Z,P,K}) where {D,Z,P,K} =
    SecondOrderSections{D,promote_type(real(Z), real(P)), K}(f)

SecondOrderSections{D,T,G}(f::Biquad{D}) where {D,T,G} = SecondOrderSections{D,T,G}([f], one(G))
SecondOrderSections{D}(f::Biquad{D,T}) where {D,T} = SecondOrderSections{D,T,Int}(f)
SecondOrderSections{D}(f::FilterCoefficients{D}) where {D} = SecondOrderSections{D}(ZeroPoleGain(f))

*(f::SecondOrderSections{D}, g::Number) where {D} = SecondOrderSections{D}(f.biquads, f.g*g)
*(g::Number, f::SecondOrderSections{D}) where {D} = SecondOrderSections{D}(f.biquads, f.g*g)
*(f1::SecondOrderSections{D}, f2::SecondOrderSections{D}) where {D} =
    SecondOrderSections{D}([f1.biquads; f2.biquads], f1.g*f2.g)
*(f1::SecondOrderSections{D}, fs::SecondOrderSections{D}...) where {D} =
    SecondOrderSections{D}(vcat(f1.biquads, map(f -> f.biquads, fs)...), f1.g*prod(f.g for f in fs))

*(f1::Biquad{D}, f2::Biquad{D}) where {D} = SecondOrderSections{D}([f1, f2], 1)
*(f1::Biquad{D}, fs::Biquad{D}...) where {D} = SecondOrderSections{D}([f1, fs...], 1)
*(f1::SecondOrderSections{D}, f2::Biquad{D}) where {D} =
    SecondOrderSections{D}([f1.biquads; f2], f1.g)
*(f1::Biquad{D}, f2::SecondOrderSections{D}) where {D} =
    SecondOrderSections{D}([f1; f2.biquads], f2.g)

Base.inv(f::SecondOrderSections{D}) where {D} = SecondOrderSections{D}(inv.(f.biquads), inv(f.g))

function Base.:^(f::SecondOrderSections{D}, e::Integer) where {D}
    if e < 0
        return inv(f)^-e
    else
        return SecondOrderSections{D}(repeat(f.biquads, e), f.g^e)
    end
end

function Base.:^(f::Biquad{D}, e::Integer) where {D}
    if e < 0
        return inv(f)^-e
    else
        return SecondOrderSections{D}(fill(f, e), 1)
    end
end


a = [1,2,3]
b = [4,5,6]
ab = [a;b]
# the above bracket notation simply generates a call to vcat
ab = vcat(a,b)
# hcat is short for `horizontal concatenation`
ab = hcat(a,b) 	#ab -> 3x2 matrix
# the append!(a,b) method is mutating, appending `b` to `a`
append!(a,b)	# a now equals [1,2,3,4,5,6]


using SymbolicManipulations
using SymbolicUtils
using Test

# Taken from SymbolicUtils
macro eqtest(expr)
    @assert expr.head == :call && expr.args[1] in [:(==), :(!=)]
    if expr.args[1] == :(==)
        :(@test isequal($(expr.args[2]), $(expr.args[3])))
    else
        :(@test !isequal($(expr.args[2]), $(expr.args[3])))
    end |> esc
end
macro eqtest_broken(expr)
    @assert expr.head == :call && expr.args[1] in [:(==), :(!=)]
    if expr.args[1] == :(==)
        :(@test_broken isequal($(expr.args[2]), $(expr.args[3])))
    else
        :(@test_broken !isequal($(expr.args[2]), $(expr.args[3])))
    end |> esc
end
SymbolicUtils.show_simplified[] = false

@testset "SymbolicManipulations.jl" begin
    @testset "together" begin
        @syms x y z

        @eqtest together(x/z + z/y) == (x*y + z^2) / (y*z)
        @eqtest together(x/z + (1 + z)/y) == (x*y + z*(1 + z)) /(y*z)
        @eqtest together((x + y)/z + (1 + z)/y) == (y*(x + y) + z*(1 + z)) /(y*z)
    end

    @testset "count_ops" begin
        @syms x y z

        @test SymbolicManipulations.count_ops(x) == 0
        @test SymbolicManipulations.count_ops(1) == 0
        @test SymbolicManipulations.count_ops(x + y) == 1
        @test SymbolicManipulations.count_ops(x + y + z) == 2

        @test SymbolicManipulations.count_ops(2x) == 1
        @test SymbolicManipulations.count_ops(2x + 0) == 1
        @test SymbolicManipulations.count_ops(2x + 1) == 2
        @test SymbolicManipulations.count_ops(2x + y + 1) == 3

        @test SymbolicManipulations.count_ops(x^2) == 1
        @test SymbolicManipulations.count_ops(x^2 * y^3) == 3

        @test SymbolicManipulations.count_ops(x^(2y + z)) == 3
    end

    @testset "fullsimplify" begin
        @syms x

        @eqtest fullsimplify(cos(2x) + 2sin(x)^2) == 1
        @eqtest fullsimplify(exp(x) * (exp(x) - 1)^-2) == 1//4*(sinh(1//2 * x))^-2
    end

    @testset "integrate" begin
        @syms x y

        @eqtest integrate(x^2, x) == x^3 // 3
        @eqtest integrate(x^y, x) == x^(y + 1) / (y + 1)
        @eqtest integrate(y^x, y) == y^(x + 1) / (x + 1)
        @eqtest integrate(5x, x) == 5//2 * x^2
        @eqtest integrate(x + x^2, x) == 1//2 * x^2 + 1//3 * x^3
        @eqtest integrate((x + x^2) / x, x) == x + 1//2 * x^2
        @eqtest integrate((x + y)^2, x) == y^2 * x + y * x^2 + 1//3 * x^3

        @eqtest_broken integrate(sin(x)^2 * cos(x), x) == sin(x)^3 // 3
        @eqtest_broken integrate((x^2 + x) / sqrt(x), x) == 2//15 * x^(3//2) * (3x + 5)
        @eqtest_broken integrate(x^4 / (1 - x^2)^(5//2), x) == asin(x) + x//3 * (4x^2 - 3) / (1 - x^2)^(3/2)
        @eqtest_broken integrate(sec(x)^2 / (1 + sec(x)^2 - 3tan(x)), x) == log(2cos(x) - sin(x)) - log(cos(x) - sin(x))
        @eqtest_broken integrate((sin(x) + cos(x))^2, x) == x - 1//2 * cos(2x)
        @eqtest_broken integrate(tan(x) * sec(x)^2, x) == sec(x)^2 // 2
        @eqtest_broken integrate(sin(x) * cos(x), x) == -1//2 * cos(x)^2

        # SAINT could not solve these:
        @eqtest_broken integrate(x * sqrt(1 + x), x) == 2//15 * (1 + x)^(3//2) * (3x - 2)
        @eqtest_broken integrate(cos(sqrt(x)), x) == 2cos(sqrt(x)) + 2sqrt(x)*sin(sqrt(x))
    end
end


# ---
# title: 1317. Convert Integer to the Sum of Two No-Zero Integers
# id: problem1317
# author: Tian Jun
# date: 2020-10-31
# difficulty: Easy
# categories: Math
# link: <https://leetcode.com/problems/convert-integer-to-the-sum-of-two-no-zero-integers/description/>
# hidden: true
# ---
#
# Given an integer `n`. No-Zero integer is a positive integer which **doesn 't
# contain any 0** in its decimal representation.
#
# Return _a list of two integers_ `[A, B]` where:
#
#   * `A` and `B` are No-Zero integers.
#   * `A + B = n`
#
# It's guarateed that there is at least one valid solution. If there are many
# valid solutions you can return any of them.
#
#
#
# **Example 1:**
#
#
#
#     Input: n = 2
#     Output: [1,1]
#     Explanation: A = 1, B = 1. A + B = n and both A and B don't contain any 0 in their decimal representation.
#
#
# **Example 2:**
#
#
#
#     Input: n = 11
#     Output: [2,9]
#
#
# **Example 3:**
#
#
#
#     Input: n = 10000
#     Output: [1,9999]
#
#
# **Example 4:**
#
#
#
#     Input: n = 69
#     Output: [1,68]
#
#
# **Example 5:**
#
#
#
#     Input: n = 1010
#     Output: [11,999]
#
#
#
#
# **Constraints:**
#
#   * `2 <= n <= 10^4`
#
#
## @lc code=start
using LeetCode

function get_no_zero_integers(n::Int)::Vector{Int}
    for i in 1:(n - 1)
        if !('0' in string(i)) && !('0' in string(n - i))
            return [i, n - i]
        end
    end
end
## @lc code=end


export AbstractUpdateRule, TikTakRule
export new_guess, weight_parameter, weight_sequence

"""
	$(SIGNATURES)

Abstract type for generating new guesses from the history of already computed points and from the set of starting points provided by the user.
"""
abstract type AbstractUpdateRule end

"""
	$(SIGNATURES)

Generate a sequence of weights for `jMax` points. Useful for tuning the parameters of the update rule.
"""
function weight_sequence(ur :: AbstractUpdateRule, jMax :: Integer)
    return [weight_parameter(ur, j)  for j = 1 : jMax]
end

"""
	$(SIGNATURES)

For generating new points. Defaults based on

Arnoud, Antoine, Fatih Guvenen, and Tatjana Kleineberg. 2019. “Benchmarking Global Optimizers.” Working Paper 26340. National Bureau of Economic Research. https://doi.org/10.3386/w26340.
"""
Base.@kwdef struct TikTakRule <: AbstractUpdateRule
    nPoints :: Int
    θ_min :: Float64 = 0.1
    θ_max :: Float64 = 0.995
    θ_pow :: Float64 = 0.5
    # Bounds for elements of the guess vector.
    lb :: Float64 = 1.0
    ub :: Float64 = 2.0
end

"""
	$(SIGNATURES)

Make a new guess vector from the history of points computed so far and from the set of user provided starting points. Implements `TikTak`.
"""
function new_guess(ur :: TikTakRule, h :: History, j :: Int)
    if j == 1
        guessV = first(h.guessesV);
    else
        wt = weight_parameter(ur, j);
        pBest = best_point(h);
        guessV = wt .* pBest.solnV .+ (1 - wt) .* h.guessesV[j];
    end
    clamp!(guessV, ur.lb, ur.ub);
    return guessV
end

"""
	$(SIGNATURES)

`TikTak` weight parameter.
"""
function weight_parameter(ur :: TikTakRule, j :: Integer)
    return clamp((j / ur.nPoints) ^ ur.θ_pow,
        ur.θ_min, ur.θ_max)
end

# ---------------


function test_smoothing()
        mat1 = [1.0, 2.0]
        mat2 = [
                1.0222393236943468,
                1.1016905849861969,
                1.1915263437469639,
                1.289583214110971,
                1.3933349449133303,
                1.5,
                1.6066650550866697,
                1.710416785889029,
                1.8084736562530361,
                1.8983094150138031,
                1.9777606763056532,
        ]
        @test isapprox(smoothing(mat1, step = 0.1), mat2)
        mat3 = [1.0 2.0; 3.0 4.0]
        mat4 = [
                1.04448 2.04448
                1.20338 2.20338
                1.38305 2.38305
                1.57917 2.57917
                1.78667 2.78667
                2.0 3.0
                2.21333 3.21333
                2.42083 3.42083
                2.61695 3.61695
                2.79662 3.79662
                2.95552 3.95552
        ]
        @test isapprox(smoothing(mat3, step = 0.1), mat4,atol=0.0001)

end


using BenchmarkTools
using RowMajorArrays

function sum_idx1(a)
    s = zero(eltype(a))
    @inbounds for i = 1:size(a, 1), j = 1:size(a, 2)
        s += a[i, j]
    end
    s
end

function sum_idx2(a)
    s = zero(eltype(a))
    @inbounds for j = 1:size(a, 2), i = 1:size(a, 1)
        s += a[i, j]
    end
    s
end

a1 = rand(1000, 1000)
a1_r = RowMajorArray(a1)

println("Array row major sum")
@btime sum_idx1($a1)
println("Array column major sum")
@btime sum_idx2($a1)

println("RowMajorArray row major sum")
@btime sum_idx1($a1_r)
println("RowMajorArray column major sum")
@btime sum_idx2($a1_r)


function apply_dirichlet_bc!(
    matrix::SparseMatrixCSC,
    rhs,
    index::Z,
    value::R,
) where {Z<:Integer,R<:Real}

    modifyrhs = matrix[:, index]
    for i in modifyrhs.nzind
        if i == index
            rhs[i] = modifyrhs[i] * value
        else
            rhs[i] -= modifyrhs[i] * value
            matrix[i, index] = 0.0
            matrix[index, i] = 0.0
        end
    end
end

function apply_dirichlet_bc!(
    matrix::Matrix,
    rhs,
    index::Z,
    value::R,
) where {Z<:Integer,R<:Real}

    m, n = size(matrix)
    @assert length(rhs) == m
    @assert m == n

    modifyrhs = matrix[:,index]
    for i = 1:n
        if i == index
            rhs[i] = matrix[i,i] * value
        else
            matrix[index, i] = 0.0
            matrix[i,index] = 0.0
            rhs[i] -= modifyrhs[i]*value
        end
    end
end

function apply_dirichlet_bc!(
    matrix,
    rhs,
    nodeids::V,
    dofs,
    values,
    dofspernode,
) where {V<:AbstractVector}

    @assert length(nodeids) == length(dofs) == length(values)
    for (nodeid, dof, val) in zip(nodeids, dofs, values)
        index = node_to_dof_id(nodeid, dof,dofspernode)
        apply_dirichlet_bc!(matrix, rhs, index, val)
    end
end

function apply_dirichlet_bc!(
    matrix,
    rhs,
    nodeids::V,
    dofs::Z,
    value::R,
    dofspernode,
) where {V<:AbstractVector,Z<:Integer,R<:Real}
    numnodes = length(nodeids)
    vecdofs = repeat([dofs],numnodes)
    vecvalues = repeat([value],numnodes)
    apply_dirichlet_bc!(matrix,rhs,nodeids,vecdofs,vecvalues,dofspernode)
end

function apply_dirichlet_bc!(
    matrix,
    rhs,
    nodeids::V,
    bcvals::M,
) where {V<:AbstractVector,M<:AbstractMatrix}

    dofspernode, numnodes = size(bcvals)
    @assert length(nodeids) == numnodes
    for (idx, nodeid) in enumerate(nodeids)
        for dof = 1:dofspernode
            index = node_to_dof_id(nodeid, dof, dofspernode)
            apply_dirichlet_bc!(matrix, rhs, index, bcvals[dof, idx])
        end
    end
end

function apply_dirichlet_bc!(
    matrix,
    rhs,
    nodeid::Z,
    bcvals::V,
) where {Z<:Integer,V<:AbstractVector}

    dofs = length(bcvals)
    for dof = 1:dofs
        index = node_to_dof_id(nodeid, dof, dofs)
        apply_dirichlet_bc!(matrix, rhs, index, bcvals[dof])
    end
end


# # Classification of the Wisconsin breast cancer dataset
#
# In this case study we will deal with the Wisconsin breast cancer dataset which can be
# browsed freely on the [UCI website](https://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Diagnostic)).
#
# In particular, this dataset contains *10 features* and 699 instances. In the work we
# will do here, however, we will skip some instances due to some missing values.
#
# The dataset contains only two classes, and the purpose is to use all ten features to
# answer a simple question:
# > Does the subject have a benign or malign tumor?
# To answer this question, we will train a Least Squares Support Vector Machine as
# implemented in `LeastSquaresSVM`.
#
# First, we need to import all the necessary packages.

using MLJ, MLJBase
using DataFrames, CSV
using CategoricalArrays
using Random, Statistics
using LeastSquaresSVM

# We then need to specify a seed to enable reproducibility of the results.
rng = MersenneTwister(801239);

# Here we are creating a list with all the headers.
headers = [
	"id", "Clump Thickness",
	"Uniformity of Cell Size", "Uniformity of Cell Shape",
	"Marginal Adhesion", "Single Epithelial Cell Size",
	"Bare Nuclei", "Bland Chromatin",
	"Normal Nucleoli", "Mitoses", "class"
];

# We define the path were the dataset is located
path = joinpath("src", "examples", "wbc.csv");

# We load the csv file and convert it to a `DataFrame`. Note that we are specifying
# to the file reader to replace the string `?` to a `missing` value. This dataset contains
# the the string `?` when there is a value missing.
data = CSV.File(path; header=headers, missingstring="?") |> DataFrame;

# We can display the first 10 rows from the dataset
first(data, 10)

# We can see that all the features have been added correctly, we can see that we have
# an unncessary feature called `id`, so we will remove it.

select!(data, Not(:id));

# We also need to remove all the missing data from the `DataFrame`
data = dropmissing(data);

# The `class` column should be of type `categorical`, following the `MLJ` API, so we
# encode it here.
transform!(data, :class => categorical, renamecols=false);

# Check statistics per column.
describe(data)

# Split the dataset into training and testing.
y, X = unpack(data, ==(:class), colname -> true);

# We will use only 2/3 for training.
train, test = partition(eachindex(y), 2 / 3, shuffle=true, rng=rng);

# Always remove mean and set the standard deviation to 1.0 when dealing with SVMs.
stand1 = Standardizer(count=true);
X = MLJBase.transform(fit!(machine(stand1, X)), X);

# Check statistics per column again to ensure standardization, but remember to do it now
# with the `X` matrix.
describe(X)

# Good, now every column has a mean very close to zero, so the standardization was
# done correctly.

# We now create our model with `LeastSquaresSVM`
model = LeastSquaresSVM.LSSVClassifier();

# These are the values for the hyperparameter grid search. We need to find the best subset
# from this set of parameters.
# Although I will not do this here, the best approach is to find a set of good hyperparameters
# and then refine the search space around that set. That way we can ensure we will always get
# the best results.
sigma_values = [0.5, 5.0, 10.0, 15.0, 25.0, 50.0, 100.0, 250.0, 500.0];
r1 = MLJBase.range(model, :σ, values=sigma_values);
gamma_values = [0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0];
r2 = MLJBase.range(model, :γ, values=gamma_values);

# We now create a `TunedModel` that will use a 10-folds stratified cross validation scheme
# in order to find the best set of hyperparameters. The stratification is needed because
# the classes are somewhat imbalanced:
# - Benign: 458 (65.5%)
# - Malignant: 241 (34.5%)

self_tuning_model = TunedModel(
    model=model,
    tuning=Grid(rng=rng),
    resampling=StratifiedCV(nfolds=10),
    range=[r1, r2],
    measure=accuracy,
    acceleration=CPUThreads(), # We use this to enable multithreading
);

# Once the best model is found, we create a `machine` with it, and fit it
mach = machine(self_tuning_model, X, y);
fit!(mach, rows=train, verbosity=0);

# We can now show the best hyperparameters found.
fitted_params(mach).best_model

# And we test the trained model. We expect somewhere around 94%-96% accuracy.
results = predict(mach, rows=test);
acc = accuracy(results, y[test]);

# Show the accuracy for the testing set
println(acc * 100.0)

# As you can see, it is fairly easy to use `LeastSquaresSVM` together with MLJ. We got a good
# accuracy result and this proves that the implementation is actually correct. This
# dataset is commonly used as a benchmark dataset to test new algorithms.


export nyquist

#==#
#
# @brief Compute nyquist diagram.
# @param[in] sys System in state space representation.
# @param[in] w Array of frequencies in which the bode diagram will be computed.
# If empty, the frequencies will be chosen according to the system poles.
# @param[in] plot Plot bode diagram (default = true).
#
# @return Real(G(jw)), Imag(G(jw)), w (freq. array) for each pair input/output.
#
#==#

"""
### function nyquist(sys::StateSpace, w::Array=[]; plot=true)

Compute the Nyquist diagram.

##### Args

* sys: System in state space representation.
* w: Array of frequencies in which the bode diagram will be computed.  If empty,
  the frequencies will be chosen according to the system poles.

##### Returns

Real(G(jw)), Imag(G(jw)), w (freq. array) for each input/output pair.

"""

function nyquist(sys::StateSpace, w::Array=[]; plot=true)
    A, B, C, D = sys.A, sys.B, sys.C, sys.D

    # Number of inputs.
    num_u = size(B,2)

    # Number of outputs.
    num_y = size(C,1)

    # Number of states.
    num_states = size(A,1)

    # Identity matrix.
    eye_s = eye(num_states)

    # Check if the vector of frequencies was provided.
    w_user = !isempty(w)

    w_final = 0.0

    tol = 0.0

    # If the user did not provide a vector with frequencies, then they will be
    # computed automatically.
    if (!w_user)
        # If not, compute the bode diagram from a frequency two decades lower
        # than the minimum frequency and two decades higher than the maximum
        # frequency.
        poles      = pole(sys)
        real_poles = abs.(real(poles[poles .!= 0]))

        # Get maximum frequency of the system.
        highest_freq = (isempty(real_poles)) ? 0.0 : maximum(real_poles)
        w_final = (highest_freq != 0) ? highest_freq*100.0 : 1e+2

        # Compute the tolerance using the DC gain.
        dc_gain = maximum(D + C*pinv(eye(A)-A)*B)
        tol     = dc_gain/50.0

        # Don't let tolerance to be 0.
        (tol == 0) && (tol = 1e-2)

        # Initialize the vector `w` with 100 points between the minimum and
        # maximum frequencies.
        w = collect(linspace(0,w_final,100))
    end

    # Frequency response for each pair input/output.
    real_fr     = Vector{Matrix{Float64}}(0)
    imag_fr     = Vector{Matrix{Float64}}(0)
    mir_imag_fr = Vector{Matrix{Float64}}(0)

    # Compute the Nyquist plot.
    i  = 1
    wi = w[1]

    while i <= length(w)
        wi = w[i]

        s = Complex(0,wi)

        fr = C*pinv(s*eye_s-A)*B+D

        real_fr_i  = Float64[real(f) for f in fr]
        imag_fr_i  = Float64[imag(f) for f in fr]

        # If w == 0 and A is singular, then we have a pole at the origin. In
        # this case, we must not consider the frequencies computed to plot the
        # graph.
        if (wi == 0.0) && (det(A) == 0.0)
            real_fr_i = ones(real_fr_i)*Inf
            imag_fr_i = ones(imag_fr_i)*Inf
        end

        if w_user || (i == 1)
            i += 1
            push!(real_fr,     +real_fr_i)
            push!(imag_fr,     +imag_fr_i)

            (!w_user) && push!(mir_imag_fr, -imag_fr_i)
        else
            # Verify the maximum distance between the current point and the last
            # one.
            Δx = maximum(abs.(real_fr_i - real_fr[i-1]))
            Δy = maximum(abs.(imag_fr_i - imag_fr[i-1]))

            # Accept the point if the tolerance is higher than the distances.
            if (maximum([Δx;Δy]) > tol) && ( (w[i]-w[i-1]) > 1e-2 )
                if i == 2
                    w = [collect(linspace(w[1],w[2],10)); w[3:end]]
                elseif i == length(w)
                    w = [w[1:i-2]; collect(linspace(w[i-1],w[i],10))]
                else
                    w = [w[1:i-2]; collect(linspace(w[i-1],w[i],10)); w[i+1:end]]
                end
            else
                i += 1
                push!(real_fr,     +real_fr_i)
                push!(imag_fr,     +imag_fr_i)
                push!(mir_imag_fr, -imag_fr_i)
            end
        end
    end

    ( plot ) && plot_nyquist(real_fr, imag_fr, num_u, num_y)

    real_fr, imag_fr, w
end

#==#
#
# @brief Compute bode diagram.
# @param[in] G Transfer function of the dynamic system.
# @param[in] w Array of frequencies in which the bode diagram will be computed.
# If empty, the frequencies will be chosen according to the system poles.
# @param[in] plot Plot bode diagram (default = true).
#
# @return Real(G(jw)), Imag(G(jw)), w (freq. array) for each pair input/output.
#
#==#

function nyquist(G::TransferFunction, w::Array=[]; plot=true)
    # Convert the transfer function to state space and call bode.jl.
    nyquist(tf2ss(G), w; plot=plot)
end


module Rainflow
import Base.show

export sort_peaks, find_boundary_vals, count_cycles, sum_cycles

""" This function sort out points where the slope is changing sign"""
function sort_peaks(signal::AbstractArray{Float64,1}, dt=collect(1.:length(signal)))
    slope = diff(signal)
    # Determines if the point is local extremum
    is_extremum = vcat(true, (slope[1:end-1].*slope[2:end]).<=0., true)
    return signal[is_extremum] , dt[is_extremum]
end

struct Cycle  # This is the information stored for each cycle found
    count::Float64
    range::Float64
    mean::Float64
    Rvalue::Float64   # value
    v_s::Float64   # value start
    t_s::Float64   # time start
    v_e::Float64   # value end
    t_e::Float64   # time end
end

show(io::IO,x::Cycle) = print(io, "Cycle: count=", x.count, ", range=",x.range, ", mean=",x.mean, ", R=", x.Rvalue)

function cycle(count::Float64, v_s::Float64, t_s::Float64, v_e::Float64, t_e::Float64)
    Cycle(count, abs(v_s-v_e), (v_s+v_e)/2, min(v_s,v_e)/max(v_s,v_e), v_s, t_s, v_e, t_e)
end

""" Count the cycles from the data """
function count_cycles(peaks::Array{Float64,1},t::Array{Float64,1})
    list = copy(peaks) # Makes a copy because there is going to be sorted in the vectors
    time = copy(t)
    currentindex = 1
    nextindex = 2
    cycles = Cycle[]
    sizehint!(cycles, length(list)) # This reduces the memory consumption a bit
    @inbounds begin
    while length(list) > (currentindex+1)
            currentvalue = abs(list[currentindex+1]-list[currentindex])
            nextvalue = abs(list[nextindex+1]-list[nextindex])
        if nextvalue > currentvalue
            if currentindex == 1 # This case counts a half and cycle deletes the poit that is counted
                push!(cycles,cycle(0.5 ,list[currentindex], time[currentindex], list[currentindex+1],time[currentindex+1]))
                popfirst!(list) # Removes the first entrance in ext and time
                popfirst!(time)
            else # This case counts one cycle and deletes the point that is counted
                push!(cycles,cycle(1. ,list[currentindex], time[currentindex], list[currentindex+1], time[currentindex+1]))
                deleteat!(list, currentindex:(currentindex+1)) # Removes the i and i+1 entrance in ext and time
                deleteat!(time, currentindex:(currentindex+1))
            end
            currentindex = 1
            nextindex = 2
        else
            currentindex += 1
            nextindex += 1
        end
    end
    for currentindex=1:length(list)-1 # This counts the rest of the points that have not been counted as a half cycle

        push!(cycles,cycle(0.5 ,list[currentindex], time[currentindex], list[currentindex+1],time[currentindex+1]))
    end
    end
    return cycles
end

mutable struct Cycles_bounds #
    min_mean::Float64
    max_mean::Float64
    max_range::Float64
    min_R::Float64
    max_R::Float64
end

show(io::IO,x::Cycles_bounds) = print(io, "Cycles_bounds : min mean value=", x.min_mean, ", max mean value=", x.max_mean, ", max range=",x.max_range, ", min R=", x.min_R, ", max R=",x.max_R)

""" Find the minimum and maximum mean value and maximum range from a vector of cycles"""
function find_boundary_vals(cycles::Array{Cycle,1})
    bounds = Cycles_bounds(Inf, -Inf, -Inf, Inf, -Inf)
    for cycle in cycles
        cycle.mean > bounds.max_mean && setfield!(bounds, :max_mean, cycle.mean)
        cycle.mean < bounds.min_mean && setfield!(bounds, :min_mean, cycle.mean)
        cycle.range > bounds.max_range && setfield!(bounds, :max_range, cycle.range)
        cycle.Rvalue < bounds.min_R && setfield!(bounds, :min_R, cycle.Rvalue)
        cycle.Rvalue > bounds.max_R && setfield!(bounds, :max_R, cycle.Rvalue)
    end
    return bounds
end

""" Returns the range index the value is belonging in """
function find_range(interval::Array{T,1},value) where {T <: Real}
    for i=1:length(interval)-1
        if interval[i] <= value < interval[i+1]
            return i
        end
    end
    error("The value where not in range")
end

Interval{T} = Union{Array{T,1}, StepRangeLen{T}}

""" Sums the cycle count given intervals of range_intervals and mean_intervals. The range_intervals and mean_intervals is given in fraction of range size"""
function sum_cycles(cycles::Array{Cycle,1}, range_intervals::Interval{T}, mean_intervals::Interval{T}) where {T <: Real}
    bounds = find_boundary_vals(cycles)
    bins = zeros(length(range_intervals)-1, length(mean_intervals)-1)
    range_in = (range_intervals*bounds.max_range)/100
    mean_in = (mean_intervals*(bounds.max_mean-bounds.min_mean))/100
    mean_in = mean_in .+ bounds.min_mean
    issorted(mean_intervals) || error("The array needs to be sorted in raising order")
    issorted(range_intervals) || error("The array needs to be sorted in raising order")
    nr_digits = 14  # The rounding is performed due to numerical noise in the floats when comparing
    mean_i = collect(mean_in)
    range_i = collect(range_in)
    #ensure the cycles is in the intervals by adding a small error to the end values of the interal.
    error_m = (bounds.max_mean-bounds.min_mean)*1e-14
    mean_i[end]+=error_m
    mean_i[1]-=error_m
    error_r = bounds.max_range*1e-14
    range_i[end]+=error_r
    range_i[1]-=error_r
    #show(mean_intervals)
    for cycle in cycles
        i = find_range(range_i, cycle.range)
        j = find_range(mean_i, cycle.mean)
        bins[i,j] += cycle.count
    end
    return bins
end

function sum_cycles(cycles::Array{Cycle,1}, nr_ranges::Int=10, nr_means::Int=1)
    range_intervals = range(0,stop=100,length=nr_ranges+1)
    mean_intervals = range(0,stop=100,length=nr_means+1)
    sum_cycles(cycles, range_intervals, mean_intervals)
end

end # module


# Tests for Profit DEA Models

# Struct for testing DEA model without monetary option
struct wrongProfitDEAmodel <: AbstractProfitDEAModel
    n::Int64
    eff::Vector
end

@testset "ProfitDEAModel" begin

    ## Test Profit DEA Model
    # Test with Zofio, Pastor and Aparicio (2013) data
    X = [1 1; 1 1; 0.75 1.5; 0.5 2; 0.5 2; 2 2; 2.75 3.5; 1.375 1.75]
    Y = [1 11; 5 3; 5 5; 2 9; 4 5; 4 2; 3 3; 4.5 3.5]
    P = [2 1; 2 1; 2 1; 2 1; 2 1; 2 1; 2 1; 2 1]
    W = [2 1; 2 1; 2 1; 2 1; 2 1; 2 1; 2 1; 2 1]

    GxGydollar = 1 ./ (sum(P, dims = 2) + sum(W, dims = 2))
    GxGydollar = repeat(GxGydollar, 1, 2)

    deaprofitdollar = deaprofit(X, Y, W, P, Gx = GxGydollar, Gy = GxGydollar)

    @test typeof(deaprofitdollar) == ProfitDEAModel

    @test nobs(deaprofitdollar) == 8
    @test ninputs(deaprofitdollar) == 2
    @test noutputs(deaprofitdollar) == 2
    @test efficiency(deaprofitdollar, :Economic)   ≈ [2; 2; 0; 2; 2; 8; 12; 4] atol = 1e-3
    @test efficiency(deaprofitdollar, :Technical)  ≈ [0; 0; 0; 0; 0; 6; 12; 3] atol = 1e-3
    @test efficiency(deaprofitdollar, :Allocative) ≈ [2; 2; 0; 2; 2; 2; 0; 1] atol = 1e-3

    @test efficiency(deaprofit(X, Y, W, P, Gx = :Monetary, Gy = :Monetary)) == efficiency(deaprofitdollar)

    @test efficiency(deaprofit(targets(deaprofitdollar, :X), targets(deaprofitdollar, :Y), W, P, Gx = :Monetary, Gy = :Monetary)) ≈ zeros(8)

    @test normfactor(deaprofitdollar) == ones(8);

    @test peersmatrix(deaprofitdollar) == deaprofitdollar.lambda

    # Check directions checking technical efficiency
    @test efficiency(deaprofit(X, Y, W, P, Gx = :Zeros, Gy = :Ones), :Technical) == efficiency(deaddf(X, Y, Gx = :Zeros, Gy = :Ones, rts = :VRS))
    @test efficiency(deaprofit(X, Y, W, P, Gx = :Ones, Gy = :Zeros), :Technical) == efficiency(deaddf(X, Y, Gx = :Ones, Gy = :Zeros, rts = :VRS))
    @test efficiency(deaprofit(X, Y, W, P, Gx = :Observed, Gy = :Observed), :Technical) == efficiency(deaddf(X, Y, Gx = :Observed, Gy = :Observed, rts = :VRS))
    @test efficiency(deaprofit(X, Y, W, P, Gx = :Mean, Gy = :Mean), :Technical) == efficiency(deaddf(X, Y, Gx = :Mean, Gy = :Mean, rts = :VRS))

    # Print
    show(IOBuffer(), deaprofitdollar)

    # Check normalization factor with different direction
    @test normfactor(deaprofit(X, Y, W, P, Gx = :Ones, Gy = :Ones)) == vec(sum(P .* ones(size(Y)), dims = 2) .+ sum(W .* ones(size(X)), dims = 2))

    # Check monetary option
    @test ismonetary(deaprofitdollar) == false

    deaprofitmonetary = deaprofit(X, Y, W, P, Gx = :Ones, Gy = :Ones, monetary = true)
    @test efficiency(deaprofitmonetary, :Economic)   ≈ [2; 2; 0; 2; 2; 8; 12; 4] atol = 1e-3
    @test efficiency(deaprofitmonetary, :Technical)  ≈ [0; 0; 0; 0; 0; 6; 12; 3] atol = 1e-3
    @test efficiency(deaprofitmonetary, :Allocative) ≈ [2; 2; 0; 2; 2; 2; 0; 1] atol = 1e-3
    @test ismonetary(deaprofitmonetary) == true

    # Test errors
    @test_throws DimensionMismatch deaprofit([1; 2 ; 3], [4 ; 5], [1; 1; 1], [4; 5], Gx = [1; 2 ; 3], Gy = [4 ; 5]) #  Different number of observations
    @test_throws DimensionMismatch deaprofit([1; 2; 3], [4; 5; 6], [1; 2; 3; 4], [4; 5; 6], Gx = [1; 2; 3], Gy = [4; 5; 6]) # Different number of observation in input prices
    @test_throws DimensionMismatch deaprofit([1; 2; 3], [4; 5; 6], [1; 2; 3], [4; 5; 6; 7], Gx = [1; 2; 3], Gy = [4; 5; 6]) # Different number of observation in output prices
    @test_throws DimensionMismatch deaprofit([1 1; 2 2; 3 3], [4; 5; 6], [1 1 1; 2 2 2; 3 3 3], [4; 5; 6], Gx = [1 1; 2 2; 3 3], Gy = [4; 5; 6]) # Different number of input prices and inputs
    @test_throws DimensionMismatch deaprofit([1; 2; 3], [4 4; 5 5; 6 6], [1; 2; 3], [4 4 4; 5 5 5; 6 6 6], Gx = [1; 2; 3], Gy = [4 4; 5 5; 6 6]) # Different number of oputput prices and outputs
    @test_throws DimensionMismatch deaprofit([1 1; 2 2; 3 3], [4; 5; 6], [1 1; 2 2; 3 3], [4; 5; 6], Gx = [1 1 1; 2 2 2; 3 3 3], Gy = [4; 5; 6]) # Different size of inputs direction
    @test_throws DimensionMismatch deaprofit([1; 2; 3], [4 4; 5 5; 6 6], [1; 2; 3], [4 4; 5 5; 6 6], Gx = [1; 2; 3], Gy = [4 4 4; 5 5 5; 6 6 6]) # Different size of outputs direction
    @test_throws ArgumentError deaprofit([1; 2; 3], [1; 2; 3], [1; 1; 1], [1; 1; 1], Gx = :Error, Gy = :Ones) # Invalid inputs direction
    @test_throws ArgumentError deaprofit([1; 2; 3], [1; 2; 3], [1; 1; 1], [1; 1; 1], Gx = :Ones, Gy = :Error) # Invalid outputs direction
    @test_throws ArgumentError targets(deaprofitdollar, :error) # Invalid target

    # Test struct defaults and errors
    @test_throws ArgumentError ismonetary(wrongProfitDEAmodel(3, [1; 2; 3])) # Model does not have info on peers

    # ------------------
    # Test Vector and Matrix inputs and outputs
    # ------------------
    # Tests against results in R

    # Inputs is Matrix, Outputs is Vector
    X = [2 2; 1 4; 4 1; 4 3; 5 5; 6 1; 2 5; 1.6	8]
    Y = [1; 1; 1; 1; 1; 1; 1; 1]
    W = [1 1; 1 1; 1 1; 1 1; 1 1; 1 1; 1 1; 1 1]
    P = [1; 1; 1; 1; 1; 1; 1; 1]

    @test efficiency(deaprofit(X, Y, W, P, Gx = X, Gy = Y)) ≈ [0; 0.1666666667; 0.1666666667; 0.375; 0.5454545455; 0.375; 0.375; 0.5283018868]

    # Inputs is Vector, Output is Matrix
    X = [1; 1; 1; 1; 1; 1; 1; 1]
    Y = [7 7; 4 8; 8 4; 3 5; 3 3; 8 2; 6 4; 1.5 5]
    W = [1; 1; 1; 1; 1; 1; 1; 1]
    P = [1 1; 1 1; 1 1; 1 1; 1 1; 1 1; 1 1; 1 1]

    @test efficiency(deaprofit(X, Y, W, P, Gx = X, Gy = Y)) ≈ [0; 0.1538461538; 0.1538461538; 0.6666666667; 1.142857143; 0.3636363636; 0.3636363636; 1]

    # Inputs is Vector, Output is Vector
    X = [2; 4; 8; 12; 6; 14; 14; 9.412]
    Y = [1; 5; 8; 9; 3; 7; 9; 2.353]
    W = [1; 1; 1; 1; 1; 1; 1; 1]
    P = [1; 1; 1; 1; 1; 1; 1; 1]

    @test efficiency(deaprofit(X, Y, W, P, Gx = X, Gy = Y)) ≈ [0.6666666667; 0; 0.0625; 0.1904761905; 0.4444444444; 0.3809523810; 0.2608695652; 0.6849978751]

end


using ProjectedGPE
ecut = 40
basis = "Hermite"

ωx = 1
ωy = π
ωz = 5
Ω = [ωx;ωy;ωz]

#check 1D
cinfo = makecinfo(ecut,Ω,basis)
#@step makecinfo("Hermite",ecut,[ωx,ωz])

#test 1D
Ω=[ωx]
M=[5]
x,wx,Tx=makealltrans(M,4,Ω,basis)

#test 2D
Ω=[ωx, ωy]
M=[5,10]
x,wx,Tx,y,wy,Ty=makealltrans(M,4,Ω,basis)

#test 3D
Ω=[ωx, ωy, ωz]
M=[25,17,33]
x,wx,Tx,y,wy,Ty,z,wz,Tz=makealltrans(M,4,Ω,basis)


if !@isdefined(AbstractModel)
    include(Pkg.dir("DifferentialDynamicProgramming","src","interfaces.jl"))
end

"""
    forward_kalman(y, C, R1, R2, P0)

#Arguments:
- `y`: DESCRIPTION
- `C`: DESCRIPTION
- `R1`: State noise
- `R2`: Meas noise
- `P0`: Initial state cov
"""
function forward_kalman(y,C,R1,R2, P0)
    na,n  = size(C)
    ma  = (n-na^2) ÷ na
    sa  = na+ma
    T   = size(y,2)
    xkk = zeros(n,T);
    Pkk = zeros(n,n,T)
    for i = 0:na-1 # TODO: This should be an input, maybe only initialize with first n_param datapoints?
        ran = (i*sa+1):((i+1)*sa)
        data_to_use = 1:min(10n, size(y,2))
        xkk[ran,1]    = C[i+1,ran,data_to_use]'\y[i+1,data_to_use]  # Initialize to semi-global ls solution
    end
    R2d = MvNormal(R2)
    Pkk[:,:,1] .= P0
    xk         = copy(xkk)
    Pk         = copy(Pkk)
    i          = 1
    Ck         = C[:,:,i]
    e          = y[:,i]-Ck*xk[:,i]
    ll         = logpdf(R2d,e)
    S          = Ck*Pk[:,:,i]*Ck' + R2
    K          = (Pk[:,:,i]*Ck')/S
    xkk[:,i]   = xk[:,i] + K*e
    Pkk[:,:,i] = (I - K*Ck)*Pk[:,:,i]
    @views for i = 2:T
        Ak         = 1 # This just assumes a random walk, no additional dynamics
        Ck         = C[:,:,i]
        xk[:,i]    = Ak*xkk[:,i-1]
        Pk[:,:,i]  = Ak*Pkk[:,:,i-1]*Ak' + R1
        e          = y[:,i]-Ck*xk[:,i]
        ll        += logpdf(R2d,e)
        S          = Ck*Pk[:,:,i]*Ck' + R2
        K          = (Pk[:,:,i]*Ck')/S
        xkk[:,i]   = xk[:,i] + K*e
        Pkk[:,:,i] = (I - K*Ck)*Pk[:,:,i]
    end

    return xkk,xk,Pkk,Pk,ll
end

function kalman_smoother(y, C, R1, R2, P0)
    T                = size(y,2)
    xkk,xk,Pkk,Pk,ll = forward_kalman(y,C,R1,R2, P0)
    xkn              = similar(xkk)
    Pkn              = similar(Pkk)
    xkn[:,end]       = xkk[:,end]
    Pkn[:,:,end]     = Pkk[:,:,end]
    @views for i = T-1:-1:1
        Ck          = Pkk[:,:,i]/Pk[:,:,i+1]
        xkn[:,i]    = xkk[:,i] + Ck*(xkn[:,i+1] - xk[:,i+1])
        Pkn[:,:,i]  = Pkk[:,:,i] + Ck*(Pkn[:,:,i+1] - Pk[:,:,i+1])*Ck'
    end
    return xkn, Pkn,ll
end

# TODO: Enable imposing of known structure with e.g. a boolean matrix and a coefficient matrix to tell the algorithm which entries are known to be ==1, ==0, ==h etc.
# Use this matrix to either set some values in C, xkn, Pkn, At,Bt to zero
eye(n) = Matrix{Float64}(I,n,n)

function KalmanModel(model::KalmanModel, d::InputOutputStateData, R1,R2, P0=100R1; extend=false, D=1)::KalmanModel
    T = length(d)-1
    n = nstates(d)
    m = ninputs(d)
    xi = oftype(Matrix, state(d))
    u = oftype(Matrix, input(d))

    x,u,xnew = xi[:,1:end-1],u[:,1:end-1],xi[:,2:end]
    @assert T > n "The calling convention for x and u is that time is the second dimention"
    Ta  = extend ? T+1 : T
    N   = n^2+n*m
    y   = copy(xnew)
    C   = zeros(n,D*N,T)
    if D == 2
        R1 = kron([1/4 1/2; 1/2 1]+0.001I, R1)
        P0 = kron([1/4 1/2; 1/2 1]+0.001I, P0)
    end
    @views for t = 1:T
        C[:,1:N,t] = kron(eye(n),[x[:,t]; u[:,t]]')
    end
    xkn, Pkn,ll    = kalman_smoother(y, C, R1, R2, P0)
    model.ll = ll
    @views for t = 1:T
        ABt      = reshape(xkn[1:N,t],n+m,n)'
        model.At[:,:,t] .= ABt[:,1:n]
        model.Bt[:,:,t] .= ABt[:,n+1:end]
    end
    @views if extend # Extend model one extra time step (primitive way)
        model.At[:,:,end] .= model.At[:,:,end-1]
        model.Bt[:,:,end] .= model.Bt[:,:,end-1]
        Pkn = cat(Pkn, Pkn[:,:,end], dims=3)
    end
    model.extended = extend
    model.Pt = Pkn

    return model
end

function KalmanModel(model::KalmanModel, prior::KalmanModel, d::AbstractIdData,args...; kwargs...)::KalmanModel
    model = KalmanModel(model, d,args...; kwargs...) # Fit model in the standard way without prior
    T = length(d)
    n = nstates(d)
    m = ninputs(d)
    # @views model2statevec(model,t) = [vec(model.At[:,:,t]);  vec(model.Bt[:,:,t])][:]
    # @views model2statevec(model,t) = [model.At[:,:,t]  model.Bt[:,:,t]] |> vec
    @views for t = 1:T     # Incorporate prior
        Pkk               = model.Pt[:,:,t]
        K̄                 = Pkk/(Pkk + prior.Pt[:,:,t])
        model.Pt[:,:,t] .-= K̄*Pkk
        x                 = model2statevec(model,t)
        xp                = model2statevec(prior,t)
        x               .+= K̄*(xp-x)
        ABt               = reshape(x,n+m,n)'
        model.At[:,:,t]  .= ABt[:,1:n]
        model.Bt[:,:,t]  .= ABt[:,n+1:end]

    end
    model
end

# function modelfit(model,d)
#     x = oftype(Matrix, state(d))
#     yhat = predict(model, d)
#     fit = nrmse(x[:,2:end],yhat)
# end

# """
#     KalmanModel(xi, R1, R2, P0=100R1; extend=false, D=1, printfit=true)::KalmanModel
#
# Estimate a Kalman model without control input
#
# #Arguments:
# - `xi`: states or iddata containing states
# - `R1`: Parameter transition noise
# - `R2`: State transition noise
# - `P0`: Initial state cov
# - `extend`: add one data point at the end to have length of parameter vector match length of input.
# - `D`: Order of integration of the noise.
# """
# function KalmanModel(model::KalmanModel, xi::AnyInput,R1,R2, P0=100R1; extend=false, D=1)::KalmanModel
#     xi = oftype(Matrix, state(xi))
#     x,xnew = xi[:,1:end-1],xi[:,2:end]
#     T = length(d)
#     n = nstates(d)
#     m = ninputs(d)
#     @assert T > n "The calling convention for x and u is that time is the second dimention"
#     Ta  = extend ? T+1 : T
#     N   = n^2
#     y   = copy(xnew)
#     C   = zeros(n,D*N,T)
#     if D == 2
#         R1 = kron([1/4 1/2; 1/2 1]+0.001I, R1)
#         P0 = kron([1/4 1/2; 1/2 1]+0.001I, P0)
#     end
#     @views for t = 1:T
#         C[:,1:N,t] = kron(eye(n),x[:,t]')
#     end
#     xkn, Pkn,ll    = kalman_smoother(y, C, R1, R2, P0)
#     model.ll = ll
#     @views for t = 1:T
#         model.At[:,:,t] .= reshape(xkn[1:N,t],n,n)'
#     end
#     @views if extend # Extend model one extra time step (primitive way)
#         model.At[:,:,end] .= model.At[:,:,end-1]
#         Pkn = cat(Pkn, Pkn[:,:,end], dims=3)
#     end
#     model.extended = extend
#     model.Pt = Pkn
#     if printfit
#         yhat = predict(model, x)
#         fit = nrmse(xnew,yhat)
#         println("Modelfit: ", round.(fit,digits=3))
#     end
#
#     return model
# end

function LTVAutoRegressive(model::LTVAutoRegressive, yi,R1,R2, P0=100R1; extend=false, D=1)
    yi = oftype(Matrix, output(yi))
    y,ynew = yi[1:end-1],yi[2:end]
    T = length(y)
    n = size(R1,1)
    ϕ   = zeros(1,D*n,T)
    T  = extend ? T : T-1
    if D == 2
        R1 = kron([1/4 1/2; 1/2 1]+0.001I, R1)
        P0 = kron([1/4 1/2; 1/2 1]+0.001I, P0)
    end
    inds = 1:n
    y0 = [zeros(n-1);y]
    size(y0)
    @views for t = 1:T
        ϕ[1,1:n,t] = y0[inds .+ (t-1)]
    end
    xkn, Pkn,ll = kalman_smoother(ynew', ϕ, R1, R2, P0)
    model.ll = ll
    @views for t = 1:T
        model.θ[:,t] .= xkn[1:n,t]
    end
    @views if extend # Extend model one extra time step (primitive way)
        model.θ[:,end] .= model.θ[:,end-1]
        Pkn = cat(Pkn, Pkn[:,:,end], dims=3)
    end
    model.extended = extend
    model.Pt = Pkn

    return model
end

function predict(model::LTVAutoRegressive, y)
    y = output(y)
    T  = length(y)
    n = size(model.θ, 1)
    @assert T<=length(model) "Can not predict further than the number of time steps in the model"
    y0 = [zeros(n-1);y]
    ynew = zeros(eltype(y), T)
    inds = 1:n
    @views for t = 1:T
        ynew[t] = model.θ[:,t]'y0[inds .+ (t-1)]
    end
    ynew
end

function rootspectrogram(model, fs)
    roots = map(1:length(model)) do i
        sys = tf(1,[1;-reverse(model.θ[:,i])],1)
        (sort(pole(sys), by=imag, rev=true)[1:end÷2])
    end
    S = reduce(hcat,roots)
    fs/(2pi) .* angle.(S)'
end


using ApproximateBayesianComputing
const ABC = ApproximateBayesianComputing
using Distributions

include(joinpath(Pkg.dir("ABC"),"src/composite.jl"))
using CompositeDistributions
import Compat.view

# Set Prior for Population Parameters
d1 = Rayleigh(0.3)
d2 = MvNormal([0.0, 1.0], ones(2))
param_prior = CompositeDist( ContinuousDistribution[d1,d2] )

# Code to generate simulated data given array of model parameters
num_data_default = 100
num_outputs = 2
function gen_data(theta::Array, n::Integer = num_data_default)
  data = Array{Float64}(3,n)
  data[1,:] = rand(Rayleigh(theta[1]),n)
  data[2:3,:] = rand(MvNormal(theta[2:3],ones(2)),n)
  return data
end

# Function to adjust originally proposed model parameters, so that they will be valid
function normalize_theta13_pos!(theta::Array)
 theta[1] = abs(theta[1])
 theta[3] = abs(theta[3])
 return theta
end

# Function to test if the proposed model parameter are valid
function is_valid_theta13_pos(theta::Array)
  theta[1]>0.0  && theta[3]>0.0
end

# True Population Parameters
theta_true = [0.3, 0.0, 1.0]

# Tell ABC what it needs to know for a simulation
abc_plan = abc_pmc_plan_type(gen_data,ABC.calc_summary_stats_mean_var,ABC.calc_dist_max, param_prior; is_valid=is_valid_theta13_pos,num_max_attempt=10000);

# Generate "true/observed data"
data_true = gen_data(theta_true)   # Draw "real" data from same model as for analysis
ss_true = abc_plan.calc_summary_stats(data_true)
#println("theta= ",theta_true," ss= ",ss_true, " d= ", 0.)

# Run ABC simulation
@time pop_out = run_abc(abc_plan,ss_true);

#=
# Optional plotting of results
using PyPlot

hist(pop_out.weights*length(pop_out.weights));
hist(pop_out.dist);

num_grid_x = 100
num_grid_y = 100
limit = 1.0
x = collect(linspace(theta_true[1]-limit,theta_true[1]+limit,num_grid_x));
y = collect(linspace(theta_true[2]-limit,theta_true[2]+limit,num_grid_y));
z = zeros(Float64,(num_param,length(x),length(y)))
for i in 1:length(x), j in 1:length(y)
    z[1,i,j] = x[i]
    z[2,i,j] = y[j]
end
z = reshape(z,(num_param,length(x)*length(y)))
zz = [ ABC.pdf(ABC.GaussianMixtureModelCommonCovar(pop_out.theta,pop_out.weights,ABC.cov_weighted(pop_out.theta',pop_out.weights)),vec(z[:,i])) for i in 1:size(z,2) ]
zz = reshape(zz ,(length(x),length(y)));
levels = [exp(-0.5*i^2)/sqrt(2pi^num_param) for i in 0:5];
PyPlot.contour(x,y,zz',levels);
plot(pop_out.theta[1,:],pop_out.theta[2,:],".");
=#


function compute()

	N=1000
	for a=1:N
		for b=a+1:N
			c=N-a-b
			if a^2+b^2==c^2
				return a*b*c

			end
		end
	end
end

print(compute())


function assemble(op::Identity,
        testnfs::AbstractTimeBasisFunction,
        trialfns::AbstractTimeBasisFunction)

    tbf = convolve(testnfs, trialfns)
    T = scalartype(tbf)
    z = zeros(T, numintervals(tbf)-1)
    Δt = timestep(tbf)
    for i in eachindex(z)
        p = tbf.polys[i]
        t = (i-1)*Δt
        z[i] = evaluate(p,t)
    end

    return z
end

type TensorOperator <: Operator
    spatial_factor
    temporal_factor
end

⊗(A::AbstractOperator, B::AbstractOperator) = TensorOperator(A, B)
function scalartype(A::TensorOperator)
    promote_type(
        scalartype(A.spatial_factor),
        scalartype(A.temporal_factor))
end

function assemble(operator::TensorOperator, testfns, trialfns)

    test_spatial_basis  = spatialbasis(testfns)
    trial_spatial_basis = spatialbasis(trialfns)

    M = numfunctions(test_spatial_basis)
    N = numfunctions(trial_spatial_basis)

    tbf = convolve(
        temporalbasis(testfns),
        temporalbasis(trialfns))

    kmax = numintervals(tbf) - 1
    k0 = fill(1,    (M,N))
    k1 = fill(kmax, (M,N))

    T = promote_type(scalartype(operator), scalartype(testfns), scalartype(trialfns))
    data = zeros(T, M, N, kmax)
    Z = SparseND.Banded3D(k0, k1, data)

    store(v, m, n, k) = (Z[m,n,k] += v)
    assemble!(operator, testfns, trialfns, store)
    return Z

end

function assemble!(operator::TensorOperator, testfns, trialfns, store)

    space_operator = operator.spatial_factor
    time_operator  = operator.temporal_factor

    space_testfns = spatialbasis(testfns)
    space_trialfns = spatialbasis(trialfns)

    time_testfns = temporalbasis(testfns)
    time_trialfns = temporalbasis(trialfns)

    zt = assemble(time_operator, time_testfns, time_trialfns)
    function store1(v,m,n)
        for (k,w) in enumerate(zt)
            store(w*v,m,n,k)
        end
    end
    assemble!(space_operator, space_testfns, space_trialfns, store1)

end

type TemporalDifferentiation
    operator
end

function assemble!(operator::TemporalDifferentiation, testfns, trialfns, store)

    trial_time_fns  = temporalbasis(trialfns)
    trial_space_fns = spatialbasis(trialfns)

    trialfns = SpaceTimeBasis(
        trial_space_fns,
        derive(trial_time_fns)
    )

    assemble!(operator.operator, testfns, trialfns, store)

end


module PowerNumbers

using Base, DualNumbers, LinearAlgebra

import Base: convert, *, +, -, ==, <, <=, >, !, !=, >=, /, ^, \, in, isapprox, promote_rule
import Base: exp, atanh, log1p, abs, log, inv, real, imag, conj, sqrt,
                sin, cos, cbrt, abs2, log10, log2, exp, exp2, expm1,
                sin, cos, tan, sec, csc, cot, sind, cosd, tand, secd, cscd, cotd, asin, acos,
                atan, asec, acsc, acot, asind, acosd, atand, asecd, acscd, acotd, sinh, cosh,
                tanh, sech, csch, coth, asinh, acosh, asech, acsch, acoth, deg2rad, rad2deg,
                zero, isless, sign

import DualNumbers: Dual, realpart, epsilon, dual

export PowerNumber, LogNumber, ϵ

include("LogNumber.jl")

struct PowerNumber{T<:Number,V<:Number} <: Number
    A::T
    B::T
    α::V
    β::V
    PowerNumber{T,V}(A,B,α,β) where {T<:Number,V<:Number} =
    if α > β
        error("Must have α<=β")
    elseif α == -Inf
        error("Must have α<=β")
    elseif α == β
        new{T,V}(A+B,0,β,β)
    elseif A == 0
        new{T,V}(B,0,β,β)
    else
        new{T,V}(A,B,α,β)
    end
end

PowerNumber(a::T, b::T, c::V, d::V) where {T,V} = PowerNumber{T,V}(a,b,c,d)
PowerNumber(a,b,c,d) = PowerNumber(promote(a,b)..., promote(c,d)...)
PowerNumber(a,b) = PowerNumber(a,0,b,b)
PowerNumber(a) = PowerNumber(a,0,zero(a),Inf)

PowerNumber{T,V}(z::PowerNumber) where {T,V} = PowerNumber{T,V}(convert(T, z.A), convert(T, z.B), convert(V, z.α), convert(V, z.β))
PowerNumber{T,V}(a) where {T,V} = PowerNumber(convert(T,a),zero(T),zero(V),convert(V,Inf))

promote_rule(::Type{T}, ::Type{PowerNumber{V,W}}) where {T,V,W} =
    promote_type(PowerNumber{promote_type(T,V),W})

const ϵ = PowerNumber(1,1)

apart(z::PowerNumber) = z.A
bpart(z::PowerNumber) = z.B
alpha(z::PowerNumber) = z.α
beta(z::PowerNumber) = z.β

PowerNumber(x::Dual) = PowerNumber(realpart(x), epsilon(x), 0, 1)
Dual(x::PowerNumber) = (alpha(x) == 0 && beta(x) == 1) ? (return Dual(apart(x), bpart(x))) : (throw("α, β must equal 0, 1 to convert to dual."))
dual(x::PowerNumber) = Dual(x)

zero(x::PowerNumber) = PowerNumber(zero(x.A), zero(x.B), x.α, x.β)

function (x::PowerNumber)(ε)
    a,b,α,β = apart(x),bpart(x),alpha(x),beta(x)
    a*ε^α + b*ε^β
end

function +(x::PowerNumber, y::PowerNumber)
    a,b,α,β = apart(x),bpart(x),alpha(x),beta(x)
    c,d,γ,δ = apart(y),bpart(y),alpha(y),beta(y)
    if δ ≈ β && d != 0
        return +(PowerNumber(a,b+d,α,β), PowerNumber(c,0,γ,δ))
    elseif δ < β #we assume β < δ
        return +(y, x)
    elseif γ > β || c == 0
        return x
    elseif γ ≈ β
        return PowerNumber(a,b+c,α,β)
    elseif γ < β && γ > α
        return PowerNumber(a,c,α,γ)
    elseif γ ≈ α
        return PowerNumber(a+c,b,α,β)
    else
        return PowerNumber(c,a,γ,α)
    end
end

+(x::PowerNumber, y::Number) = x + PowerNumber(y,0,0,Inf)
+(y::Number, x::PowerNumber) = +(x::PowerNumber, y::Number)

function *(x::PowerNumber, y::PowerNumber)
    a,b,α,β = apart(x),bpart(x),alpha(x),beta(x)
    c,d,γ,δ = apart(y),bpart(y),alpha(y),beta(y)
    return PowerNumber(a*c,α+γ) + PowerNumber(b*c,β+γ) + PowerNumber(a*d,α+δ) + PowerNumber(b*d,β+δ)
end

*(x::PowerNumber, y::Number) = PowerNumber(y*apart(x),y*bpart(x),alpha(x),beta(x))
*(y::Number, x::PowerNumber) = *(x::PowerNumber, y::Number)

-(x::PowerNumber) = PowerNumber(-apart(x),-bpart(x),alpha(x),beta(x))
-(x::PowerNumber, y::PowerNumber) = +(x, -y)
-(x::PowerNumber, y::Number) = +(x::PowerNumber, -y::Number)
-(y::Number, x::PowerNumber) = +(-x::PowerNumber, y::Number)

function inv(x::PowerNumber)
    a,b,α,β = apart(x),bpart(x),alpha(x),beta(x)
    if α == Inf
        error("Not defined for α = Inf")
    end
    α != β ? (return PowerNumber(1/a,-b*a^(-2),-α,β-2*α)) : (return PowerNumber(1/a,-β))
end

/(z::PowerNumber, x::PowerNumber) = z*inv(x)
/(z::PowerNumber, x::Number) = z*inv(x)
/(x::Number, z::PowerNumber) = x*inv(z)

^(z::PowerNumber, p::Integer) = invoke(^, Tuple{Number,Integer}, z, p)
function ^(z::PowerNumber, p::Number)
    a,b,α,β = apart(z),bpart(z),alpha(z),beta(z)
    α == β ? (return PowerNumber(a^p,α*p)) : (return PowerNumber(a^p,(a^(p-1))*b*p,p*α,β+(p-1)*α))
end

sqrt(z::PowerNumber) = z^0.5
cbrt(z::PowerNumber) = z^(1/3)

==(a::PowerNumber, b::PowerNumber) = apart(a) == apart(b) && bpart(a) == bpart(b) &&
                                    alpha(a) == alpha(b) && beta(a) == beta(b)

==(a::Number, b::PowerNumber) = PowerNumber(a) == b
==(a::PowerNumber, b::Number) = a == PowerNumber(b)

isapprox(a::PowerNumber, b::PowerNumber; opts...) = ≈(apart(a), apart(b); opts...) && ≈(bpart(a), bpart(b); opts...) &&
                                                ≈(alpha(a), alpha(b); opts...) && ≈(beta(a), beta(b); opts...)

function log(z::PowerNumber{T,V}) where {T,V}
    a,b,α,β = apart(z),bpart(z),alpha(z),beta(z)
    a != 0 ? (return LogNumber(promote(α, log(a))...)) : error("Cannot evaluate log at 0")
end

log1p(z::PowerNumber) = log(z+1)

function atanh(z::PowerNumber)
    (log(1+z)-log(1-z))/2
end

for op in (:real, :imag, :conj)
    @eval $op(l::PowerNumber) = PowerNumber($op(apart(l)), $op(bpart(l)), alpha(l), beta(l))
end

functionlist = (:abs, :abs2, :log10, :log2, :exp, :exp2, :expm1,
                :sin, :cos, :tan, :sec, :csc, :cot, :sind, :cosd, :tand, :secd, :cscd, :cotd, :asin, :acos,
                :atan, :asec, :acsc, :acot, :asind, :acosd, :atand, :asecd, :acscd, :acotd, :sinh, :cosh,
                :tanh, :sech, :csch, :coth, :asinh, :acosh, :asech, :acsch, :acoth, :deg2rad, :rad2deg)

for op in functionlist
    @eval function $op(z::PowerNumber)
        a,b,α,β = apart(z),bpart(z),alpha(z),beta(z)
        if iszero(α)
            fz = $op(dual(a,b))
            p = β
        elseif α > 0
            fz = $op(dual(0,a))
            p = α
        else
            error("Alpha must be non-negative")
        end
	    PowerNumber(realpart(fz), epsilon(fz), 0, p)
	end
end

sign(z::PowerNumber) = sign(z.A)
abs(z::PowerNumber{<:Real,<:Real}) = sign(z) * z

function isless(z::PowerNumber{T}, w::Real) where T
    a,b,α,β = apart(z),bpart(z),alpha(z),beta(z)
    (α > 0 || iszero(a)) && return isless(zero(T), w)
    α < 0 && return signbit(a) # TODO: What if w == -Inf
    isless(a,w)
end

function isless(w::Real, z::PowerNumber{T}) where T
    a,b,α,β = apart(z),bpart(z),alpha(z),beta(z)
    (α > 0 || iszero(a)) && return isless(w, zero(T))
    α < 0 && return !signbit(a) # TODO: What if w == -Inf
    isless(w,a)
end

function Base.show(io::IO, x::PowerNumber)
    if x.α == x.β || iszero(x.B)
        @assert iszero(x.B)
        if iszero(x.α)
            print(io, "$(x.A) + o(ϵ^$(x.β))")
        else
            print(io, "($(x.A))ϵ^$(x.α) + o(ϵ^$(x.β))")
        end
    else
        if iszero(x.α)
            print(io, "$(x.A) + ($(x.B))ϵ^$(x.β) + o(ϵ^$(x.β))")
        else
            print(io, "($(x.A))ϵ^$(x.α) + ($(x.B))ϵ^$(x.β) + o(ϵ^$(x.β))")
        end
    end
end

end # module


using JuMP
using Gurobi
using Plasmo
using PlasmoAlgorithms

mp = Model(solver = GurobiSolver())
sp1 = Model(solver = GurobiSolver())
sp2 = Model(solver = GurobiSolver())
sp3 = Model(solver = GurobiSolver())

@variable(mp,x[1:2]>=0)
@constraint(mp,x[1]+x[2]>=50)
@objective(mp,Min,x[1]+2x[2])

@variable(sp1,x[1:2]>=0)
@variable(sp1,y[1:2]>=0)
@constraint(sp1, x[1]+y[1]>=60)
@constraint(sp1, x[2]+y[2]>=70)
@objective(sp1, Min, 3y[1]+4y[2])

@variable(sp2,y[1:2]>=0)
@variable(sp2, z[1:2]>=0)
@constraint(sp2, y[1]+z[1]>=80)
@constraint(sp2, y[2]+z[2]>=90)
@objective(sp2, Min, 5z[1]+6z[2])

@variable(sp3,x[1:2]>=0)
@variable(sp3,t[1:2]>=0)
@constraint(sp3,x[1]+t[2]>=43)
@constraint(sp3,x[2]+t[1]>=75)
@objective(sp3,Min,.5t[1]+5t[2])

g = ModelGraph()
setsolver(g, GurobiSolver())
n1 = add_node(g)
n2 = add_node(g)
n3 = add_node(g)
n4 = add_node(g)

setmodel(n1, mp)
setmodel(n2, sp1)
setmodel(n3, sp2)
setmodel(n4, sp3)

edge1 = Plasmo.add_edge(g,n1,n2)
edge2 = Plasmo.add_edge(g,n2,n3)
edge2 = Plasmo.add_edge(g,n1,n4)

@linkconstraint(g,[i in 1:2], n1[:x][i] == n2[:x][i])
@linkconstraint(g,[i in 1:2], n1[:x][i] == n4[:x][i])
@linkconstraint(g,[i in 1:2], n2[:y][i] == n3[:y][i])

bendersolve(g,max_iterations =10)


abstract type InferenceTarget end

abstract type LinearInferenceTarget <: InferenceTarget end

abstract type PosteriorNumeratorTarget <: LinearInferenceTarget end

# TODO: Design this more carefully, maybe add function location() instead of requiring .x access

#function integration_domain(t::LinearInferenceTarget)
#end

#-------- Marginal Density --------------------------------
struct MarginalDensityTarget <: LinearInferenceTarget
    x::Float64
end

function cf(target::MarginalDensityTarget, t)
    cf(Normal(target.x), t)
end

function riesz_representer(target::MarginalDensityTarget, t)
    pdf(Normal(), target.x - t)
end

pretty_label(target::MarginalDensityTarget) = L"f(x)"

#-------- Marginal Density --------------------------------
struct MarginalDistributionTarget <: LinearInferenceTarget
    x::Float64
end

function riesz_representer(target::MarginalDistributionTarget, t)
    cdf(Normal(), target.x - t)
end

pretty_label(target::MarginalDistributionTarget) = L"F(x)"

#----------- LFSRNumerator ---------------------------------
# running under assumption X_i >=0...
struct LFSRNumerator <: PosteriorNumeratorTarget
    x::Float64
end

function cf(target::LFSRNumerator, t)
    x = target.x
    exp(im*t*x- t^2/2)*(1+im*erfi((t-im*x)/sqrt(2)))/2
end

function riesz_representer(target::LFSRNumerator, t)
    pdf(Normal(), target.x - t)*(t>=0)
end

#--------- PosteriorMeanNumerator ---------------------------------

struct PosteriorMeanNumerator <: PosteriorNumeratorTarget
    x::Float64
end

function cf(target::PosteriorMeanNumerator, t)
    x = target.x
    cf(Normal(target.x), t)*(x + im*t)
end

function riesz_representer(target::PosteriorMeanNumerator, t)
    pdf(Normal(), target.x - t)*t
end

#--------------- GeneralPosteriorLinearTarget --------------------------

struct GeneralPosteriorLinearTarget <: PosteriorNumeratorTarget
    f::Function
    x::Float64
end

function riesz_representer(target::GeneralPosteriorLinearTarget, t)
    pdf(Normal(), target.x - t)*target.f(t)
end

#---------Should probably just turn this all into one function-----
struct CalibratedNumerator <: LinearInferenceTarget
    num::LinearInferenceTarget
    θ̄::Float64 #Pilot
end

function riesz_representer(target::CalibratedNumerator, t)
    x = target.num.x
    θ̄ = target.θ̄
    riesz_representer(target.num, t) - θ̄*riesz_representer(MarginalDensityTarget(x),t)
end
#----------------------------------------------------------------------

# TODO: Unify these two into one...
struct PriorTailProbability <: LinearInferenceTarget
    cutoff::Float64
end

function riesz_representer(target::PriorTailProbability, t)
    one(Float64)*(abs(t) >= target.cutoff)
end

struct OneSidedPriorTailProbability <: LinearInferenceTarget
    cutoff::Float64
end

function riesz_representer(target::OneSidedPriorTailProbability, t)
    one(Float64)*(t >= target.cutoff)
end
#------------------- Beyond linear functionals ------------------------

struct PosteriorTarget{T} <: InferenceTarget where T<:LinearInferenceTarget
    num::T
    denom::MarginalDensityTarget
end

function PosteriorTarget(target::T) where T<:PosteriorNumeratorTarget
    x = target.x
    PosteriorTarget{T}(target, MarginalDensityTarget(x))
end

pretty_label(target::PosteriorTarget{LFSRNumerator}) = L"\theta(x) := \Pr[\mu \geq 0 \mid X=x]"
pretty_label(target::PosteriorTarget{PosteriorMeanNumerator}) = L"\theta(x) := E[\mu | X=x]"

#--------------------- default API-------------------------------------------
# Broadcasting behaviour

broadcastable(target::InferenceTarget) = Ref(target)


#----------------------------------------------------------------------------------------------------------------------------------------
# Implements solution to the diffusion equation in the steady-state and time-domain through a turbid parallelepiped [1]

# [1] Alwin Kienle, "Light diffusion through a turbid parallelepiped," J. Opt. Soc. Am. A 22, 1883-1888 (2005)
#----------------------------------------------------------------------------------------------------------------------------------------

#--------------------------------------
# Steady-State Fluence
#--------------------------------------
"""
    fluence_DA_paralpip_CW(μa, μsp; n_ext = 1.0, n_med = 1.0, rd = [4.0, 5.0, 0.0], rs = [5.0, 5.0], L = [10.0, 10.0, 10.0], xs = 10)

Compute the steady-state fluence in a parallelepiped [lx, ly, lz].

# Arguments
- `μa`: absorption coefficient (cm⁻¹)
- `μsp`: reduced scattering coefficient (cm⁻¹)

# Optional arguments
- `n_ext`: the boundary's index of refraction (air or detector)
- `n_med`: the sample medium's index of refraction
- `rd`: target location within medium [x,y,z] with origin x,y = 0 at corner of parallelepiped; z assumed to be 0
- `rs`: the location of the source [x,y] with origin x,y = 0 at corner of parallelepiped; z assumed to be 0
- `L`: the dimensions [lx, ly, lz] of the parallelepiped
- `xs`: the number of sources to compute in the series

# Examples
julia> `fluence_DA_paralpip_CW(0.1, 10.0, n_ext = 1.0, n_med = 1.0, rd = [24.0, 25.0, 0.0], rs = [25.0,25.0], L = [50.0,50.0,50.0], xs = 20)`
"""
function fluence_DA_paralpip_CW(μa, μsp; n_ext = 1.0, n_med = 1.0, rd = [4.0, 5.0, 0.0], rs = [5.0, 5.0], L = [10.0, 10.0, 10.0], xs = 10)
    params = DiffusionKernelParams(μsp, n_med, n_ext)
    μeff = sqrt(3 * μa * μsp)

    return _kernel_fluence_DA_paralpip_CW(μeff, params.D, params.zb, rd, rs, L, params.z0, xs)
end

function _kernel_fluence_DA_paralpip_CW(μeff, D, zb, rd, rs, L, z0, xs)
    x = rd[1]
    y = rd[2]
    z = rd[3]
    xu = rs[1]
    yu = rs[2]
    lx = L[1]
    ly = L[2]
    lz = L[3]

    ϕ = zero(eltype(D))

    for l in -xs:xs, m in -xs:xs, n in -xs:xs
    	ϕ += _sources_DA_paralpip_CW(l, m, n, x, y, z, lx, ly, lz, xu, yu, z0, zb, μeff)
    end

    return ϕ / (4 * π * D)
end
function _sources_DA_paralpip_CW(l, m, n, x, y, z, lx, ly, lz, xu, yu, z0, zb, μeff)

    x1l = 2 * l * lx + 4 * l * zb + xu
    x2l = 2 * l * lx + (4 * l - 2) * zb - xu
    y1m = 2 * m * ly + 4 * m * zb + yu
    y2m = 2 * m * ly + (4 * m - 2) * zb - yu
    z1n = 2 * n * lz + 4 * n * zb + z0
    z2n = 2 * n * lz + (4 * n - 2) * zb - z0

    r1 = sqrt((x - x1l)^2 + (y - y1m)^2 + (z - z1n)^2)
    r2 = sqrt((x - x1l)^2 + (y - y1m)^2 + (z - z2n)^2)
    r3 = sqrt((x - x1l)^2 + (y - y2m)^2 + (z - z1n)^2)
    r4 = sqrt((x - x1l)^2 + (y - y2m)^2 + (z - z2n)^2)
    r5 = sqrt((x - x2l)^2 + (y - y1m)^2 + (z - z1n)^2)
    r6 = sqrt((x - x2l)^2 + (y - y1m)^2 + (z - z2n)^2)
    r7 = sqrt((x - x2l)^2 + (y - y2m)^2 + (z - z1n)^2)
    r8 = sqrt((x - x2l)^2 + (y - y2m)^2 + (z - z2n)^2)

    ϕ  = exp(-μeff * r1) / r1 - exp(-μeff * r2) / r2 - exp(-μeff * r3) / r3 + exp(-μeff * r4) / r4
    ϕ += -exp(-μeff * r5) / r5 + exp(-μeff * r6) / r6 + exp(-μeff * r7) / r7 - exp(-μeff * r8) / r8

    return ϕ
end

#--------------------------------------
# Time-Domain Fluence
#--------------------------------------
"""
    fluence_DA_paralpip_TD(t, μa, μsp; n_ext = 1.0, n_med = 1.0, rd = [4.0, 5.0, 0.0], rs = [5.0, 5.0], L = [10.0, 10.0, 10.0], xs = 10)

Compute the time-domain fluence in a parallelepiped [lx, ly, lz].

# Arguments
- `t`: the time vector (ns).
- `μa`: absorption coefficient (cm⁻¹)
- `μsp`: reduced scattering coefficient (cm⁻¹)

# Optional arguments
- `n_ext`: the boundary's index of refraction (air or detector)
- `n_med`: the sample medium's index of refraction
- `rd`: target location within medium [x,y,z] with origin x,y = 0 at corner of parallelepiped; z assumed to be 0
- `rs`: the location of the source [x,y] with origin x,y = 0 at corner of parallelepiped; z assumed to be 0
- `L`: the dimensions [lx, ly, lz] of the parallelepiped
- `xs`: the number of sources to compute in the series

# Examples
```
julia> fluence_DA_paralpip_TD(0.5, 0.1, 10.0, n_ext = 1.0, n_med = 1.0, rd = [24.0, 25.0, 0.0], rs = [25.0,25.0], L = [50.0,50.0,50.0], xs = 20)
```
"""
function fluence_DA_paralpip_TD(t, μa, μsp; n_ext = 1.0, n_med = 1.0, rd = [4.0, 5.0, 0.0], rs = [5.0, 5.0], L = [10.0, 10.0, 10.0], xs = 10)
    params = DiffusionKernelParams(μsp, n_med, n_ext)
    return map(t -> _kernel_fluence_DA_paralpip_TD(params.D, params.ν, t, μa, params.zb, rd, rs, L, params.z0, xs), t)
end
function _kernel_fluence_DA_paralpip_TD(D, ν, t, μa, zb, rd, rs, L, z0, xs)
    x = rd[1]
    y = rd[2]
    z = rd[3]
    xu = rs[1]
    yu = rs[2]
    lx = L[1]
    ly = L[2]
    lz = L[3]

    tmp = 4 * D * ν * t
    ϕ1 = ν * exp(-μa * ν * t) / ((π * tmp)^(3/2))
    ϕ2 = zero(eltype(ϕ1))
    ϕ3 = zero(eltype(ϕ1))
    ϕ4 = zero(eltype(ϕ1))

    for m in -xs:xs
    	ϕ2, ϕ3, ϕ4 = _sources_DA_paralpip_TD!(ϕ2, ϕ3, ϕ4, m, zb, lx, ly, lz, xu, yu, z0, x, y, z, tmp)
    end

    return ϕ1 * ϕ2 * ϕ3 * ϕ4
end
@inline function _sources_DA_paralpip_TD!(ϕ2, ϕ3, ϕ4, m, zb, lx, ly, lz, xu, yu, z0, x, y, z, tmp)
    tmp1 = 4 * m * zb
    tmp2 = (4 * m - 2) * zb
    tmp3 = 2 * m

    x1l = tmp3 * lx + tmp1 + xu
    x2l = tmp3 * lx + tmp2 - xu
    y1m = tmp3 * ly + tmp1 + yu
    y2m = tmp3 * ly + tmp2 - yu
    z1n = tmp3 * lz + tmp1 + z0
    z2n = tmp3 * lz + tmp2 - z0

    ϕ2 += exp(-(x - x1l)^2 / tmp) - exp(-(x - x2l)^2 / tmp)
    ϕ3 += exp(-(y - y1m)^2 / tmp) - exp(-(y - y2m)^2 / tmp)
    ϕ4 += exp(-(z - z1n)^2 / tmp) - exp(-(z - z2n)^2 / tmp)

    return ϕ2, ϕ3, ϕ4
end

### TD Reflectance ###
"""
    flux_DA_paralpip_TD(t, μa, μsp; n_ext = 1.0, n_med = 1.0, rd = [4.0, 5.0, 0.0], rs = [5.0, 5.0], L = [10.0, 10.0, 10.0], xs = -10:10)

Compute the time-domain flux from a parallelepiped [lx, ly, lz] applying Fick's law to Eqn. 6 Kienle 05.

# Arguments
- `t`: the time vector (ns).
- `μa`: absorption coefficient (cm⁻¹)
- `μsp`: reduced scattering coefficient (cm⁻¹)

# Optional arguments
- `n_ext`: the boundary's index of refraction (air or detector)
- `n_med`: the sample medium's index of refraction
- `rd`: target location within medium [x,y,z] with origin x,y = 0 at corner of parallelepiped; z assumed to be 0
- `rs`: the location of the source [x,y] with origin x,y = 0 at corner of parallelepiped; z assumed to be 0
- `L`: the dimensions [lx, ly, lz] of the parallelepiped
- `xs`: the number of sources to compute in the series

# Examples
julia> `flux_DA_paralpip_TD(0.1:0.1:2.0, 0.1, 10.0)`
"""
function flux_DA_paralpip_TD(t, μa, μsp; n_ext = 1.0, n_med = 1.0, rd = [4.0, 5.0, 0.0], rs = [5.0, 5.0], L = [10.0, 10.0, 10.0], xs = 10)
    params = DiffusionKernelParams(μsp, n_med, n_ext)
    return map(t -> _flux_DA_paralpip_TD(t, μa, params.D, params.ν, params.z0, params.zb, rd, rs, L, xs), t)
end

function _flux_DA_paralpip_TD(t, μa, D, ν, z0, zb, rd, rs, L, xs)
    x = rd[1]
    y = rd[2]
    xu = rs[1]
    yu = rs[2]
    lx = L[1]
    ly = L[2]
    lz = L[3]

    if rd[3] == zero(eltype(μa))
        f = _refl_DA_paralpip_TD
    elseif rd[3] == L[3]
        f =  _trans_DA_paralpip_TD
    else
        @assert z == zero(eltype(z)) || z == s
    end

    Rt1 = exp(-μa * ν * t) / (2 * (4 * π * D * ν)^(3/2) * t^(5/2))
    Rt2 = zero(eltype(Rt1))
    Rt3 = zero(eltype(Rt1))
    Rt4 = zero(eltype(Rt1))
    tmp = 4 * D * ν * t

    for m in -xs:xs
        x1l = 2 * m * lx + 4 * m * zb + xu
        x2l = 2 * m * lx + (4 * m - 2) * zb - xu
        y1m = 2 * m * ly + 4 * m * zb + yu
        y2m = 2 * m * ly + (4 * m - 2) * zb - yu
        z1n = 2 * m * lz + 4 * m * zb + z0
        z2n = 2 * m * lz + (4 * m - 2) * zb - z0

        Rt2 += exp(-(x - x1l)^2 / tmp) - exp(-(x - x2l)^2 / tmp)
        Rt3 += exp(-(y - y1m)^2 / tmp) - exp(-(y - y2m)^2 / tmp)
        Rt4 += f(z1n, tmp, z2n, lz)
    end

    Rt1 *= Rt2 * Rt3 * Rt4
	return Rt1
end

_refl_DA_paralpip_TD(z1n, tmp, z2n, lz) = z1n * exp(-z1n^2 / tmp) - z2n * exp(-z2n^2 / tmp)
_trans_DA_paralpip_TD(z1n, tmp, z2n, lz) = (lz - z1n) * exp(-(lz - z1n)^2 / tmp) - (lz - z2n) * exp(-(lz - z2n)^2 / tmp)


using ExponentialUtilities, YaoArrayRegister

export TimeEvolution, time_evolve

"""
    TimeEvolution{N, TT, GT} <: PrimitiveBlock{N}

TimeEvolution, where GT is block type. input matrix should be hermitian.

!!!note:
    `TimeEvolution` contructor check hermicity of the input block by default, but sometimes it can be slow. Turn off the check manually by specifying optional parameter `check_hermicity = false`.
"""
mutable struct TimeEvolution{N, T, Tt, Hamilton <: AbstractBlock{N}} <: PrimitiveBlock{N}
    H::BlockMap{Complex{T}, Hamilton}
    dt::Tt
    tol::T

    function TimeEvolution(
        H::BlockMap{Complex{T}, TH},
        dt::Tt, tol::T; check_hermicity::Bool=true) where {N, Tt, T, TH <: AbstractBlock{N}}
        (check_hermicity && !ishermitian(H)) && error("Time evolution Hamiltonian has to be a Hermitian")
        return new{N, T, Tt, TH}(H, dt, tol)
    end
end

"""
    TimeEvolution(H, dt[; tol::Real=1e-7])

Create a [`TimeEvolution`](@ref) block with Hamiltonian `H` and time step `dt`. The
`TimeEvolution` block will use Krylove based `expv` to calculate time propagation.

Optional keywords are tolerance `tol` (default is `1e-7`)
`TimeEvolution` block can also be used for
[imaginary time evolution](http://large.stanford.edu/courses/2008/ph372/behroozi2/) if dt is complex.
"""
TimeEvolution(H::AbstractBlock, dt; tol::Real=1e-7, check_hermicity=true) =
    TimeEvolution(BlockMap(H), dt, tol, check_hermicity=check_hermicity)

TimeEvolution(M::BlockMap, dt; tol::Real, check_hermicity=true) =
    TimeEvolution(M, dt, tol, check_hermicity=check_hermicity)

time_evolve(M::BlockMap, dt; kwargs...) = TimeEvolution(M, dt; kwargs...)
time_evolve(M::AbstractBlock, dt; kwargs...) = TimeEvolution(M, dt; kwargs...)
time_evolve(dt; kwargs...) = @λ(M->time_evolve(M, dt; kwargs...))

function mat(::Type{T}, te::TimeEvolution{N}) where {T, N}
    A = Matrix{T}(te.H.block)
    return exp(-im*T(te.dt) * A)
end

function apply!(reg::ArrayReg, te::TimeEvolution)
    st = state(reg)
    dt = real(te.dt) == 0 ? imag(te.dt) : -im*te.dt
    @inbounds for j in 1:size(st, 2)
        v = view(st, :, j)
        Ks = arnoldi(te.H, v; tol=te.tol, ishermitian=true)
        expv!(v, dt, Ks)
    end
    return reg
end

cache_key(te::TimeEvolution) = (te.dt, cache_key(te.H))

# parametric interface
niparams(::Type{<:TimeEvolution}) = 1
getiparams(x::TimeEvolution) = x.dt
setiparams!(r::TimeEvolution, param::Real) = (r.dt = param; r)

function Base.:(==)(lhs::TimeEvolution, rhs::TimeEvolution)
    return lhs.H == rhs.H && lhs.dt == rhs.dt
end

function Base.adjoint(te::TimeEvolution)
    return TimeEvolution(te.H, -adjoint(te.dt); tol=te.tol)
end
Base.copy(te::TimeEvolution) = TimeEvolution(te.H, te.dt, tol=te.tol)

function YaoBase.isunitary(te::TimeEvolution)
    iszero(imag(te.dt)) || return false
    return true
end


# This file is a part of AstroLib.jl. License is MIT "Expat".

function ismeuv(wave::T, hcol::T, he1col::T, he2col::T, fano::Bool) where {T<:AbstractFloat}
    r = wave/911.75
    if r > 1
        return zero(T)
    end
    # minexp = -708.39642  from alog((machar(double=double).xmin)) where double = 1b in IDL
    z = sqrt(r/(1 - r))
    denom = -expm1(-2*T(π)*z)
    tauH = ((3.44e-16)*(r^4)*exp(-4*z*atan(1/z))*hcol)/denom
    r *= 4
    tauHe2 = zero(T)
    if r < 1
        z = sqrt(r/(1 - r))
        denom = -4*expm1(-2*T(π)*z)
        tauHe2 = ((3.44e-16)*(r^4)*exp(-4*z*atan(1/z))*he2col)/denom
    end
    q  = (2.81, 2.51, 2.45, 2.44)
    fano_gamma = (2.64061e-03, 6.20116e-04, 2.56061e-04, 1.320159e-04)
    esubi = (4.421529414644497, 4.679309217126802, 4.738680412951545, 4.764345016358231)
    tauHe1 = zero(T)
    if wave < 503.97
        x = log10(wave)
        if wave < 46
            y = @evalpoly(x, -2.465188e+01, 4.354679, -3.553024, 5.573040,
                          -5.872938, 3.720797, -1.226919, 1.576657e-01)
        else
            y = @evalpoly(x, -2.953607e+01, 7.083061, 8.678646e-01, -1.221932,
                          4.052997e-02, 1.317109e-01, -3.265795e-02,
                          2.500933e-03)
            if fano
                episilon = 911.2671/wave
                for i = 1:4
                    x = 2*(episilon - esubi[i])/fano_gamma[i]
                    y += log10(((x - q[i])^2)/(1 + x^2))
                end
            end
        end
        tauHe1 = exp10(y)*he1col
    end
    return tauH + tauHe1 + tauHe2
end

"""
    ismeuv(wave, hcol[, he1col=hcol*0.1, he2col=0, fano=false]) -> tau

### Purpose ###

Compute the continuum interstellar EUV optical depth

### Explanation ###

The EUV optical depth is computed from the photoionization of hydrogen and helium.

### Arguments ###

* `wave`: wavelength value (in Angstroms). Useful range is 40 - 912 A;
  at shorter wavelength metal opacity should be considered, at longer wavelengths
  there is no photoionization.
* `hcol`: interstellar hydrogen column density in cm-2.
* `he1col` (optional): neutral helium column density in cm-2.
  Default is 0.1*hcol (10% of hydrogen column)
* `he2col` (optional): ionized helium column density in cm-2
  Default is 0.
* `fano` (optional boolean keyword): If this keyword is true, then the 4 strongest
  auto-ionizing resonances of He I are included. The shape of these resonances
  is given by a Fano profile - see Rumph, Bowyer, & Vennes 1994, AJ, 107, 2108.
  If these resonances are included then the input wavelength vector should have
  a fine (>~0.01 A) grid between 190 A and 210 A, since the resonances are very narrow.

### Output ###

* `tau`: Vector giving resulting optical depth, non-negative values.

### Example ###

One has a model EUV spectrum with wavelength, w (in Angstroms).
Find the EUV optical depth by 1e18 cm-2 of HI, with N(HeI)/N(HI) = N(HeII)/N(HI) = 0.05.

```jldoctest
julia> using AstroLib

julia> ismeuv.([670, 910], 1e19, 5e17, 5e17)
2-element Array{Float64,1}:
 27.35393320556168
 62.683796028917286
```

### Notes ###

Code of this function is based on IDL Astronomy User's Library.
"""
ismeuv(wave::Real, hcol::Real, he1col::Real=hcol/10, he2col::Real=0,
       fano::Bool=false) =
           ismeuv(promote(float(wave), float(hcol), float(he1col),
                          float(he2col))..., fano)


@testset "half disk" begin
    ϕ = π/6  # start angle
    θ = ϕ-π/2  # outward normal to diameter
    c = 0.01 .* [cos(θ), sin(θ)]  # center of disk
    r = 2  # radius
    ∆ϕ = π  # half disk
    b = Sector(c, r, ϕ, ∆ϕ)  # note sector includes origin

    ∆vₛ = r * [cos(ϕ), sin(ϕ)]  # start of arc
    ∆vₑ = r * [cos(ϕ+∆ϕ), sin(ϕ+∆ϕ)]  # end of arc
    ∆vs = (∆vₛ, ∆vₑ)

    @test b == deepcopy(b)
    @test b ≈ Sector(c, r, ϕ+2π, ∆ϕ)
    @test b ≈ Sector(c, r, ϕ-2π, ∆ϕ)
    @test b ≈ Sector(c, r, ϕ+∆ϕ, -∆ϕ)
    @test b ≈ Sector(c, r, ϕ+∆ϕ-2π, -∆ϕ)
    @test hash(b) == hash(deepcopy(b))
    @test [0,0] ∈ b  # origin
    @test c ∈ b  # center
    @test (s = one⁻; s.*(∆vₛ+c) ∈ b && s.*(∆vₑ+c) ∈ b)  # slightly inside of end points of arc
    @test (s = one⁺; s.*(∆vₑ+c) ∉ b && s.*(∆vₑ+c) ∉ b)  # slightly outside of end points of arc

    @test all([(xn = surfpt_nearby(s.*c,b); xn ≈ (c, [cos(θ), sin(θ)])) for s = (one⁻⁻, 1, one⁺⁺)])  # handle point at center properly

    @test all([(p₀ = ∆vₛ/2 + c; p = s.*p₀; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1]) ≈ ϕ && n ≈ [cos(θ), sin(θ)]) for s = (one⁻⁻,1,one⁺⁺)])  # around start side
    @test all([(p₀ = ∆vₑ/2 + c; p = s.*p₀; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1]) ≈ ϕ+∆ϕ-2π && n ≈ [cos(θ), sin(θ)]) for s = (one⁻⁻,1,one⁺⁺)])  # around end side
    @test all([(α = ϕ+0.7∆ϕ; d̂ = [cos(α),sin(α)]; p = s.*r.*d̂ + c; surfpt_nearby(p,b) ≈ (r.*d̂.+c, d̂)) for s = (one⁻⁻,1,one⁺⁺)])  # around arc

    @test all([(p₀ = ∆v + c; p = one⁺⁺.*p₀; surfpt_nearby(p,b) ≈ (p₀, normalize(p-p₀))) for ∆v = ∆vs])  # outside end points of arc
    @test (p = ∆vₛ + c; surfpt_nearby(p,b) ≈ (∆vₛ+c, normalize([cos(ϕ),sin(ϕ)]+[cos(θ),sin(θ)])))   # at start point of arc
    @test (p = ∆vₑ + c; surfpt_nearby(p,b) ≈ (∆vₑ+c, normalize([cos(ϕ+∆ϕ),sin(ϕ+∆ϕ)]+[cos(θ),sin(θ)])))   # at end point of arc

    @test bounds(b) ≈ ([c[1]-r, c[2]+r*sin(ϕ+∆ϕ)], [c[1]+r*cos(ϕ), c[2]+r])
    @test checkbounds(b)

    @test (∆ = rand(2); translate(b,∆) ≈ Sector(c+∆, r, ϕ, ∆ϕ))
end  # @testset "half disk"

@testset "Sector, acute angle" begin
    ϕ = π/6  # start angle
    θ₁ = ϕ-π/2  # outward normal to start edge
    ∆ϕ = π/6  # half disk
    θ₂ = (ϕ+∆ϕ) + π/2  # outward normal to end edge

    ϕ₀ = ϕ + ∆ϕ/2
    c = -0.01 .* [cos(ϕ₀), sin(ϕ₀)]  # center
    r = 2  # radius
    b = Sector(c, r, ϕ, ∆ϕ)  # note sector includes origin

    ∆vₛ = r * [cos(ϕ), sin(ϕ)]  # start of arc
    ∆vₑ = r * [cos(ϕ+∆ϕ), sin(ϕ+∆ϕ)]  # end of arc
    ∆vs = (∆vₛ, ∆vₑ)

    @test b == deepcopy(b)
    @test b ≈ Sector(c, r, ϕ+2π, ∆ϕ)
    @test b ≈ Sector(c, r, ϕ-2π, ∆ϕ)
    @test b ≈ Sector(c, r, ϕ+∆ϕ, -∆ϕ)
    @test b ≈ Sector(c, r, ϕ+∆ϕ-2π, -∆ϕ)
    @test hash(b) == hash(deepcopy(b))
    @test [0,0] ∈ b  # origin
    @test c ∈ b  # center
    @test (s = one⁻; s.*(∆vₛ+c) ∈ b && s.*(∆vₑ+c) ∈ b)  # slightly outside of end points of arc
    @test (s = one⁺; s.*(∆vₑ+c) ∉ b && s.*(∆vₑ+c) ∉ b)  # slightly inside of end points of arc

    @test surfpt_nearby(c,b) ≈ (c, -[cos(ϕ₀), sin(ϕ₀)])  # handle point at center properly
    @test all([(p = c+0.1r.*[cos(θ),sin(θ)]; surfpt_nearby(p,b) ≈ (c, normalize(p-c))) for θ = (ϕ+π, ϕ+∆ϕ+π, ϕ₀+π, ϕ+π+0.1∆ϕ, ϕ+∆ϕ+π-0.1∆ϕ)])  # outside center

    @test all([(p₀ = ∆vₛ/2 + c; p = s.*p₀; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1]) ≈ ϕ && n ≈ [cos(θ₁), sin(θ₁)]) for s = (one⁻⁻,1,one⁺⁺)])  # around start side
    @test all([(p₀ = ∆vₑ/2 + c; p = s.*p₀; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1]) ≈ ϕ+∆ϕ && n ≈ [cos(θ₂), sin(θ₂)]) for s = (one⁻⁻,1,one⁺⁺)])  # around end side
    @test all([(α = ϕ+0.7∆ϕ; d̂ = [cos(α),sin(α)]; p = s.*r.*d̂ + c; surfpt_nearby(p,b) ≈ (r.*d̂.+c, d̂)) for s = (one⁻⁻,1,one⁺⁺)])  # around arc

    @test all([(p₀ = ∆v + c; p = one⁺⁺.*p₀; surfpt_nearby(p,b) ≈ (p₀, normalize(p-p₀))) for ∆v = ∆vs])  # outside end points of arc
    @test (p = ∆vₛ + c; surfpt_nearby(p,b) ≈ (∆vₛ+c, normalize([cos(ϕ),sin(ϕ)]+[cos(θ₁),sin(θ₁)])))   # at start point of arc
    @test (p = ∆vₑ + c; surfpt_nearby(p,b) ≈ (∆vₑ+c, normalize([cos(ϕ+∆ϕ),sin(ϕ+∆ϕ)]+[cos(θ₂),sin(θ₂)])))   # at end point of arc

    @test bounds(b) ≈ (c, [c[1]+r*cos(ϕ), c[2]+r*sin(ϕ+∆ϕ)])
    @test checkbounds(b)

    @test (∆ = rand(2); translate(b,∆) ≈ Sector(c+∆, r, ϕ, ∆ϕ))
end  # @testset "Sector, acute angle"

@testset "Half pipe" begin
    ϕ = π/6  # start angle
    θ = ϕ-π/2  # outward normal to diameter
    c = 0.01 .* [cos(θ), sin(θ)]  # center of disk
    r = 2  # radius
    ∆ϕ = π  # half disk
    h2 = 1.1

    b = SectoralPrism([c; 0], r, ϕ, ∆ϕ, 2h2)

    ∆vₛ = r * [cos(ϕ), sin(ϕ)]  # start of arc
    ∆vₑ = r * [cos(ϕ+∆ϕ), sin(ϕ+∆ϕ)]  # end of arc
    ∆vs = (∆vₛ, ∆vₑ)

    @test b == deepcopy(b)
    @test b ≈ SectoralPrism([c; 0], r, ϕ+2π, ∆ϕ, 2h2)
    @test b ≈ SectoralPrism([c; 0], r, ϕ-2π, ∆ϕ, 2h2)
    @test b ≈ SectoralPrism([c; 0], r, ϕ+∆ϕ, -∆ϕ, 2h2)
    @test b ≈ SectoralPrism([c; 0], r, ϕ+∆ϕ-2π, -∆ϕ, 2h2)
    @test hash(b) == hash(deepcopy(b))
    @test [0,0,0] ∈ b  # center
    @test [c; 0] ∈ b  # center
    @test all([(s = one⁻; [s.*(∆v+c);t*h2] ∈ b) for t = (-one⁻,0,one⁻), ∆v = ∆vs])  # slightly inside of end points of arc
    @test all([(s = one⁺; [s.*(∆v+c);t*h2] ∉ b) for t = (-one⁻,0,one⁻), ∆v = ∆vs])  # slightly outside of end points of arc

    @test all([(xn = surfpt_nearby([s.*c;t*h2/2], b); xn ≈ ([c;t*h2/2], [cos(θ),sin(θ),0])) for s = (one⁻⁻, 1, one⁺⁺), t = (-1,0,1)])  # handle point at center properly

    @test all([(p₀ = ∆vₛ/2 + c; p = [s.*p₀;t*h2]; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1])≈ϕ && x[3]==t*h2 && n ≈ [cos(θ),sin(θ),0]) for s = (one⁻⁻,1,one⁺⁺), t = (-0.5,0,0.5)])  # around start side, within height
    @test all([(s = 1; p₀ = ∆vₛ/2 + c; p = [s.*p₀;t*h2]; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1])≈ϕ && x[3]==t*h2 && n ≈ normalize([cos(θ),sin(θ),t])) for t = (-1,1)])  # at start side, at base
    @test all([(s = one⁻⁻; p₀ = ∆vₛ/2 + c; p = [s.*p₀;t*h2]; (x,n) = surfpt_nearby(p,b); x==p && n==[0,0,t]) for t = (-1,1)])  # inside start side, at base
    @test all([(s = one⁺⁺; p₀ = ∆vₛ/2 + c; p = [s.*p₀;t*h2]; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1])≈ϕ && x[3]≈t*h2 && n ≈ normalize([cos(θ),sin(θ),0])) for t = (-1,1)])  # outside start side, at base
    @test all([(s = one⁺⁺; p₀ = ∆vₛ/2 + c; p = [s.*p₀;t*h2]; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1])≈ϕ && x[3]≈sign(t)*h2 && n ≈ normalize(p-x)) for t = (-one⁺⁺,one⁺⁺)])  # outside start side, outside base

    @test all([(p₀ = ∆vₑ/2 + c; p = [s.*p₀;t*h2]; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1])≈ϕ+∆ϕ-2π && x[3]==t*h2 && n ≈ [cos(θ),sin(θ),0]) for s = (one⁻⁻,1,one⁺⁺), t = (-0.5,0,0.5)])  # around end side, within height
    @test all([(s = 1; p₀ = ∆vₑ/2 + c; p = [s.*p₀;t*h2]; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1])≈ϕ+∆ϕ-2π && x[3]==t*h2 && n ≈ normalize([cos(θ),sin(θ),t])) for t = (-1,1)])  # at end side, at base
    @test all([(s = one⁻⁻; p₀ = ∆vₑ/2 + c; p = [s.*p₀;t*h2]; (x,n) = surfpt_nearby(p,b); x==p && n==[0,0,t]) for t = (-1,1)])  # inside end side, at base
    @test all([(s = one⁺⁺; p₀ = ∆vₑ/2 + c; p = [s.*p₀;t*h2]; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1])≈ϕ+∆ϕ-2π && x[3]≈t*h2 && n ≈ normalize([cos(θ),sin(θ),0])) for t = (-1,1)])  # outside end side, at base
    @test all([(s = one⁺⁺; p₀ = ∆vₑ/2 + c; p = [s.*p₀;t*h2]; (x,n) = surfpt_nearby(p,b); atan(x[2]-c[2],x[1]-c[1])≈ϕ+∆ϕ-2π && x[3]≈sign(t)*h2 && n ≈ normalize(p-x)) for t = (-one⁺⁺,one⁺⁺)])  # outside end side, outside base

    @test all([(α = ϕ+0.7∆ϕ; d̂ = [cos(α),sin(α)]; p = [s.*r.*d̂ + c; t*h2]; surfpt_nearby(p,b) ≈ ([r.*d̂.+c; t*h2], [d̂;0])) for s = (one⁻⁻,1,one⁺⁺), t = (-0.5,0,0.5)])  # around arc, within height
    @test all([(s = 1; α = ϕ+0.7∆ϕ; d̂ = [cos(α),sin(α)]; p = [s.*r.*d̂ + c; t*h2]; surfpt_nearby(p,b) ≈ ([r.*d̂.+c; t*h2], normalize([d̂;t]))) for t = (-1,1)])  # at arc, at base
    @test all([(s = one⁻⁻; α = ϕ+0.7∆ϕ; d̂ = [cos(α),sin(α)]; p = [s.*r.*d̂ + c; t*h2]; surfpt_nearby(p,b) ≈ (p, [0,0,t])) for t = (-1,1)])  # inside arc, at base
    @test all([(s = one⁺⁺; α = ϕ+0.7∆ϕ; d̂ = [cos(α),sin(α)]; p = [s.*r.*d̂ + c; t*h2]; surfpt_nearby(p,b) ≈ ([r.*d̂.+c; t*h2], normalize([d̂;0]))) for t = (-1,1)])  # outside arc, at base
    @test all([(s = one⁺⁺; α = ϕ+0.7∆ϕ; d̂ = [cos(α),sin(α)]; p = [s.*r.*d̂ + c; t*h2]; (x,n) = surfpt_nearby(p,b); (x,n) ≈ ([r.*d̂.+c; sign(t)*h2], normalize(p-x))) for t = (-one⁺⁺,one⁺⁺)])  # outside arc, outside base

    @test all([(p = [∆vₛ + c; t*h2]; surfpt_nearby(p,b) ≈ (p, normalize(normalize([cos(ϕ),sin(ϕ),0]+[cos(θ),sin(θ),0])+[0,0,t]))) for t = (-1,1)])  # at start point of arc
    @test all([(p = [∆vₑ + c; t*h2]; surfpt_nearby(p,b) ≈ (p, normalize(normalize([cos(ϕ+∆ϕ),sin(ϕ+∆ϕ),0]+[cos(θ),sin(θ),0])+[0,0,t]))) for t = (-1,1)])  # at start point of arc
    @test all([(p₀ = [∆v + c; t*h2]; p = one⁺⁺.*p₀; surfpt_nearby(p,b) ≈ (p₀, normalize(p-p₀))) for ∆v = ∆vs, t = (-1,1)])  # outside end points of arc

    @test bounds(b) ≈ ([c[1]-r, c[2]+r*sin(ϕ+∆ϕ), -h2], [c[1]+r*cos(ϕ), c[2]+r, h2])
    @test checkbounds(b)

    @test (∆ = rand(3); translate(b,∆) ≈ SectoralPrism([c;0]+∆, r, ϕ, ∆ϕ, 2h2))
end  # @testset "Half pipe"

@testset "Half pipe, rotated" begin
    ϕ = π/2  # start angle
    ∆ϕ = π
    r = 2  # radius
    h2 = 1.1
    θ = π/6  # tilt angle measured from z-axis towards x-axis
    ax = [sin(θ), 0, cos(θ)]

    b = SectoralPrism([0,0,0], r, ϕ, ∆ϕ, 2h2, ax)

    @test bounds(b) ≈ ([-r*cos(θ)-h2*sin(θ),-r,-h2*cos(θ)], [h2*sin(θ),r,r*sin(θ)+h2*cos(θ)])
    @test checkbounds(b)
end  # Half pipe, rotated


# This code separately visualize the 3D radiation pattern
# of electric and magnetic resonances.
# Because the amplitude of scattering intensity is normalized,
# the setting of wavelength is not very important...

using jlmie
using Plots

# settings
# structure
nmat = 3.5  # refractive index of a sphere
radius = 100*1e-9

# environment
nenv = 1.00

# wavelenth (not very important)
lbd0ED = 618*1e-9  # ED
lbd0MD = 831*1e-9  # MD
lbd0 = [lbd0ED lbd0MD]

# angular range
theta = range(0,π,length=37) # per 5 degrees with length 37
phi = range(0,2π,length=73) # per 5 degrees with length 73

# other settings
n = 1
EorH = 2  # 1: Eelectric, 2: Magnemtic
Isff = zeros(length(theta),length(phi))
for i = 1:length(theta)
    for j = 1:length(phi)
        Isff[i,j] = jlmie_Isff_EM_n(nmat,radius,lbd0[EorH],nenv,theta[i],phi[j],n)[EorH]
    end
end
Isff = Isff/maximum(Isff)
mat_x = Isff.* (sin.(theta) * cos.(phi)')
mat_y = Isff.* (sin.(theta) * sin.(phi)')
mat_z = Isff.* (cos.(theta) * cos.(phi*0)')

# Just a sphere
# mat_x = 1 .* (sin.(theta) * cos.(phi)')
# mat_y = 1 .* (sin.(theta) * sin.(phi)')
# mat_z = 1 .* (cos.(theta) * cos.(phi*0)')

# show results
pyplot()
# show results
plt = plot(mat_x,mat_y,mat_z,
        fill_z=abs.(Isff),
        st=:surface,
        camera=(45,30), # azimuth, elevate
        xlabel="x",
        ylabel="y",
        zlabel="z",
        # colorbar_title="Scattering intensity (arb. units)",
        xlims=(-1,1),
        ylims=(-1,1),
        zlims=(-1,1),
)
display(plt)

# save
if false
    runningfilename = splitext(splitpath(@__FILE__)[end])[1]
    outfilename = ".\\result\\" * runningfilename
    savefig(plt,outfilename)
    println("Graph has been saved in result directory")
end