# discrete-time case
"""
    PeriodicMatrix(M, T) -> A::PeriodicMatrix

Discrete-time periodic matrix representation.

The discrete-time periodic matrix object `A` is built from a
`p`-vector `M` of real matrices, the associated time period `T` and
the number of subperiods specified via the keyword argument `nperiod = k`.

`M` contains the cyclic component matrices `M[i]`, `i = 1,..., p`,
where `M[i]` represents the value `M(Δ(i-1))` of a time periodic matrix `M(t)`
of period `T/k`, with `Δ := T/(kp)`, the associated sample time.
It is assumed that `M[i] := M[mod(i-1,p)+1]` for arbitrary `i`.
All component matrices are allowed to have arbitrary (time-varying) dimensions.
The component matrices `M`, the period `T`, the number of subperiods `k`, the discrete period `p`
and the sample time `Δ` can be accessed via `A.M`, `A.period`, `A.nperiod`, `A.dperiod` and `A.Ts`, respectively.
"""
struct  PeriodicMatrix{Domain,T} <: AbstractPeriodicArray{Domain,T}
    M::Vector{Matrix{T}}
    period::Float64
    nperiod::Int
end
# additional constructors
function  PeriodicMatrix(M::Vector{MT}, period::Real; nperiod::Int = 1) where {MT <: Array}
   period > 0 || error("period must be positive")
   nperiod > 0 || error("number of subperiods must be positive")
   any(ndims.(M) .> 2) && error("only vectors with vector or matrix elements supported")
   m = size.(M,2)
   T = promote_type(eltype.(M)...)
   return any(m .== 1) ?  PeriodicMatrix{:d,T}([T.(reshape(M[i],size(M[i],1),m[i])) for i in 1:length(M)], Float64(period), nperiod)  :
                   PeriodicMatrix{:d,T}([T.(M[i]) for i in 1:length(M)], Float64(period), nperiod)
end
PeriodicMatrix{:d,T}(A::Vector{Matrix{T}}, period::Real; nperiod::Int = 1) where {T} =
      PeriodicMatrix{:d,T}(A, period, nperiod)

# period change
function PeriodicMatrix{:d,T}(A::PeriodicMatrix{:d,T1}, period::Real) where {T,T1}
   period > 0 || error("period must be positive")
   Aperiod = A.period
   r = rationalize(Aperiod/period)
   n, d = numerator(r), denominator(r)
   min(n,d) == 1 || error("new period is incommensurate with the old period")
   if period >= Aperiod
      PeriodicMatrix{:d,T}([T.(A.M[i]) for i in 1:length(A)], Aperiod*d, A.nperiod*d)
   elseif period < Aperiod
      nperiod = div(A.nperiod,n)
      nperiod < 1 && error("new period is incommensurate with the old period")
      PeriodicMatrix{:d,T}([T.(A.M[i]) for i in 1:length(A)], Aperiod/n, nperiod)
   end
end
set_period(A::AbstractVecOrMat,period::Real) = A
function set_period(A::AbstractPeriodicArray{:d,T}, period::Real) where T
   period > 0 || error("period must be positive")
   Aperiod = A.period
   r = rationalize(Aperiod/period)
   n, d = numerator(r), denominator(r)
   min(n,d) == 1 || error("new period is incommensurate with the old period")
   if period >= Aperiod
      PeriodicMatrix{:d,T}(A.M, Aperiod*d, A.nperiod*d)
   elseif period < Aperiod
      nperiod = div(A.nperiod,n)
      nperiod < 1 && error("new period is incommensurate with the old period")
      PeriodicMatrix{:d,T}(A.M, Aperiod/n, nperiod)
   end
end

# function PeriodicMatrix{:d,T}(A::Vector{Matrix{T1}}, period::Real) where {T,T1}
#     PeriodicMatrix([T.(A[i]) for i in 1:length(A)], period)
# end
# PeriodicMatrix(M::Vector{MT}, period::Real) where {T <: Real, MT <: Array{T}} =
#            PeriodicMatrix{:d,T}(M, period)
PeriodicMatrix(M::Vector{Matrix{T}}, period::Real; nperiod::Int = 1) where {T <: Real} =
       PeriodicMatrix{:d,T}(M, period, nperiod)
PeriodicMatrix(M::VecOrMat{T}, period::Real; nperiod::Int = 1) where {T <: Real} =
   PeriodicMatrix{:d,T}([reshape(M,size(M,1),size(M,2))], period, nperiod)
function Base.getproperty(A::PeriodicMatrix, d::Symbol)
   if d === :dperiod
      return length(getfield(A, :M))
   elseif d === :Ts
      return A.period/A.dperiod/A.nperiod
   else
      getfield(A, d)
   end
end
Base.propertynames(A::PeriodicMatrix) = (:dperiod, :Ts, fieldnames(typeof(A))...)
isconstant(A::PeriodicMatrix) = (A.dperiod == 1)
Base.size(A::PeriodicMatrix) = (size.(A.M,1),size.(A.M,2))
Base.size(A::PeriodicMatrix, d::Integer) = size.(A.M,d)
Base.length(A::PeriodicMatrix) = A.dperiod
Base.eltype(A::PeriodicMatrix{:d,T}) where T = T
"""
    PeriodicArray(M, T; nperiod = k) -> A::PeriodicArray

Discrete-time periodic array representation.

The discrete-time periodic array object `A` is built from a `m×n×p` real array
`M`, the associated time period `T` and the number of subperiods specified via
the keyword argument `nperiod = k`.
`M` contains the cyclic component matrices `M[:,:,i]`, `i = 1,..., p`,
where `M[:,:,i]` represents the value `M(Δ(i-1))` of a time periodic matrix `M(t)`
of period `T/k`, with `Δ := T/(kp)`, the associated sample time.
It is assumed that  `M[:,:,k] := M[:,:,mod(k-1,p)+1]` for arbitrary `k`.
The component matrices `M`, the period `T`, the number of subperiods `k`, the discrete period `p`
and the sample time `Δ` can be accessed via `A.M`, `A.period`, `A.nperiod`, `A.dperiod` and `A.Ts`, respectively.
"""
struct PeriodicArray{Domain,T} <: AbstractPeriodicArray{Domain,T}
    M::Array{T,3}
    period::Float64
    nperiod::Int
end
# additional constructors
function  PeriodicArray{:d,T}(M::Array{T,3}, period::Real; nperiod::Int = 1) where {T <: Real}
   period > 0 || error("period must be positive")
   nperiod > 0 || error("number of subperiods must be positive")
   PeriodicArray{:d,T}(M, Float64(period), nperiod)
end
function PeriodicArray{:d,T}(A::PeriodicArray{:d,T1}, period::Real) where {T,T1}
   period > 0 || error("period must be positive")
   #isconstant(A) && (return PeriodicArray{:d,T}(convert(Array{T,3},A.M), period; nperiod = 1))
   Aperiod = A.period
   r = rationalize(Aperiod/period)
   n, d = numerator(r), denominator(r)
   min(n,d) == 1 || error("new period is incommensurate with the old period")
   if period >= Aperiod
      PeriodicArray{:d,T}(convert(Array{T,3},A.M), Aperiod*d; nperiod = A.nperiod*d)
   elseif period < Aperiod
      nperiod = div(A.nperiod,n)
      nperiod < 1 && error("new period is incommensurate with the old period")
      PeriodicArray{:d,T}(convert(Array{T,3},A.M), Aperiod/n; nperiod)
   end
end

#PeriodicArray{:d,T}(M::Array{T1,3}, period::Real; nperiod::Int = 1) where {T,T1} = PeriodicArray(T.(M), period; nperiod)
PeriodicArray(M::Array{T,3}, period::Real; nperiod::Int = 1) where {T <: Real} = PeriodicArray{:d,T}(M, period, nperiod)
PeriodicArray(M::VecOrMat{T}, period::Real; nperiod::Int = 1) where T = PeriodicArray(reshape(M,size(M,1),size(M,2),1), period; nperiod)
function Base.getproperty(A::PeriodicArray, d::Symbol)
   if d === :dperiod
      return size(getfield(A, :M), 3)
   elseif d === :Ts
      return A.period/A.dperiod/A.nperiod
   else
      getfield(A, d)
   end
end
Base.propertynames(A::PeriodicArray) = (:dperiod, :Ts, fieldnames(typeof(A))...)
isconstant(A::PeriodicArray) = (A.dperiod == 1)
Base.size(A::PeriodicArray) = (size(A.M,1),size(A.M,2))
Base.size(A::PeriodicArray, d::Integer) = size(A.M,d)
Base.length(A::PeriodicArray) = A.dperiod
Base.eltype(A::PeriodicArray{:d,T}) where T = T
iscontinuous(A::AbstractPeriodicArray) = typeof(A).parameters[1] == :c
#iscontinuous(A) = typeof(A).parameters[1] == :c
iscontinuous(A::Type) = A.parameters[1] == :c
"""
    PeriodicFunctionMatrix(f, T; nperiod = k) -> A::PeriodicFunctionMatrix

Continuous-time periodic function matrix representation.

The continuous-time periodic function matrix object `A` is built from a
time periodic real matrix function `f(t)` of real time variable `t`,
the associated time period `T` and the associated number of subperiods
specified via the keyword argument `nperiod = k`.
It is assumed that  `F(t) = F(t+T/k)` for any real time value `t`.
The function `f(t)`, the period `T`, the row and column dimensions
of `f(t)`, the number of subperiods `k` can be accessed via `A.f`, `A.period`,
the tuple `A.dims` and `A.nperiod`, respectively.
"""
struct PeriodicFunctionMatrix{Domain,T} <: AbstractPeriodicArray{Domain,T}
   f::Function
   period::Float64
   dims::Tuple{Int,Int}
   nperiod::Int
   _isconstant::Bool
end
# additional constructors
function PeriodicFunctionMatrix{:c,Tf}(f::Function, period::Real; isconst::Bool = false, nperiod::Int = 1) where {Tf}
   period > 0 || error("period must be positive")
   nperiod > 0 || error("number of subperiods must be positive")
   F0 = f(period)
   nd = ndims(F0)
   nd <= 2 || error("two-dimensional function array expected, got an $nd -dimensional array")
   m, n = size(F0,1),size(F0,2)
   eltype(F0) == Tf ? PeriodicFunctionMatrix{:c,Tf}(t -> n == 1 ? reshape(f(t),m,n) : f(t), Float64(period), (m,n), nperiod, isconst) :
                      PeriodicFunctionMatrix{:c,Tf}(t -> n == 1 ? convert(Matrix{Tf},reshape(f(Tf(t)),m,n)) : convert(Matrix{Tf},f(Tf(t))), Float64(period), (m,n), nperiod, isconst)
end
PeriodicFunctionMatrix(f::F, period::Real; isconst::Bool = false, nperiod::Int = 1) where {F<:Function}  =
             PeriodicFunctionMatrix{:c,eltype(f(period))}(f, period; isconst, nperiod)
function PeriodicFunctionMatrix(A::VecOrMat{T}, period::Real) where {T <: Real}
   if T == Num
      @variables t
      f = eval(build_function(reshape(A,size(A,1),size(A,2)), t, expression=Val{false})[1])
      PeriodicFunctionMatrix{:c,Float64}(t -> f(t), period; isconst = false)
   else
      PeriodicFunctionMatrix{:c,T}(t -> reshape(A,size(A,1),size(A,2)), period; isconst = true)
   end
end
function PeriodicFunctionMatrix{:c,T}(at::PeriodicFunctionMatrix, period::Real) where {T}
   period > 0 || error("period must be positive")
   Aperiod = at.period
   r = rationalize(Aperiod/period)
   n, d = numerator(r), denominator(r)
   min(n,d) == 1 || error("new period is incommensurate with the old period")
   if period >= Aperiod
      PeriodicFunctionMatrix{:c,T}(at.f, Aperiod*d, at.dims, at.nperiod*d, at._isconstant)
   elseif period < Aperiod
      nperiod = div(A.nperiod,n)
      nperiod < 1 && error("new period is incommensurate with the old period")
      PeriodicFunctionMatrix{:c,T}(at.f, Aperiod/n, at.dims, at.nperiod, at._isconstant)
   end
end
#set_period(A::PeriodicFunctionMatrix, period::Real) = PeriodicFunctionMatrix{:c,eltype(A)}(A,period)

# function PeriodicFunctionMatrix(at::PeriodicFunctionMatrix, period::Real = at.period; isconst::Bool = isconstant(at))
#    # at.period = period
#    # at._isconstant = isconst
#    # return at
#    return PeriodicFunctionMatrix(at.f, period; isconst)
# end
# properties
isconstant(A::PeriodicFunctionMatrix) = A._isconstant
function isperiodic(f::Function, period::Real)
   t = rand(typeof(period))
   return f(t) ≈ f(t+period)
end
isperiodic(A::PeriodicFunctionMatrix) = isconstant(A) ? true : isperiodic(A.f,A.period/A.nperiod)
Base.size(A::PeriodicFunctionMatrix) = A.dims
Base.size(A::PeriodicFunctionMatrix, d::Integer) = d <= 2 ? size(A)[d] : 1
Base.eltype(A::PeriodicFunctionMatrix{:c,T}) where T = T

"""
    PeriodicSymbolicMatrix(F, T; nperiod = k) -> A::PeriodicSymbolicMatrix

Continuous-time periodic symbolic matrix representation.

The continuous-time periodic symbolic matrix object `A` is built from `F`, a
symbolic periodic real matrix or vector of symbolic variable `t`,
the associated time period `T` and the associated number of subperiods
specified via the keyword argument `nperiod = k`.
It is assumed that  `F(t) = F(t+T/k)` for any real time value `t`.
The symbolic matrix `F`, the period `T` and the number of subperiods `k`
can be accessed via `A.F`, `A.period` and `A.nperiod`, respectively.
"""
struct PeriodicSymbolicMatrix{Domain,T} <: AbstractPeriodicArray{Domain,T}
   F::Matrix{<:Num}
   period::Float64
   nperiod::Int
end
# additional constructors
function  PeriodicSymbolicMatrix{:c,T}(F::VecOrMat{T}, period::Real; nperiod::Int = 1) where {T <: Num}
   period > 0 || error("period must be positive")
   nperiod > 0 || error("number of subperiods must be positive")
   # check that array F is depending only on t
   tt = rand()
   @variables t
   Ft = substitute.(F, (Dict(t => tt),))
   m, n = size(Ft,1), size(Ft,2)
   any(length.(Symbolics.get_variables.(Ft)) .> 0 ) && error("t must be the only variable in F")
   PeriodicSymbolicMatrix{:c,T}(n == 1 ? reshape(F,m,n) : F, Float64(period), nperiod)
end
PeriodicSymbolicMatrix(F::VecOrMat{T}, period::Real; nperiod::Int = 1) where {T <: Union{Num,Real}} =
    PeriodicSymbolicMatrix{:c,Num}(Num.(F), period; nperiod)
# PeriodicSymbolicMatrix(F::VecOrMat{T}, period::Real; nperiod::Int = 1) where {T <: Real} =
#     PeriodicSymbolicMatrix{:c,Num}(Num.(F), Float64(period), nperiod)
# PeriodicSymbolicMatrix{:c,Num}(F::VecOrMat{T}, period::Real; nperiod::Int = 1) where {T <: Number} =
#     PeriodicSymbolicMatrix{:c,Num}(Num.(F), Float64(period), nperiod)
function PeriodicSymbolicMatrix{:c,T}(A::PeriodicSymbolicMatrix, period::Real) where {T}
   period > 0 || error("period must be positive")
   Aperiod = A.period
   r = rationalize(Aperiod/period)
   n, d = numerator(r), denominator(r)
   min(n,d) == 1 || error("new period is incommensurate with the old period")
   if period >= Aperiod
      PeriodicSymbolicMatrix{:c,T}(A.F, Aperiod*d, A.nperiod*d)
   elseif period < Aperiod
      nperiod = div(A.nperiod,n)
      nperiod < 1 && error("new period is incommensurate with the old period")
      PeriodicSymbolicMatrix{:c,T}(A.F, Aperiod/n, A.nperiod)
   end
end
set_period(A::PM, period::Real) where {T, PM <: AbstractPeriodicArray{:c,T}} = PM(A,period)

# properties
isconstant(A::PeriodicSymbolicMatrix) = all(length.(Symbolics.get_variables.(A.F)) .== 0)
function isperiodic(A::VecOrMat{T}, period::Real) where {T <: Num}
   tt = rand()
   @variables t
   At = substitute.(A, (Dict(t => tt),))
   return norm(At - substitute.(A, (Dict(t => tt+period),))) <= eps(10.)*max(1.,norm(At))
end
isperiodic(A::PeriodicSymbolicMatrix) = isconstant(A) ? true : isperiodic(A.F,A.period)
Base.size(A::PeriodicSymbolicMatrix) = size(A.F)
Base.size(A::AbstractPeriodicArray, d::Integer) = d <= 2 ? size(A)[d] : 1
Base.eltype(A::PeriodicSymbolicMatrix{:c,T}) where T = T

struct FourierFunctionMatrix{Domain,T} <: AbstractPeriodicArray{Domain,T}
   M::Fun
   period::Float64
   nperiod::Int
end
# additional constructors
"""
     FourierFunctionMatrix(Afun, T) -> A::FourierFunctionMatrix

Continuous-time Fourier function matrix representation.

The Fourier function matrix object `A` of period `T` is built using
the Fourier series representation of a periodic matrix `Afun(t)` of subperiod `T′ = T/k`,
where each entry of `Afun(t)` has the form

             p
      a_0 +  ∑ ( ac_i*cos(i*t*2*π/T′)+as_i*sin(i*2*π*t/T′) ) ,
            i=1

where `k ≥ 1` is the number of subperiods (default: `k = 1`).
The matrix `Afun` containing the Fourier representation, the period `T` and the
number of subperiods `k` can be accessed via `A.M`, `A.period` and `A.nperiod`, respectively.
"""
function FourierFunctionMatrix{:c,T}(A::Fun, period::Real) where {T}
   period > 0 || error("period must be positive")
   n, m = size(A,1), size(A,2)
   sint = domain(A)
   (sint.a == 0 && sint.b > 0) || error("the domain must be of the form 0..period")
   ti = rationalize(period/sint.b)
   denominator(ti) == 1 || error("only integer multiple periods supported")
   FourierFunctionMatrix{:c,eltype(domain(A))}(m == 1 ? reshape(A,n,m) : A, Float64(period), numerator(ti))
end
FourierFunctionMatrix(A::Fun, period::Real)  =
       FourierFunctionMatrix{:c,eltype(domain(A))}(A::Fun, period::Real)
FourierFunctionMatrix(A::Fun)  = FourierFunctionMatrix{:c,eltype(domain(A))}(A::Fun, domain(A).b)
function FourierFunctionMatrix{:c,T}(A::FourierFunctionMatrix, period::Real) where {T}
   period > 0 || error("period must be positive")
   Aperiod = A.period
   r = rationalize(Aperiod/period)
   n, d = numerator(r), denominator(r)
   min(n,d) == 1 || error("new period is incommensurate with the old period")
   if period >= Aperiod
      FourierFunctionMatrix{:c,T}(A.M, Aperiod*d, A.nperiod*d)
   elseif period < Aperiod
      nperiod = div(A.nperiod,n)
      nperiod < 1 && error("new period is incommensurate with the old period")
      FourierFunctionMatrix{:c,T}(A.M, Aperiod/n, A.nperiod)
   end
end
FourierFunctionMatrix(A0::VecOrMat{T}, period::Real) where {T <: Real}  =
    FourierFunctionMatrix{:c,float(T)}(Fun(t->float(T).(A0),Fourier(0..period)), period)
FourierFunctionMatrix{:c,T}(A0::VecOrMat, period::Real) where {T <: Real}  =
    FourierFunctionMatrix{:c,float(T)}(Fun(t->float(T).(A0),Fourier(0..period)), period)
function isconstant(A::FourierFunctionMatrix)
   for i = 1:size(A.M,1)
       for j = 1: size(A.M,2)
           ncoefficients(chop(A.M[i,j])) <= 1 || (return false)
       end
   end
   return true
end
isperiodic(A::FourierFunctionMatrix) = true
Base.size(A::FourierFunctionMatrix) = size(A.M)
Base.eltype(A::FourierFunctionMatrix{:c,T}) where T = T

struct HarmonicArray{Domain,T} <: AbstractPeriodicArray{Domain,T}
   values::Array{Complex{T},3}
   period::Float64
   nperiod::Int
end
# additional constructors
"""
     HarmonicArray(Ahr, T; nperiod = k) -> A::HarmonicArray

Continuous-time harmonic array representation.

The harmonic array object `A` of period `T` is built using
the harmonic representation of a periodic matrix `Ahr(t)` of subperiod `T′ = T/k` in the form

                     p
     Ahr(t) = A_0 +  ∑ ( Ac_i*cos(i*t*2*π/T′)+As_i*sin(i*2*π*t/T′) ) ,
                    i=1

where `k ≥ 1` is the number of subperiods (default: `k = 1`).
The `m×n×(p+1)` complex array `Ahr` contains the harmonic components as follows:
`Ahr[:,:,1]` contains the constant term `A_0` (the mean value) and
the real and imaginary parts of `Ahr[:,:,i+1]`
for `i = 1, ..., p` contain the coefficient matrices `Ac_i` and `As_i`, respectively.
The complex matrix `Ahr` containing the harmonic components, the period `T` and the
number of subperiods `k` can be accessed via `A.values`, `A.period` and `A.nperiod`, respectively.
"""
function HarmonicArray{:c,T}(Ahr::Array{<:Complex,3}, period::Real; nperiod::Int = 1) where {T}
   period > 0 || error("period must be positive")
   nperiod > 0 || error("number of subperiods must be positive")
   (size(Ahr,3) > 0 && iszero(imag(view(Ahr,:,:,1)))) || error("imaginary part of constant term must be zero")
   HarmonicArray{:c,T}(convert(Array{Complex{T},3},Ahr), Float64(period), nperiod)
end
HarmonicArray(Ahr::Array{Complex{T},3}, period::Real; nperiod::Int = 1) where T = HarmonicArray{:c,T}(Ahr, period; nperiod)
# change period and type
function HarmonicArray{:c,T}(A::HA, period::Real) where {HA <: HarmonicArray} where {T}
   period > 0 || error("period must be positive")
   Aperiod = A.period
   r = rationalize(Aperiod/period)
   n, d = numerator(r), denominator(r)
   min(n,d) == 1 || error("new period is incommensurate with the old period")
   if period >= Aperiod
      HarmonicArray{:c,T}(convert(Array{Complex{T},3},A.values), Aperiod*d; nperiod = A.nperiod*d)
   elseif period < Aperiod
      nperiod = div(A.nperiod,n)
      nperiod < 1 && error("new period is incommensurate with the old period")
      HarmonicArray{:c,T}(convert(Array{Complex{T},3},A.values), Aperiod/n; nperiod)
   end
end
"""
     HarmonicArray(A0, Ac, As, T) -> A::HarmonicArray

Construct a harmonic array representation from the harmonic components.

The harmonic array object `A` is built for
the harmonic representation `Ahr(t)` of a periodic matrix of period `T` in the form

                     p
     Ahr(t) = A_0 +  ∑ ( Ac_i*cos(i*t*2*π/T)+As_i*sin(i*2*π*t/T) ) ,
                    i=1

where the constant term `A_0` is contained in the real matrix `A0`, and `Ac` and `As` are
vectors of real matrices such that the `i`-th (cosinus) coefficient matrix
`Ac_i` is contained in `Ac[i]` and the `i`-th (sinus) coefficient matrix
`As_i` is contained in `As[i]`. `p` is the maximum of length of the vectors of matrices `Ac` and `As`.
If the length of `Ac` or `As` is less than `p`, then zero trailing matrices are assumed in the respective matrix.
All component matrices must have the same dimensions.
The complex matrix containing the harmonic components and the period `T`
can be accessed via `A.values` and `A.period`, respectively.
"""
function HarmonicArray(A0::MT, Acos::Union{Vector{MT},Nothing},
                               Asin::Union{Vector{MT},Nothing}, period::Real; nperiod::Int = 1) where {T <: Real, MT <: VecOrMat{T}}
   nc = isnothing(Acos) ? 0 : length(Acos)
   ns = isnothing(Asin) ? 0 : length(Asin)
   nmin = min(nc,ns)
   N = max(1,max(nc,ns)+1)
   ahr = Array{Complex{T},3}(undef, size(A0,1), size(A0,2), N)
   ahr[:,:,1] = A0
   [ahr[:,:,i+1] = complex.(Acos[i],Asin[i]) for i in 1:nmin]
   [ahr[:,:,i+1] = Acos[i] for i in nmin+1:nc]
   [ahr[:,:,i+1] = im*Asin[i] for i in nmin+1:ns]
   #HarmonicArray(ahr, period)
   HarmonicArray{:c,T}(ahr, period, nperiod)
end
HarmonicArray(A0::VecOrMat{T}, period::Real; nperiod::Int = 1) where {T <: Real}  =
          HarmonicArray(complex(reshape(A0,size(A0,1),size(A0,2),1)), period; nperiod)
HarmonicArray(A0::VecOrMat{T}, Acos::Vector{MT}, period::Real; nperiod::Int = 1) where {T <: Real, MT <: VecOrMat{T}}  =
          HarmonicArray(A0, Acos, nothing, period; nperiod)

# properties
isconstant(A::HarmonicArray) = size(A.values,3) <= 1
isperiodic(A::HarmonicArray) = true
Base.size(A::HarmonicArray) = (size(A.values,1),size(A.values,2))
Base.eltype(A::HarmonicArray{:c,T}) where T = T

"""
    PeriodicTimeSeriesMatrix(At, T; nperiod = k) -> A::PeriodicTimeSeriesMatrix

Continuous-time periodic time series matrix representation.

The continuous-time periodic time series matrix object `A` of period `T` is built from a
`p`-vector `At` of real matrices and the associated subperiod `T′ = T/k`, where
`k ≥ 1` is the number of subperiods (default: `k = 1`).
`At` contains the cyclic component matrices `At[i]`, `i = 1,..., p`,
where `At[i]` represents the value `A(Δ(i-1))` of a time periodic matrix `A(t)`
of period `T′`, with `Δ := T′/p`, the associated sampling time.
It is assumed that `At[i] := At[mod(i-1,p)+1]` for arbitrary `i`.
All component matrices must have the same dimensions.
The component matrices `At`, the period `T` and the number of subperiods `k`
can be accessed via `A.values`, `A.period`, and `A.nperiod`, respectively.
"""
struct PeriodicTimeSeriesMatrix{Domain,T} <: AbstractPeriodicArray{Domain,T}
   values::Vector{Array{T,2}}
   period::Float64
   nperiod::Int
end
# additional constructors
function PeriodicTimeSeriesMatrix{:c,T}(At::Union{Vector{Vector{T}},Vector{Matrix{T}}}, period::Real; nperiod::Int = 1) where {T <: Real}
   period > 0 || error("period must be positive")
   nperiod > 0 || error("number of subperiods must be positive")
   N = length(At)
   #N <= 1 && (return PeriodicTimeSeriesMatrix{:c,T}(At, Float64(period); nperiod) ) # the constant matrix case
   n1, n2 = size(At[1],1), size(At[1],2)
   (all(size.(At,1) .== n1) && all(size.(At,2) .== n2)) || error("all component matrices must have the same dimensions")

   # adjust final data to matrix type
   PeriodicTimeSeriesMatrix{:c,T}(n2 == 1 ? [reshape(At[j],n1,n2) for j in 1:N] : At, Float64(period), nperiod)
end
PeriodicTimeSeriesMatrix{:c,T}(A::Vector{Matrix{T1}}, period::Real; nperiod::Int = 1) where {T,T1} =
   PeriodicTimeSeriesMatrix([T.(A[i]) for i in 1:length(A)], period; nperiod)
PeriodicTimeSeriesMatrix(At::Union{Vector{Vector{T}},Vector{Matrix{T}}}, period::Real; nperiod::Int = 1) where {T <: Real} =
     PeriodicTimeSeriesMatrix{:c,T}(At, period; nperiod)
PeriodicTimeSeriesMatrix(At::VecOrMat{T}, period::Real; nperiod::Int = 1) where {T <: Real}  =
        PeriodicTimeSeriesMatrix([reshape(At,size(At,1),size(At,2))], period; nperiod)
# period change
function PeriodicTimeSeriesMatrix{:c,T}(A::PeriodicTimeSeriesMatrix{:c,T1}, period::Real) where {T,T1}
   Aperiod = A.period
   r = rationalize(Aperiod/period)
   n, d = numerator(r), denominator(r)
   min(n,d) == 1 || error("new period is incommensurate with the old period")
   if period >= Aperiod
      PeriodicTimeSeriesMatrix{:c,T}([T.(A.values[i]) for i in 1:length(A)], Aperiod*d; nperiod = A.nperiod*d)
   elseif period < Aperiod
      nperiod = div(A.nperiod,n)
      nperiod < 1 && error("new period is incommensurate with the old period")
      PeriodicTimeSeriesMatrix{:c,T}([T.(A.values[i]) for i in 1:length(A)], Aperiod/n; nperiod)
   end
end

# properties
isconstant(At::PeriodicTimeSeriesMatrix) = length(At.values) <= 1
isperiodic(At::PeriodicTimeSeriesMatrix) = true
Base.length(At::PeriodicTimeSeriesMatrix) = length(At.values)
Base.size(At::PeriodicTimeSeriesMatrix) = length(At) > 0 ? size(At.values[1]) : (0,0)
Base.eltype(At::PeriodicTimeSeriesMatrix{:c,T}) where T = T

# conversions to discrete-time PeriodicMatrix
Base.convert(::Type{PeriodicMatrix}, A::PeriodicArray{:d,T}) where T =
             PeriodicMatrix{:d,T}([A.M[:,:,i] for i in 1:size(A.M,3)],A.period; nperiod = A.nperiod)

# conversions to discrete-time PeriodicArray
# function Base.convert(::Type{PeriodicArray}, A::PeriodicMatrix{:d,T}) where T
#     N = length(A)
#     m, n = size.(A,1), size.(A,2)
#     N == 0 && PeriodicArray(Array{T,3}(undef,0,0,0),A.period)
#     if any(m .!= m[1]) || any(m .!= m[1])
#        @warn "Non-constant dimensions: the resulting component matrices padded with zeros"
#        t = zeros(T,maximum(m),maximum(n),N)
#        [copyto!(view(t,1:m[i],1:n[i],i),A[i]) for i in 1:N]
#        PeriodicArray{:d,T}(t,A.period)
#     else
#        t = zeros(T,m[1],n[1],N)
#        [copyto!(view(t,:,:,i),A[i]) for i in 1:N]
#        PeriodicArray{:d,T}(t,A.period)
#     end
# end
Base.convert(::Type{PeriodicArray}, A::PeriodicMatrix) where T = pm2pa(A)

# conversions to continuous-time PeriodicFunctionMatrix
function Base.convert(::Type{PeriodicFunctionMatrix{:c,T}}, A::PeriodicFunctionMatrix) where T
   return eltype(A) == T ? A : PeriodicFunctionMatrix{:c,T}(x -> T.(A.f(T(x))), A.period, A.dims, A.nperiod, A._isconstant)
end

function Base.convert(::Type{PeriodicFunctionMatrix{:c,T}}, A::FourierFunctionMatrix) where T
   return eltype(A) == T ? A : PeriodicFunctionMatrix{:c,T}(x -> T.(A.M(T(x))), A.period, size(A), A.nperiod, isconstant(A))
end
function Base.convert(::Type{PeriodicFunctionMatrix}, A::FourierFunctionMatrix)
   return PeriodicFunctionMatrix{:c,eltype(A)}(x -> A.M(x), A.period, size(A.M), A.nperiod, isconstant(A))
end

function Base.convert(::Type{PeriodicFunctionMatrix{:c,T}}, A::PeriodicSymbolicMatrix) where T
   @variables t
   f = eval(build_function(A.F, t, expression=Val{false})[1])
   PeriodicFunctionMatrix{:c,T}(x -> f(T(x)), A.period, size(A), A.nperiod, isconstant(A))
end
function Base.convert(::Type{PeriodicFunctionMatrix}, A::PeriodicSymbolicMatrix) where T
   @variables t
   f = eval(build_function(A.F, t, expression=Val{false})[1])
   PeriodicFunctionMatrix{:c,Float64}(x -> f(x), A.period, size(A), A.nperiod, isconstant(A))
end
# function Base.convert(::Type{PeriodicFunctionMatrix{:c,T}}, A::PeriodicSymbolicMatrix) where T
#    @variables t
#    f = eval(build_function(A.F, t, expression=Val{false})[1])
#    PeriodicFunctionMatrix{:c,T}(x -> f(x), A.period, size(A), A.nperiod, isconstant(A))
# end

# PeriodicFunctionMatrix(ahr::HarmonicArray, period::Real = ahr.period; exact = true)  =
#           PeriodicFunctionMatrix(t::Real -> hreval(ahr,t;exact)[1], period)
Base.convert(::Type{PeriodicFunctionMatrix}, ahr::HarmonicArray)  where T =
         PeriodicFunctionMatrix{:c,real(eltype(ahr.values))}(t::Real -> hreval(ahr,t), ahr.period, size(ahr), ahr.nperiod, isconstant(ahr))
Base.convert(::Type{PeriodicFunctionMatrix{:c,T}}, ahr::HarmonicArray)  where T =
         PeriodicFunctionMatrix{:c,T}(t::Real -> hreval(ahr,T(t)), ahr.period, size(ahr), ahr.nperiod, isconstant(ahr))
Base.convert(::Type{PeriodicFunctionMatrix}, At::PeriodicTimeSeriesMatrix)  where T =
    ts2pfm(At; method = "cubic")
# function PeriodicFunctionMatrix(A::PeriodicTimeSeriesMatrix; method = "linear")
#    N = length(A.values)
#    N == 0 && error("empty time array")
#    N == 1 && (return t -> A.values[1])
#    #dt = A.time[2]-A.time[1]
#    dt = A.period/N
#    ts = (0:N)*dt
#    if method == "linear"
#       itp = LinearInterpolation(ts, push!(copy(A.values),A.values[1]))
#       return PeriodicFunctionMatrix(t -> itp(mod(t, A.period)), A.period )
#    elseif method == "cubic"
#       n1, n2 = size(A.values[1])
#       intparray = Array{Any,2}(undef,n1, n2)
#       [intparray[i,j] = CubicSplineInterpolation(ts,[getindex.(A.values,i,j);A.values[1][i,j]],bc=Line(OnGrid())) for i in 1:n1, j in 1:n2]
#       return PeriodicFunctionMatrix(t -> [intparray[i,j](mod(t, A.period)) for i in 1:n1, j in 1:n2 ], A.period )
#    end
# end

# conversions to continuous-time Fourier function matrix
function Base.convert(::Type{FourierFunctionMatrix}, A::PeriodicFunctionMatrix)
   T = float(eltype(A))
   return FourierFunctionMatrix{:c,T}(Fun(x -> T.(A.f(x)), Fourier(0..A.period/A.nperiod)), Float64(A.period), A.nperiod)
end
function Base.convert(::Type{FourierFunctionMatrix}, A::HarmonicArray)
   tA = convert(PeriodicFunctionMatrix,A)
   return FourierFunctionMatrix{:c,eltype(tA)}(Fun(x -> tA.f(x), Fourier(0..tA.period/tA.nperiod)), Float64(tA.period), tA.nperiod)
end
function Base.convert(::Type{FourierFunctionMatrix}, A::PeriodicSymbolicMatrix)
   tA = convert(PeriodicFunctionMatrix,A)
   return FourierFunctionMatrix{:c,eltype(tA)}(Fun(x -> tA.f(x), Fourier(0..tA.period/tA.nperiod)), Float64(tA.period), tA.nperiod)
end
function Base.convert(::Type{FourierFunctionMatrix}, A::PeriodicTimeSeriesMatrix)
   tA = convert(PeriodicFunctionMatrix,A)
   return FourierFunctionMatrix{:c,eltype(tA)}(Fun(x -> tA.f(x), Fourier(0..tA.period/tA.nperiod)), Float64(tA.period), tA.nperiod)
end

# conversions to continuous-time PeriodicSymbolicMatrix
function Base.convert(::Type{PeriodicSymbolicMatrix}, A::PeriodicFunctionMatrix) where T
   @variables t
   PeriodicSymbolicMatrix(Num.(A.f(t)), A.period; nperiod = A.nperiod)
end
function Base.convert(::Type{PeriodicSymbolicMatrix{:c,T}}, A::PeriodicFunctionMatrix) where T
   @variables t
   PeriodicSymbolicMatrix(Num.(A.f(t)), A.period; nperiod = A.nperiod)
end
Base.convert(::Type{PeriodicSymbolicMatrix}, ahr::HarmonicArray)  where T =
   PeriodicSymbolicMatrix(hr2psm(ahr), ahr.period; nperiod = ahr.nperiod)
Base.convert(::Type{PeriodicSymbolicMatrix}, A::PeriodicTimeSeriesMatrix) =
   PeriodicSymbolicMatrix(hr2psm(convert(HarmonicArray,A)), A.period; nperiod = A.nperiod)
Base.convert(::Type{PeriodicSymbolicMatrix}, A::FourierFunctionMatrix) =
   convert(PeriodicSymbolicMatrix,convert(HarmonicArray,A))

# conversion to continuous-time HarmonicArray
Base.convert(::Type{HarmonicArray}, A::PeriodicTimeSeriesMatrix) = ts2hr(A)
Base.convert(::Type{HarmonicArray}, A::PeriodicFunctionMatrix) = pfm2hr(A)
Base.convert(::Type{HarmonicArray}, A::PeriodicSymbolicMatrix) = psm2hr(A)
#Base.convert(::Type{HarmonicArray}, A::FourierFunctionMatrix) = pfm2hr(convert(PeriodicFunctionMatrix,A))
Base.convert(::Type{HarmonicArray}, A::FourierFunctionMatrix) = ffm2hr(A)

# conversions to PeriodicTimeSeriesMatrix
function Base.convert(::Type{PeriodicTimeSeriesMatrix}, A::PeriodicSymbolicMatrix)
    tA = convert(PeriodicFunctionMatrix,A)
    PeriodicTimeSeriesMatrix(tA.f.((0:127)*tA.period/128/tA.nperiod), tA.period; nperiod = tA.nperiod)
end
Base.convert(::Type{PeriodicTimeSeriesMatrix}, A::PeriodicFunctionMatrix) =
         PeriodicTimeSeriesMatrix(A.f.((0:127)*A.period/128/A.nperiod), A.period; nperiod = A.nperiod)
Base.convert(::Type{PeriodicTimeSeriesMatrix}, A::HarmonicArray) =
    PeriodicTimeSeriesMatrix(tvmeval(A, collect((0:127)*A.period/128/A.nperiod)), A.period; nperiod = A.nperiod)
Base.convert(::Type{PeriodicTimeSeriesMatrix}, A::PeriodicMatrix) =
    convert(PeriodicTimeSeriesMatrix, pm2pa(A))
Base.convert(::Type{PeriodicTimeSeriesMatrix}, A::PeriodicArray) =
    PeriodicTimeSeriesMatrix([A.M[:,:,i] for i in 1:size(A.M,3)], A.period; nperiod = A.nperiod)
Base.convert(::Type{PeriodicTimeSeriesMatrix}, A::FourierFunctionMatrix) =
   convert(PeriodicTimeSeriesMatrix,convert(PeriodicFunctionMatrix,A))


using Test
using Clustering
using Distances
using Random
using LinearAlgebra

import Distances.pairwise!

# custom distance metric
struct MySqEuclidean <: SemiMetric end

# redefinition of Distances.pairwise! for MySqEuclidean type
function pairwise!(r::AbstractMatrix, dist::MySqEuclidean,
                   a::AbstractMatrix, b::AbstractMatrix; dims::Integer=2)
    dims == 2 || throw(ArgumentError("only dims=2 supported for MySqEuclidean distance"))
    mul!(r, transpose(a), b)
    sa2 = sum(abs2, a, dims=1)
    sb2 = sum(abs2, b, dims=1)
    @inbounds r .= sa2' .+ sb2 .- 2r
end

@testset "kmeans() (k-means)" begin

@testset "Argument checks" begin
    Random.seed!(34568)
    @test_throws ArgumentError kmeans(randn(2, 3), 1)
    @test_throws ArgumentError kmeans(randn(2, 3), 4)
    @test kmeans(randn(2, 3), 2) isa KmeansResult
    @test_throws ArgumentError kmeans(randn(2, 3), 2, display=:mylog)
    for disp in keys(Clustering.DisplayLevels)
        @test kmeans(randn(2, 3), 2, display=disp) isa KmeansResult
    end
end

Random.seed!(34568)

m = 3
n = 1000
k = 10

x = rand(m, n)
xt = copy(transpose(x))

equal_kmresults(km1::KmeansResult, km2::KmeansResult) =
    all(getfield(km1, η) == getfield(km2, η) for η ∈ fieldnames(KmeansResult))

@testset "non-weighted" begin
    Random.seed!(34568)
    r = kmeans(x, k; maxiter=50)
    @test isa(r, KmeansResult{Matrix{Float64}, Float64, Int})
    @test nclusters(r) == k
    @test size(r.centers) == (m, k)
    @test length(r.assignments) == n
    @test all(a -> 1 <= a <= k, r.assignments)
    @test length(r.costs) == n
    @test length(counts(r)) == k
    @test sum(counts(r)) == n
    @test wcounts(r) == counts(r)
    @test sum(r.costs) ≈ r.totalcost

    Random.seed!(34568)
    r_t = kmeans(xt', k; maxiter=50)
    @test equal_kmresults(r, r_t)
end

@testset "non-weighted (float32)" begin
    Random.seed!(34568)
    x32 = map(Float32, x)
    x32t = copy(x32')
    r = kmeans(x32, k; maxiter=50)
    @test isa(r, KmeansResult{Matrix{Float32}, Float32, Int})
    @test nclusters(r) == k
    @test size(r.centers) == (m, k)
    @test length(r.assignments) == n
    @test all(a -> 1 <= a <= k, r.assignments)
    @test length(r.costs) == n
    @test length(counts(r)) == k
    @test sum(counts(r)) == n
    @test wcounts(r) == counts(r)
    @test sum(r.costs) ≈ r.totalcost

    Random.seed!(34568)
    r_t = kmeans(x32t', k; maxiter=50)
    @test equal_kmresults(r, r_t)
end

@testset "weighted" begin
    w = rand(n)
    Random.seed!(34568)
    r = kmeans(x, k; maxiter=50, weights=w)
    @test isa(r, KmeansResult{Matrix{Float64}, Float64, Float64})
    @test nclusters(r) == k
    @test size(r.centers) == (m, k)
    @test length(r.assignments) == n
    @test all(a -> 1 <= a <= k, r.assignments)
    @test length(r.costs) == n
    @test length(counts(r)) == k
    @test sum(counts(r)) == n

    cw = zeros(k)
    for i = 1:n
        cw[r.assignments[i]] += w[i]
    end
    @test wcounts(r) ≈ cw
    @test dot(r.costs, w) ≈ r.totalcost

    Random.seed!(34568)
    r_t = kmeans(xt', k; maxiter=50, weights=w)
    @test equal_kmresults(r, r_t)
end

@testset "custom distance" begin
    Random.seed!(34568)
    r = kmeans(x, k; maxiter=50, init=:kmcen, distance=MySqEuclidean())
    r2 = kmeans(x, k; maxiter=50, init=:kmcen)
    @test isa(r, KmeansResult{Matrix{Float64}, Float64, Int})
    @test nclusters(r) == k
    @test size(r.centers) == (m, k)
    @test length(r.assignments) == n
    @test all(a -> 1 <= a <= k, r.assignments)
    @test length(r.costs) == n
    @test length(counts(r)) == k
    @test sum(counts(r)) == n
    @test wcounts(r) == r.counts
    @test sum(r.costs) ≈ r.totalcost
    @test equal_kmresults(r, r2)

    Random.seed!(34568)
    r_t = kmeans(xt', k; maxiter=50, init=:kmcen, distance=MySqEuclidean())
    @test equal_kmresults(r, r_t)
end

@testset "Argument checks" begin
    Random.seed!(34568)
    n = 50
    k = 10
    x = randn(m, n)

    @testset "init=" begin
        @test_throws ArgumentError kmeans(x, k, init=1:(k-2))
        @test_throws ArgumentError kmeans(x, k, init=1:(k+2))
        @test kmeans(x, k, init=1:k, maxiter=5) isa KmeansResult

        @test_throws ArgumentError kmeans(x, k, init=:myseeding)
        for algname in (:kmpp, :kmcen, :rand)
            alg = Clustering.seeding_algorithm(algname)
            @test kmeans(x, k, init=algname) isa KmeansResult
            @test kmeans(x, k, init=alg) isa KmeansResult
        end
    end
end

@testset "Integer data" begin
    x = rand(Int16, m, n)
    Random.seed!(654)
    r = kmeans(x, k; maxiter=50)

    @test isa(r, KmeansResult{Matrix{Float64}, Float64, Int})
end

@testset "kmeans! data types" begin
    Random.seed!(1101)
    for TX in (Int, Float32, Float64)
        for TC in (Float32, Float64)
            for TW in (Nothing, Int, Float32, Float64)
                x = rand(TX, m, n)
                c = rand(TC, m, k)
                if TW == Nothing
                    r = kmeans!(x, c; maxiter=1)
                    @test isa(r, KmeansResult{Matrix{TC},<:Real,Int})
                else
                    w = rand(TW, n)
                    r = kmeans!(x, c; weights=w, maxiter=1)
                    @test isa(r, KmeansResult{Matrix{TC},<:Real,TW})
                end
            end
        end
    end
end

end


# ComplexDual encompasses all complex dual types
const ComplexDual{T,V,N} = Complex{Dual{T,V,N}}

# Constructor for ComplexDual based on real Partials for dreal and dimag
@inline function ComplexDual{T,V,N}(z::Number,dr::Partials{N,V},di::Partials{N,V}) where {T,N,V<:Real}
    zr, zi = reim(z)
    return Dual{T}(zr,dr) + im*Dual{T}(zi,di)
end

@inline ComplexDual{T}(z::Number,u::Partials{N,V},v::Partials{N,V}) where {T,N,V} =
    ComplexDual{T,V,N}(z,u,v)

# Constructor for ComplexDual based on complex Partials for dz and dz*
@inline function ComplexDual{T}(z::Number,dz::Partials{N,V},dzstar::Partials{N,V}) where {T,N,V<:Complex}
    dr, di = reim_partials(dz,dzstar)
    return ComplexDual{T}(z,dr,di)
end

@inline ComplexDual{T}(z::Number,dz::Complex,dzstar::Complex) where {T} =
    ComplexDual{T}(z,Partials(tuple(dz)),Partials(tuple(dzstar)))

@inline Base.one(::Type{<:ComplexDual{T}},z::Number) where T = ComplexDual{T}(z,one(z),zero(z))

@inline valtype(::ComplexDual{T,V,N}) where {T,V,N} = V
@inline valtype(::Type{ComplexDual{T,V,N}}) where {T,V,N} = V
@inline valtype(::Array{<:ComplexDual{T,V,N}}) where {T,V,N} = V
@inline valtype(::Type{<:Array{<:ComplexDual{T,V,N}}}) where {T,V,N} = V

# ==== extensions of Partials ==== #
@inline Base.conj(v::Partials{N,V}) where {N,V} = Partials{N,V}(conj.(v.values))
@inline Base.real(v::Partials{N,Complex{V}}) where {N,V} = Partials{N,V}(real.(v.values))
@inline Base.imag(v::Partials{N,Complex{V}}) where {N,V} = Partials{N,V}(imag.(v.values))

@inline Base.:*(x::Complex, partials::Partials) = partials*x

@inline function Base.:*(partials::Partials, x::Complex)
        return Partials(ForwardDiff.scale_tuple(partials.values, x))
end

# extract the partial derivatives from a complex dual number
@inline function ForwardDiff.partials(d::ComplexDual)
    dr, di = reim(d)
    return partials(dr), partials(di)
end

# assemble the derivatives d/dz and d/dz* from the partials
@inline function _derivs(dvrx,dvry,dvix,dviy)
    return 0.5*(dvrx+dviy) + 0.5*im*(dvix-dvry), 0.5*(dvrx-dviy) + 0.5*im*(dvix+dvry)
end

# Given a complex dual number, extract Partials of dz and dz*
@inline function dz_partials(d::ComplexDual{T,V,N}) where {T,V,N}
    dr, di = partials(d)
    tmp  = [_derivs(dr[i], dr[i+1], di[i],di[i+1]) for i in 1:2:N]
    return Partials(tuple(first.(tmp)...)), Partials(tuple(last.(tmp)...))
end

# For the dz and dz* partials with respect to the ith element
@inline Base.@propagate_inbounds function dz_partials(d::ComplexDual, i)
    dr, di = partials(d)
    ix = 2*i.-1
    iy = 2*i
    return _derivs(dr[ix], dr[iy], di[ix],di[iy])
end

@inline Base.@propagate_inbounds dz_partials(::Type{T}, d::ComplexDual{T}, i...) where T = dz_partials(d, i...)

# For dealing with arrays of complex duals, return the dz and dz*
# partials as separate arrays
@inline dz_partials(d::AbstractVector{ComplexDual{T,V,N}}) where {T,V,N} =
  (tmp = dz_partials.(d); return first.(tmp), last.(tmp))

# For the dz and dz* partials with respect to the ith element
@inline Base.@propagate_inbounds dz_partials(d::AbstractVector{ComplexDual{T,V,N}},i) where {T,V,N} =
  (tmp = dz_partials.(d,i); return first.(tmp), last.(tmp))

@inline Base.@propagate_inbounds dz_partials(::Type{T}, d::AbstractVector{ComplexDual{T,V,N}}, i...) where {T,V,N} = dz_partials(d, i...)

# Given complex Partials for d/dz and d/dz*, return partials for dr/dx,y and di/dx,y
@inline function reim_partials(dz::Partials{N,V},dzstar::Partials{N,V}) where {T,V<:Complex,N}
    drx, dix = reim(dz+dzstar)
    dry, diy = reim(im*dz-im*dzstar)
    return _splice_xy(drx,dry), _splice_xy(dix,diy)
end

# Given partials for the Create separate partials for the real part and the imaginary part.
# These are organized as drx[1],dry[1],drx[2],dry[2],... and dix[1],diy[1],dix[2],diy[2],...
@inline function _splice_xy(dx::Partials{N},dy::Partials{N}) where {N}
    return Partials(tuple([ifelse(mod(i,2) == 0,dy[max(i÷2,1)],dx[(i+1)÷2]) for i in 1:2N]...))
end

# Return the value of a complex dual
@inline function value(d::ComplexDual)
    dr, di = reim(d)
    return value(dr)+im*value(di)
end


using Test
using TestSetExtensions
using ReferenceTests
using MetaBalls:MetaBalls

include("generators.jl")
include("c_code.jl")

@test_reference "voxels.txt" map((x) -> edge_lookup(x), generate_voxels(16))

@testset "edge lookup" begin
    @test_reference "voxels.txt" map((x) -> MetaBalls.edge_lookup(x), generate_voxels(16))
end


# This file is part of IntegerSequences.
# Copyright Peter Luschny. License is MIT.

(@__DIR__) ∉ LOAD_PATH && push!(LOAD_PATH, (@__DIR__))

module ZumkellerNumbers

using NumberTheory, IterTools, CombinationsIterator

export ModuleZumkellerNumbers
export isZumkeller, is083207, I083207, F083207, L083207, V083207

"""
A Zumkeller number ``n`` is an integer whose divisors can be partitioned into two disjoint sets whose sums are both ``σ(n)/2``.

* isZumkeller, is083207, I083207, F083207, L083207, V083207
"""
const ModuleZumkellerNumbers = ""

"""
Is ``n`` a Zumkeller number? A Zumkeller number ``n`` is an integer whose divisors can be partitioned into two disjoint sets whose sums are both ``σ(n)/2``.
"""
function isZumkeller(n::Int)
    n == 0 && return false
    s = σ(n)
    ((s % 2 ≠ 0) || (s < 2n)) && return false
    S = s >> 1 - n
    D = [d for d in Divisors(n) if d ≤ S]
    D == [] && return true
    for c in Combinations(D)
        S == sum(c) && return true
    end
    return false
end

"""
Is ``n`` a Zumkeller number?
"""
is083207(n) = isZumkeller(n)

"""
Iterate over the first ``n`` Zumkeller numbers.
"""
I083207(n) = Iterators.take(Iterators.filter(isZumkeller, Iterators.countfrom(1)), n)

"""
Iterate over the Zumkeller numbers ``z`` which are below ``n, (1 ≤ z ≤ n)``.
"""
F083207(n) = Iterators.filter(isZumkeller, 1:n)

"""
List the first ``n`` Zumkeller numbers.
"""
L083207(n) = collect(I083207(n))

"""
Return the ``n``-th Zumkeller number.
"""
V083207(n) = nth(I083207(n), n)

#START-TEST-########################################################

using Test

function test()
    @testset "Zumkeller" begin
        @test isZumkeller(17000) == true
        @test isZumkeller(27472) == true
        @test isZumkeller(29062) == false
        @test isZumkeller(43464) == true
    end
end

function demo()
    println(L083207(10))
    for a in I083207(30) print(a, ", ") end; println("...")
    for a in F083207(30) print(a, ", ") end; println("...")

    for n in 20:30
        println(n, " ↦ ",  isZumkeller(n))
    end

    for (index, value) in enumerate(I083207(10))
        println("$index -> $value")
    end

    println(V083207(10))

    for n in 1:6
        println(n, " ↦ ", L083207(n))
    end
end

"""
@time (for n in 1:2000 isZumkeller(n) end) ::
    0.311896 seconds (1.86 M allocations: 63.875 MiB, 31.28% gc time)
@time L083207(500) ::
    0.295718 seconds (2.21 M allocations: 75.920 MiB, 40.89% gc time)
"""
function perf()
    GC.gc()
    @time (for n in 1:2000 isZumkeller(n) end)
    @time L083207(500)
end

function main()
    test()
    demo()
    perf()
end

main()

end # module

#=
[6, 12, 20, 24, 28, 30, 40, 42, 48, 54]
6, 12, 20, 24, 28, 30, 40, 42, 48, 54, 56, 60, 66, 70, 78, 80, 84,
88, 90, 96, 102, 104, 108, 112, 114, 120, 126, 132, 138, 140, ...
6, 12, 20, 24, 28, 30, ...

20 ↦ true
21 ↦ false
22 ↦ false
23 ↦ false
24 ↦ true
25 ↦ false
26 ↦ false
27 ↦ false
28 ↦ true
29 ↦ false
30 ↦ true

1 -> 6
2 -> 12
3 -> 20
4 -> 24
5 -> 28
6 -> 30
7 -> 40
8 -> 42
9 -> 48
10 -> 54
54

1 ↦ [6]
2 ↦ [6, 12]
3 ↦ [6, 12, 20]
4 ↦ [6, 12, 20, 24]
5 ↦ [6, 12, 20, 24, 28]
6 ↦ [6, 12, 20, 24, 28, 30]

  0.311551 seconds (1.86 M allocations: 63.820 MiB, 60.62% gc time)
  0.294909 seconds (2.21 M allocations: 75.858 MiB, 41.31% gc time)
=#


"""
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-                   MEASUREMENTS / STATE PREPARATION SETTINGS                  -
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
"""

"""
    fullbases(N::Int; local_basis = "Pauli")

Generate the full set of bases for a choice of local single-qubit basis set.
Predefined option: "Pauli", # bases = 3^N
"""
function fullbases(N::Int; local_basis = "Pauli")
  local_basis == "Pauli" && (local_basis = ["X","Y","Z"])
  if N >15
    print("The $(N)-qubit set of Pauli bases contains $(3^N) bases.\n This may take a while...\n\n")
  end
  !(local_basis isa AbstractArray) && error("Basis not recognized")
  A = Iterators.product(ntuple(i->local_basis, N)...) |> collect
  B = reverse.(reshape(A,length(A),1))
  return  reduce(hcat, getindex.(B,i) for i in 1:N)
end

"""
    fullpreparations(N::Int; local_input_states="Pauli")

Generate the full set of input states to a channel.
Predefined option: "Pauli", 6^N
                   "Tetra", 4^N
"""
function fullpreparations(N::Int; local_input_states="Pauli")
  if N > 5
    print("The $(N)-qubit set of Pauli eigenstates contains $(6^N) bases.\n This may take a while...\n\n")
  end
  local_input_states == "Pauli" && (local_input_states = ["X+", "X-", "Y+", "Y-", "Z+", "Z-"])
  local_input_states == "Tetra" && (local_input_states = ["T1","T2","T3","T4"])
  !(local_input_states isa AbstractArray) && error("States not recognized")
  A = Iterators.product(ntuple(i->local_input_states, N)...) |> collect
  B = reverse.(reshape(A,length(A),1))
  return  reduce(hcat, getindex.(B,i) for i in 1:N)
end

"""
    randombases(N::Int, nbases::Int; local_basis = "Pauli")

Generate `nbases` measurement bases. By default, each
local basis is randomly selected between `["X","Y","Z"]`, with
`"Z"` being the default basis where the quantum state is written.
"""
function randombases(N::Int, nbases::Int; local_basis = "Pauli")
  local_basis == "Pauli" && (local_basis = ["X","Y","Z"])
  return rand(local_basis, nbases, N)
end

"""
    measurementgates(basis::Vector)

Given as input a measurement basis, returns the corresponding
gate data structure. If the basis is `"Z"`, no action is required.
If not, a quantum gate corresponding to the given basis rotation
is added to the list.

Example:
  basis = `["X","Z","Z","Y"]`

  -> `gate_list = [("basisX", 1),("basisY", 4)]`
"""
function measurementgates(basis::Vector)
  gate_list = Tuple[]
  for j in 1:length(basis)
    if basis[j] ≠ "Z"
      push!(gate_list, ("basis$(basis[j])", j, (dag=true,)))
    end
  end
  return gate_list
end

"""
    randompreparations(N::Int, nshots::Int;
                       local_input_state = ["X+","X-","Y+","Y-","Z+","Z-"],
                       ndistinctstates = nothing)

Generate `nshots` input states to a quantum circuit. By default, each
single-qubit state is randomly selected between the 6 eigenstates of
the Pauli matrices, `["X+","X-","Y+","Y-","Z+","Z-"]`.
If `ndistinctstates` is provided, the output consist of `numprep`
different input states, each being repeated `nshots÷ndistinctstates`
times.
"""
function randompreparations(
  N::Int,
  npreps::Int;
  local_input_states= "Pauli",
)

  local_input_states == "Pauli" && (local_input_states = ["X+", "X-", "Y+", "Y-", "Z+", "Z-"])
  local_input_states == "Tetra" && (local_input_states = ["T1","T2","T3","T4"])
  # One shot per basis
  return rand(local_input_states, npreps, N)
end

"""
    readouterror!(measurement::Union{Vector, Matrix}, p1given0, p0given1)

Add readout error to a single projective measurement.

# Arguments:
  - `measurement`: bit string of projective measurement outcome
  - `p1given0`: readout error probability 0 -> 1
  - `p0given1`: readout error probability 1 -> 0
"""
function readouterror!(
  measurement::Union{Vector,Matrix}, p1given0::Float64, p0given1::Float64
)
  for j in 1:size(measurement)[1]
    if measurement[j] == 0
      measurement[j] = StatsBase.sample([0, 1], Weights([1 - p1given0, p1given0]))
    else
      measurement[j] = StatsBase.sample([0, 1], Weights([p0given1, 1 - p0given1]))
    end
  end
  return measurement
end

"""
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-                            PROJECTIVE MEASUREMENTS                           -
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
"""

"""
    getsamples(T::ITensor, nshots::Int)

Generate `nshots` projective measurements for an input quantum state `T`,
which can either be a wavefunction or density matrix (dense).
"""
function getsamples(T::ITensor, nshots::Int;
    readout_errors = (p1given0 = nothing, p0given1 = nothing))

  p1given0 = readout_errors[:p1given0]
  p0given1 = readout_errors[:p0given1]

  # Get the number of qubits
  N = nsites(T)

  # Get a dense array which can be
  # - Vector with dim 2^N for a wavefunction |ψ⟩
  # - Matrix with dim (2^N,2^N) for a density matrix ρ
  A = array(T)

  # Compute the full probability distribution
  # P(σ) = |⟨σ|ψ⟩|² (Tr[ρ|σ⟩⟨σ|]
  probs = (A isa Vector ? abs2.(A) : real(diag(A)))
  @assert sum(probs) ≈ 1

  # Sample the distribution exactly
  index = StatsBase.sample(0:1<<N-1, StatsBase.Weights(probs), nshots)

  # Map integer to binary vectors and massage the structure
  M = hcat(digits.(index,base=2,pad=N)...)'
  measurements = reverse(M,dims=2)
  if !isnothing(p1given0) || !isnothing(p0given1)
    p1given0 = (isnothing(p1given0) ? 0.0 : p1given0)
    p0given1 = (isnothing(p0given1) ? 0.0 : p0given1)
    for n in 1:nshots
      readouterror!(measurements[n,:], p1given0, p0given1)
    end
  end
  return measurements
end

getsamples(T::ITensor; kwargs...) =
  vcat(getsamples!(T,1; kwargs...)...)

"""
    getsamples!(M::Union{MPS,MPO};
                readout_errors = (p1given0 = nothing,
                                  p0given1 = nothing))

Generate a single projective measurement in the MPS/MPO reference basis.
If `readout_errors` is non-trivial, readout errors with given probabilities
are applied to the measurement outcome.
"""
function getsamples!(M::Union{MPS,MPO}; readout_errors=(p1given0=nothing, p0given1=nothing))
  p1given0 = readout_errors[:p1given0]
  p0given1 = readout_errors[:p0given1]
  orthogonalize!(M, 1)
  measurement = sample(M)
  measurement .-= 1
  if !isnothing(p1given0) || !isnothing(p0given1)
    p1given0 = (isnothing(p1given0) ? 0.0 : p1given0)
    p0given1 = (isnothing(p0given1) ? 0.0 : p0given1)
    readouterror!(measurement, p1given0, p0given1)
  end
  return measurement
end

"""
    PastaQ.getsamples!(M::Union{MPS,MPO}, nshots::Int; kwargs...)

Perform `nshots` projective measurements of a wavefunction
`|ψ⟩` or density operator `ρ` in the MPS/MPO reference basis.
Each measurement consists of a binary vector `σ = (σ₁,σ₂,…)`,
drawn from the probabilty distribution:
- `P(σ) = |⟨σ|ψ⟩|²`  :  if `M = ψ is MPS`
- `P(σ) = ⟨σ|ρ|σ⟩`   :  if `M = ρ is MPO`
"""
function getsamples(M0::Union{MPS,MPO}, nshots::Int; kwargs...)
  nthreads = Threads.nthreads()
  data = [Vector{Vector{Int64}}(undef, 0) for _ in 1:nthreads]
  M = orthogonalize!(copy(M0), 1)

  Threads.@threads for j in 1:nshots
    nthread = Threads.threadid()
    sample_ = getsamples!(M; kwargs...)
    push!(data[nthread], sample_)
  end
  return permutedims(hcat(vcat(data...)...))
end

"""
    getsamples(M::Union{MPS,MPO,ITensor}, bases::Matrix; kwargs...)

Generate a dataset of measurements acccording to a set
of input `bases`, performing `nshots` measurements per basis.
For a single measurement, `Û` is the depth-1
local circuit rotating each qubit, the  data-point `σ = (σ₁,σ₂,…)`
is drawn from the probability distribution:
- `P(σ) = |⟨σ|Û|ψ⟩|²`    :  if `M = ψ is MPS`
- `P(σ) = <σ|Û ρ Û†|σ⟩`  :  if `M = ρ is MPO`
"""
function getsamples(M0::Union{MPS,MPO,ITensor}, bases::Matrix{<:String}, nshots::Int; kwargs...)
  N = nsites(M0)
  @assert N == size(bases)[2]
  nthreads = Threads.nthreads()
  data = [Vector{Vector{Pair{String,Int}}}(undef, 0) for _ in 1:nthreads]
  M = copy(M0)
  !(M isa ITensor) && orthogonalize!(M, 1)

  Threads.@threads for n in 1:size(bases, 1)
    nthread = Threads.threadid()
    meas_gates = measurementgates(bases[n, :])
    M_meas = runcircuit(copy(M), meas_gates)
    measurements = getsamples(M_meas, nshots; kwargs...)
    basisdata = [[bases[n,j] => measurements[k,j] for j in 1:N] for k in 1:nshots]
    append!(data[nthread], basisdata)
  end
  return permutedims(hcat(vcat(data...)...))
end

getsamples(M::Union{MPS,MPO,ITensor}, bases::Vector{<:Vector}, nshots::Int; kwargs...) =
  getsamples(M, permutedims(hcat(bases...)), nshots; kwargs...)

getsamples(M::Union{MPS,MPO,ITensor}, bases::Union{Matrix,Vector{<:Vector}}; kwargs...) =
  getsamples(M, bases, 1; kwargs...)

"""
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
                            PROCESS TOMOGRAPHY DATA                                -
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
"""

"""
    projectchannel(U0::MPO,prep::Array)
    projectchannel(U::ITensor, prep::Array)

Project the unitary circuit (MPO) into a state `prep`
made out of single-qubit Pauli eigenstates (e.g. `|ϕ⟩ =|+⟩⊗|0⟩⊗|r⟩⊗…).
The resulting MPS describes the quantum state obtained by applying
the quantum circuit to `|ϕ⟩`. Same for a dense ITensors.
"""
function _projectchannel(U0::Union{MPO,ITensor}, prep::Array)
  U = copy(U0)
  s = U isa MPO ? firstsiteinds(U) : inds(U; plev = 0)
  for j in 1:length(s)
    if U isa MPO
      U[j] = U[j] * state(prep[j], s[j])
    else
      U = U * state(prep[j], s[j])
    end
  end
  U isa ITensor && return noprime!(U)
  return convert(MPS,noprime!(U))
end

function _projectchannel(Λ0::Choi, prep::Array)
  Λ = copy(Λ0.X)
  s = Λ isa MPO ? firstsiteinds(Λ; tags="Input") : inds(Λ; tags = "Input", plev = 0)
  for j in 1:length(s)
    # No conjugate on the gate (transpose input!)
    if Λ isa MPO
      Λ[j] = Λ[j] * dag(state(prep[j], s[j]))
      Λ[j] = Λ[j] * prime(state(prep[j], s[j]))
    else
      Λ = Λ * dag(state(prep[j], s[j]))
      Λ = Λ * prime(state(prep[j], s[j]))
    end
  end
  return Λ
end

projectchannel(M::Union{MPO,ITensor}, prep::AbstractArray) =
  ischoi(M) ? _projectchannel(Choi(M), prep) : _projectchannel(M, prep)

function getsamples(
  M0::Union{LPDO,MPO,ITensor},
  preps_and_bases::Matrix{<:Pair},
  nshots::Int,
  kwargs...
)
  N = nsites(M0)
  @assert N == size(preps_and_bases, 2)

  nthreads = Threads.nthreads()
  data = [Matrix{Pair{String,Pair{String,Int}}}(undef, 0, N) for _ in 1:nthreads]
  M = copy(M0)

  Threads.@threads for k in 1:size(preps_and_bases, 1)
    nthread = Threads.threadid()
    # input state to the channel
    prep  = first.(preps_and_bases[k,:])
    # measurement basis
    basis = last.(preps_and_bases[k,:])

    # project the channel into the input state
    Mproj = projectchannel(M, prep)

    # perform measurement basis rotation
    meas_gates = measurementgates(basis)
    M_meas = runcircuit(Mproj, meas_gates)

    # perform projective measurement
    measurements = getsamples(M_meas, nshots; kwargs...)
    basisdata = [[prep[j] => (basis[j] => measurements[k,j]) for j in 1:N] for k in 1:nshots]
    data[nthread] = vcat(data[nthread], permutedims(hcat(basisdata...)))
  end
  return vcat(data...)
end

function getsamples(
  M0::Union{LPDO,MPO,ITensor},
  preps::Matrix,
  bases::Matrix,
  nshots::Int;
  kwargs...
)

  N = nsites(M0)
  npreps = size(preps, 1)
  nbases = size(bases, 1)
  ntotal = npreps * nbases

  preps_and_bases = Matrix{Pair{String,String}}(undef, 0, N)
  for p in 1:npreps
    x = [preps[p,:] .=> bases[b,:] for b in 1:nbases]
    preps_and_bases = vcat(preps_and_bases, permutedims(hcat(x...)))
  end
  return getsamples(M0, preps_and_bases, nshots; kwargs...)
end

getsamples(M::Union{LPDO,MPO, ITensor}, preps::Vector{<:Vector}, bases::Vector{<:Vector}, nshots::Int; kwargs...) =
  getsamples(M, permutedims(hcat(preps...)), permutedims(hcat(bases...)), nshots; kwargs...)

getsamples(M::Union{LPDO,MPO, ITensor}, preps::Union{Matrix,Vector{<:Vector}}, bases::Union{Matrix,Vector{<:Vector}}; kwargs...) =
  getsamples(M, preps, bases, 1; kwargs...)


# ---
# jupyter:
#   jupytext:
#     formats: ipynb,jl:hydrogen
#     text_representation:
#       extension: .jl
#       format_name: hydrogen
#       format_version: '1.3'
#       jupytext_version: 1.11.2
#   kernelspec:
#     display_name: Julia 1.6.1
#     language: julia
#     name: julia-1.6
# ---

# %%
using SparseArrays

A = zeros( Integer, 2400, 2400 )
for i in 1:2400
    if rand(1:4) == 1
        for j in i+1:2400
            if rand(1:15) == 1
                A[i,j] = 1
                A[j,i] = 1
            end
        end
    end
end

sum( A)
A = sparse( A )
B = A^2

println( "some stats A: size ", size(A), " min value ", minimum( A ), " max value ", maximum( A ) )
println( "some stats B: size ", size(B), " min value ", minimum( B ), " max value ", maximum( B ) )

# %%
A

# %%
B

# %%
a = spzeros(Integer, 5, 5)
a .= 1
a

# %%
b = a^2

# %%

0011/eltype of square of sparse array with Integer eltype is Any.jl


using DelayDiffEq, DiffEqProblemLibrary.DDEProblemLibrary
DDEProblemLibrary.importddeproblems()

using PGFPlotsX

using DelimitedFiles
using LinearAlgebra

function plot_waltman(alg; plotdensity = 10_000)
    # compute solution
    sol = solve(DDEProblemLibrary.prob_dde_RADAR5_waltman_5, MethodOfSteps(alg);
                reltol = 1e-6, abstol = [1e-21, 1e-21, 1e-21, 1e-21, 1e-9, 1e-9])
    ts = range(sol.t[1]; stop = sol.t[end], length = plotdensity)

    # load RADAR5 solution
    radar5 = readdlm(joinpath(@__DIR__, "..", "data", "waltman_radar5.out"))
    radar5_ts = radar5[:, 1]
    radar5_us = radar5[:, 2:7]

    # create log plot of first four components
    @pgf logaxis = SemiLogYAxis()
    for i in 1:4
        @pgf push!(logaxis, PlotInc({ thick }, Table(radar5_ts, radar5_us[:, i])),
                   PlotInc({ thick, dashed }, Table(ts, sol(ts; idxs = i).u)))
    end

    # create standard plot of last two components
    @pgf axis = Axis(
        PlotInc({ thick }, Table(radar5_ts, radar5_us[:, 5])),
        PlotInc({ thick, dashed }, Table(ts, sol(ts; idxs = 5).u)),
        PlotInc({ thick }, Table(radar5_ts, radar5_us[:, 6])),
        PlotInc({ thick, dashed }, Table(ts, sol(ts; idxs = 6).u)),
    )

    # create empty group plot
    @pgf gp = GroupPlot(
        {
            group_style = {
                group_size = "2 by 1",
                xlabels_at = "edge bottom",
                ylabels_at = "edge left",
                horizontal_sep = raw"0.1\textwidth",
            },
            xlabel = raw"$t$", xmin = ts[1], xmax = ts[end],
            width = raw"0.5\textwidth", height = raw"0.7\textheight",
            grid = "major",
            label_style = { font = raw"\footnotesize" },
            tick_label_style = { font = raw"\footnotesize" },
            legend_pos = "north east",
            no_marks
        },
        logaxis,
        axis
    )
end

# specify LinSolveFactorize due to https://github.com/YingboMa/RecursiveFactorization.jl/issues/4
pgfsave(joinpath(@__DIR__, "..", "figures", "generated", "waltman.tex"),
        plot_waltman(Rosenbrock23(linsolve = LinSolveFactorize(lu)));
        include_preamble = false)


# Copyright Andy Nowacki 2015-, all rights reserved.
# See the file LICENSE.md for licence details.

"""
# CIJ

Deal with linear elastic constants, especially for geophysical applications.

##
"""
module CIJ

using Dates: now
import LinearAlgebra
using LinearAlgebra: cross, dot, norm
using Printf: @printf

using StaticArrays
using Rotations

export
    # Types and constructors
    EC,
    # Constructors of elastic constants
    Panning_VTI,
    global_VTI,
    grechka_cracks,
    grechka_cracks!,
    iso,
    pitl,
    tandon_and_weng,
    thom,
    thom_st,
    # Properties of a tensor
    Au,
    C2S,
    C2S!,
    Hill_average,
    HillG,
    HillK,
    Reuss_average,
    ReussG,
    ReussK,
    VRH,
    Voigt_average,
    VoigtG,
    VoigtK,
    is_iso,
    is_stable,
    phase_vels,
    # Conversion between representations
    S2C,
    S2C!,
    cij,
    cijkl,
    # Operations
    rot3,
    symm,
    symm!

"Lookup index matrix to convert between elasticity tensor and Voigt matrix"
const VOIGT_CONTRACTION_MATRIX = @SMatrix [1 6 5
                                           6 2 4
                                           5 4 3]

"Default precision of floating point numbers used in `EC` type"
const DEFAULT_FLOAT = Float64

# Types
include("types.jl")

# Functions
include("initialisation.jl")
include("inquiry.jl")
include("conversion.jl")
include("transformation.jl")
include("util.jl")
include("averaging.jl")
include("effective_medium.jl")
include("measures.jl")
include("velocities.jl")
include("io.jl")

# Data
include("sample_data.jl")

end # module CIJ


using HDF5
using FastGaussQuadrature
using LaTeXStrings
using LinearAlgebra
using Particles
using Plots
using Random
using ReducedBasisMethods
using Statistics

# HDF5 file containing training data
fpath = "../runs/BoT_Np5e4_k_010_050_np_10_T25.h5"

# read sampling parameters
params = read_sampling_parameters(fpath)

# read training parameters
μₜ = h5read(fpath, "parameters/mu_train")

# read integrator parameters
IP = IntegratorParameters(fpath)

# create spline Poisson solver
poisson = PoissonSolverPBSplines(fpath)

# read snaptshot data
X = h5read(fpath, "snapshots/X")
V = h5read(fpath, "snapshots/V")
E = h5read(fpath, "snapshots/E")
# D = h5read(fpath, "snapshots/D")
# Φ = h5read(fpath, "snapshots/Phi")

# Reference draw
P₀ = ParticleList(X[:,1], V[:,1], ones(IP.nₚ) .* poisson.L ./ IP.nₚ)

# EVD

# XV = hcat(X, V);
XV = copy(X)
for p in 1:IP.nparam
    # XV = hcat(XV, V[:,1+(p-1)*IP.nₛ])
    global XV = hcat(XV, V[:,1+(p-1)*IP.nₛ]) #, E[:,p*IP.nₛ])
    #print(1+(p-1)*IP.nₛ, " ", p*IP.nₛ, "\n" )
end

# clear
X, V = 0, 0
GC.gc()

# @time F = eigen(Xₑₓₜ' * Xₑₓₜ)
# Λ, Ω = sorteigen(F.values, F.vectors)
@time F = eigen(XV' * XV)
@time Λ, Ω = sorteigen(F.values, F.vectors)

@time Fₑ = eigen(E' * E)
@time Λₑ, Ωₑ = sorteigen(Fₑ.values, Fₑ.vectors)

# clear
F, Fₑ = 0, 0
GC.gc()

# plot
plot(xlabel = L"$i$", ylabel = L"$\lambda_i$", yscale = :log10,
     grid = true, gridalpha = 0.5)
plot!(abs.(Λ )[1:1000], linewidth = 2, alpha = 0.25, label = L"$X$")
plot!(abs.(Λₑ)[1:1000], linewidth = 2, alpha = 0.5,  label = L"$F$")
savefig("../runs/BoT_Np5e4_k_010_050_np_10_T25_SVDs_BoT_1.pdf")

# plot
plot(xlabel = L"$i$", ylabel = L"$\lambda_i$", yscale = :log10,
     grid = true, gridalpha = 0.5, legend = :none)
plot!(abs.(Λ ), linewidth = 2, alpha = 0.25, label = L"$X_v$")
plot!(abs.(Λₑ), linewidth = 2, alpha = 0.5,  label = L"$E$")
savefig("../runs/BoT_Np5e4_k_010_050_np_10_T25_SVDs_BoT_2.pdf")

# Projection Matrices
@time k, Ψ = get_Ψ(XV, Λ, Ω, 1e-9, 0)
@time kₑ, Ψₑ = get_Ψ(E, Λₑ, Ωₑ, 1e-5, 0)

# clear
XV, E = 0, 0
Λ, Λₑ, Ω, Ωₑ = 0, 0, 0, 0
GC.gc()

# DEIM
@time Πₑ = deim_get_Π(Ψₑ)

# save to HDF5
h5save("../runs/BoT_Np5e4_k_010_050_np_10_T25_projections.h5", IP, poisson, params, μₜ, k, kₑ, Ψ, Ψₑ, Πₑ, P₀)


module TestPkg

using DifferentialEquations

export oscillator, solve_ode, relaxation

function oscillator(u,p,t)
  x, y, z = u
  A, B, G, K, N = p
  du1 = B[1]*(A[1] - x) + G[1,3] * (1.0 - x) * abs(z)^N[1,3] / (abs(z)^N[1,3] + K[1,3]^N[1,3])
  du2 = B[2]*(A[2] - y) + G[2,1] * (1.0 - y) * abs(x)^N[2,1] / (abs(x)^N[2,1] + K[2,1]^N[2,1])
  du3 = B[3]*(A[3] - z) - G[3,2] * z * abs(y)^N[3,2] / (abs(y)^N[3,2] + K[3,2]^N[3,2])
  [du1,du2,du3]
end

function relaxation(u,p,t)
  x, y, z = u
  A, B, G, K, N = p
  du1 = B[1]*(A[1] - x) + G[1,3] * (1.0 - x) * abs(z)^N[1,3] / (abs(z)^N[1,3] + K[1,3]^N[1,3])
  du2 = B[2]*(A[2] - y) + G[2,1] * (1.0 - y) * abs(x)^N[2,1] / (abs(x)^N[2,1] + K[2,1]^N[2,1])
  du3 = B[3]*(A[3] - z) - G[3,2] * z * abs(y)^N[3,2] / (abs(y)^N[3,2] + K[3,2]^N[3,2]) + G[3,3] * (1.0 - z) * abs(z)^N[3,3] / (abs(z)^N[3,3] + K[3,3]^N[3,3])
  [du1,du2,du3]
end

function solve_ode(prob)
  sol = solve(prob, isoutofdomain=(y,p,t)->any(x->x<0,y))
  return sol
end

end


module spectra
using FFTW

get_freq(t, fs) = fftshift(fftfreq(round(Int,fs*(maximum(t)-minimum(t)))+1, fs))
get_freq(t) = get_freq(t, 1.0/(t[2]-t[1]))

get_spectrum(v::Vector) = fftshift(fft((v.*hanning(length(v)))))
get_spectrum(v::Matrix) = fftshift(fft((v'.*hanning(size(v,2)))',2),2)
function get_spectrum(v::Matrix, tlim, ndt)
    j = tlim*ndt
    j = j[1]:j[2]
    get_spectrum(v[:,j])
end
function get_spectrum(v::Vector, tlim, ndt)
    j = tlim*ndt
    j = j[1]:j[2]
    get_spectrum(v[j])
end

export get_freq, get_spectrum
end


"""
    mutation!(model, de, group)

Mutates each particle by adding random noise y ~ normal(0, σ), where σ is
a small number.

# Arguments

- `model`: model containing a likelihood function with data and priors
- `de`: differential evolution object
- `group`: a group of particles
"""
function mutation!(model, de, group)
    @unpack σ,bounds = de
    noise = Normal(0.0, σ)
    for pt in group
        # add noise to each parameter
        proposal = pt + noise
        # add Loglikelihood of prior to particle weight
        de.evaluate_fitness!(de, model, proposal)
        # update whether proposal was accepted and add proposal if accepted
        de.update_particle!(de, pt, proposal)
    end
    return nothing
end


module Aardvark

    export EMD
    export emd

    using Distances

    struct EMD <: Distances.Metric
        ground_distance::AbstractArray{<:AbstractFloat, 2}
        max_iter::Integer
        raise_error::Bool
        function EMD(ground_distance, max_iter = 100000, raise_error=true)
            new(ground_distance, max_iter, raise_error)
        end
    end

    function (dist::EMD)(x, y)
        # I do not know how to pass a slice to Cxx so for now we need to make sure
        # we are not getting slices passed here
        return emd(Array(x),
                   Array(y),
                   dist.ground_distance,
                   dist.max_iter)
    end

    module CppEMD
        using CxxWrap

        filename = joinpath(string(@__DIR__), "../lib/libEMD.so")
        @wrapmodule(filename)

        function __init__()
            @initcxx
        end
    end

    function emd(a::Array{<:AbstractFloat, 1},
                 b::Array{<:AbstractFloat, 1},
                 M::Array{<:AbstractFloat, 2},
                 max_iter::Integer=100000;
                 raise_error = true,
                 log = false)

        """
        raise_error: Whether to raise an error or to silently return -1.0

        Example of a problem without a solution
        a = [92  0.0130435
             93  0.00434783
             95  0.0347826
             96  0.0695652
             97  0.0652174
             98  0.0434783
             99  0.0434783
             100 0.926087]
        b = [92   0.0130435
             93   0.00434783
             95   0.0347826
             96   0.0623188
             97   0.0652174
             98   0.0869565
             99   0.0434783
             100  0.889855]
        a = sparsevec(a[:, 1], a[:, 2])
        b = sparsevec(b[:, 1], b[:, 2])
        ground_distance = hist.hist_to_distance_matrix(100)

        """

        # We can't pass views here
        # Cxx seems to take care of the C major format of the arrays though
        n1 = size(M)[1]
        n2 = size(M)[2]
        nmax = n1 + n2 - 1
        result_code = 0
        nG = 0

        cost = Ref(0.0)
        alpha = zeros(n1)
        beta = zeros(n2)
        G = zeros((1, 1))
        Gv = zeros()
        iG = zeros()
        jG = zeros()

        G = zeros((n1, n2))

        result_code = CppEMD.EMD_wrap(n1, n2, a, b, M, G, alpha, beta, cost, max_iter)
        if result_code == 0 && raise_error
            error("Problem not feasible.")
        elseif result_code == 0
            cost = Ref(-1.0)
        end
        if log
            return G, cost, alpha, beta, result_code
        else
            return cost[]
        end
end

end


## Constants
go = 9.80665;
d2r = pi/180;
r2d = 180/pi;

REARTH = 6370987.308;         # Mean earth radius - m
WEII3 = 7.292115e-5;          # Angular rotation of earth - rad/s
GO = 9.80665;                 # Gravity Constant
AGRAV = 9.80675445;           # Standard value of gravity acceleration - m/s^2
GCONST = 6.673e-11;                # Universal gravitational constant - Nm^2/kg^2
EARTH_MASS = 5.973332e24;     # Mass of the earth - kg
MU = 3.9860044e14;            # Gravitational parameter=G*EARTH_MASS - m^3/s^2
C20 = -4.8416685e-4;          # Second degree zonal gravitational coefficient - ND
FLATTENING = 3.33528106e-3;   # Flattening of the Earth (WGS84) - ND
SMAJOR_AXIS = 6378137.;       # Semi-major axis of Earth's ellipsoid (WGS84) - m
GW_CLONG = 0.0;               # Greenwich celestial longitude at start of simulation - rad
RGAS = 287.053;               # Ideal gas constant - J/(K*kg)=N*m/(K*kg)
KBOLTZ = 1.38e-23;            # Boltzmann's constant - Ws/K
R2D = 57.29577951309314;      # Radians to Degrees
D2R = 0.01745329251994;       # Degrees to Radiancs

#numerical constants
PI = 3.141592653589793;       # Circumference of unit diameter circle
EPS = 1.e-10;                 # Machine precision error (type double)
SMALL = 1e-7;                 # Small real number
ILARGE = 9999;                # Large integer number
LARGE = 1e10;                 # Large real number (type double)

#conversion factors
M2F = 3.280834;               # Conversion factor m->ft
M2NMI = 5.399568e-4;          # Conversion factor  m->nm

# WGS84 Constants
J2 = -sqrt(5)*C20;
aa = 6378137.0;
finv = 298.257223563;
ff = 1.0/finv;
bb = aa*( 1.0 - ff );
e2 = 1.0 - ( 1.0 - ff )^2;

wgs_aa = aa;
wgs_finv = finv;
wgs_ff = ff;
wgs_bb = bb;
wgs_e2 = e2;

###########################################################################
## Transformation about Axis 1
function TR1(x)
  Tr1 = [
         1.0    0.0      0.0
         0.0    cos(x) sin(x)
         0.0   -sin(x) cos(x)
  ];

  return Tr1;
end

###########################################################################
## Transformation about Axis 2
function TR2(x)
  Tr2 = [
         cos(x)    0.0      -sin(x)
         0.0       1.0       0.0
         sin(x)    0.0       cos(x)
  ];

  return Tr2;
end

###########################################################################
## Transformation about Axis 3
function TR3(x)
  Tr3 = [
         cos(x)    sin(x)     0.0
         -sin(x)   cos(x)     0.0
         0.0       0.0        1.0
  ];

  return Tr3;
end

###########################################################################
## Transformation from NED to Body
function TR_BN(roll, pitch, heading)

  TRBN = TR1(roll)*TR2(pitch)*TR3(heading);
  return TRBN;

end

###########################################################################
## Transformation from ECEF to NED
function TR_NE(latitude, longitude)
  clat = cos(latitude);
  slat = sin(latitude);
  clon = cos(longitude);
  slon = sin(longitude);

  TRNE = [
          -slat*clon   -slat*slon   clat
          -slon         clon        0.0
          -clat*clon   -clat*slon   -slat
  ];
  return TRNE;
end

###########################################################################
## Transformation from ECI to ECEF
function TR_EI(time)
  TREI = TR3(WEII3*time);
  return TREI;
end

###########################################################################
## Transformation from Body to Wind Skid To Turn
function TR_WB(alpha, beta)
  TRWB = TR3(beta)*TR2(-alpha);
  return TRWB;
end

###########################################################################
## Transformation from Body to Wind Skid To Turn
function TR_VN(gam, azi)
    TRVN = zeros(3,3);
    TRVN[1,1] = cos(gam)*cos(azi);
    TRVN[1,2] = cos(gam)*sin(azi);
    TRVN[1,3] = -sin(gam);
    TRVN[2,1] = -sin(azi);
    TRVN[2,2] = cos(azi);
    TRVN[2,3] = 0.0;
    TRVN[3,1] = sin(gam)*cos(azi);
    TRVN[3,2] = sin(gam)*sin(azi);
    TRVN[3,3] = cos(gam);

  return TRVN;
end

###########################################################################
## Transformation from Body to Wind Skid To Turn
function TR_BV( alfa,  phii)
  ca = cos(alfa);
  sa = sin(alfa);
  cp = cos(phii);
  sp = sin(phii);

  TBV = zeros(3,3);
  TBV[1,1] = ca;
  TBV[1,2] = sa*sp;
  TBV[1,3] = -sa*cp;
  TBV[2,1] = 0.0;
  TBV[2,2] = cp;
  TBV[2,3] = sp;
  TBV[3,1] = sa;
  TBV[3,2] = -ca*sp;
  TBV[3,3] = cp*ca;

  return TBV;
end
###########################################################################
# Atmosphere Function from Zipfel's book
function Atmosphere(alt, vmag)

  # Universal Air Constant
  Rair = 287.058;

  # Atmosphere Parameters
  if( alt < 11000.0 )
    Temp = 288.15 - 0.0065*alt;
    Press = 101325.0*(Temp/288.15)^5.2559;
  else
    Temp = 216.0;
    Press = 22630.0*exp(-0.00015769*(alt - 11000.0));
  end
  rho = Press/(Rair*Temp);
  aa = sqrt(1.4*Rair*Temp);

  # Mach and Dynamic Pressure
  Mach = vmag/aa;
  qbar = 0.5*rho*vmag*vmag;

  return [Press, Temp, rho, aa, Mach, qbar];

end

###########################################################################
function ECEF2LLH(rm_ecef)

  X = rm_ecef[1];
  Y = rm_ecef[2];
  Z = rm_ecef[3];

  # Longitude
  lon = atan(Y,X);

  # Eccentricity
  ee = e2*(aa/bb)^2;

  p = sqrt( X.*X + Y.*Y);
  r = sqrt( p.*p + Z.*Z);
  u = atan( bb.*Z.*( 1 + ee.*bb./r)./(aa.*p));

  # Latitude
  lat = atan( (Z + ee.*bb.*sin(u).^3)./( p - e2.*aa.*cos(u).^3) );

  v = aa./sqrt( 1.0 - e2.*sin(lat).^2 );

  # Altitude
  alt = p.*cos(lat) + Z.*sin(lat) - aa*aa./v;

  return [lat lon alt];
end

###########################################################################
function LLH2ECEF(lat, lon, alt)

  vv = aa/sqrt( 1.0 - e2*sin(lat)*sin(lat) );
  xx = ( vv + alt )*cos(lat)*cos(lon);
  yy = ( vv + alt )*cos(lat)*sin(lon);
  zz = ( vv*( 1.0 - e2 ) + alt)*sin(lat);

  rm_ecef = [xx;yy;zz];
  return rm_ecef;
end

###########################################################################
function WGS84_GRAVITY(rm_ecef)
  rmag = norm(rm_ecef);
  px = rm_ecef[1];
  py = rm_ecef[2];
  pz = rm_ecef[3];
  tmp1 = pz/rmag;

  vec = [
          ( 1.0 + 1.5*J2*(aa/rmag)^2*(1 - 5*tmp1^2) )*px/rmag
          ( 1.0 + 1.5*J2*(aa/rmag)^2*(1 - 5*tmp1^2) )*py/rmag
          ( 1.0 + 1.5*J2*(aa/rmag)^2*(3 - 5*tmp1^2) )*pz/rmag
  ];

  # Raw Gravity Vector
  GRAV_ECEF = -MU/( rmag^2 )*vec

  # Earth Rotation Vector
  wei_ecef = [0.0;0.0;WEII3];

  # Effective Gravity Vector
  grav_ecef = GRAV_ECEF - cross( wei_ecef , cross( wei_ecef, rm_ecef ) );

  return grav_ecef;
end

###########################################################################
function QuatInit(roll, pitch, heading)
  q0 = cos(heading/2)*cos(pitch/2)*cos(roll/2) + sin(heading/2)*sin(pitch/2)*sin(roll/2);
  q1 = cos(heading/2)*cos(pitch/2)*sin(roll/2) - sin(heading/2)*sin(pitch/2)*cos(roll/2);
  q2 = cos(heading/2)*sin(pitch/2)*cos(roll/2) + sin(heading/2)*cos(pitch/2)*sin(roll/2);
  q3 = sin(heading/2)*cos(pitch/2)*cos(roll/2) - cos(heading/2)*sin(pitch/2)*sin(roll/2);

  quat = [q0;q1;q2;q3];

  # Normalization
  quat = quat/norm(quat);

  return quat;
end
###########################################################################
function QuatInit( Tr_bi::Array{Float64,2} )
  t11 = Tr_bi[1,1];
  t12 = Tr_bi[1,2];
  t13 = Tr_bi[1,3];
  t23 = Tr_bi[2,3];
  t33 = Tr_bi[3,3];

  pitch = asin(-t13);
  heading = atan(t12,t11);
  roll = atan(t23,t33);

  q0 = cos(heading/2)*cos(pitch/2)*cos(roll/2) + sin(heading/2)*sin(pitch/2)*sin(roll/2);
  q1 = cos(heading/2)*cos(pitch/2)*sin(roll/2) - sin(heading/2)*sin(pitch/2)*cos(roll/2);
  q2 = cos(heading/2)*sin(pitch/2)*cos(roll/2) + sin(heading/2)*cos(pitch/2)*sin(roll/2);
  q3 = sin(heading/2)*cos(pitch/2)*cos(roll/2) - cos(heading/2)*sin(pitch/2)*sin(roll/2);

  quat = [q0;q1;q2;q3];

  # Normalization
  quat = quat/norm(quat);

  return quat;
end

###########################################################################

function QuatDerivative(quat::Array{Float64,1}, wb_body::Array{Float64,1} )
  pp = wb_body[1];
  qq = wb_body[2];
  rr = wb_body[3];

  lam = 1.0 - norm(quat)^2;

  BB = [
        0   -pp   -qq    -rr
        pp   0     rr    -qq
        qq  -rr    0      pp
        rr   qq   -pp     0
  ];

  kgain = 100.0;
  quatdot = 0.5*BB*quat + kgain*lam*quat;

  return quatdot;
end

###########################################################################

function QuatToDCM(quat)

  q0 = quat[1];
  q1 = quat[2];
  q2 = quat[3];
  q3 = quat[4];

  Tr_bi = zeros(3,3);
  Tr_bi[1,1] = q0^2 + q1^2 - q2^2 - q3^2;
  Tr_bi[2,2] = q0^2 - q1^2 + q2^2 - q3^2;
  Tr_bi[3,3] = q0^2 - q1^2 - q2^2 + q3^2;

  Tr_bi[2,1] = 2.0*(q1*q2 - q0*q3);
  Tr_bi[1,2] = 2.0*(q1*q2 + q0*q3);

  Tr_bi[3,1] = 2.0*(q1*q3 + q0*q2);
  Tr_bi[1,3] = 2.0*(q1*q3 - q0*q2);

  Tr_bi[3,2] = 2.0*(q2*q3 - q0*q1);
  Tr_bi[2,3] = 2.0*(q2*q3 + q0*q1);

  return Tr_bi';
end

###########################################################################
function ComputeEuler( Tr_bi::Array{Float64,2} )

  t11 = Tr_bi[1,1];
  t12 = Tr_bi[1,2];
  t13 = Tr_bi[1,3];
  t23 = Tr_bi[2,3];
  t33 = Tr_bi[3,3];

  pitch = asin(-t13);
  heading = atan(t12,t11);
  roll = atan(t23,t33);

  euler = [roll; pitch; heading];

  return euler;
end

###########################################################################
function Pause()
  print("Program Paused:  Press any key to Continue, or press q to quit: ");
  xx = readline();
  if xx == "q\r\n"
    error("Terminated Program");
  end
  return
end

###########################################################################
function CloseAll()

  for i = 1:500
    close();
  end

  return
end

###########################################################################
function Bound(xx::Float64, xmin::Float64, xmax::Float64)
  yy = xx;

  if (xx > xmax)
    yy = xmax;
  elseif(xx < xmin)
    yy = xmin;
  else
    yy = xx;
  end

  return yy;
end


using CPUTime
using Plots
using DifferentialEquations

f = @ode_def begin
    dx = v
    dv = -4(-D*v + ω^2*l*m*sin(θ))/(-4M + 3m*cos(θ)^2 - 4m) +
    3cos(θ)*(-d + ω + g*l*m*sin(θ))/(l*(-4M + 3m*cos(θ)^2 -4m)) +
    -4/(-4M + 3m*cos(θ)^2 -4m)
    dθ = ω
    dω = 3cos(θ)*(D*v + ω^2*l*m*sin(θ))/(l*(-4M + 3m*cos(θ)^2 -4*m)) +
    -3(M + m)*(-d + ω + g*l*m*sin(θ))/(l^2*(-4M*m + 3m^2*cos(θ)^2 - 4m^2)) +
    3cos(θ)/(l*(-4M + 3m*cos(θ)^2 - 4m))
end M m L l D d g

function do_3_1()
    # パラメータ
    M = 5.0  # 車両質量[kg]
    m = 1.0  # 振子質量[kg]
    L = 1.5  # 振子長さ[m]
    l = L/2
    D = 0.01  # 車両と地面の動摩擦係数
    d = 0.01  # 振子と車両の動摩擦係数
    g = 9.80665  # 重力加速度[m/s²]
    p = [M, m, L, l, D, d, g]

    x0 = [0.0, 0.0, π/6, 0.0]  # 初期値を設定
    tspan = (0.0, 3.0)  # 解く範囲を設定

    prob = ODEProblem(f ,x0 ,tspan, p)
    sol = solve(prob)  # ソルバで解く

    anim = @animate for i in 1:length(sol.t)
        now = round(sol.t[i], digits=1)
        s = "t = " * string(now) * " [s]"
        x, v, θ, ω = sol.u[i]
        plot(
            [x, x+L*cos(-θ+π/2)],
            [0, L*sin(-θ+π/2)],
            #size=(400,400),
            linewidth=5,
            xlims = (-3L, 3L),
            ylims = (-1.1L, 1.1L),
            aspect_ratio = 1,
            legend=false,
            annotate=(0, -1, s),
        )
    end
    gif(anim, "example_3_1_jl.gif", fps = 30)

    return sol, anim

end

@time sol, anim = do_3_1()


@with_kw struct SpikingSynapseParameter
    τpre::SNNFloat = 20ms
    τpost::SNNFloat = 20ms
    Wmax::SNNFloat = 0.01
    ΔApre::SNNFloat = 0.01 * Wmax
    ΔApost::SNNFloat = -ΔApre * τpre / τpost * 1.05
end

@with_kw mutable struct SpikingSynapse
    param::SpikingSynapseParameter = SpikingSynapseParameter()
    rowptr::Vector{SNNInt} # row pointer of sparse W
    colptr::Vector{SNNInt} # column pointer of sparse W
    I::Vector{SNNInt}      # postsynaptic index of W
    J::Vector{SNNInt}      # presynaptic index of W
    index::Vector{SNNInt}  # index mapping: W[index[i]] = Wt[i], Wt = sparse(dense(W)')
    W::Vector{SNNFloat}  # synaptic weight
    tpre::Vector{SNNFloat} = simzeros(W) # presynaptic spiking time
    tpost::Vector{SNNFloat} = simzeros(W) # postsynaptic spiking time
    Apre::Vector{SNNFloat} = simzeros(W) # presynaptic trace
    Apost::Vector{SNNFloat} = simzeros(W) # postsynaptic trace
    fireI::Vector{Bool} # postsynaptic firing
    fireJ::Vector{Bool} # presynaptic firing
    g::Vector{SNNFloat} # postsynaptic conductance
    records::Dict = Dict()
end

function SpikingSynapse(pre, post, sym; σ = 0.0, p = 0.0)
    w = σ * sprand(post.N, pre.N, p)
    rowptr, colptr, I, J, index, W = dsparse(w)
    fireI, fireJ = post.fire, pre.fire
    g = getfield(post, sym)
    SpikingSynapse(;@symdict(rowptr, colptr, I, J, index, W, fireI, fireJ, g)...)
end

function forward!(c::SpikingSynapse, param::SpikingSynapseParameter)
    @unpack colptr, I, W, fireJ, g = c
    @inbounds for j in 1:(length(colptr) - 1)
        if fireJ[j]
            for s in colptr[j]:(colptr[j+1] - 1)
                g[I[s]] += W[s]
            end
        end
    end
end

function plasticity!(c::SpikingSynapse, param::SpikingSynapseParameter, dt::SNNFloat, t::SNNFloat)
    @unpack rowptr, colptr, I, J, index, W, tpre, tpost, Apre, Apost, fireI, fireJ, g = c
    @unpack τpre, τpost, Wmax, ΔApre, ΔApost = param
    @inbounds for j in 1:(length(colptr) - 1)
        if fireJ[j]
            for s in colptr[j]:(colptr[j+1] - 1)
                Apre[s] *= exp32(- (t - tpre[s]) / τpre)
                Apost[s] *= exp32(- (t - tpost[s]) / τpost)
                Apre[s] += ΔApre
                tpre[s] = t
                W[s] = clamp(W[s] + Apost[s], 0f0, Wmax)
            end
        end
    end
    @inbounds for i in 1:(length(rowptr) - 1)
        if fireI[i]
            for st in rowptr[i]:(rowptr[i+1] - 1)
                s = index[st]
                Apre[s] *= exp32(- (t - tpre[s]) / τpre)
                Apost[s] *= exp32(- (t - tpost[s]) / τpost)
                Apost[s] += ΔApost
                tpost[s] = t
                W[s] = clamp(W[s] + Apre[s], 0f0, Wmax)
            end
        end
    end
end


@with_kw struct rtgM2{A,B,C} <: RTGmodel
    model::A = rtgM2_model()
    protein_lookup = get_protein_lookup(model)
    p::C = init_p(model; idx_hill_coefs=[])
    u::B = init_u(model, protein_lookup)
end

"""
This model is modified version of rtgM1.
Major changes:
1. Change input to be michaelis menten
2. Change the translocation to be Michaelis menten
3. Rtg1p and Rtg3p enter nucleus separately
"""
function rtgM2_model()
    rtgM2 = @reaction_network  begin
        #=
        This model is modified version of rtgM1.
        Major changes:
        1. Change input to be michaelis menten
        2. Change the translocation to be Michaelis menten
        3. Rtg1p and Rtg3p enter nucleus separately
        =#
        #=
        Input
        =#
        (1), s→s
        #=
        Modulation Layer
        =#
        (k1md*s*(k1mk+s), kn1md), Rtg2_ina_c ↔ Rtg2_act_c #[1]
        (k2md, kn2md), Rtg2_act_c + Mks ↔ Rtg2Mks_c
        (k3md, kn3md), Bmh + Mks ↔ BmhMks
        #=
        Activation Model:
        =#
        # NLSact: Nuclear Localization Signal (NLS) activation
        (k2n + mm(BmhMks, k1, k1m)), Rtg13_a_c → Rtg13_i_c
        (k2, k2n), Rtg3_i_c ↔ Rtg3_a_c

        # Rtg1Binding: Rtg13 formation
        (k3, kn3), Rtg1_c + Rtg3_a_c ↔ Rtg13_a_c
        (k3, kn3), Rtg1_c + Rtg3_i_c ↔ Rtg13_i_c

        (k3nu, kn3nu), Rtg1_n + Rtg3_a_n ↔ Rtg13_a_n #[3]

        # Translocation
        ( mm(Rtg1_c, kt1, ktd1)  , mm(Rtg1_c, kt1nu, ktd1nu)), Rtg1_c ↔ Rtg1_n #[2]
        ( mm(Rtg3_a_c, kt2, ktd2) , mm(Rtg3_a_n, kt2nu, ktd2nu)), Rtg3_a_c ↔ Rtg3_a_n #[2]

        #(k4, kn4), Rtg13_a_c ↔ Rtg13_a_n # Deleted [2]
    end k1mk k1md k1mk kn1md k2md kn2md k3md kn3md k1 k1m k2 k2n k3 kn3 k4 kn4 kt1 ktd1 kt1nu ktd1nu kt2 ktd2 kt2nu ktd2nu k3nu kn3nu

    return rtgM2
end


using jInv.Mesh
using jInv.Utils
using jInv.ForwardShare
using ShapeReconstructionPaLS.Utils
using ParamLevelSet
using ShapeReconstructionPaLS.ShapeFromSilhouette;
using Statistics
using Distributed
using LinearAlgebra
using SparseArrays
using Test

println("Test SoftMax")
n = 20;
m = 10;
u = rand(20).-0.1;
At = sprand(n,m,4.0/m);
At.nzval[:] .= 1.0;

# d0,Jt = softMaxProjWithSensMat(At,u);
d0,Jt = softMaxProjWithSigmoid(At,u);

du0 = 0.1*randn(size(u));

U = spdiagm(0 => u);
dataMax = maximum(At'*U,dims=2)[:];
dataMax = SigmoidFunc(maximum(At'*U,dims=2)[:]);

r = norm(dataMax .- d0)./norm(d0);
@test r < 0.025
for ii = 1:8
	du = (0.5^ii)*du0;
	ut = u + du;
	# dt, = softMaxProjWithSensMat(At,ut);
	dt, = softMaxProjWithSigmoid(At,ut);
	println("norm(dt-d0): ",norm(dt-d0),", norm(dt - d0 - J0*dm): ",norm(dt - d0 - Jt'*du));
end


function embiggen(arr, increase)
  result = falses(increase*2 .+ size(arr))
  result[[1:n for n=size(arr)]...] = arr
  return result
end

neighbours = [(x, y, z) for x=-1:1, y=-1:1, z=-1:1 if (x,y,z) != (0,0,0)]

function neighbour_count(arr)
  return sum(map(shift -> circshift(arr, shift), neighbours))
end

function evolve(x, n)
  x = embiggen(x, n)
  for it=1:n
    cs = neighbour_count(x)
    x = (cs .== 3) .| (x .& (cs .== 2))
  end
  return x
end

input="##..#.#.\n#####.##\n#######.\n#..#..#.\n#.#...##\n..#....#\n....#..#\n..##.#.."

init = hcat([[x == '#' for x=line] for line=split(input)]...)
init = reshape(BitArray(init), size(init)..., 1)
println(sum(evolve(init, 6)))


module OmegaCoreTestModels

function test_model()
  # Normally distributed random variable with id 1
  x = 1 ~ Normal(0, 1)

  # Normally distributed random variable with id 2 and x as mean
  y = 2 ~ Uniform(x, pw(+, x, 1))
  x_ = 0.1
  y_ = 0.3

  # An omega object -- like a trace
  ω = SimpleΩ(Dict((1,) => x_, (2,) => y_))

  # model -- tuple-valued random variable ω -> (x(ω), y(ω)), becase we want joint pdf
  m = (x, y)ₚ
  (x, y, m)
end

end


a = 5
b = 7
da = 0.1
db = 0.3

dc_max = da + db

println("|(a+b)-(a+da)+(b+db)|: ", abs((a+b)-((a+da)+(b+db))), "<=", dc_max)
println("|(a+b)-(a-da)+(b+db)|: ", abs((a+b)-((a-da)+(b+db))), "<=", dc_max)
println("|(a+b)-(a+da)+(b-db)|: ", abs((a+b)-((a+da)+(b-db))), "<=", dc_max)
println("|(a+b)-(a-da)+(b-db)|: ", abs((a+b)-((a-da)+(b-db))), "<=", dc_max)
println("|(a-b)-(a+da)+(b+db)|: ", abs((a-b)-((a+da)-(b+db))), "<=", dc_max)
println("|(a-b)-(a-da)+(b+db)|: ", abs((a-b)-((a-da)-(b+db))), "<=", dc_max)
println("|(a-b)-(a+da)+(b-db)|: ", abs((a-b)-((a+da)-(b-db))), "<=", dc_max)
println("|(a-b)-(a-da)+(b-db)|: ", abs((a-b)-((a-da)-(b-db))), "<=", dc_max)


#=

Olympic puzzle in Julia ConstraintSolver.jl

Benchmark for Prolog (BProlog)
"""
  File   : olympic.pl
  Author : Neng-Fa ZHOU
  Date   : 1993

  Purpose: solve a puzzle taken from Olympic Arithmetic Contest

   Given ten variables with the following configuration:

               X7   X8   X9   X10

                  X4   X5   X6

                     X2   X3

                        X1

  We already know that X1 is equal to 3 and want to assign each variable
  with a different integer from {1,2,...,10} such that for any three
  variables
                      Xi   Xj

                         Xk
  the following constraint is satisfied:

                    |Xi-Xj| = Xk
"""

Model created by Hakan Kjellerstrand, hakank@gmail.com
See also my Julia page: http://www.hakank.org/julia/

=#

using ConstraintSolver, JuMP
using Cbc, GLPK, Ipopt
using Printf
const CS = ConstraintSolver
include("constraints_utils.jl")

function olympic(print_solutions=true,all_solutions=true)

    cbc_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
    glpk_optimizer = optimizer_with_attributes(GLPK.Optimizer)
    ipopt_optimizer = optimizer_with_attributes(Ipopt.Optimizer)

    model = Model(optimizer_with_attributes(CS.Optimizer,   "all_solutions"=> all_solutions,
                                                            # "all_optimal_solutions"=>all_solutions,
                                                            "logging"=>[],

                                                            "traverse_strategy"=>:BFS,
                                                            # "traverse_strategy"=>:DFS,
                                                            # "traverse_strategy"=>:DBFS,

                                                            # "branch_split"=>:Smallest,
                                                            # "branch_split"=>:Biggest,
                                                            "branch_split"=>:InHalf,

                                                            # https://wikunia.github.io/ConstraintSolver.jl/stable/options/#branch_strategy-(:Auto)
                                                            "branch_strategy" => :IMPS, # default
                                                            # "branch_strategy" => :ABS, # Activity Based Search
                                                            # "activity.decay" => 0.999, # default 0.999
                                                            # "activity.max_probes" => 10, # default, 10
                                                            # "activity.max_confidence_deviation" => 20, # default 20

                                                            # "simplify"=>false,
                                                            # "simplify"=>true, # default

                                                            "time_limit"=>6,

                                                            # "backtrack" => false, # default true
                                                            # "backtrack_sorting" => false, # default true

                                                            # "lp_optimizer" => cbc_optimizer,
                                                            # "lp_optimizer" => glpk_optimizer,
                                                            # "lp_optimizer" => ipopt_optimizer,
                                        ))

    n = 10
    @variable(model, 1 <= x[1:n] <= n, Int)
    x1,x2,x3,x4,x5,x6,x7,x8,x9,x10 = x
    @constraint(model, x in CS.AllDifferentSet())

    @constraint(model,x1 == 3)
    my_abs(model,x2,x3,x1)
    my_abs(model,x4,x5,x2)
    my_abs(model,x5,x6,x3)
    my_abs(model,x7,x8,x4)
    my_abs(model,x8,x9,x5)
    my_abs(model,x9,x10,x6)

    # Solve the problem
    optimize!(model)

    status = JuMP.termination_status(model)
    # println("status:$status")
    if status == MOI.OPTIMAL
        num_sols = MOI.get(model, MOI.ResultCount())
        println("num_sols:$num_sols\n")
        if print_solutions
            for sol in 1:num_sols
                println("solution #$sol")
                xx = convert.(Integer,JuMP.value.(x; result=sol))
                # println("x:$xx")
                @printf "%d  %d  %d %d\n"  xx[7] xx[8] xx[9] xx[10]
                @printf "  %d  %d  %d\n" xx[4] xx[5] xx[6]
                @printf "    %d  %d\n" xx[2] xx[3]
                @printf "      %d\n" xx[1]
                println()

            end
        end
    else
        println("status:$status")
    end

    return status
end

@time olympic()


# Load modules
using Plots, LaTeXStrings
pyplot()
using ThinFilmsTools
using Optim

##
function glass_reflectance(beam, incident, emergent)
    # Create a measured spectrum of reflection
    layers2 = [ LayerTMMO(incident),
                LayerTMMO(RIdb.glass(beam.λ./1e3); d=250.),
                LayerTMMO(emergent) ]
    sol = tmm_optics(beam, layers2)
    return vec(beam.p.*sol.Spectra.Rp .+ (1.0 - beam.p).*sol.Spectra.Rs)
end
##

# Wavelength range [nm]
λ = 250:900
# Angle of incidence [degrees]
θ = [5.]
# Polarisation (1.0 = p, 0.0 = s, between 0.0 and 1.0 = average)
pol = 0.5
beam = PlaneWave(λ, θ; p=pol)

# Refractive indices of incident (0) and substrate (2)
incident = RIdb.air(beam.λ)
emergent = RIdb.silicon(beam.λ)

# Define the RI model to use
layers = [
    LayerTMMO(incident),
    ModelFit(:sellmeier),
    LayerTMMO(emergent),
]

# Create reflectance spectrum to fit
Rexp = glass_reflectance(beam, incident, emergent)
# plot(beam.λ, Rexp_norm)
# gui()

options = Optim.Options(
    g_abstol=1e-8, g_reltol=1e-8, iterations=10^5, show_trace=true, store_trace=true,
);

seed = vcat(280.0, [1.0, 0.23, 1.0, 6e-3, 2e-2, 1e-2].* (rand(6).*0.2.+1.0))

solOptim = fit_tmm_optics(
    Reflectance(Rexp), [seed], beam, layers;
    options=options, alg=SAMIN(), lb=0.5.*seed, ub=1.5.*seed,
)

plot(FitSpectrum(), solOptim.beam.λ, solOptim.spectrumExp, solOptim.spectrumFit)
gui()


"""
## formm!

This subroutine forms the derivatives of the invariants with respect to
stress in 2- or 3-d. See equation 6.25.

### Function
```julia
formm!(stress, m1, m2, m3)
```

### Arguments
```julia
* stress::Vector{Float64}    : Stress vector, see eq 6.25
* m1::Matrix{Float64}        : m1 matrix
* m2::Matrix{Float64}        : m2 matrix
* m3::Matrix{Float64}        : m3 matrix
```

### Return values
```julia
* m1::Matrix{Float64}        : m1 matrix
* m2::Matrix{Float64}        : m2 matrix
* m3::Matrix{Float64}        : m3 matrix
```
"""
function formm!(stress::Vector{Float64}, m1::Matrix{Float64},
  m2::Matrix{Float64}, m3::Matrix{Float64})
  #
  # This subroutine forms the derivatives of the invariants with respect to
  # stress in 2- or 3-d.
  #
  # Updates: m1, m2, m3
 nst = size(stress, 1)
 if nst == 4
   sx = stress[1]
   sy = stress[2]
   txy = stress[3]
   sz = stress[4]
   dx = (2.0*sx-sy-sz)/3.0
   dy = (2.0*sy-sz-sx)/3.0
   dz = (2.0*sz-sx-sy)/3.0
   sigm = (sx+sy+sz)/3.0
   m1[1,1:2] .= 1.0
   m1[2,1:2] .= 1.0
   m1[4,1:2] .= 1.0
   m1[1,4] = 1.0
   m1[4,4] = 1.0
   m1[2,4] = 1.0
   m1[:,:] = m1[:,:]/9.0/sigm
   m2[1,1] = 2.0/3.0
   m2[2,2] = 2.0/3.0
   m2[4,4] =  2.0/3.0
   m2[2,4] = -1.0/3.0
   m2[4,2] = -1.0/3.0
   m2[1,2] = -1.0/3.0
   m2[2,1] = -1.0/3.0
   m2[1,4] = -1.0/3.0
   m2[4,1] = -1.0/3.0
   m2[3,3] = 2.0
   m3[3,3] = -dz
   m3[1:2,3] .= txy/3.0
   m3[3,1:2] .= txy/3.0
   m3[3,4] = -2.0*txy/3.0
   m3[4,3] = -2.0*txy/3.0
   m3[1,1] = dx/3.0
   m3[2,4] = dx/3.0
   m3[4,2] = dx/3.0
   m3[2,2] = dy/3.0
   m3[1,4] = dy/3.0
   m3[4,1] = dy/3.0
   m3[4,4] = dz/3.0
   m3[1,2] = dz/3.0
   m3[2,1] = dz/3.0
 elseif nst == 6
   sx = stress[1]
   sy = stress[2]
   sz = stress[3]
   txy = stress[4]
   tyz = stress[5]
   tzx = stress[6]
   sigm = (sx+sy+sz)/3.0
   dx = sx-sigm
   dy = sy-sigm
   dz = sz-sigm
   m1[1:3,1:3] = 1.0/(3.0*sigm)
   for i in 1:3
     m2[i,i] = 2.0
     m2[i+3,i+3] = 6.0
   end
   m2[1,2] = -1.0
   m2[1,3] = -1.0
   m2[2,3] = -1.0
   m3[1,1] = dx
   m3[1,2] = dz
   m3[1,3] = dy
   m3[1,4] = txy
   m3[1,5] = -2.0*tyz
   m3[1,6] = tzx
   m3[2,2] = dy
   m3[2,3] = dx
   m3[2,4] = txy
   m3[2,5] = tyz
   m3[2,6] = -2.0*tzx
   m3[3,3] = dz
   m3[3,4] = -2.0*txy
   m3[3,5] = tyz
   m3[3,6] = tzx
   m3[4,4] = -3.0*dz
   m3[4,5] = 3.0*tzx
   m3[4,6] = 3.0*tyz
   m3[5,5] = -3.0*dx
   m3[5,6] = 3.0*txy
   m3[6,6] = -3.0*dy
   for i in 1:6
     for j in i+1:6
       m1[j,i] = m1[i,j]
       m2[j,i] = m2[i,j]
       m3[j,i] = m3[i,j]
     end
   end
   m1[:,:] = m1/3.0
   m2[:,:] = m2/3.0
   m3[:,:] = m3/3.0
 else
   println("nst size not recognised in formm.")
 end
 (m1, m2, m3)
end