using Printf, Test

include("../../hybmc/termstructures/YieldCurve.jl")
include("../../hybmc/models/HullWhiteModel.jl")
include("../../hybmc/simulations/McSimulation.jl")
include("../../hybmc/simulations/Payoffs.jl")

function setup()
    curve = YieldCurve(0.03)
    mean = 0.03
    times = [ 1.0,    2.0,    5.0,    10.0     ]
    vols  = [ 0.0060, 0.0070, 0.0080,  0.0100  ]
    model = HullWhiteModel(curve,mean,times,vols)
    return model
end

@testset "test_zeroBondPrice" begin
    model = setup()
    obsTime = 10.0
    matTime = 20.0
    states  = [ -0.10, -0.05, 0.00, 0.03, 0.07, 0.12 ]
    refResult = [  # reference results checked against QuantLib
        1.7178994273756056,
        1.1153102854629025,
        0.7240918839816156,
        0.5587692049384843,
        0.39550396436404667,
        0.25677267968500767]
    for (x,res) in zip(states,refResult)
        pv = zeroBondPrice(model,obsTime,matTime,x)
        @test isapprox(pv,res,atol=1.0e-16, rtol=0.0)
        #println(pv)
    end
end

@testset "test_couponBondOption" begin
    model = setup()
    excTime = 10.0
    payTimes  = [ 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 20.0 ]
    cashFlows = cat(0.025*ones(10), [1.0], dims=1) # bond cash flows
    callOrPut = 1.0 # call
    strikes   = [ 0.8, 0.9, 1.0, 1.1, 1.2 ]
    refResult = [  # reference results checked against QuantLib
    0.12572604970665557,
    0.07466919528750032,
    0.039969719617809936,
    0.019503405494683164,
    0.008804423283331253]
    for (strike,res) in zip(strikes,refResult)
        pv = couponBondOption(model,excTime,payTimes,cashFlows,strike,callOrPut)
        @test isapprox(pv,res,atol=1.0e-8, rtol=0.0)
        #println(pv)
    end
    return nothing
end

@testset "test_monteCarloSimulation" begin
    model = setup()
    times = Array(range(0.0, stop=10.0, length=11))
    nPaths = 2^11
    seed = 1234
    # risk-neutral simulation
    mcSim = McSimulation(model,times,nPaths,seed)
    discZeroBonds = [
        (1.0 / numeraire(model,times[i],mcSim.X[:,i,j]))
        for i = 1:size(times)[1], j = 1:nPaths ]
    mcZeroBondsRiskNeutral = mean(discZeroBonds, dims=2)
    zeroBonds = [ discount(model.yieldCurve,t) for t in times ]
    @printf("  T     ZeroRate    RiskNeutral\n")
    for k = 2:size(times)[1]
        t           = times[k]
        zeroRate    = -log(zeroBonds[k])/t
        riskNeutral = -log(mcZeroBondsRiskNeutral[k])/t
        @printf(" %4.1f   %8.5lf    %8.5lf\n", t, zeroRate, riskNeutral)
        @test isapprox(zeroRate,riskNeutral,atol=1.0e-8, rtol=2.0e-2)
    end
    return nothing
end

@testset "test_liborPayoff" begin
    model = setup()
    # Libor Rate payoff
    liborTimes = Array(range(0.0, stop=10.0, length=11))
    dT0 = 2.0 / 365
    dT1 = 0.5
    cfs = [ Pay(LiborRate(t,t+dT0,t+dT0+dT1),t+dT0+dT1) for t in liborTimes]
    times = observationTimes(cfs)
    #
    nPaths = 2^11
    seed = 4321
    # risk-neutral simulation
    mcSim = McSimulation(model,times,nPaths,seed)
    cfPVs = discountedAt(cfs, mcSim)
    cfPVs = mean(cfPVs, dims=2)
    discounts0 = [ discount(mcSim.model.yieldCurve,t+dT0) for t in liborTimes ]
    discounts1 = [ discount(mcSim.model.yieldCurve,t+dT0+dT1) for t in liborTimes ]
    liborsCv = (discounts0./discounts1 .- 1.0)./dT1
    liborsMC = cfPVs ./ discounts1
    println("  T     LiborRate  MnteCarlo")
    for (k,t) in enumerate(liborTimes)
        @printf(" %4.1f   %7.5f     %7.5f\n", t, liborsCv[k], liborsMC[k])
    end
end


################################################################
#
# representation of words as vector of pointers to generators
#
################################################################

@inline Base.push!(pw::Word, i) = push!(pw.gens_ptrs, i)
@inline Base.getindex(pw::Word, n) = pw.gens_ptrs[n]
@inline Base.setindex!(pw::Word, val, n) = pw.gens_ptrs[n] = val

@inline Base.iterate(pw::Word) = iterate(pw.gens_ptrs)
@inline Base.iterate(pw::Word, s) = iterate(pw.gens_ptrs, s)
@inline Base.length(pw::Word) = length(pw.gens_ptrs)
@inline Base.eltype(::Word{I}) where I = I
@inline Base.empty!(w::Word) = empty!(w.gens_ptrs)

@inline function Base.:(==)(pw1::Word, pw2::Word)
    return pw1.gens_ptrs == pw2.gens_ptrs
end

Base.hash(pw::Word, h::UInt) = hash(Word, hash(pw.gens_ptrs, h))

@doc doc"""
    Word(orb, gens_inv, pt[, op=^])
Return a `Word` `w` consising of pointers to inverses of generators `gens_inv`.

The group element `r` such that `first(orb)^r = pt` can be reconstructed via
`prod(gens_inv[i] for i in reverse(w))` e.g. if
 * `g = gens[1]*gens[3]*gens[2]` and
 * `δ = first(orb)^g`
then the result _may be_ `Word([2,3,1])`.
"""
function Word(gens_inv::AbstractVector{<:GroupElement}, orb::AbstractOrbit{T, I}, pt, op=^) where {T, I<:Integer}
    return append!(Word(I[]), gens_inv, orb, pt, op)
end

function Base.append!(w::Word, gens_inv, orb, pt, op=^)
    γ = pt
    while γ ≠ first(orb)
        idx = orb[γ]
        push!(w, idx)
        γ = op(γ, gens_inv[idx]) #assume that this is already the inverse!
    end
    return w
end

@doc doc"""
    (w::Word)(gens[, init])
Computes the evaluation of group word `w` as a group element in generators `gens`.

Optional `init` element is by default the group identity.
"""
@inline function (pw::Word)(gens::AbstractVector{<:GroupElement}, init=one(first(gens)))
    res = init
    @inbounds for i in pw
        res = GroupsCore.mul!(res, res, gens[i])
    end
    return res
end

@doc doc"""
    representative(orb, gens, pt[, ^])
Return a representative of the coset of `Stab(orb)` which takes `first(orb)`
to `pt` i.e. an element `g` of `⟨gens⟩` such that `first(orb)^g = pt`.
"""
function representative(gens::AbstractVector{<:GroupElement}, orb::AbstractOrbit{I, <:Integer}, pt::I, op=^) where I<:Integer
    gens_inv = inv.(gens)
    word = Word(gens_inv, orb, pt, op)
    g = word(gens_inv)
    return inv(g)
end


include(joinpath(dirname(@__FILE__), "initialization.jl"))
include(joinpath(dirname(@__FILE__), "updateP!.jl"))
include(joinpath(dirname(@__FILE__), "updateD!.jl"))
include(joinpath(dirname(@__FILE__), "updateA!.jl"))

function TrainDPL(Data::Matrix{Float64}, Label::Matrix{Int64}, DictSize::Int64, τ::Float64, λ::Float64, γ::Float64, DEMO=false)
    # This is the DPL training function

    # Initialize D and P, compute the inverse matrix used in Eq. (10), update A for one time
    DataMat, D, P, DataInvMat, A = initialization(Data, Label, DictSize, τ, λ, γ, DEMO)

    # Alternatively update P, D and A
    for i = 1:20
        updateP!(P, A, DataInvMat, DataMat, τ)
        updateD!(D, A, DataMat)
        updateA!(A, D, DataMat, P, τ, DictSize)
        if DEMO
            println("Training ... iteration $i finished.")
        end
    end

    EncoderMat = zeros(size(P[1], 1) * size(P, 1), size(P[1], 2))
    # Reorganize the P matrix to make the classification fast
    for i = 1:size(P, 1)
        @inbounds EncoderMat[(i - 1)*DictSize+1 : i*DictSize, :] = P[i]
    end

    D, EncoderMat
end


equalto(x) = y -> y == x

array = rand(Bool, (300, 400, 500))
indices = CartesianIndices(array)

for fn in [equalto(true), equalto(false)]
    tracker = EulerTracker(array, fn)

    for i in 1:2000
        idx = rand(indices)
        tracker[idx] = true - tracker[idx]
    end

    @test euler_characteristic(tracker) == BitArray(tracker) .|> fn |> euler_characteristic
end


using Test
using LibSymspg

include("test_api.jl")

@testset "Cell standardize(export)" begin
    latt = [4.0 0.0 0.0;
            0.0 4.0 0.0;
            0.0 0.0 4.0]
    positions = [0. 0. 0.; 0.5 0.5 0.5]
    types = [1, 1]
    new_latt, new_positions, new_types = find_primitive(latt, positions, types, 1e-5)
    # test arguments not modified
    @test latt ≈ [4.0 0.0 0.0; 0.0 4.0 0.0; 0.0 0.0 4.0]
    @test positions ≈ [0. 0. 0.; 0.5 0.5 0.5]
    @test types == [1, 1]

    @test new_latt ≈ [-2.0 2.0 2.0; 2.0 -2.0 2.0; 2.0 2.0 -2.0]
    @show new_positions
    @test new_positions ≈ [0.0 0.0 0.0]
    @test new_types == [1]

    latt = [-2.0 2.0 2.0; 2.0 -2.0 2.0; 2.0 2.0 -2.0]
    positions = Array{Float64, 2}([0.0 0.0 0.0])
    types = [1]
    latt, positions, types = refine_cell(latt, positions, types, 1e-5)
    @test latt ≈ [4.0 0.0 0.0; 0.0 4.0 0.0; 0.0 0.0 4.0]
    @test positions ≈ [0. 0. 0.; 0.5 0.5 0.5]
    @test types == [1, 1]
end

@testset "lattice reduce(export)" begin
    latt = [4.0 20.0 0.0; 0.0 2.0 0.0; 0.0 0.0 12.0]
    niggli_reduce!(latt, 1e-3)
    @test latt ≈ [0.0 -2.0 0.0; 4.0 0.0 0.0; 0.0 0.0 12.0]

    latt = [4.0 20.0 0.0; 0.0 2.0 0.0; 0.0 0.0 12.0]
    delaunay_reduce!(latt, 1e-3)
    @test latt ≈ [0.0 2.0 0.0; -4.0 -0.0 0.0; -0.0 -0.0 12.0]
end

# @testset "reciprocal mesh(export)" begin
#     latt = [5.0 0.0 5.0; 0.0 5.0 5.0; 5.0 5.0 0.0]
#     positions = Array{Float64, 2}([0.0 0.25; 0.0 0.25; 0.0 0.25])
#     types = [1, 1]
#     na = 2
#     mesh = [3,3,3]
#     is_shift = [true,true,true]
#     nir, grid_address, ir_mapping_table =
#         ir_reciprocal_mesh(mesh, is_shift, true, latt, positions, types, na, 1e-5)
#     @test nir == 4
# end

@testset "symmetry(export)" begin
    latt = [4.0 0.0 0.0; 2.0 3.4641 0.0; 0.0 0.0 12.0]
    positions = [0.0 0.0 0.0; 1/3 1/3 1/3]
    types = [1, 1]
    num_atom = 2
    @test get_spacegroup(latt, positions, types, num_atom) == ("P-3m1", 164)

    rots, trans = get_symmetry(latt, positions, types, 1e-3)
    @test length(rots) == length(trans) == 12
    @test size(rots[1]) == (3, 3)
    @test size(trans[1]) == (3,)
end


using LinearAlgebra

export  ABtDecomposition, toCartesian, rank, size, *, getindex, ACA

struct ABtDecomposition{T} <: AbstractArray{T,2}
    A::Array{T,2}
    Bt::Array{T,2}
end

# required in order to inherit from AbstractArray
function ABtDecomposition(A::Array{T,2},Bt::Array{U,2}) where{T<:Number,U<:Number}
    if size(A,2) != size(Bt,1)
        throw(DomainError("size(A,2) != size(Bt,1). The input arrays should be compatible for the direct matrix-matrix multiplication."))
    end
    cT = promote_type(T,U)
    return ABtDecomposition{cT}(
        T == cT ? A  : convert(Array{cT,2},A),
        U == cT ? Bt : convert(Array{cT,2},Bt)
    )
end

# get cartesian index
function toCartesian(i,M,N)
    return ((i-1)%M + 1, i÷N + 1)
end

LinearAlgebra.:rank(A::ABtDecomposition) = size(A.A,2)

Base.size(ABt::ABtDecomposition) = (size(ABt.A,1),size(ABt.Bt,2))
Base.size(ABt::ABtDecomposition,i) = begin
    if i == 1
        return size(ABt.A,1)
    elseif i == 2
        return size(ABt.Bt,2)
    else
        throw(ArgumentError("Invalid dimension. Should be 1 or 2."))
    end
end

Base.:*(A::ABtDecomposition{T},B::Array{S}) where {T,S} = A.A*(A.Bt*B)
Base.:*(A::Array{T},B::ABtDecomposition{S}) where {T,S} = (A*B.A)*B.Bt
Base.:*(A::ABtDecomposition,B::ABtDecomposition) = ABtDecomposition(A.A,(A.Bt*B.A)*B.Bt)

Base.getindex(A::ABtDecomposition,i::Int) = sum(
    A.A[toCartesian(i,size(A.A,1),size(A.A,2))[1],:] .*
    A.Bt[:,toCartesian(i,size(A.Bt,1),size(A.Bt,2))[2]]
)
Base.getindex(A::ABtDecomposition,i::Int,j::Int) = sum(A.A[i,:] .* A.Bt[:,j])

function ACA(I, J, fct::Function, tol::T) where {T<:Real}
    #= Adaptive Cross Approximation algorithm for the
        low-rank approximation of full matrices. =#
    # functions returning the rows or the columns of the matrix
    row(i) = fct(I[i],J)
    col(j) = fct(I,J[j])

    Nr = length(I)
    Nc = length(J)
    # pivot lists
    Ir = collect(1:Nr)
    Ic = collect(1:Nc)

    # first row
    k  = 1
    Bt = reshape(row(Ir[k]),(1,Nc))
    deleteat!(Ir,k)

    k = argmax(abs.(Bt[Ic]))
    c = Ic[k]
    β = Bt[c]
    deleteat!(Ic,k)

    A = col(c)/β
    A = reshape(A,(Nr,1))

    aa = real(dot(A,A)); bb = real(dot(Bt,Bt)); res = aa*bb
    ε = sqrt(aa)*sqrt(bb)/sqrt(res)

    anp1 = A[:,1:1]

    flag = true
    n    = 1
    while ε > tol
        if n*(Nr+Nc) > Nr*Nc
            flag = false
            break
        end

        k = argmax(abs.(anp1[Ir]))
        r = Ir[k]
        deleteat!(Ir,k)

        bnp1 = reshape(row(r),(1,Nc))
        u    = A[r:r,:]
        bnp1 .-= u*Bt

        k = argmax(abs.(bnp1[Ic]))
        c = Ic[k]
        β = bnp1[c]
        deleteat!(Ic,k)

        anp1 = reshape(col(c),(Nr,1))
        v    = Bt[:,c:c]
        anp1 .= (anp1-A*v)/β

        aa = real(dot(anp1,anp1)); bb = real(dot(bnp1,bnp1))
        u = A'*anp1
        v = Bt*bnp1'
        ab = real(dot(u,v))

        res += 2*ab+aa*bb
        ε = sqrt(aa)*sqrt(bb)/sqrt(res)

        A  = hcat(A, anp1)
        Bt = vcat(Bt,bnp1)

        n += 1
    end
    return ABtDecomposition(A, Bt), flag
end


######################
##### ROSENBROOK #####
######################
function rosenbrook1()
    model = Model()
    @variable(model, x)
    @variable(model, y)
    @NLobjective(model, Min, (2.0 - x)^2 + 100 * (y - x^2)^2)
    return model
end

function test_rosenbrook1(solver)
    @testset "test_rosenbrook1" begin
        model = rosenbrook1()
        setsolver(model,solver)
        @test_broken solve(model) == :Optimal
    end
end

function rosenbrook2()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y >= 0.0)
    @NLobjective(model, Min, (2.0 - x)^2 + 100 * (y - x^2)^2)
    @constraint(model, x + y >= 0.1)
    return model
end

function test_rosenbrook2(solver)
    @testset "test_rosenbrook2" begin
        model = rosenbrook2()
        setsolver(model,solver)
        @test solve(model) == :Optimal
        check_rosenbrook(model)
    end
end

function rosenbrook3()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y >= 0.0)
    @NLobjective(model, Min, (2.0 - x)^2 + 100 * (y - x^2)^2)
    @constraint(model, x^2 + y^2 >= 0.5)
    return model
end

function test_rosenbrook3(solver)
    @testset "test_rosenbrook3" begin
        model = rosenbrook3()
        setsolver(model,solver)
        @test solve(model) == :Optimal
        check_rosenbrook(model)
    end
end

function rosenbrook4()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y >= 0.0)
    @NLobjective(model, Min, (2.0 - x)^2 + 100 * (y - x^2)^2)
    return model
end

function test_rosenbrook4(solver)
    @testset "test_rosenbrook4" begin
        model = rosenbrook4()
        setsolver(model,solver)
        @test_broken solve(model) == :Optimal
    end
end

function check_rosenbrook(model)
    tol = 1e-3
    @test abs(getvalue(model[:x]) - 2.0) < tol
    @test abs(getvalue(model[:y]) - 4.0) < tol
end

########################
##### FEASIBLE LPS #####
########################

function toy_lp1()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y >= 0.0)
    @NLobjective(model, Min, -x - 100 * y)
    @constraint(model, x + y <= 1.0)
    return model
end

function check_toy_lp1(model)
    tol = 1e-3
    @test abs(getvalue(model[:x]) - 0.0) < tol
    @test abs(getvalue(model[:y]) - 1.0) < tol
end

function test_toy_lp1(solver)
    model = toy_lp1()
    setsolver(model,solver)
    status = solve(model)
    @test status == :Optimal
    check_toy_lp1(model)
end

function toy_lp2()
    model = Model()
    @variable(model, 0.0 <= x <= 1.0)
    @variable(model, 0.0 <= y <= 1.0)
    @NLobjective(model, Min, -x - 100 * y)
    @constraint(model, x + y <= 2.0)
    return model
end

function check_toy_lp2(model)
    tol = 1e-3
    @test abs(getvalue(model[:x]) - 1.0) < tol
    @test abs(getvalue(model[:y]) - 1.0) < tol
end

function test_toy_lp2(solver)
    model = toy_lp2()
    setsolver(model,solver)
    status = solve(model)
    @test status == :Optimal
    check_toy_lp2(model)
end

function toy_lp3()
    model = Model()
    @variable(model, 0.0 <= x <= 1.0)
    @variable(model, 0.0 <= y <= 1.0)
    @NLobjective(model, Min, x)
    @constraint(model, 1.0 <= x + y <= 2.0)
    return model
end

function check_toy_lp3(model)
    tol = 1e-3
    @test abs(getvalue(model[:x]) - 0.0) < tol
    @test abs(getvalue(model[:y]) - 1.0) < tol
end

function test_toy_lp3(solver)
    model = toy_lp3()
    setsolver(model,solver)
    status = solve(model)
    @test status == :Optimal
    check_toy_lp3(model)
end

function check_toy_lp4(model)
    tol = 1e-3
    @test abs(getvalue(model[:x]) - 0.0) < tol
    @test abs(getvalue(model[:y]) - 1.0) < tol
end

function toy_lp4()
    model = Model()
    @variable(model, x, lowerbound=0.0, upperbound=1.0)
    @variable(model, y, lowerbound=0.0, upperbound=1.0)
    @NLobjective(model, Min, x)
    @constraint(model, 1.0 <= x + y <= 2.0)
    return model
end

function check_toy_lp4(model)
    tol = 1e-3
    @test abs(getvalue(model[:x]) - 0.0) < tol
    @test abs(getvalue(model[:y]) - 1.0) < tol
end

function test_toy_lp4(solver)
    model = toy_lp4()
    setsolver(model,solver)
    status = solve(model)
    @test status == :Optimal
    check_toy_lp4(model)
end

function toy_lp5()
    model = Model()
    @variable(model, x, lowerbound=0.0, upperbound=1.0)
    @variable(model, y, lowerbound=0.0, upperbound=1.0)
    @NLobjective(model, Min, x)
    @constraint(model, x + y == 1.0)
    @constraint(model, x * 32.5 + y * 32.5 == 32.5)
    @constraint(model, 3.0 * x + 3.0 * y <= 3.0)
    return model
end

function test_toy_lp5(solver)
    model = toy_lp5()
    setsolver(model,solver)
    status = solve(model)
    @test status == :Optimal
    check_toy_lp4(model)
end

function toy_lp6()
    model = Model()
    @variable(model, x, lowerbound=0.0, upperbound=1.0)
    @variable(model, y, lowerbound=0.0, upperbound=1.0)
    @NLobjective(model, Min, x)
    @constraint(model, x + y == 1.0)
    @constraint(model, x * 5.5 + y * 5.5 == 5.5)
    return model
end

function test_toy_lp6(solver)
    model = toy_lp6()
    setsolver(model,solver)
    status = solve(model)
    @test status == :Optimal
    check_toy_lp4(model)
end

function toy_lp7()
    model = Model()
    @variable(model, x, lowerbound=0.0, upperbound=1.0)
    @variable(model, y, lowerbound=0.0, upperbound=1.0)
    @NLobjective(model, Min, x)
    @constraint(model, 2.0 * x + y == 1.0)
    return model
end

function test_toy_lp7(solver)
    model = toy_lp7()
    setsolver(model,solver)
    status = solve(model)
    @test status == :Optimal
    check_toy_lp4(model)
end

function toy_lp8()
    model = Model()
    @variable(model, x, lowerbound=0.0, upperbound=1.0)
    @variable(model, y, lowerbound=0.0, upperbound=1.0)
    @NLobjective(model, Min, x)
    @constraint(model, x + y >= 1.0)
    @constraint(model, x * 5.5 + y * 5.5 <= 5.5)
    return model
end

function test_toy_lp8(solver)
    model = toy_lp8()
    setsolver(model,solver)
    status = solve(model)
    @test status == :Optimal
    check_toy_lp4(model)
end

##########################
##### INFEASIBLE LPS #####
##########################

function toy_lp_inf1()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y >= 0.0)
    @NLobjective(model, Min, x + 100 * y)
    @constraint(model, x + 2 * y <= -1.0)
    return model
end

function toy_lp_inf2()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y >= 0.0)
    @NLobjective(model, Min, x + 100 * y)
    @constraint(model, x + 2 * y <= 2.0)
    @constraint(model, x + 2 * y >= 4.0)
    return model
end

#######################
##### CONVEX NLPS #####
#######################

function circle1()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y >= 0.0)
    @NLobjective(model, Min, x + 100 * y)
    @NLconstraint(model, x^2 + y^2 <= 1.0)
    @NLconstraint(model, (x-2.0)^2 + y^2 <= 1.0)
    return model
end

function check_circle1(model)
    tol = 1e-3
    @test abs(getvalue(model[:x]) - 1.0) < tol
    @test abs(getvalue(model[:y]) - 0.0) < tol
end

function circle2()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y >= 0.0)
    @NLobjective(model, Min, x^3 + y^3)
    @NLconstraint(model, x^2 + y^2 <= 1.0)
    return model
end

function check_circle2(model)
    tol = 1e-2
    @test abs(getvalue(model[:x]) - 0.0) < tol
    @test abs(getvalue(model[:y]) - 0.0) < tol
end

function quad_opt()
    model = Model()
    @variable(model, x)
    @variable(model, y)
    @NLobjective(model, Min, y)
    @NLconstraint(model, x^2 <= y)
    return model
end

function check_quad_opt(model)
    tol = 1e-2
    @test abs(getvalue(model[:x]) - 0.0) < tol
    @test abs(getvalue(model[:y]) - 0.0) < tol
end

##########################
##### NONCONVEX NLPS #####
##########################
function circle_nc1()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y >= 0.0)
    @NLobjective(model, Min, x + 100 * y)
    @NLconstraint(model, x^2 + y^2 == 1.0)
    @NLconstraint(model, (x-2.0)^2 + y^2 == 1.0)
    return model
end

function check_circle_nc1(model)
    tol = 1e-3
    @test abs(getvalue(model[:x]) - 1.0) < tol
    @test abs(getvalue(model[:y]) - 0.0) < tol
end

function circle_nc2()
    model = Model()
    @variable(model, x, start = 1.0)
    @variable(model, y, start = 1.0)
    @NLobjective(model, Min, x)
    @NLconstraint(model, x^2 + y^2 == 1.0)
    return model
end

function check_circle_nc2(model)
    tol = 1e-3
    @test abs(getvalue(model[:x]) + 1.0) < tol
    @test abs(getvalue(model[:y]) - 0.0) < tol
end

function circle_nc_inf1()
    model = Model()
    @variable(model, x, start = 1.0)
    @variable(model, y, start = 1.0)
    @NLobjective(model, Min, x)
    @NLconstraint(model, x^2 + y^2 == 1.0)
    @NLconstraint(model, x^2 + 2 * y^2 == 4.0)
    return model
end

##############################
##### UNBOUNDED PROBLEMS #####
##############################

function lp_unbd()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y)
    @NLobjective(model, Min, -x)
    @NLconstraint(model, x - y <= 1.0)
    return model
end

function circle_nc_unbd()
    model = Model()
    @variable(model, x)
    @variable(model, y)
    @NLobjective(model, Min, x + 0.1 * y)
    @NLconstraint(model, x^2 + y^2 >= 1.0)
    return model
end

function quad_unbd()
    model = Model()
    @variable(model, x)
    @variable(model, y)
    @NLobjective(model, Min, x)
    @NLconstraint(model, x^2 <= y)
    return model
end

#####################################
##### UNBOUNDED FEASIBLE REGION #####
#####################################

function unbd_feas()
    model = Model()
    @variable(model, x >= 0.0)
    @variable(model, y >= 0.0)
    @variable(model, z >= 0.0)
    @NLobjective(model, Min, y)
    @NLconstraint(model, x^2 <= y)
    @NLconstraint(model, z >= 0.0)
    return model
end

function test_unbd_feas(solver)
    println("test_unbd_feas")
    model = unbd_feas()
    setsolver(model,solver)
    status = solve(model)
    @test status == :Optimal
    @test getvalue(model[:z]) < 1e5
    @show getvalue(model[:z])
end

###########################
##### STARTING POINTS #####
###########################

function starting_point_prob(start::Float64)
    model = Model()
    @variable(model, x, start = start)
    @NLobjective(model, Min, -x^2)
    @NLconstraint(model, -1.0 <= x <= 1.0)
    return model
end

function test_starting_point(solver,starting_point::Float64)
    if starting_point == 0.0
        warn("don't select this as a starting point")
    end
    model = starting_point_prob(starting_point)
    setsolver(model,solver)
    status = solve(model)
    @test status == :Optimal
    if sign(starting_point) < 0.0
        @test abs(getvalue(model[:x]) - 1.0) < 1e-4
    else
        @test abs(getvalue(model[:x]) + 1.0) < 1e-4
    end
end


@doc doc"""
    ProductManifold{TM<:Tuple, TRanges<:Tuple, TSizes<:Tuple} <: Manifold

Product manifold $M_1 \times M_2 \times \dots \times M_n$ with product geometry.
`TRanges` and `TSizes` statically define the relationship between representation
of the product manifold and representations of point, tangent vectors
and cotangent vectors of respective manifolds.

# Constructor

    ProductManifold(M_1, M_2, ..., M_n)

generates the product manifold $M_1 \times M_2 \times \dots \times M_n$.
Alternatively, the same manifold can be contructed using the `×` operator:
`M_1 × M_2 × M_3`.
"""
struct ProductManifold{TM<:Tuple} <: Manifold
    manifolds::TM
end

ProductManifold(manifolds::Manifold...) = ProductManifold{typeof(manifolds)}(manifolds)

function cross(M1::Manifold, M2::Manifold)
    return ProductManifold(M1, M2)
end

function cross(M1::ProductManifold, M2::Manifold)
    return ProductManifold(M1.manifolds..., M2)
end

function cross(M1::Manifold, M2::ProductManifold)
    return ProductManifold(M1, M2.manifolds...)
end

function cross(M1::ProductManifold, M2::ProductManifold)
    return ProductManifold(M1.manifolds..., M2.manifolds...)
end

"""
    submanifold(M::ProductManifold, i::Integer)

Extract the `i`th factor of the product manifold `M`.
"""
function submanifold(M::ProductManifold, i::Integer)
    return M.manifolds[i]
end

"""
    submanifold(M::ProductManifold, i::Val)

Extract the factor of the product manifold `M` indicated by indices in `i`.
For example, for `i` equal to `Val((1, 3))` the product manifold constructed
from the first and the third factor is returned.
"""
function submanifold(M::ProductManifold, i::Val)
    return ProductManifold(select_from_tuple(M.manifolds, i))
end

"""
    submanifold(M::ProductManifold, i::AbstractVector)

Extract the factor of the product manifold `M` indicated by indices in `i`.
For example, for `i` equal to `[1, 3]` the product manifold constructed
from the first and the third factor is returned.

This function is not type-stable, for better preformance use
`submanifold(M::ProductManifold, i::Val)`.
"""
submanifold(M::ProductManifold, i::AbstractVector) = submanifold(M, Val(tuple(i...)))

abstract type AbstractReshaper end

"""
    StaticReshaper()

Reshaper that constructs [`SizedAbstractArray`](@ref).
"""
struct StaticReshaper <: AbstractReshaper end

"""
    make_reshape(reshaper::AbstractReshaper, ::Type{Size}, data) where Size

Reshape array `data` to size `Size` using method provided by `reshaper`.
"""
function make_reshape(reshaper::AbstractReshaper, ::Type{Size}, data) where Size
    error("make_reshape is not defined for reshaper of type $(typeof(reshaper)), size $(Size) and data of type $(typeof(data)).")
end

function make_reshape(::StaticReshaper, ::Type{Size}, data) where Size
    return SizedAbstractArray{Size}(data)
end

"""
    ArrayReshaper()

Reshaper that constructs `Base.ReshapedArray`.
"""
struct ArrayReshaper <: AbstractReshaper end

function make_reshape(::ArrayReshaper, ::Type{Size}, data) where Size
    return reshape(data, size_to_tuple(Size))
end

"""
    ShapeSpecification(reshapers, manifolds::Manifold...)

A structure for specifying array size and offset information for linear
storage of points and tangent vectors on the product manifold of `manifolds`.

The first argument, `reshapers`, indicates how a view representing a point
in the [`ProductArray`](@ref) will be reshaped. It can either be an object
of type `AbstractReshaper` that will be applied to all views or a tuple
of such objects that will be applied to subsequent manifolds.

Two main reshaping methods are provided by types [`StaticReshaper`](@ref)
that is faster for manifolds represented by small arrays (up to about 100
elements) and [`ArrayReshaper`](@ref) that is faster for larger arrays.

For example, consider the shape specification for the product of
a sphere and group of rotations:

```julia-repl
julia> M1 = Sphere(2)
Sphere{2}()

julia> M2 = Manifolds.Rotations(2)
Manifolds.Rotations{2}()

julia> reshaper = Manifolds.StaticReshaper()
Manifolds.StaticReshaper()

julia> shape = Manifolds.ShapeSpecification(reshaper, M1, M2)
Manifolds.ShapeSpecification{(1:3, 4:7),Tuple{Tuple{3},Tuple{2,2}},
  Tuple{Manifolds.StaticReshaper,Manifolds.StaticReshaper}}(
  (Manifolds.StaticReshaper(), Manifolds.StaticReshaper()))
```

`TRanges` contains ranges in the linear storage that correspond to a specific
manifold. `Sphere(2)` needs three numbers and is first, so it is allocated the
first three elements of the linear storage (`1:3`). `Rotations(2)` needs four
numbers and is second, so the next four numbers are allocated to it (`4:7`).
`TSizes` describe how the linear storage must be reshaped to correctly
represent points. In this case, `Sphere(2)` expects a three-element vector, so
the corresponding size is `Tuple{3}`. On the other hand, `Rotations(2)`
expects two-by-two matrices, so its size specification is `Tuple{2,2}`.
"""
struct ShapeSpecification{TRanges, TSizes, TReshapers}
    reshapers::TReshapers
end

function ShapeSpecification(reshapers, manifolds::Manifold...)
    sizes = map(m -> representation_size(m, MPoint), manifolds)
    lengths = map(prod, sizes)
    ranges = UnitRange{Int64}[]
    k = 1
    for len ∈ lengths
        push!(ranges, k:(k+len-1))
        k += len
    end
    TRanges = tuple(ranges...)
    TSizes = Tuple{map(s -> Tuple{s...}, sizes)...}
    if isa(reshapers, AbstractReshaper)
        rtuple = map(m -> reshapers, manifolds)
        return ShapeSpecification{TRanges, TSizes, typeof(rtuple)}(rtuple)
    else
        return ShapeSpecification{TRanges, TSizes, typeof(reshapers)}(reshapers)
    end
end

"""
    ProductArray(shape::ShapeSpecification, data)

An array-based representation for points and tangent vectors on the
product manifold. `data` contains underlying representation of points
arranged according to `TRanges` and `TSizes` from `shape`.
Internal views for each specific sub-point are created and stored in `parts`.
"""
struct ProductArray{TM<:ShapeSpecification,T,N,TData<:AbstractArray{T,N},TV<:Tuple,TReshaper} <: AbstractArray{T,N}
    data::TData
    parts::TV
    reshapers::TReshaper
end

# The two-argument version of this constructor is substantially faster than
# the generic one.
function ProductArray(M::Type{ShapeSpecification{TRanges, Tuple{Size1, Size2}, TReshapers}}, data::TData, reshapers) where {TRanges, Size1, Size2, TReshapers, T, N, TData<:AbstractArray{T,N}}
    views = (make_reshape(reshapers[1], Size1, view(data, TRanges[1])),
             make_reshape(reshapers[2], Size2, view(data, TRanges[2])))
    return ProductArray{M, T, N, TData, typeof(views), typeof(reshapers)}(data, views, reshapers)
end

function ProductArray(M::Type{ShapeSpecification{TRanges, Tuple{Size1, Size2, Size3}, TReshapers}}, data::TData, reshapers) where {TRanges, Size1, Size2, Size3, TReshapers, T, N, TData<:AbstractArray{T,N}}
    views = (make_reshape(reshapers[1], Size1, view(data, TRanges[1])),
             make_reshape(reshapers[2], Size2, view(data, TRanges[2])),
             make_reshape(reshapers[3], Size3, view(data, TRanges[3])))
    return ProductArray{M, T, N, TData, typeof(views), typeof(reshapers)}(data, views, reshapers)
end

function ProductArray(M::Type{ShapeSpecification{TRanges, TSizes, TReshapers}}, data::TData, reshapers) where {TRanges, TSizes, TReshapers, T, N, TData<:AbstractArray{T,N}}
    views = map((size, range, reshaper) -> make_reshape(reshaper, size, view(data, range)), size_to_tuple(TSizes), TRanges, reshapers)
    return ProductArray{M, T, N, TData, typeof(views), typeof(reshapers)}(data, views, reshapers)
end

function ProductArray(M::ShapeSpecification, data)
    return ProductArray(typeof(M), data, M.reshapers)
end

@doc doc"""
    prod_point(M::ShapeSpecification, pts...)

Construct a product point from product manifold `M` based on point `pts`
represented by [`ProductArray`](@ref).

# Example
To construct a point on the product manifold $S^2 \times \mathbb{R}^2$
from points on the sphere and in the euclidean space represented by,
respectively, `[1.0, 0.0, 0.0]` and `[-3.0, 2.0]` you need to construct shape
specification first. It describes how linear storage of `ProductArray`
corresponds to array representations expected by `Sphere(2)` and `Euclidean(2)`.

    M1 = Sphere(2)
    M2 = Euclidean(2)
    reshaper = Manifolds.StaticReshaper()
    Mshape = Manifolds.ShapeSpecification(reshaper, M1, M2)

Next, the desired point on the product manifold can be obtained by calling
`Manifolds.prod_point(Mshape, [1.0, 0.0, 0.0], [-3.0, 2.0])`.
"""
function prod_point(M::ShapeSpecification, pts...)
    data = mapreduce(vcat, pts) do pt
        reshape(pt, :)
    end
    # Array(data) is used to ensure that the data is mutable
    # `mapreduce` can return `SArray` for some arguments
    return ProductArray(M, Array(data))
end

"""
    submanifold_component(x::ProductArray, i::Integer)

Project the product array `x` to its `i`th component. A new array is returned.
"""
function submanifold_component(x::ProductArray, i::Integer)
    return x.parts[i]
end

Base.BroadcastStyle(::Type{<:ProductArray{ShapeSpec}}) where ShapeSpec<:ShapeSpecification = Broadcast.ArrayStyle{ProductArray{ShapeSpec}}()

function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{ProductArray{ShapeSpec}}}, ::Type{ElType}) where {ShapeSpec, ElType}
    A = find_pv(bc)
    return ProductArray(ShapeSpec, similar(A.data, ElType), A.reshapers)
end

Base.dataids(x::ProductArray) = Base.dataids(x.data)

"""
    find_pv(x...)

`A = find_pv(x...)` returns the first `ProductArray` among the arguments.
"""
@inline find_pv(bc::Base.Broadcast.Broadcasted) = find_pv(bc.args)
@inline find_pv(args::Tuple) = find_pv(find_pv(args[1]), Base.tail(args))
@inline find_pv(x) = x
@inline find_pv(a::ProductArray, rest) = a
@inline find_pv(::Any, rest) = find_pv(rest)

size(x::ProductArray) = size(x.data)
Base.@propagate_inbounds getindex(x::ProductArray, i) = getindex(x.data, i)
Base.@propagate_inbounds setindex!(x::ProductArray, val, i) = setindex!(x.data, val, i)

(+)(v1::ProductArray{ShapeSpec}, v2::ProductArray{ShapeSpec}) where ShapeSpec<:ShapeSpecification = ProductArray(ShapeSpec, v1.data + v2.data, v1.reshapers)
(-)(v1::ProductArray{ShapeSpec}, v2::ProductArray{ShapeSpec}) where ShapeSpec<:ShapeSpecification = ProductArray(ShapeSpec, v1.data - v2.data, v1.reshapers)
(-)(v::ProductArray{ShapeSpec}) where ShapeSpec<:ShapeSpecification = ProductArray(ShapeSpec, -v.data, v.reshapers)
(*)(a::Number, v::ProductArray{ShapeSpec}) where ShapeSpec<:ShapeSpecification = ProductArray(ShapeSpec, a*v.data, v.reshapers)

eltype(::Type{ProductArray{TM, TData, TV}}) where {TM, TData, TV} = eltype(TData)
similar(x::ProductArray{ShapeSpec}) where ShapeSpec<:ShapeSpecification = ProductArray(ShapeSpec, similar(x.data), x.reshapers)
similar(x::ProductArray{ShapeSpec}, ::Type{T}) where {ShapeSpec<:ShapeSpecification, T} = ProductArray(ShapeSpec, similar(x.data, T), x.reshapers)

"""
    ProductMPoint(parts)

A more general but slower representation of points on a product manifold.

# Example:

A product point on a product manifold `Sphere(2) × Euclidean(2)` might be
created as

    ProductMPoint([1.0, 0.0, 0.0], [2.0, 3.0])

where `[1.0, 0.0, 0.0]` is the part corresponding to the sphere factor
and `[2.0, 3.0]` is the part corresponding to the euclidean manifold.
"""
struct ProductMPoint{TM<:Tuple} <: MPoint
    parts::TM
end

ProductMPoint(points...) = ProductMPoint{typeof(points)}(points)
eltype(x::ProductMPoint) = eltype(Tuple{map(eltype, x.parts)...})
similar(x::ProductMPoint) = ProductMPoint(map(similar, x.parts)...)
similar(x::ProductMPoint, ::Type{T}) where T = ProductMPoint(map(t -> similar(t, T), x.parts)...)

function submanifold_component(x::ProductMPoint, i::Integer)
    return x.parts[i]
end

"""
    ProductTVector(parts)

A more general but slower representation of tangent vectors on a product
manifold.
"""
struct ProductTVector{TM<:Tuple} <: TVector
    parts::TM
end

ProductTVector(vectors...) = ProductTVector{typeof(vectors)}(vectors)
eltype(x::ProductTVector) = eltype(Tuple{map(eltype, x.parts)...})
similar(x::ProductTVector) = ProductTVector(map(similar, x.parts)...)
similar(x::ProductTVector, ::Type{T}) where T = ProductTVector(map(t -> similar(t, T), x.parts)...)

(+)(v1::ProductTVector, v2::ProductTVector) = ProductTVector(map(+, v1.parts, v2.parts)...)
(-)(v1::ProductTVector, v2::ProductTVector) = ProductTVector(map(-, v1.parts, v2.parts)...)
(-)(v::ProductTVector) = ProductTVector(map(-, v.parts))
(*)(a::Number, v::ProductTVector) = ProductTVector(map(t -> a*t, v.parts))

function submanifold_component(x::ProductTVector, i::Integer)
    return x.parts[i]
end

"""
    ProductCoTVector(parts)

A more general but slower representation of cotangent vectors on a product
manifold.
"""
struct ProductCoTVector{TM<:Tuple} <: CoTVector
    parts::TM
end

ProductCoTVector(covectors...) = ProductCoTVector{typeof(covectors)}(covectors)
eltype(x::ProductCoTVector) = eltype(Tuple{map(eltype, x.parts)...})
similar(x::ProductCoTVector) = ProductCoTVector(map(similar, x.parts)...)
similar(x::ProductCoTVector, ::Type{T}) where T = ProductCoTVector(map(t -> similar(t, T), x.parts)...)

function submanifold_component(x::ProductCoTVector, i::Integer)
    return x.parts[i]
end

function isapprox(M::ProductManifold, x, y; kwargs...)
    return all(t -> isapprox(t...; kwargs...), ziptuples(M.manifolds, x.parts, y.parts))
end

function isapprox(M::ProductManifold, x, v, w; kwargs...)
    return all(t -> isapprox(t...; kwargs...), ziptuples(M.manifolds, x.parts, v.parts, w.parts))
end

function representation_size(M::ProductManifold, ::Type{T}) where {T}
    return (mapreduce(m -> sum(representation_size(m, T)), +, M.manifolds),)
end

manifold_dimension(M::ProductManifold) = mapreduce(manifold_dimension, +, M.manifolds)

struct ProductMetric <: Metric end

function det_local_metric(M::MetricManifold{ProductManifold, ProductMetric}, x::ProductArray)
    dets = map(det_local_metric, M.manifolds, x.parts)
    return prod(dets)
end

function inner(M::ProductManifold, x, v, w)
    subproducts = map(inner, M.manifolds, x.parts, v.parts, w.parts)
    return sum(subproducts)
end

function norm(M::ProductManifold, x, v)
    norms_squared = map(norm, M.manifolds, x.parts, v.parts).^2
    return sqrt(sum(norms_squared))
end

function distance(M::ProductManifold, x, y)
    return sqrt(sum(map(distance, M.manifolds, x.parts, y.parts).^2))
end

function exp!(M::ProductManifold, y, x, v)
    map(exp!, M.manifolds, y.parts, x.parts, v.parts)
    return y
end

function exp(M::ProductManifold, x::ProductMPoint, v::ProductTVector)
    return ProductMPoint(map(exp, M.manifolds, x.parts, v.parts)...)
end

function log!(M::ProductManifold, v, x, y)
    map(log!, M.manifolds, v.parts, x.parts, y.parts)
    return v
end

function log(M::ProductManifold, x::ProductMPoint, y::ProductMPoint)
    return ProductTVector(map(log, M.manifolds, x.parts, y.parts)...)
end

function injectivity_radius(M::ProductManifold, x)
    return min(map(injectivity_radius, M.manifolds, x.parts)...)
end

function injectivity_radius(M::ProductManifold)
    return min(map(injectivity_radius, M.manifolds)...)
end

"""
    ProductRetraction(retractions::AbstractRetractionMethod...)

Product retraction of `retractions`. Works on [`ProductManifold`](@ref).
"""
struct ProductRetraction{TR<:Tuple} <: AbstractRetractionMethod
    retractions::TR
end

ProductRetraction(retractions::AbstractRetractionMethod...) = ProductRetraction{typeof(retractions)}(retractions)

function retract!(M::ProductManifold, y, x, v, method::ProductRetraction)
    map(retract!, M.manifolds, y.parts, x.parts, v.parts, method.retractions)
    return y
end

struct InverseProductRetraction{TR<:Tuple} <: AbstractInverseRetractionMethod
    inverse_retractions::TR
end

"""
    InverseProductRetraction(inverse_retractions::AbstractInverseRetractionMethod...)

Product inverse retraction of `inverse_retractions`.
Works on [`ProductManifold`](@ref).
"""
InverseProductRetraction(inverse_retractions::AbstractInverseRetractionMethod...) = InverseProductRetraction{typeof(inverse_retractions)}(inverse_retractions)

function inverse_retract!(M::ProductManifold, v, x, y, method::InverseProductRetraction)
    map(inverse_retract!, M.manifolds, v.parts, x.parts, y.parts, method.inverse_retractions)
    return v
end

"""
    is_manifold_point(M::ProductManifold, x; kwargs...)

Check whether `x` is a valid point on the [`ProductManifold`](@ref) `M`.

The tolerance for the last test can be set using the ´kwargs...`.
"""
function is_manifold_point(M::ProductManifold, x::MPoint; kwargs...)
    return all(t -> is_manifold_point(t...; kwargs...), ziptuples(M.manifolds, x.parts))
end

function is_manifold_point(M::ProductManifold, x::ProductArray; kwargs...)
    return all(t -> is_manifold_point(t...; kwargs...), ziptuples(M.manifolds, x.parts))
end

"""
    is_tangent_vector(M::ProductManifold, x, v; kwargs... )

Check whether `v` is a tangent vector to `x` on the [`ProductManifold`](@ref)
`M`, i.e. atfer [`is_manifold_point`](@ref)`(M, x)`, and all projections to
base manifolds must be respective tangent vectors.

The tolerance for the last test can be set using the ´kwargs...`.
"""
function is_tangent_vector(M::ProductManifold, x::MPoint, v::TVector; kwargs...)
    is_manifold_point(M, x)
    return all(t -> is_tangent_vector(t...; kwargs...), ziptuples(M.manifolds, x.parts, v.parts))
end

function is_tangent_vector(M::ProductManifold, x::ProductArray, v::ProductArray; kwargs...)
    is_manifold_point(M, x)
    return all(t -> is_tangent_vector(t...; kwargs...), ziptuples(M.manifolds, x.parts, v.parts))
end

"""
    ProductPointDistribution(M::ProductManifold, distributions)

Product distribution on manifold `M`, combined from `distributions`.
"""
struct ProductPointDistribution{TM<:ProductManifold, TD<:(NTuple{N,Distribution} where N)} <: MPointDistribution{TM}
    manifold::TM
    distributions::TD
end

function ProductPointDistribution(M::ProductManifold, distributions::MPointDistribution...)
    return ProductPointDistribution{typeof(M), typeof(distributions)}(M, distributions)
end

function ProductPointDistribution(distributions::MPointDistribution...)
    M = ProductManifold(map(d -> support(d).manifold, distributions)...)
    return ProductPointDistribution(M, distributions...)
end

function support(d::ProductPointDistribution)
    return MPointSupport(d.manifold)
end

function rand(rng::AbstractRNG, d::ProductPointDistribution)
    return ProductMPoint(map(d -> rand(rng, d), d.distributions)...)
end

function _rand!(rng::AbstractRNG, d::ProductPointDistribution, x::AbstractArray{<:Number})
    x .= rand(rng, d)
    return x
end

function _rand!(rng::AbstractRNG, d::ProductPointDistribution, x::ProductMPoint)
    map(t -> _rand!(rng, t[1], t[2]), d.distributions, x.parts)
    return x
end

"""
    ProductTVectorDistribution([m::ProductManifold], [x], distrs...)

Generates a random tangent vector at point `x` from manifold `m` using the
product distribution of given distributions.

Manifold and `x` can be automatically inferred from distributions `distrs`.
"""
struct ProductTVectorDistribution{TM<:ProductManifold, TD<:(NTuple{N,Distribution} where N), TX} <: TVectorDistribution{TM, TX}
    manifold::TM
    x::TX
    distributions::TD
end

function ProductTVectorDistribution(M::ProductManifold, x::Union{AbstractArray, MPoint}, distributions::TVectorDistribution...)
    return ProductTVectorDistribution{typeof(M), typeof(distributions), typeof(x)}(M, x, distributions)
end

function ProductTVectorDistribution(M::ProductManifold, distributions::TVectorDistribution...)
    x = ProductMPoint(map(d -> support(d).x, distributions))
    return ProductTVectorDistribution(M, x, distributions...)
end

function ProductTVectorDistribution(distributions::TVectorDistribution...)
    M = ProductManifold(map(d -> support(d).manifold, distributions)...)
    x = ProductMPoint(map(d -> support(d).x, distributions)...)
    return ProductTVectorDistribution(M, x, distributions...)
end

function support(tvd::ProductTVectorDistribution)
    return TVectorSupport(tvd.manifold, ProductMPoint(map(d -> support(d).x, tvd.distributions)...))
end

function rand(rng::AbstractRNG, d::ProductTVectorDistribution)
    return ProductTVector(map(d -> rand(rng, d), d.distributions)...)
end

function _rand!(rng::AbstractRNG, d::ProductTVectorDistribution, v::AbstractArray{<:Number})
    v .= rand(rng, d)
    return v
end

function _rand!(rng::AbstractRNG, d::ProductTVectorDistribution, v::ProductTVector)
    map(t -> _rand!(rng, t[1], t[2]), d.distributions, v.parts)
    return v
end


"""
    struct Johnson <: ShortestPathAlgorithm

The structure used to configure and specify that [`shortest_paths`](@ref)
should use the [Johnson algorithm](https://en.wikipedia.org/wiki/Johnson%27s_algorithm).
No additional configuration parameters are specified or required.

### Implementation Notes
`Johnson` supports the following shortest-path functionality:
- non-negative distance matrices / weights
- all-pairs shortest paths

### Performance
Complexity: O(|V|*|E|)
"""
struct Johnson <: ShortestPathAlgorithm end

struct JohnsonResult{T, U<:Integer} <: ShortestPathResult
    parents::Matrix{U}
    dists::Matrix{T}
end

function shortest_paths(g::AbstractGraph{U}, distmx::AbstractMatrix{T}, ::Johnson) where {T, U<:Integer}
    nvg = nv(g)
    type_distmx = typeof(distmx)
    #Change when parallel implementation of Bellman Ford available
    wt_transform = LightGraphs.Experimental.ShortestPaths.dists(shortest_paths(g, vertices(g), distmx, BellmanFord()))

    if !type_distmx.mutable && type_distmx !=  LightGraphs.DefaultDistance
        distmx = sparse(distmx) #Change reference, not value
    end

    #Weight transform not needed if all weights are positive.
    if type_distmx !=  LightGraphs.DefaultDistance
        for e in edges(g)
            distmx[src(e), dst(e)] += wt_transform[src(e)] - wt_transform[dst(e)]
        end
    end

    dists = Matrix{T}(undef, nvg, nvg)
    parents = Matrix{U}(undef, nvg, nvg)
    for v in vertices(g)
        dijk_state = dijkstra_shortest_paths(g, v, distmx)
        dists[v, :] = dijk_state.dists
        parents[v, :] = dijk_state.parents
    end

    broadcast!(-, dists, dists, wt_transform)
    for v in vertices(g)
        dists[:, v] .+= wt_transform[v] #Vertical traversal prefered
    end

    if type_distmx.mutable
        for e in edges(g)
            distmx[src(e), dst(e)] += wt_transform[dst(e)] - wt_transform[src(e)]
        end
    end

    return JohnsonResult(parents, dists)
end

shortest_paths(g::AbstractGraph, alg::Johnson) = shortest_paths(g, weights(g), alg)

function paths(s::JohnsonResult{T, U}, v::Integer) where {T, U <: Integer}
    pathinfo = s.parents[v, :]
    paths = Vector{Vector{U}}()
    for i in 1:length(pathinfo)
        if (i == v) || (s.dists[v, i] == typemax(T))
            push!(paths, Vector{U}())
        else
            path = Vector{U}()
            currpathindex = U(i)
            while currpathindex != 0
                push!(path, currpathindex)
                currpathindex = pathinfo[currpathindex]
            end
            push!(paths, reverse(path))
        end
    end
    return paths
end

paths(s::JohnsonResult) = [paths(s, v) for v in 1:size(s.parents, 1)]
paths(st::JohnsonResult, s::Integer, d::Integer) = paths(st, s)[d]


# ---------------------------------------------------------------------------
# empirical CDF

@recipe function f(ecdf::StatsBase.ECDF)
    seriestype := :steppost
    legend --> :topleft
    x = [ecdf.sorted_values[1]; ecdf.sorted_values]
    if :weights in propertynames(ecdf) && !isempty(ecdf.weights)
         # support StatsBase versions >v0.32.0
        y = [0; cumsum(ecdf.weights) ./ sum(ecdf.weights)]
    else
        y = range(0, 1; length = length(x))
    end
    x, y
end

@userplot ECDFPlot
recipetype(::Val{:ecdfplot}, args...) = ECDFPlot(args)
@recipe function f(p::ECDFPlot)
    x = p.args[1]
    if !isa(x, StatsBase.ECDF)
        x = StatsBase.ecdf(x)
    end
    x
end


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/FEMPlates.jl/blob/master/LICENSE

using Test, FEMBase, FEMPlates

# Create 1x1 element, assemble stiffness matrix.

X = Dict(1 => [0.0, 0.0], 2 => [1.0, 0.0],
         3 => [1.0, 1.0], 4 => [0.0, 1.0])
element = Element(Quad4, [1, 2, 3, 4])
update!(element, "geometry", X)
update!(element, "youngs modulus", 4880.0*6.0*3.0*4.0)
update!(element, "thickness", 1.0)
update!(element, "poissons ratio", 1/3)
problem = Problem(MindlinPlate, "test plate", 3)
add_elements!(problem, [element])
time = 0.0
assemble!(problem, time)
K = Matrix(problem.assembly.K)
#display(K)
fixed_dofs = [1, 2, 3, 10, 11, 12]
free_dofs = setdiff(1:12, fixed_dofs)
f = zeros(12)
f[[4, 7]] .= 10.0
u = zeros(12)
u[free_dofs] = K[free_dofs, free_dofs] \ f[free_dofs]
#display(u)
u_expected = [
0.0
0.0
0.0
0.000306921
-0.000249543
-0.0000458344
0.000306921
-0.000249543
0.0000458344
0.0
0.0
0.0
]
@test isapprox(u, u_expected,atol=1.0e-8)


include("../src/eta_quotient.jl")
include("../src/eta_quotient_eigenforms.jl")

function test_eta_quotient_length()
   print("eta_quotient.length...")

   @test eta_quotient([[1,24]]).length == 10
   @test eta_quotient([[1,24]], 100).length == 100

   println("PASS")
end

function test_eta_quotient_correctness()
   print("eta_quotient.correctness...")

   for g in keys(ETA_QUOTIENT_EIGENFORM)
     eta = eta_quotient(g, 20)
     @test eta - ETA_QUOTIENT_EIGENFORM[g] == 0
   end

   eta1 = eta_quotient([[1,24]],100)
   @test eta1 - delta_qexp(100) == 0

   println("PASS")
end

function test_eta_quotient()
   test_eta_quotient_length()
   test_eta_quotient_correctness()

   println("")
end


@testset "scaledkernel" begin
    rng = MersenneTwister(123456)
    x = randn(rng)
    y = randn(rng)
    s = rand(rng) + 1e-3

    k = SqExponentialKernel()
    ks = ScaledKernel(k, s)
    @test ks(x, y) == s * k(x, y)
    @test ks(x, y) == (s * k)(x, y)

    @testset "kernelmatrix" begin
        rng = MersenneTwister(123456)

        Nx = 5
        Ny = 4
        D = 3

        k = SqExponentialKernel()
        s = rand(rng) + 1e-3
        ks = s * k

        @testset "$(typeof(x))" for (x, y) in [
            (ColVecs(randn(rng, D, Nx)), ColVecs(randn(rng, D, Ny))),
            (RowVecs(randn(rng, Nx, D)), RowVecs(randn(rng, Ny, D))),
        ]
            @test kernelmatrix(ks, x, y) ≈ s .* kernelmatrix(k, x, y)

            @test kernelmatrix(ks, x) ≈ s .* kernelmatrix(k, x)

            @test kerneldiagmatrix(ks, x) ≈ s .* kerneldiagmatrix(k, x)

            tmp = Matrix{Float64}(undef, length(x), length(y))
            @test_broken kernelmatrix!(tmp, ks, x, y) ≈ kernelmatrix(ks, x, y)

            tmp_square = Matrix{Float64}(undef, length(x), length(x))
            @test_broken kernelmatrix!(tmp_square, ks, x) ≈ kernelmatrix(ks, x)

            tmp_diag = Vector{Float64}(undef, length(x))
            @test_broken kerneldiagmatrix!(tmp_diag, ks, x) ≈ kerneldiagmatrix(ks, x)
        end
    end
    test_ADs(x->exp(x[1]) * SqExponentialKernel(), rand(1))
end


using GLMakie # openGL backend, uses GPu, very interactive, runs outside browser
# using WGLMakie # WebGL backend, interactive, runs inside browser
# using Cairo # 2d plots
using Makie.MakieLayout

# Plots latitudes and longitude of sphere.
function plotLatLon3d!(
        scene :: Union{Scene,LScene};
        lat_num :: Int64 = 10, # number of latitudes
        lon_num :: Int64 = 20, # number of longitude
        lat_den :: Int64 = 100, # number of points per latitude, den for density
        lon_den :: Int64 = 50 # number of points per longitude
) :: Nothing
    lats = [ [ sphericalToCartesian( SphericalCoord(theta, phi) ) for phi in LinRange(0, 2π, lat_den) ] for theta in LinRange(0, π, lat_num)]
    lons = [ [ sphericalToCartesian( SphericalCoord(theta, phi) ) for theta in LinRange(0, π, lon_den)] for phi in LinRange(0, 2π, lon_num)]

    # plot each line separately because plotting library connects consecutive ponts in the array in second argument using a line
    for l=lons
       lines!(scene, [l[j].x for j in 1:lon_den], [l[j].y for j in 1:lon_den], [l[j].z for j in 1:lon_den])
    end

    for l=lats
       lines!(scene, [l[j].x for j in 1:lat_den], [l[j].y for j in 1:lat_den], [l[j].z for j in 1:lat_den])
    end
    nothing
end

# Plots latitudes and longitude of sphere.
function plotLatLon2d!(
        scene :: Union{Scene,LScene};
        lat_num :: Int64 = 10, # number of latitudes
        lon_num :: Int64 = 20, # number of longitude
        lat_den :: Int64 = 100, # number of points per latitude, den for density
        lon_den :: Int64 = 50 # number of points per longitude
) :: Nothing
    lats = [ [ SphericalCoord(theta, phi) for phi in LinRange(0, 2π, lat_den) ] for theta in LinRange(0, π, lat_num)]
    lons = [ [ SphericalCoord(theta, phi) for theta in LinRange(0, π, lon_den)] for phi in LinRange(0, 2π, lon_num)]

    # plot each line separately because plotting library connects consecutive ponts in the array in second argument using a line
    for l=lons
       lines!(scene, [l[j].phi for j in 1:lon_den], [l[j].theta for j in 1:lon_den])
    end

    for l=lats
       lines!(scene, [l[j].phi for j in 1:lat_den], [l[j].theta for j in 1:lat_den])
    end
    nothing
end

# convert field specified in spherical coords to normal cartesian coordinates
function cartesianField( sphericalField )
    field = (x,y,z) ->  sphericalToCartesianVelocity(
        cartesianToSpherical( CartesianCoord( x,y,z ) ),
        sphericalField( cartesianToSpherical( CartesianCoord( x,y,z ) ) )
    )
    return( field )
end

function plotVelocityField3d!( threeDPlot::Union{Scene,LScene}, fieldGenerator, objects::Vector{<:Object}, lat_den::Int64, lon_den::Int64 )::Nothing
    sphericalField = @lift( p -> fieldGenerator( p, $objects ) )
    ∇f = @lift( cartesianField( $sphericalField ) )

    # create arrows plot for velocity vector field on a lot long grid
    gx = reduce(vcat,[[cospi(φ)*sinpi(θ) for φ in LinRange(0, 2π, round( Int, lat_den*sin(θ)))] for θ in LinRange(0, π, lon_den) ])
    gy = reduce(vcat,[[sinpi(φ)*sinpi(θ) for φ in LinRange(0, 2π, round( Int, lat_den*sin(θ)))] for θ in LinRange(0, π, lon_den) ])
    gz = reduce(vcat,[[cospi(θ)          for φ in LinRange(0, 2π, round( Int, lat_den*sin(θ)))] for θ in LinRange(0, π, lon_den) ])

    # we normalize the vectors because vectors can be too large at some places messing the entire plot
    n∇fxyz = @lift( map((x,y,z) -> normalize($∇f(x,y,z)), gx,gy,gz) )
    vx = @lift( map(v->v.x, $n∇fxyz) )
    vy = @lift( map(v->v.y, $n∇fxyz) )
    vz = @lift( map(v->v.z, $n∇fxyz) )
    arrows!(threeDPlot, gx, gy, gz, vx, vy, vz, arrowsize = 0.01, lengthscale = 0.05 )

    φs = LinRange(0, 2π, lat_den)
    θs = LinRange(0, π, lon_den)

    # plot the magnitude of velocity vector field on spherical surface
    sphereGrid = [ sphericalToCartesian( SphericalCoord(theta, phi) ) for phi in φs, theta in θs ]
    # TODO: we are recomputing ∇f on the sphere grid again, we already did that for a 'similar' grid when making arrows, can we reuse those computations
    colorsGrid = @lift( map( p->log(1 + norm($∇f(p.x,p.y,p.z))), sphereGrid ) )
    surface!(threeDPlot, map(s->s.x,sphereGrid), map(s->s.y,sphereGrid), map(s->s.z,sphereGrid), color=colorsGrid, colormap=:dense )
    nothing
end

function plotVelocityField2d!( twoDPlot::Union{Scene,LScene}, fieldGenerator, objects::Vector{<:Object}, lat_den::Int64, lon_den::Int64 )::Nothing
    # now we plot streams on theta-phi chart
    # phi is x axis and theta is y axis
    sphereCoordToPoint2f(vector::SphericalCoord, position::SphericalCoord ) = Point2f0( csc( position.theta)*vector.phi, vector.theta )
    # velocity func in polar coordinates
    # this gives (dϕ/dt, dθ/dt)
    ∇f_polar = @lift( (phi,theta) -> sphereCoordToPoint2f(
        fieldGenerator(
            SphericalCoord(theta,phi),
            $objects
        ),
        SphericalCoord(theta, phi)
    ) )

    φs = LinRange(0, 2π, lat_den)
    θs = LinRange(0, π, lon_den)

    # plot streams on theta phi chart
    streamplot!( twoDPlot, ∇f_polar, φs, θs, arrow_size=0.1 ) # streamplot is computationally expensive

    # plot vector magnitude
    # we don't need to use spherical coordinate norm as the spherical vector already include sin(theta) in e_phi component
    # TODO: we are recomputing ∇f on the sphere grid again, we already did this when making density surface plot on 3d chart, we can reuse the computations
    # TODO: in here vector norm should be on (sin(θ) dϕ/dt, dθ/dt), rather than (dϕ/dt, dθ/dt)
    colorsGridTwoD = @lift( [ log(1+norm($∇f_polar(phi,theta))) for phi in φs, theta in θs ] )
    heatmap!(twoDPlot, φs, θs, colorsGridTwoD, colormap=:dense)
    nothing
end

# Create a slider animation.
function animate(
    simObjects :: Vector{<:Vector{<:Object}}; #array of all time simuatled array of objects
    plotLatLon :: Bool = true,
    fieldGenerator = nothing, # a (point::SphericalCoord, objects::Vector{Object}} -> vector::SphericalCoord) function, if want to plot arrows and density on sphere
    resolution :: Tuple{Int64,Int64} = (1000,600),
    makeMovie :: Bool = true,
    cameraPosition :: Vec{3,Float32} = Vec3f0(-2, 0, 0),
    cameraLookingAt :: Vec{3,Float32} = Vec3f0(-1, 0, 0),
    frameRate ::Int64 = 1,
    movieName ::String = "animation.mp4",
) :: Nothing
    numFrames = length(simObjects)
    scene, layout = layoutscene(resolution = resolution) # create a new scene

    # a slider to move forward or backward in animation
    frameSlider = layout[1,:] = Slider(scene, range = LinRange(1, numFrames, numFrames), startvalue = 1)

    threeDPlot = layout[2,1] = LScene(scene) # full interactive shphere plot
    twoDPlot = layout[2,2] = LScene(scene) # theta phi chart

    cam3d!(threeDPlot.scene)
    update_cam!( threeDPlot.scene, cameraPosition, cameraLookingAt )

    # create an observable state which the plots will 'watch'
    # later we will simply update the state and scene will be automatically updated
    # objectState is dependent on slider so we 'lift' it to an observable
    sliderVal = frameSlider.value
    simObjectstate = @lift( simObjects[round( Int, $sliderVal)] )

    lat_den = 100 # number of points per latitude (= number of longitudes for grid below)
    lon_den = 50 # number of points per longitude (=number of longitude for grid below)

    if plotLatLon
        plotLatLon3d!(threeDPlot, lat_den = lat_den, lon_den = lon_den) # plot latitude and longitude
        plotLatLon2d!(twoDPlot, lat_den = lat_den, lon_den = lon_den)
    end

    # plot arrows and density for the fields if the fieldGenerator is provided
    if fieldGenerator !== nothing
        plotVelocityField3d!( threeDPlot, fieldGenerator, simObjectstate, lat_den, lon_den )
        plotVelocityField2d!( twoDPlot, fieldGenerator, simObjectstate, lat_den, lon_den )
    else
        φs = LinRange(0, 2π, lat_den)
        θs = LinRange(0, π, lon_den)
        sphereGrid = [ sphericalToCartesian( SphericalCoord(theta, phi) ) for phi in φs, theta in θs ]
        colorsGrid =  map( p->2, sphereGrid )
        surface!(threeDPlot, map(s->s.x,sphereGrid), map(s->s.y,sphereGrid), map(s->s.z,sphereGrid), color=colorsGrid, colormap=:dense )
    end

    # plot all simObjects
    for i in 1:length(simObjects[1])
        o = @lift( $simObjectstate[i] )
        plot3d!( threeDPlot, o )
        plot2d!( twoDPlot, o )
    end

    if makeMovie
        # create animation: update the state obervable node by moving slider, record, loop
        record(scene, movieName, 1:numFrames; framerate = frameRate) do frameNum
            set_close_to!(frameSlider, frameNum)
        end
    end

    # create an interactive simulation controlled by slider
    RecordEvents(scene, "output")
    nothing
end


function reflection_matrix(delta, mx, my, MinvT, bar::Barrier, grd::Grid)

    # define function we want to map onto grd
    function f(x, y, grd)
        x, y = reflection(x, y, delta, bar)
        row_dat, row_js = interpolation_row(x, y, mx, my, MinvT, grd)
        return hcat(row_dat, row_js)
    end

    M = grid_map(f, (mx*my,2), grd)
    dat = M[:,1]
    js = Int64[M[:,2]...]
    is = vcat([k*ones(Int64, mx*my) for k=1:grd.Nk]...)

    R2d = sparse(is, js, dat, grd.Nk, grd._Nx*grd._Ny) * transpose(grd.Proj)
    R3d = kron(sparse(I, grd.Nz, grd.Nz), R2d)

    return R3d
end

function boundary_operators(mx, my, MinvT, deltas::Vector{Float64}, bars::Vector{Barrier}, qs::Vector{Float64}, grd::Grid)

    Nb = length(bars)

    PEN = SparseMatrixCSC[sparse(I, grd.Nz*grd.Nk, grd.Nz*grd.Nk)]
    REF = SparseMatrixCSC[]
    DCHLT = SparseMatrixCSC[]
    NMANN = SparseMatrixCSC[]

    for i=1:Nb
        P = Diagonal(f_to_grid((x,y)-> smoothstep(x, y, deltas[i], bars[i]), grd))
        for j=1:length(PEN)
            PEN[j] = P * PEN[j]
        end
        append!(PEN, [(I - P)/qs[i]])
    end

    for i=1:Nb
        R = reflection_matrix(deltas[i], mx, my, MinvT, bars[i], grd)
        @info "Calculated reflection matrix $i"
        append!(REF, [R])
        append!(DCHLT, [qs[i]^2 * 0.5*PEN[i+1]*(I + R)])
        append!(NMANN, [0.5*PEN[i+1]*(I - R)])
    end

    return PEN, REF, DCHLT, NMANN
end

function grid_map(f::Function, outsize::Tuple{Int64, Int64}, grd::Grid)

    n, m = outsize
    output = SharedArray{Float64}((n*grd.Nk,m))

    # @distributed unfortunately only works if nprocs > 1
    if nprocs() > 1
        @distributed for i=1:grd.Nk
            k = 1 + n*(i-1)
            output[k:k+n-1,:] = f(grd.points[:,i]..., grd)
        end
    else
        for i=1:grd.Nk
            k = 1 + n*(i-1)
            output[k:k+n-1,:] = f(grd.points[:,i]..., grd)
        end
    end

    return output[:,:]
end


@testset "Rosenbrock Tests" begin
## Breakout these since no other test of their adaptivity

using OrdinaryDiffEq, DiffEqDevTools, Test, LinearAlgebra, LinearSolve
using DiffEqProblemLibrary.ODEProblemLibrary: importodeproblems; importodeproblems()
import DiffEqProblemLibrary.ODEProblemLibrary: prob_ode_linear, prob_ode_2Dlinear,
                              prob_ode_bigfloatlinear, prob_ode_bigfloat2Dlinear

dts = (1/2) .^ (6:-1:3)
testTol = 0.2

### Rosenbrock23()

prob = prob_ode_linear

sim = test_convergence(dts,prob,Rosenbrock23())
@test sim.𝒪est[:final] ≈ 2 atol=testTol

sol = solve(prob,Rosenbrock23())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,Rosenbrock23())
@test sim.𝒪est[:final] ≈ 2 atol=testTol

sol = solve(prob,Rosenbrock23())
@test length(sol) < 20

prob = prob_ode_bigfloat2Dlinear

sim = test_convergence(dts,prob,Rosenbrock23(linsolve=QRFactorization()))
@test sim.𝒪est[:final] ≈ 2 atol=testTol

sol = solve(prob,Rosenbrock23(linsolve=QRFactorization()))
@test length(sol) < 20

### Rosenbrock32()

prob = prob_ode_linear

sim = test_convergence(dts,prob,Rosenbrock32())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,Rosenbrock32())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,Rosenbrock32())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,Rosenbrock32())
@test length(sol) < 20

### ROS3P()

prob = prob_ode_linear

sim = test_convergence(dts,prob,ROS3P())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,ROS3P())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,ROS3P())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,ROS3P())
@test length(sol) < 20

### Rodas3()

prob = prob_ode_linear

sim = test_convergence(dts,prob,Rodas3())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,Rodas3())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,Rodas3())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,Rodas3())
@test length(sol) < 20

println("4th order Rosenbrocks")

### RosShamp4

prob = prob_ode_linear

sim = test_convergence(dts,prob,RosShamp4())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,RosShamp4())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,RosShamp4())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,RosShamp4())
@test length(sol) < 20

### Veldd4

prob = prob_ode_linear

sim = test_convergence(dts,prob,Veldd4())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,Veldd4())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,Veldd4())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,Veldd4())
@test length(sol) < 20

### Velds4

prob = prob_ode_linear

sim = test_convergence(dts,prob,Velds4())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,Velds4())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,Velds4())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,Velds4())
@test length(sol) < 20

### GRK4T

prob = prob_ode_linear

sim = test_convergence(dts,prob,GRK4T())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,GRK4T())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,GRK4T())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,GRK4T())
@test length(sol) < 20

### GRK4A
dts = (1/2) .^ (7:-1:4)

prob = prob_ode_linear

sim = test_convergence(dts,prob,GRK4A())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,GRK4A())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,GRK4A())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,GRK4A())
@test length(sol) < 20

### Ros4LStab

prob = prob_ode_linear

sim = test_convergence(dts,prob,Ros4LStab())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,Ros4LStab())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,Ros4LStab())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,Ros4LStab())
@test length(sol) < 20

### Rosenbrock-W Algorithms

println("Rosenbrock-W")

### ROS34PW1a
prob = prob_ode_linear

sim = test_convergence(dts,prob,ROS34PW1a())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,ROS34PW1a())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,ROS34PW1a())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,ROS34PW1a())
@test length(sol) < 20

### ROS34PW1b
prob = prob_ode_linear

sim = test_convergence(dts,prob,ROS34PW1b())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,ROS34PW1b())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,ROS34PW1b())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,ROS34PW1b())
@test length(sol) < 20

### ROS34PW2
prob = prob_ode_linear

sim = test_convergence(dts,prob,ROS34PW2())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,ROS34PW2())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,ROS34PW2())
@test sim.𝒪est[:final] ≈ 3 atol=testTol

sol = solve(prob,ROS34PW2())
@test length(sol) < 20

### ROS34PW3
prob = prob_ode_linear

sim = test_convergence(dts,prob,ROS34PW3())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,ROS34PW3())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,ROS34PW3())
@test sim.𝒪est[:final] ≈ 4 atol=testTol

sol = solve(prob,ROS34PW3())
@test length(sol) < 20

### RosenbrockW6S4OS
sim = test_convergence(dts,prob,RosenbrockW6S4OS())#test inplace
@test sim.𝒪est[:final] ≈ 4 atol=testTol

prob = prob_ode_linear

sim = test_convergence(dts,prob,RosenbrockW6S4OS())#test non-inplace
@test sim.𝒪est[:final] ≈ 4 atol=testTol

### Rodas4 Algorithms

println("RODAS")

dts = (1/2) .^ (7:-1:4)

prob = prob_ode_linear

sim = test_convergence(dts,prob,Rodas4(),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4())
@test length(sol) < 20

sim = test_convergence(dts,prob,Rodas4(autodiff=false),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4(autodiff=false))
@test length(sol) < 20

sim = test_convergence(dts,prob,Rodas42(),dense_errors=true)
@test sim.𝒪est[:final] ≈ 5 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas42())
@test length(sol) < 20

sim = test_convergence(dts,prob,Rodas4P(),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4P())
@test length(sol) < 20

sim = test_convergence(dts,prob,Rodas4P2(),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4P2())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,Rodas4(),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4())
@test length(sol) < 20

println("Rodas4 with finite diff")

sim = test_convergence(dts,prob,Rodas4(autodiff=false),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4(autodiff=false))
@test length(sol) < 20

sim = test_convergence(dts,prob,Rodas4(autodiff=false,
                       diff_type=Val{:forward}),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4(autodiff=false,diff_type=Val{:forward}))
@test length(sol) < 20

sim = test_convergence(dts,prob,Rodas4(autodiff=false,
                       diff_type=Val{:complex}),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4(autodiff=false,diff_type=Val{:complex}))
@test length(sol) < 20

sim = test_convergence(dts,prob,Rodas42(),dense_errors=true)
@test sim.𝒪est[:final] ≈ 5 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas42())
@test length(sol) < 20

sim = test_convergence(dts,prob,Rodas4P(),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4P())
@test length(sol) < 20

sim = test_convergence(dts,prob,Rodas4P2(),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4P2())
@test length(sol) < 20

println("Rodas4P2 with finite diff")

sim = test_convergence(dts,prob,Rodas4P2(autodiff=false),dense_errors=true)
@test sim.𝒪est[:final] ≈ 4 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas4P2(autodiff=false))
@test length(sol) < 20

### Rodas5
println("Rodas5")

prob = prob_ode_linear

dts = (1/2) .^ (6:-1:3)
sim = test_convergence(dts,prob,Rodas5(),dense_errors=true)
@test sim.𝒪est[:final] ≈ 5 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas5())
@test length(sol) < 20

prob = prob_ode_2Dlinear

sim = test_convergence(dts,prob,Rodas5(),dense_errors=true)
@test sim.𝒪est[:final] ≈ 5 atol=testTol
@test sim.𝒪est[:L2] ≈ 4 atol=testTol

sol = solve(prob,Rodas5())
@test length(sol) < 20

prob = ODEProblem((u,p,t)->0.9u, 0.1, (0., 1.0))
@test_nowarn solve(prob, Rosenbrock23(autodiff=false))
end


### A Pluto.jl notebook ###
# v0.14.2

using Markdown
using InteractiveUtils

# ╔═╡ 794a5356-6e05-44b4-ad9d-375a1fa3613b
begin
	using PlutoUI
	using DataFrames
	using Random
	using Test
end

# ╔═╡ b3dcf3f4-9d68-11eb-01a6-317fe0e0cd7e
md"""
## Introduction DataFrames / 6 - Manipulating rows

ref. [Introduction to DataFrames, Bogumił Kamiński](https://github.com/bkamins/Julia-DataFrames-Tutorial/blob/master/06_rows.ipynb)

$(html"
© Pascal, April 2021
")
"""

# ╔═╡ d6993a5b-8158-4ce3-accb-8e8c514229b3
PlutoUI.TableOfContents(indent=true, depth=4, aside=true)

# ╔═╡ de97ded0-3952-4188-9a43-a8f383e1ae65
md"""
### Selecting rows
"""

# ╔═╡ 9ccb6d8f-a8e0-42c2-8691-2eac247e591a
Random.seed!(42)

# ╔═╡ 385aee9f-5c17-4bd2-b3b2-58171fcc5990
df = DataFrame(rand(4, 5), :auto)

# ╔═╡ 472fe012-dd24-46d3-b1b1-f217054c40f0
md"""
using `:` as row selector will **copy** columns
"""

# ╔═╡ 147bcb72-1d40-458e-abb4-02c8d8ddfa76
df[:, :]

# ╔═╡ 7699775c-8dec-4467-af21-89bd8354d409
@assert df[:, :] == copy(df)   # meaning copy(df) ≡ df[:, :]

# ╔═╡ 09617e8f-1d09-4145-831f-38fd98313dd1
md"""
We can get a subset of rows of a DataFrame without copying using view to get a `SubDataFrame`
"""

# ╔═╡ 80277a09-24ea-4dae-9a41-39853a46fbaa
vdf = view(df, 1:3, 1:3)

# ╔═╡ 9b61d582-6d26-483d-bb66-174b44b38ff4
md"""
we still have a detailed reference to the parent:
"""

# ╔═╡ 5e7c5b0f-be5d-43aa-b13a-4202b1cb34bf
parent(vdf), parentindices(vdf)

# ╔═╡ 26a2e983-0a3c-41b3-ae2a-79395644e2d4
md"""
Selecting a single row returns a `DataFrameRow` object which is also a view
"""

# ╔═╡ f7dbfd3d-2450-4db3-9d25-d08011c62bf2
dfr = df[3, :]

# ╔═╡ cdee6bee-59d8-4fe4-80d9-183b1fa3e18b
parent(dfr), parentindices(dfr), rownumber(dfr)

# ╔═╡ 96b5f96f-57de-4fef-bc73-3f092b00be74
md"""
Let us add a column to a DataFrame by assigning a scalar broadcasting.
"""

# ╔═╡ 912c5b66-38d9-49f4-881c-4f2a988104c2
df[!, :Z] .= 1

# ╔═╡ 560497da-2699-411f-96f9-334919bfffdf
df

# ╔═╡ d114f107-4e31-4585-915f-a17369023a74
md"""
Earlier we used `:` for column selection in a view (`SubDataFrame` and `DataFrameRow`). In this case a view will have all columns of the parent after the parent is mutated.
"""

# ╔═╡ 5b92be5d-a394-4f8e-b081-bfaef94f3165
dfr

# ╔═╡ 3ac61fe5-3e33-44f5-86e9-c5945a2fc03c
parent(dfr), parentindices(dfr), rownumber(dfr)

# ╔═╡ e7da86b9-595c-450c-8a42-60cb63633982
md"""
**Note** that `parent` and `parentindices` refer to the true source of data for a `DataFrameRow` and `rownumber` refers to row number in the direct object that was used to create `DataFrameRow`
"""

# ╔═╡ 6a211df8-ec06-4c24-a362-5e7f604b944a
df₁ = DataFrame(a=1:4)

# ╔═╡ d552bdef-2106-4465-8244-0c8c2144d1f7
dfv₁ = view(df₁, [3, 2], :)

# ╔═╡ f4c515e2-a550-42ec-836c-6f6968c383bc
dfr₁ = dfv₁[1, :]

# ╔═╡ 31a0adc2-c50d-4949-a234-f71e36f46c3c
parent(dfr₁), parentindices(dfr₁), rownumber(dfr₁)

# ╔═╡ ca9927e5-3ff2-4c73-8206-4d7495668312
md"""
### Reordering rows

We create some random data frame (and hope that x.x is not sorted :), which is quite likely with 12 rows)

"""

# ╔═╡ 5d950253-8184-492f-a649-9123161ba9cc
x₀ = DataFrame(id=1:12, x = rand(12), y = [zeros(6); ones(6)])

# ╔═╡ 479bbe19-5faf-4f94-8fc2-45f541fffba2
md"""
Check if a DataFrame or a subset of its columns is sorted
"""

# ╔═╡ e68e64a2-4bd7-446d-b550-0cfa157831e4
issorted(x₀), issorted(x₀, :x)

# ╔═╡ 96733dd9-e40a-4ee9-85f9-082feab93144
sort!(x₀, :x)   ## in-place sort

# ╔═╡ 2d6c26cc-f52f-4d77-a17e-def865adc34c
y₀ = sort(x₀, :id)  ## Create new dataframe

# ╔═╡ e7b9b478-e2a7-4633-b963-2408a203ef33
md"""
We sort by two columns, first is decreasing, second is increasing.
"""

# ╔═╡ 9e30faf0-2617-43db-9177-64fad45f7b03
sort(x₀, [:y, :x], rev=[true, false])

# ╔═╡ fe56f87b-50df-4a8a-b531-232a0a51293c
@assert sort(x₀, [:y, :x], rev=[true, false]) == sort(x₀, [order(:y, rev=true), :x])
# 2 equivalent ways of sorting

# ╔═╡ 1c003f86-9321-4809-a4b9-a98add2ad39d
md"""
Now let us try some more fancy sorting stuff
"""

# ╔═╡ bace7faf-5eee-4f3d-af48-f9c7ff31ce27
sort(x₀, [order(:y, rev=true), order(:x, by=v -> -v)])
# :y descendant then :x  descendant (as well)

# ╔═╡ 09530879-8714-4332-a925-6f9298d6dc8b
md"""
And here is how we can reorder rows (here randomly)
"""

# ╔═╡ 00818c32-887c-4e3d-8a5f-18c1dcaedba1
x₀[shuffle(1:10), :]

# ╔═╡ 800fcbb6-e1ca-4c63-96af-30bf4680a97b
md"""
It is also easy to swap rows using broadcasted assignment
"""

# ╔═╡ 86916666-4b3e-49dd-bb97-1321f34097f4
begin
	sort!(x₀, :id)
	x₀[[1, 10], :] .= x₀[[10, 1], :]
	x₀
end

# ╔═╡ a4615e9b-d3d9-443a-9146-e411f9bbd749
md"""
### Merging/Adding rows
"""

# ╔═╡ b82a4d73-57f9-48fa-9c76-7d2cf853edd3
x₁ = DataFrame(rand(3, 5), :auto)

# ╔═╡ 6c8bd34e-2f6f-44bd-a6cd-e7b79597f7ae
md"""
merge by rows - dataframes must have the same column names; the same is `vcat`
"""

# ╔═╡ 69d2f06d-866a-4e95-92e9-9b4f06eaff04
[x₁; x₁]

# ╔═╡ 05bdca3f-033a-41c6-9dac-888b08b55cce
md"""
we can efficiently `vcat` a vector of DataFrames using `reduce`
"""

# ╔═╡ 6e4115db-22b2-487a-bda1-255364597014
reduce(vcat, [x₁, x₁, x₁])

# ╔═╡ b03679d0-2ecc-4b64-9aa6-735c6d18a407
md"""
get y with other order of names
"""

# ╔═╡ 1b40287c-b7b6-4ed8-bbe1-1141ef6fb04a
y₁ = x₁[:, reverse(names(x₁))]

# ╔═╡ bd4429da-3378-4392-9cd9-bba21ff9ec37
md"""
`vcat` is still possible as it does column name matching
"""

# ╔═╡ 4becce78-c6d2-43ec-a8ed-58edbf3290ff
vcat(x₁, y₁)

# ╔═╡ e741cbed-3da6-4713-ac85-93b8a683fd87
md"""
However column names must still match or...
"""

# ╔═╡ 39f529a7-8ffa-4149-9780-0d98feee2975
@test_throws ArgumentError vcat(x₁, y₁[:, 1:3])

# ╔═╡ 9df70270-20db-4a83-9c40-55edc37c99b4
md"""
...Unless we pass `:intersect`, `:union` or specific column names as keyword argument `cols`
"""

# ╔═╡ 037843b8-810b-4e78-8262-f9eb631986c4
vcat(x₁, y₁[:, 1:3], cols=:intersect)

# ╔═╡ 5a2aa52a-8739-4c49-95af-e2cd82a96523
vcat(x₁, y₁[:, 1:3], cols=:union)

# ╔═╡ af43af30-7221-4fbb-aef8-63f08956b010
vcat(x₁, y₁[:, 1:3], cols=[:x1, :x5])

# ╔═╡ 198ba262-a6b4-4a2f-8d87-23249c78cb99
md"""
`append!` modifies x in place
"""

# ╔═╡ dd22d95d-840d-41fa-8e1e-1cafb160676e
append!(x₁, y₁)

# ╔═╡ 7fea33ff-04e0-4d67-9bf5-d450269ed8a5
md"""
Standard `repeat` function works on rows; also `inner` and `outer` keyword arguments are accepted
"""

# ╔═╡ a8d073a5-e8ac-4cc0-a668-4bc478533c10
repeat(x₁, 2)

# ╔═╡ 6eb15245-caf0-4598-8741-00bb545ab7dd
md"""
`push!` adds one row to x at the end; one must pass a correct number of values unless `cols` keyword argument is passed.
"""

# ╔═╡ d43682a9-6399-404b-9d88-7a15a198eb08
push!(x₁, 1:5); x₁

# ╔═╡ 70fbd712-7a30-4776-ae1b-109a0a0ca1fe
md"""
It also works with dictionaries
"""

# ╔═╡ 8f227a0c-0083-4abf-8857-7518add1ace8
push!(x₁, Dict(:x1=> 11, :x2=> 12, :x3=> 13, :x4=> 14, :x5=> 15)); x₁

# ╔═╡ ed3ed957-056d-46c9-9877-10490d054122
md"""
and `NamedTuples` via name matching
"""

# ╔═╡ 83a04fdc-93c6-4377-ab35-c18151ca668c
push!(x₁, (x2=2, x1=1, x4=4, x3=3, x5=5))

# ╔═╡ 770b0df0-6095-4e39-b08e-ca49d64cd778
md"""
and DataFrameRow also via name matching
"""

# ╔═╡ 014c6489-a973-48fe-ad5d-a3f487b510b2
push!(x₁, x₁[1, :]); x₁

# ╔═╡ 84aed6e2-afbd-4657-8453-5e1afb91a061
md"""
For more consult the documentation of `push!`, `append!` and `vcat` for allowed values of `cols` keyword argument. This keyword argument governs the way these functions perform column matching of passed arguments. Also `append!` and `push!` support a `promote` keyword argument that decides if column type promotion is allowed.

Let us here just give a quick example of how heterogeneous data can be stored in the dataframe using these functionalities:
"""

# ╔═╡ 3f12d46a-786b-4a20-83ec-7eca121f2770
src = [(a=1, b=2), (a=missing, b=10, c=20), (b="s", c=1, d=1)]

# ╔═╡ b981bce1-dd57-4451-86a5-e9ee14eeb185
dfₛ = DataFrame();

# ╔═╡ 98973c9c-dcd3-44dc-835e-b60d2e7ac4b0
for r in src
    push!(dfₛ, r, cols=:union) ## if cols is :union then promote is true by default
end

# ╔═╡ fa7a9bc6-6609-4e84-93c6-7924bd653fde
dfₛ

# ╔═╡ cf16bd85-9da2-44ac-8e1a-051f04546b60
md"""
We see that `push!` dynamically added columns as needed and updated their element types
"""

# ╔═╡ 7ff49b11-9b98-4e8c-af33-f8ecbc2565c8
md"""
### Subsetting/Removing rows
"""

# ╔═╡ c1f5641e-adad-4ab5-8118-c7a9763cff6f
x₂ = DataFrame(id=1:10, val='a':'j')

# ╔═╡ 3db600e4-3fbb-4fb8-8df6-9438753f18b9
md"""
by using indexing
"""

# ╔═╡ 1474158e-c2aa-457b-abbb-f10089054f60
x₂[1:2, :]

# ╔═╡ d3050da0-db13-40e0-a5f5-bb290178f2a0
md"""
a single row selection creates a DataFrameRow
"""

# ╔═╡ b6972fc7-2d40-47b3-b8a1-825cfa59ef67
x₂[1, :]

# ╔═╡ e51b2218-e998-415f-aa9c-ae7582a04619
md"""
but this is a DataFrame.
"""

# ╔═╡ aaea674f-88c7-43b7-8327-31abdc145787
x₂[1:1, :]

# ╔═╡ df89ef70-ab45-4fdf-830a-154260225e2f
md"""
And here is a view.
"""

# ╔═╡ 02650cd1-e572-4d70-a39e-da92ce674659
view(x₂, 1:2, :)

# ╔═╡ 94ac6ebb-86b6-4e31-a64d-e4139b850316
md"""
selects columns 1 and 2
"""

# ╔═╡ d9a8ac15-2774-4e51-ab1b-8c0e95cf158c
view(x₂, :, 1:2)

# ╔═╡ d8d13d16-10ad-4e1e-8265-36516448e702
md"""
indexing by Bool, exact length math is required
"""

# ╔═╡ b85c34fc-9d01-4f7e-9104-452ec672101c
x₂[repeat([true, false], 5), :]

# ╔═╡ 7cd99843-0ead-4119-9221-7dd8c03d3b27
md"""
Alternatively we can also create a view
"""

# ╔═╡ d7e6404c-d0a9-4ee1-8372-edd4ee4fa520
view(x₂, repeat([true, false], 5), :)

# ╔═╡ bad9c21d-c35b-4e4e-8e18-51aeb41c57a9
md"""
we can delete one row in place
"""

# ╔═╡ 2d042183-e3f6-4e00-b86c-bdc1f11bc703
delete!(x₂, 7)

# ╔═╡ b80e344e-8550-4f47-bef6-69833a04277c
md"""
or a collection of rows, also in place
"""

# ╔═╡ 61d6ee03-b042-4f79-a017-dcd59e4eca80
delete!(x₂, 6:7)

# ╔═╡ 26e4a50f-eeb7-4bee-ba4e-e87c479c760c
md"""
We can also create a new DataFrame when deleting rows using `Not` indexing
"""

# ╔═╡ 0b758728-d696-4043-aa02-f3fc27189de8
x₂[Not(1:2), :]

# ╔═╡ 2a2a8d79-f872-4139-bd8e-9119ffed0bcd
x₂

# ╔═╡ 223176a3-aba4-4e6e-a5b1-c51d3ac0bf7e
md"""
Now let us move to row filtering
"""

# ╔═╡ d0d96bda-a7ca-4417-9992-7e3bd748d30b
x₃ = DataFrame([1:4, 2:5, 3:6], :auto)

# ╔═╡ 36088a45-a24c-4d43-9ca1-de665cccb25c
md"""
Let us create a new DataFrame where filtering function operates on DataFrameRow
"""

# ╔═╡ fa126b33-51b7-4aa7-9cc6-c7998ec93fcf
filter(r -> r.x1 > 2.5, x₃)

# ╔═╡ 60ba6b50-48ee-460d-bbd9-61e681d72c74
filter(r -> r.x1 > 2.5, x₃, view=true)  ## the same but as a view

# ╔═╡ 3edc2f7f-0b92-421b-ab10-0a5ee432eed0
filter(:x1 => >(2.5), x₃)               ## Another way

# ╔═╡ a7dcb351-fff6-4bff-96b9-287ca467dfc5
md"""
In place modification of x₃, an example with do-block syntax
"""

# ╔═╡ 04d68d08-4c9e-4e59-aeb4-8c1502aa64be
filter!(x₃) do r
    r.x1 > 2.5 && (return r.x2 < 4.5)
    r.x3 < 3.5
end

# ╔═╡ 616f8df8-0bc1-49eb-b161-6be6fe3dc932
md"""
A common operation is selection of rows for which a value in a column is contained in a given set. Here are a few ways in which you can achieve this.
"""

# ╔═╡ eb5af9d1-ab74-448f-a38b-8f6b1521a510
sdf₁ = DataFrame(x=1:12, y=mod1.(1:12, 4))

# ╔═╡ 9dbef9da-3d2a-4a47-98f3-48947176452a
md"""
We select rows for which column `y` has value 1 or 4.
"""

# ╔═╡ 1d1cccb2-d0ce-4cbb-b81c-5a887d382bf9
filter(r -> r.y ∈ [1, 4], sdf₁)

# ╔═╡ bcc5685b-13f8-40d8-a9de-953ae04f08dc
filter(:y => ∈([1,4]), sdf₁)

# ╔═╡ db9f9a23-2301-423e-b4ee-4d3de02a55f4
sdf₁[in.(sdf₁.y, Ref([1, 4])), :]

# ╔═╡ 31249b78-474f-4ccc-9ca4-540c9ef258aa
md"""
### Deduplicating
"""

# ╔═╡ 3d6aab60-11d2-4c78-bd84-4ceb3b5bfc20
begin
	xdf = DataFrame(A=[1,2], B=["x","y"])
	append!(xdf, xdf)
	xdf.C = 1:4
	xdf
end

# ╔═╡ 5b5da74e-c5ea-4154-8515-b625dc2b300d
md"""
Get first unique rows for given index
"""

# ╔═╡ cfaa2f0f-1228-4f61-81ad-a5cc08a901ed
unique(xdf, [1, 2])

# ╔═╡ a412e28a-e2d4-45af-a193-75bdefbecc75
md"""
Now we look at whole rows
"""

# ╔═╡ bcd063bb-3629-4060-a0e5-d1f56458cc36
unique(xdf)

# ╔═╡ 7be5c6d8-a7f2-4243-b50f-109f8090a636
md"""
Get indicators of non-unique rows
"""

# ╔═╡ 02a45544-c83b-4917-84ca-2b7f6474a771
nonunique(xdf, :A)

# ╔═╡ 488788a4-071e-4d29-96ba-319f235076a9
md"""
modify `xdf` in place
"""

# ╔═╡ e16de281-1676-430d-9049-9fd81d402dcb
unique!(xdf, :B)

# ╔═╡ 701b11d4-017f-48ea-998f-2203a58b98fb
md"""
### Extracting one row from a DataFrame into standard collections
"""

# ╔═╡ ee246276-451a-4fb1-ab79-81acef89a416
x = DataFrame(x=[1,missing,2], y=["a", "b", missing], z=[true,false,true])

# ╔═╡ dd5860ed-3bc2-44d4-ba25-c155a68f8e05
cols = [:y, :z]

# ╔═╡ c66d68b3-5410-4612-841b-ae53acc5ed69
md"""
We can use a conversion to a Vector or an Array
"""

# ╔═╡ d22c3b36-bfe0-4c70-bdd4-045c52098e34
Vector(x[1, cols])

# ╔═╡ 21f9eb7e-9b62-4e6a-b19e-0ff3648dd2bd
@assert Vector(x[1, cols]) == Array(x[1, cols]) # the same

# ╔═╡ 8a68f92e-265c-4099-b5db-42adfa4aa94e
md"""
Now we will get a vector of vectors
"""

# ╔═╡ 42960153-7f10-4e9e-952f-5fb084268191
[Vector(x[i, cols]) for i ∈ axes(x, 1)]

# ╔═╡ 69329cda-551a-4876-be22-a4b3cdda3c1d
md"""
It is easy to convert a DataFrameRow into a NamedTuple
"""

# ╔═╡ aa3c7475-c680-4c99-837a-5dae1e0ae789
copy(x[1, cols])

# ╔═╡ f4c268e8-a5a1-4bf7-861a-afb557b9bd43
md"""
or a Tuple
"""

# ╔═╡ 1e312c3c-8cde-4de3-8781-90cbe992a023
Tuple(x[1, cols])

# ╔═╡ 0b445f9c-a038-436f-b6e6-a7cacfdb43a6
md"""
### Working with a collection of rows of a DataFrame

We can use eachrow to get a vector-like collection of DataFrameRows
"""

# ╔═╡ 17f9c040-20b8-427f-b3d0-906480bea76c
ndf = DataFrame(reshape(1:12, 3, 4), :auto)

# ╔═╡ 629b54fb-7c55-4a11-b868-c7ff4a1c6e7d
er_df = eachrow(ndf)

# ╔═╡ 053ee3b7-ed9e-4111-86b0-491d0693b8c3
er_df[1]

# ╔═╡ 94f5aa8c-a4d4-42e9-ab67-60e90defa945
last(er_df)

# ╔═╡ bfd74ab1-6cfd-4f82-b72f-f5d32e198676
@assert last(er_df) == er_df[end]

# ╔═╡ e6b663c6-3fab-4b93-9fc4-151a3511236f
md"""
As DataFrameRows objects keeps connection to the parent dataframe you can get the columns of the parent using `getproperty`
"""

# ╔═╡ 06e1bd78-289c-47af-80fa-d83ae43e87f1
er_df.x1

# ╔═╡ 4134d57c-4472-48b9-a12b-cb6799d0cc75
md"""
### Flattening a DataFrame

Occasionally we have a dataframe whose one column is a vector of collections. We can expand (flatten) such a column using the `flatten` function
"""

# ╔═╡ 8e1c24ba-85c9-4d64-a038-3854d2f733c4
fdf = DataFrame(a = 'a':'c', b = [[1, 2, 3], [4, 5], 6])

# ╔═╡ 9aa32873-4528-4a6b-b0a4-031b0e105a55
flatten(fdf, :b)

# ╔═╡ 58a333e7-1d2c-4911-87d6-cbbfab505044

# ╔═╡ 1d8d14d1-cc81-486d-89e6-0d7266e44d7a
md"""
### Only one row

`only` from Julia Base is also supported in `DataFrames.jl` and succeeds if the dataframe has only one row, in which case it is returned.
"""

# ╔═╡ f2030d95-3dcf-450f-a1cb-acf284d4870a
udf = DataFrame(a=1)

# ╔═╡ d2e6ba37-05bd-4760-9ebc-409fc40adbde
only(udf)

# ╔═╡ b8d0b8cb-021d-42bd-90a6-858496b8a9c0
udf₂ = repeat(udf, 2)

# ╔═╡ fe183565-5068-49d3-b6a9-3bc5de81cc84
@test_throws ArgumentError only(udf₂)

# ╔═╡ c54cdbf1-7787-4f0f-b9ff-d4389f4a7c5e
html"""

"""

# ╔═╡ Cell order:
# ╟─b3dcf3f4-9d68-11eb-01a6-317fe0e0cd7e
# ╠═794a5356-6e05-44b4-ad9d-375a1fa3613b
# ╟─d6993a5b-8158-4ce3-accb-8e8c514229b3
# ╟─de97ded0-3952-4188-9a43-a8f383e1ae65
# ╠═9ccb6d8f-a8e0-42c2-8691-2eac247e591a
# ╠═385aee9f-5c17-4bd2-b3b2-58171fcc5990
# ╟─472fe012-dd24-46d3-b1b1-f217054c40f0
# ╠═147bcb72-1d40-458e-abb4-02c8d8ddfa76
# ╠═7699775c-8dec-4467-af21-89bd8354d409
# ╟─09617e8f-1d09-4145-831f-38fd98313dd1
# ╠═80277a09-24ea-4dae-9a41-39853a46fbaa
# ╟─9b61d582-6d26-483d-bb66-174b44b38ff4
# ╠═5e7c5b0f-be5d-43aa-b13a-4202b1cb34bf
# ╟─26a2e983-0a3c-41b3-ae2a-79395644e2d4
# ╠═f7dbfd3d-2450-4db3-9d25-d08011c62bf2
# ╠═cdee6bee-59d8-4fe4-80d9-183b1fa3e18b
# ╟─96b5f96f-57de-4fef-bc73-3f092b00be74
# ╠═912c5b66-38d9-49f4-881c-4f2a988104c2
# ╠═560497da-2699-411f-96f9-334919bfffdf
# ╟─d114f107-4e31-4585-915f-a17369023a74
# ╠═5b92be5d-a394-4f8e-b081-bfaef94f3165
# ╠═3ac61fe5-3e33-44f5-86e9-c5945a2fc03c
# ╟─e7da86b9-595c-450c-8a42-60cb63633982
# ╠═6a211df8-ec06-4c24-a362-5e7f604b944a
# ╠═d552bdef-2106-4465-8244-0c8c2144d1f7
# ╠═f4c515e2-a550-42ec-836c-6f6968c383bc
# ╠═31a0adc2-c50d-4949-a234-f71e36f46c3c
# ╟─ca9927e5-3ff2-4c73-8206-4d7495668312
# ╠═5d950253-8184-492f-a649-9123161ba9cc
# ╟─479bbe19-5faf-4f94-8fc2-45f541fffba2
# ╠═e68e64a2-4bd7-446d-b550-0cfa157831e4
# ╠═96733dd9-e40a-4ee9-85f9-082feab93144
# ╠═2d6c26cc-f52f-4d77-a17e-def865adc34c
# ╟─e7b9b478-e2a7-4633-b963-2408a203ef33
# ╠═9e30faf0-2617-43db-9177-64fad45f7b03
# ╠═fe56f87b-50df-4a8a-b531-232a0a51293c
# ╟─1c003f86-9321-4809-a4b9-a98add2ad39d
# ╠═bace7faf-5eee-4f3d-af48-f9c7ff31ce27
# ╟─09530879-8714-4332-a925-6f9298d6dc8b
# ╠═00818c32-887c-4e3d-8a5f-18c1dcaedba1
# ╟─800fcbb6-e1ca-4c63-96af-30bf4680a97b
# ╠═86916666-4b3e-49dd-bb97-1321f34097f4
# ╟─a4615e9b-d3d9-443a-9146-e411f9bbd749
# ╠═b82a4d73-57f9-48fa-9c76-7d2cf853edd3
# ╟─6c8bd34e-2f6f-44bd-a6cd-e7b79597f7ae
# ╠═69d2f06d-866a-4e95-92e9-9b4f06eaff04
# ╟─05bdca3f-033a-41c6-9dac-888b08b55cce
# ╠═6e4115db-22b2-487a-bda1-255364597014
# ╟─b03679d0-2ecc-4b64-9aa6-735c6d18a407
# ╠═1b40287c-b7b6-4ed8-bbe1-1141ef6fb04a
# ╟─bd4429da-3378-4392-9cd9-bba21ff9ec37
# ╠═4becce78-c6d2-43ec-a8ed-58edbf3290ff
# ╟─e741cbed-3da6-4713-ac85-93b8a683fd87
# ╠═39f529a7-8ffa-4149-9780-0d98feee2975
# ╟─9df70270-20db-4a83-9c40-55edc37c99b4
# ╠═037843b8-810b-4e78-8262-f9eb631986c4
# ╠═5a2aa52a-8739-4c49-95af-e2cd82a96523
# ╠═af43af30-7221-4fbb-aef8-63f08956b010
# ╟─198ba262-a6b4-4a2f-8d87-23249c78cb99
# ╠═dd22d95d-840d-41fa-8e1e-1cafb160676e
# ╟─7fea33ff-04e0-4d67-9bf5-d450269ed8a5
# ╠═a8d073a5-e8ac-4cc0-a668-4bc478533c10
# ╟─6eb15245-caf0-4598-8741-00bb545ab7dd
# ╠═d43682a9-6399-404b-9d88-7a15a198eb08
# ╟─70fbd712-7a30-4776-ae1b-109a0a0ca1fe
# ╠═8f227a0c-0083-4abf-8857-7518add1ace8
# ╟─ed3ed957-056d-46c9-9877-10490d054122
# ╠═83a04fdc-93c6-4377-ab35-c18151ca668c
# ╟─770b0df0-6095-4e39-b08e-ca49d64cd778
# ╠═014c6489-a973-48fe-ad5d-a3f487b510b2
# ╟─84aed6e2-afbd-4657-8453-5e1afb91a061
# ╠═3f12d46a-786b-4a20-83ec-7eca121f2770
# ╠═b981bce1-dd57-4451-86a5-e9ee14eeb185
# ╠═98973c9c-dcd3-44dc-835e-b60d2e7ac4b0
# ╠═fa7a9bc6-6609-4e84-93c6-7924bd653fde
# ╟─cf16bd85-9da2-44ac-8e1a-051f04546b60
# ╟─7ff49b11-9b98-4e8c-af33-f8ecbc2565c8
# ╠═c1f5641e-adad-4ab5-8118-c7a9763cff6f
# ╟─3db600e4-3fbb-4fb8-8df6-9438753f18b9
# ╠═1474158e-c2aa-457b-abbb-f10089054f60
# ╟─d3050da0-db13-40e0-a5f5-bb290178f2a0
# ╠═b6972fc7-2d40-47b3-b8a1-825cfa59ef67
# ╟─e51b2218-e998-415f-aa9c-ae7582a04619
# ╠═aaea674f-88c7-43b7-8327-31abdc145787
# ╟─df89ef70-ab45-4fdf-830a-154260225e2f
# ╠═02650cd1-e572-4d70-a39e-da92ce674659
# ╟─94ac6ebb-86b6-4e31-a64d-e4139b850316
# ╠═d9a8ac15-2774-4e51-ab1b-8c0e95cf158c
# ╟─d8d13d16-10ad-4e1e-8265-36516448e702
# ╠═b85c34fc-9d01-4f7e-9104-452ec672101c
# ╟─7cd99843-0ead-4119-9221-7dd8c03d3b27
# ╠═d7e6404c-d0a9-4ee1-8372-edd4ee4fa520
# ╟─bad9c21d-c35b-4e4e-8e18-51aeb41c57a9
# ╠═2d042183-e3f6-4e00-b86c-bdc1f11bc703
# ╟─b80e344e-8550-4f47-bef6-69833a04277c
# ╠═61d6ee03-b042-4f79-a017-dcd59e4eca80
# ╟─26e4a50f-eeb7-4bee-ba4e-e87c479c760c
# ╠═0b758728-d696-4043-aa02-f3fc27189de8
# ╠═2a2a8d79-f872-4139-bd8e-9119ffed0bcd
# ╟─223176a3-aba4-4e6e-a5b1-c51d3ac0bf7e
# ╠═d0d96bda-a7ca-4417-9992-7e3bd748d30b
# ╟─36088a45-a24c-4d43-9ca1-de665cccb25c
# ╠═fa126b33-51b7-4aa7-9cc6-c7998ec93fcf
# ╠═60ba6b50-48ee-460d-bbd9-61e681d72c74
# ╠═3edc2f7f-0b92-421b-ab10-0a5ee432eed0
# ╟─a7dcb351-fff6-4bff-96b9-287ca467dfc5
# ╠═04d68d08-4c9e-4e59-aeb4-8c1502aa64be
# ╟─616f8df8-0bc1-49eb-b161-6be6fe3dc932
# ╠═eb5af9d1-ab74-448f-a38b-8f6b1521a510
# ╟─9dbef9da-3d2a-4a47-98f3-48947176452a
# ╠═1d1cccb2-d0ce-4cbb-b81c-5a887d382bf9
# ╠═bcc5685b-13f8-40d8-a9de-953ae04f08dc
# ╠═db9f9a23-2301-423e-b4ee-4d3de02a55f4
# ╟─31249b78-474f-4ccc-9ca4-540c9ef258aa
# ╠═3d6aab60-11d2-4c78-bd84-4ceb3b5bfc20
# ╟─5b5da74e-c5ea-4154-8515-b625dc2b300d
# ╠═cfaa2f0f-1228-4f61-81ad-a5cc08a901ed
# ╟─a412e28a-e2d4-45af-a193-75bdefbecc75
# ╠═bcd063bb-3629-4060-a0e5-d1f56458cc36
# ╟─7be5c6d8-a7f2-4243-b50f-109f8090a636
# ╠═02a45544-c83b-4917-84ca-2b7f6474a771
# ╟─488788a4-071e-4d29-96ba-319f235076a9
# ╠═e16de281-1676-430d-9049-9fd81d402dcb
# ╟─701b11d4-017f-48ea-998f-2203a58b98fb
# ╠═ee246276-451a-4fb1-ab79-81acef89a416
# ╠═dd5860ed-3bc2-44d4-ba25-c155a68f8e05
# ╟─c66d68b3-5410-4612-841b-ae53acc5ed69
# ╠═d22c3b36-bfe0-4c70-bdd4-045c52098e34
# ╠═21f9eb7e-9b62-4e6a-b19e-0ff3648dd2bd
# ╟─8a68f92e-265c-4099-b5db-42adfa4aa94e
# ╠═42960153-7f10-4e9e-952f-5fb084268191
# ╟─69329cda-551a-4876-be22-a4b3cdda3c1d
# ╠═aa3c7475-c680-4c99-837a-5dae1e0ae789
# ╟─f4c268e8-a5a1-4bf7-861a-afb557b9bd43
# ╠═1e312c3c-8cde-4de3-8781-90cbe992a023
# ╟─0b445f9c-a038-436f-b6e6-a7cacfdb43a6
# ╠═17f9c040-20b8-427f-b3d0-906480bea76c
# ╠═629b54fb-7c55-4a11-b868-c7ff4a1c6e7d
# ╠═053ee3b7-ed9e-4111-86b0-491d0693b8c3
# ╠═94f5aa8c-a4d4-42e9-ab67-60e90defa945
# ╠═bfd74ab1-6cfd-4f82-b72f-f5d32e198676
# ╟─e6b663c6-3fab-4b93-9fc4-151a3511236f
# ╠═06e1bd78-289c-47af-80fa-d83ae43e87f1
# ╟─4134d57c-4472-48b9-a12b-cb6799d0cc75
# ╠═8e1c24ba-85c9-4d64-a038-3854d2f733c4
# ╠═9aa32873-4528-4a6b-b0a4-031b0e105a55
# ╠═58a333e7-1d2c-4911-87d6-cbbfab505044
# ╟─1d8d14d1-cc81-486d-89e6-0d7266e44d7a
# ╠═f2030d95-3dcf-450f-a1cb-acf284d4870a
# ╠═d2e6ba37-05bd-4760-9ebc-409fc40adbde
# ╠═b8d0b8cb-021d-42bd-90a6-858496b8a9c0
# ╠═fe183565-5068-49d3-b6a9-3bc5de81cc84
# ╟─c54cdbf1-7787-4f0f-b9ff-d4389f4a7c5e


using StochasticSearch
using FactCheck

include("parameter.jl")
include("configuration.jl")
include("run.jl")
include("simulated_annealing.jl")
include("iterative_first_improvement.jl")
include("iterative_probabilistic_improvement.jl")
include("randomized_first_improvement.jl")
include("iterative_greedy_construction.jl")
include("iterated_local_search.jl")
include("unit_value.jl")

FactCheck.exitstatus()


module RoesserModels

VERSION < v"0.4-" && using Docile

@doc doc"""
The Roesser model is a 2D state-space model, useful for 2D spatial dynamics
or a combination of 1D spatial dynamics and time.

References:
  \cite{Roesser:1975}
"""->
type RoesserModel
  A1::Array{Float64}
  A2::Array{Float64}
  A3::Array{Float64}
  A4::Array{Float64}
  B1::Array{Float64}
  B2::Array{Float64}
  C1::Array{Float64}
  C2::Array{Float64}
  D::Array{Float64}
  in_dim::Int
  out_dim::Int
  x_h_dim::Int
  x_v_dim::Int
  function RoesserModel(A1, A2, A3, A4, B1, B2, C1, C2, D)
    x_h_dim = size(A1, 1)
    x_v_dim = size(A4, 1)
    out_dim = size(C1, 1)
    in_dim = size(B1, 2)

    if ((size(A1, 2) != x_h_dim) || (size(A3,2) != x_h_dim)
        || (size(B1,1) != x_h_dim) || (size(C1,2) != x_h_dim)
        || (size(A2,1) != x_h_dim))
      error("Matrix dimensions for Horizontal states do not agree");
    end
    if  ((size(A2,2) != x_v_dim) || (size(A4,1) != x_v_dim)
         || (size(A4,2) != x_v_dim) || (size(B2,1) != x_v_dim)
         || (size(C2,2) != x_v_dim))
      error("Matrix dimensions for Vertical states do not agree");
    end
    if ((size(C2,1) != out_dim) || (size(D,1) != out_dim)
        || (size(B2,2) != in_dim) || (size(D,2) != in_dim))
      error("Matrix dimensions for Input/Output do not agree")
    end
    new(A1, A2, A3, A4, B1, B2, C1, C2, D, in_dim, out_dim, x_h_dim, x_v_dim)
  end
end

@doc doc"""
The CRSD (Causal Recursive Separable in Denominator) model is a specific type
of Roesser Models.

References:
  \cite{Roesser:1975}
"""->
function CrsdRoesserModel(A1, A2, A4, B1, B2, C1, C2, D)
  x_h_dim = size(A1, 1)
  x_v_dim = size(A4, 1)
  out_dim = size(C1, 1)
  in_dim = size(B1, 2)
  A3 = zeros(x_v_dim, x_h_dim);
  return RoesserModel(A1, A2, A3, A4, B1, B2, C1, C2, D)
end

@doc doc"""
Simulates the Roesser model in an iterative approach.

"""->
function simulate(system::RoesserModel, inputs)
  if system.in_dim > 1 && size(inputs,3) != system.in_dim
    error("Input dimension mismatch")
  end

  M = size(inputs,1)
  N = size(inputs,2)
  output = zeros(M, N, system.out_dim)
  x_h = zeros(size(inputs,1)+1, size(inputs,2)+1, system.x_h_dim)
  x_v = zeros(size(inputs,1)+1, size(inputs,2)+1, system.x_v_dim)

  A1 = system.A1
  A2 = system.A2
  A3 = system.A3
  A4 = system.A4
  B1 = system.B1
  B2 = system.B2
  C1 = system.C1
  C2 = system.C2
  D = system.D
  for i = 1:M
    for j = 1:N
      input_ij = vec(inputs[i,j,1:end])';
      x_h[i+1,j, 1:end] = A1*vec(x_h[i,j, 1:end]) + A2*vec(x_v[i,j,1:end]) + B1 * input_ij
      x_v[i,j+1, 1:end] = A3*vec(x_h[i,j, 1:end]) + A4*vec(x_v[i,j,1:end]) + B2 * input_ij
      output[i,j, 1:end] = C1 * vec(x_h[i,j, 1:end]) + C2 * vec(x_v[i,j,1:end]) + D * input_ij
    end
  end
  return output
end

@doc doc"""
Generate a completely random system. No check is performed for observability
or controllability.

"""->
function randomsystem(nh::Integer, nv::Integer, outputs::Integer, inputs::Integer)
  A1 = randn(nh, nh);
  A2 = randn(nh, nv);
  A3 = randn(nv, nh);
  A4 = randn(nv, nv);
  C1 = randn(outputs, nh);
  C2 = randn(outputs, nv);
  B1 = randn(nh, inputs);
  B2 = randn(nv, inputs);
  D = randn(outputs, inputs);

  return RoesserModel(A1, A2, A3, A4, B1, B2, C1, C2, D);
end

@doc doc"""
Tests the model for global controllability, according to the criteria
formulated by Kung in 1976.

References:
  \cite{Kung:1976}
"""->
function is_global_controllable(system::RoesserModel, M::Integer, N::Integer)

end

@doc doc"""
Tests the model for global controllability, according to the criteria
formulated by Kung in 1976.

References:
  \cite{Kung:1976}
"""->
function is_global_observable(system::RoesserModel, M::Integer, N::Integer)

end

@doc doc"""
Tests the model for local controllability, according to the criteria
formulated by Roesser in 1975.

References:
  \cite{Roesser:1975}
  \cite{Kung:1976}
"""->
function is_local_controllable(system::RoesserModel)

end

@doc doc"""
Tests the model for local observability, according to the criteria
formulated by Roesser in 1975.

References:
  \cite{Roesser:1975}
  \cite{Kung:1976}
"""->
function is_local_observable(system::RoesserModel)

end

@doc doc"""
Tests the model for modal controllability, according to the criteria
formulated by Kung in 1976.

References:
  \cite{Kung:1976}
"""->
function is_modal_controllable(system::RoesserModel)

end

@doc doc"""
Tests the model for modal observability, according to the criteria
formulated by Kung in 1976.

References:
  \cite{Kung:1976}
"""->
function is_modal_observable(system::RoesserModel)

end

end # module