#error analysis of fault prediction network
cd(@__DIR__)
using Pkg
Pkg.activate(".")

using Flux
using Plots
using BSON: @load
using Random
using Statistics

#load network to test
@load "Models/Fault_Model.bson" Fault_Model

#function for parsing data
function get_data(filename)
    f1 = open(filename)
    tempdata = readlines(f1)
    tempdata[1] = tempdata[1][4:end]  #cutting of beggining bc some weird chars
    close(f1)
    data = []
    regvec = zeros(length(tempdata),
                   length(split(tempdata[1], ",")))
    #parsing datavector to float array... not regularized
    for i in 1:length(tempdata)
        regvec[i,:] =  parse.(Float64,split(tempdata[i], ","))
    end
    return regvec
end

#normalizing data
function norm_data(data, scalingmatrix=nothing)
    #getting regularization values and normalizing data
    regvec = data
    normvec = zeros(length(regvec[:,1]),
                    length(regvec[1,:]))
    #checking if optional scalingmatrix was passed, if not will calc mean and std
    if scalingmatrix == nothing
        scalingmatrix = zeros(length(regvec[1,:]),2)
        for i in 1:length(regvec[1,:])
            scalingmatrix[i,1] = Statistics.mean(regvec[:,i]) #first index is mean
            scalingmatrix[i,2] = Statistics.std(regvec[:,i])  #second index is std
        end
        for i in 1:length(regvec[1,:])
            #normalizing regvec
            normvec[:,i] = (regvec[:,i] .- scalingmatrix[i,1]) ./ scalingmatrix[i,2]
        end
    else
        #for case of normalizing test data by training std and mean
        for i in 1:length(regvec[1,:])
            #normalizing regvec
            normvec[:,i] = (regvec[:,i] .- scalingmatrix[i,1]) ./ scalingmatrix[i,2]
        end
    end
    return normvec, scalingmatrix
end

#function to convert vector of vector to matrix
function vecvec_to_matrix(vecvec)
    dim1 = length(vecvec)
    dim2 = length(vecvec[1])
    my_array = zeros(dim1, dim2)
    for i in 1:dim1
        for j in 1:dim2
            my_array[i,j] = vecvec[i][j]
        end
    end
    return my_array
end

#getting training data
fname = "Dataset/FINAL_DATA.txt"
data_tmp = get_data(fname)
#seperating test and train data
train_data_tmp = []; test_data_tmp = [];
for i in 1:length(data_tmp[:,1])
    if data_tmp[i,2] in [0.3,0.38,0.46,0.53,0.61,0.65] #training case
        push!(train_data_tmp, data_tmp[i,:])
    else #testing case
        push!(test_data_tmp, data_tmp[i,:])
    end
end
train_data = vecvec_to_matrix(train_data_tmp)
test_data = vecvec_to_matrix(test_data_tmp)
#normalizing data
train_norm, scalingmatrix = norm_data(train_data)
test_norm, scalingmatrix = norm_data(test_data,scalingmatrix)
data_norm, scalingmatrix = norm_data(data_tmp,scalingmatrix)
#converting data to format which works with network
data = []
for i in 1:length(train_norm[:,1])
    #adjust x and y for fault detection here
    push!(data,(train_norm[i,[1,2,3]],train_norm[i,4]))
end

#Normalized Root Mean Squared Error
function RMSE(data_set,model)
    #calculate root mean squared error
    N = length(data_set[:,1])
    print(N,'\n')
    MSE_sum = 0.0
    for i in 1:N
        MSE_sum += 1/N * (data_set[i,3] - model(data_set[i,[1,2,4]])[1])^2.0
    end
    return sqrt(MSE_sum)  # no norm needed bc data is already normalized
end

print("Error Anlysis")
print("\nOverall root mean squared error: ")
print(RMSE(data_norm,Fault_Model))
print("\nTest root mean squared error: ")
print(RMSE(test_norm,Fault_Model))
print("\nTrain root mean squared error: ")
print(RMSE(train_norm,Fault_Model))
#TODO: represent interval of prediction (normalied goes -2 to 0.5 ish)

#plotting
alt_mean = scalingmatrix[1,1]
alt_std = scalingmatrix[1,2]
mach_SCAT = [];pressure_SCAT = [];fault_SCAT = [];model_SCAT = [];
specified_alt = (12000 - alt_mean)/alt_std
for i in 1:length(test_norm[:,1])
    if test_norm[i,1] == specified_alt
        push!(mach_SCAT,test_norm[i,2])
        push!(fault_SCAT,test_norm[i,3])
        push!(pressure_SCAT,test_norm[i,4])
        push!(model_SCAT,Fault_Model(test_norm[i,[1,2,4]])[1])
    end
end
#creating 3d scatter for showing network results
scatter(pressure_SCAT,mach_SCAT,fault_SCAT,label="Actual",
        title="Predictions with Testing Data [All]",
        xlabel="Pressure",
        ylabel="Mach",
        zlabel="Fault Parameter")
scatter!(pressure_SCAT,mach_SCAT,model_SCAT,label="Predicted")


using SNOW
using Test
using Zygote

checkallocations = false
snopttest = false

@testset "derivatives" begin

function test1!(g, x)

    f = x[1]^2 - x[2]

    Zygote.ignore() do
        g[1] = x[2] - 2*x[1]
        g[2] = -x[2]
        g[3] = x[1]^2
    end
    return f
end

# ---- test sparsity pattern  --------
ng = 3
lx = [-5; -5]
ux = [5; 5]
sp = SparsePattern(ForwardAD(), test1!, ng, lx, ux)
@test sp.rows == [1, 3, 1, 2]
@test sp.cols == [1, 1, 2, 2]
# -------------------------------------

# ----- test derivatives ------------
nx = 2
ng = 3
g = zeros(ng)
df = zeros(nx)
dg = zeros(ng*nx)
x = [1.0, 2.0]

# forward AD
cache = SNOW.createcache(DensePattern(), ForwardAD(), test1!, nx, ng)
SNOW.evaluate!(g, df, dg, x, cache)

@test df == [2*x[1]; -1.0]
@test dg == [-2.0, 0.0, 2*x[1], 1.0, -1.0, 0.0]

# reverse AD
cache = SNOW.createcache(DensePattern(), ReverseAD(), test1!, nx, ng)
SNOW.evaluate!(g, df, dg, x, cache)
@test df == [2*x[1]; -1.0]
@test dg == [-2.0, 0.0, 2*x[1], 1.0, -1.0, 0.0]

# finite diff
cache = SNOW.createcache(DensePattern(), ForwardFD(), test1!, nx, ng)
SNOW.evaluate!(g, df, dg, x, cache)

@test isapprox(df, [2*x[1]; -1.0])
@test isapprox(dg, [-2.0, 0.0, 2*x[1], 1.0, -1.0, 0.0])

# sparse with reverse and forward
dg = zeros(length(sp.rows))
cache = SNOW.createcache(sp, [ReverseAD(), ForwardAD()], test1!, nx, ng)
SNOW.evaluate!(g, df, dg, x, cache)

@test df == [2*x[1]; -1.0]
@test dg == [-2.0, 2*x[1], 1.0, -1.0]

# sparse with zygote and forward
dg = zeros(length(sp.rows))
cache = SNOW.createcache(sp, [RevZyg(), ForwardAD()], test1!, nx, ng)
SNOW.evaluate!(g, df, dg, x, cache)

@test df == [2*x[1]; -1.0]
@test dg == [-2.0, 2*x[1], 1.0, -1.0]

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

end

# if checkallocations
# # -------- test allocations --------------
#     using BenchmarkTools

#     function test2!(g, x)
#         f = x[1]^2 - x[2]

#         Zygote.ignore() do
#             nx = length(x)
#             for i = 1:nx
#                 g[i] = x[i]^2
#             end
#             g[nx+1:end] .= x[1]
#         end
#         return f
#     end

#     nx = 300
#     ng = 500
#     g = zeros(ng)
#     df = zeros(nx)
#     dg = zeros(ng*nx)
#     x = 2*ones(nx)
#     cache = SNOW.createcache(DensePattern(), ForwardAD(), test2!, nx, ng)
#     @btime SNOW.evaluate!($g, $df, $dg, $x, $cache)
#     # 577.123 μs (6 allocations: 2.30 MiB)

#     cache = SNOW.createcache(DensePattern(), ReverseAD(), test2!, nx, ng)
#     @btime SNOW.evaluate!($g, $df, $dg, $x, $cache)
#     # 4.188 ms (6 allocations: 2.30 MiB)

#     cache = SNOW.createcache(DensePattern(), FD("forward"), test2!, nx, ng)
#     @btime SNOW.evaluate!($g, $df, $dg, $x, $cache)
#     # 358.457 μs (6 allocations: 2.30 MiB)

#     lx = -5*ones(nx)
#     ux = 5*ones(nx)
#     sp = SparsePattern(ForwardAD(), test2!, ng, lx, ux)
#     dg = zeros(length(sp.rows))
#     cache = SNOW.createcache(sp, [ReverseAD(), ForwardAD()], test2!, nx, ng)
#     @btime SNOW.evaluate!($g, $df, $dg, $x, $cache)
#     # 17.391 μs (0 allocations: 0 bytes)

#     cache = SNOW.createcache(sp, [RevZyg(), ForwardAD()], test2!, nx, ng)
#     @btime SNOW.evaluate!($g, $df, $dg, $x, $cache)
#     # 9.865 μs (6 allocations: 11.63 KiB)
# end
# # ----------------------------------------

@testset "optimization" begin

function barnes(g, x)

    a1 = 75.196
    a3 = 0.12694
    a5 = 1.0345e-5
    a7 = 0.030234
    a9 = 3.5256e-5
    a11 = 0.25645
    a13 = 1.3514e-5
    a15 = -5.2375e-6
    a17 = 7.0e-10
    a19 = -1.6638e-6
    a21 = 0.0005
    a2 = -3.8112
    a4 = -2.0567e-3
    a6 = -6.8306
    a8 = -1.28134e-3
    a10 = -2.266e-7
    a12 = -3.4604e-3
    a14 = -28.106
    a16 = -6.3e-8
    a18 = 3.4054e-4
    a20 = -2.8673

    x1 = x[1]
    x2 = x[2]
    y1 = x1*x2
    y2 = y1*x1
    y3 = x2^2
    y4 = x1^2

    # --- function value ---

    f = a1 + a2*x1 + a3*y4 + a4*y4*x1 + a5*y4^2 +
        a6*x2 + a7*y1 + a8*x1*y1 + a9*y1*y4 + a10*y2*y4 +
        a11*y3 + a12*x2*y3 + a13*y3^2 + a14/(x2+1) +
        a15*y3*y4 + a16*y1*y4*x2 + a17*y1*y3*y4 + a18*x1*y3 +
        a19*y1*y3 + a20*exp(a21*y1)

    # --- constraints ---

    g[1] = 1 - y1/700.0
    g[2] = y4/25.0^2 - x2/5.0
    g[3] = (x1/500.0- 0.11) - (x2/50.0-1)^2

    return f
end

x0 = [10.0; 10.0]
lx = [0.0; 0.0]
ux = [65.0; 70.0]

ng = 3
options = Options(solver=IPOPT(), derivatives=ForwardFD())
xopt, fopt, info, out = minimize(barnes, x0, ng, lx, ux, -Inf, 0.0, options)

@test isapprox(xopt[1], 49.5263; atol=1e-4)
@test isapprox(xopt[2], 19.6228; atol=1e-4)
@test isapprox(fopt, -31.6368; atol=1e-4)
@test info == :Solve_Succeeded || info == :Solved_To_Acceptable_Level

if snopttest
    options = Options(solver=SNOPT(), derivatives=ForwardAD())
    xopt, fopt, info, out = minimize(barnes, x0, ng, lx, ux, -Inf, 0.0, options)

    @test isapprox(xopt[1], 49.5263; atol=1e-4)
    @test isapprox(xopt[2], 19.6228; atol=1e-4)
    @test isapprox(fopt, -31.6368; atol=1e-4)
    @test info == "Finished successfully: optimality conditions satisfied"

    options = Options(solver=SNOPT(), derivatives=ComplexStep())
    xopt, fopt, info, out = minimize(barnes, x0, ng, lx, ux, -Inf, 0.0, options)

    @test isapprox(xopt[1], 49.5263; atol=1e-4)
    @test isapprox(xopt[2], 19.6228; atol=1e-4)
    @test isapprox(fopt, -31.6368; atol=1e-4)
    @test info == "Finished successfully: optimality conditions satisfied"
end

function sparsegrad(g, x)

    f = x[1]^2 - x[2]

    g[1] = x[2] - 2*x[1]
    g[2] = -x[2]

    return f
end

x0 = [0.0; 0.0]
lx = [-10.0, -10.0]
ux = [10.0, 10.0]
ng = 2

# detect sparsity pattern
sp = SparsePattern(ForwardAD(), sparsegrad, ng, lx, ux)

if snopttest
    options = Options(solver=SNOPT(), derivatives=[ReverseAD(), ForwardAD()], sparsity=sp)
    xopt, fopt, info, out = minimize(sparsegrad, x0, ng, lx, ux, -Inf, 0.0, options)

    @test isapprox(xopt[1], 1.0; atol=1e-6)
    @test isapprox(xopt[2], 2.0; atol=1e-6)
    @test isapprox(fopt, -1.0; atol=1e-5)
    @test info == "Finished successfully: optimality conditions satisfied"
end

end

# @testset "problem format" begin

# function test3!(g, prob, x)
#     return nothing
# end

# prob = SNOW.createproblem(test3!, "test3")

# x = [1.0, 2.0, 3.0]
# lx = [0.0, 0.0, 0.0]
# ux = 10*ones(3)
# names = ["c1", "c2", "c3"]
# SNOW.add_dv!(prob, x, lx, ux, names)

# x2 = [8.0, 2.0]
# lx2 = [0.0, 0.0, 0.0]
# ux2 = 5*ones(3)
# names2 = ["t1", "t2"]
# SNOW.add_dv!(prob, x2, lx2, ux2, names2)

# x = rand(5)
# SNOW.get_dvs(prob, x)

# end


using .JuMP

struct DiscretizedFunctionVariable{S<:AbstractRealLineDiscretizer,V,M}
    discretizer::S
    finite_param::V
    model::M
end

function Base.show(io::IO,  discvar::DiscretizedFunctionVariable)
    println(io, "DiscretizedFunctionVariable with JuMP param:")
    Base.show(io, discvar.finite_param)
end

function add_discretized_function!(model, discretizer)
    n = length(discretizer)
    tmp_vars = @variable(model, [i = 1:n])
    DiscretizedFunctionVariable(discretizer, tmp_vars, model)
end

function (f::DiscretizedFunctionVariable)(z)
    idx = findfirst(f.discretizer, z)
    f.finite_param[idx]
end

function JuMP.value(f::DiscretizedFunctionVariable)
    DiscretizedFunction(f.discretizer, JuMP.value.(f.finite_param))
end

function JuMP.value(f::DiscretizedFunctionVariable, flift)
    DiscretizedFunction(discretizer = f.discretizer,
        dictionary = Dictionary(flift.(f.discretizer), JuMP.value.(f.finite_param)))
end


using GLVisualize, GLAbstraction, Reactive, GeometryTypes, Colors, GLWindow
import GLVisualize: widget, mm, play_slider

if !isdefined(:runtests)
    window = glscreen()
end

description = """
Usage of the play slider widget
"""

iconsize = 8mm
editarea, viewarea = x_partition_abs(window.area, round(Int, 8.2 * iconsize))
edit_screen = Screen(window, area = editarea)
viewscreen = Screen(window, area = viewarea)

function xy_data(x,y,i, N)
    x = ((x/N)-0.5f0)*i
    y = ((y/N)-0.5f0)*i
    r = sqrt(x*x + y*y)
    Float32(sin(r)/r)
end
surf(i, N) = Float32[xy_data(x, y, i, N) for x=1:N, y=1:N]

play_viz, slider_value = play_slider(
    edit_screen, iconsize, linspace(1f0, 50f0, 100)
)
color_viz, color_s = widget(RGBA{Float32}.(colormap("Blues", 7)), edit_screen)

# startstop is not needed here, since the slider value will start and stop
my_animation = map(slider_value) do t
    surf(t, 128)
end

# pair of name => any visualization object
# will be layouted nicely
controls = Pair[
    "color" => color_viz,
    "play" => play_viz,
]

_view(visualize(my_animation, :surface, color_map = color_s), viewscreen)
_view(visualize(
    controls,
    text_scale = 4mm,
    width = 8iconsize
), edit_screen, camera = :fixed_pixel)

if !isdefined(:runtests)
    renderloop(window)
end


@testset "Step" begin
    function _getbucket(t, buckets)
        i = findlast(x -> t > x, cumsum(buckets))
        i = isnothing(i) ? 1 : i + 1

        return i
    end

    λ = rand()
    γ = rand()
    step_sizes = [rand(1:10), rand(1:10)]
    s = Step(λ = λ, γ = γ, step_sizes = step_sizes)
    @test s == Step(λ, γ, step_sizes)
    @test Step(λ, γ, 1).step_sizes == [1]
    @test all(λ == s(t) for t in 1:step_sizes[1])
    @test all(λ * γ == s(t) for t in (step_sizes[1] + 1):(step_sizes[1] + step_sizes[2] + 1))
    @test all(p == s(t) for (t, p) in zip(1:100, s))
    @test Base.IteratorEltype(typeof(s)) == Base.HasEltype()
    @test eltype(s) == eltype(λ)
    @test Base.IteratorSize(typeof(s)) == Base.IsInfinite()
    @test axes(s) == (OneToInf(),)
end

@testset "Exp" begin
    λ = rand()
    γ = rand()
    s = Exp(λ = λ, γ = γ)
    @test s == Exp(λ, γ)
    @test all(λ * γ^(t - 1) == s(t) for t in 1:100)
    @test all(p == s(t) for (t, p) in zip(1:100, s))
    @test Base.IteratorEltype(typeof(s)) == Base.HasEltype()
    @test eltype(s) == eltype(λ)
    @test Base.IteratorSize(typeof(s)) == Base.IsInfinite()
    @test axes(s) == (OneToInf(),)
end

@testset "Poly" begin
    λ = rand()
    p = rand(1:20)
    max_iter = rand(1:100)
    s = Poly(λ = λ, p = p, max_iter = max_iter)
    @test s == Poly(λ, p, max_iter)
    @test all(λ * (1 - (t - 1) / max_iter)^p == s(t) for t in 1:max_iter)
    @test all(p == s(t) for (t, p) in zip(1:max_iter, s))
    @test Base.IteratorEltype(typeof(s)) == Base.HasEltype()
    @test eltype(s) == eltype(λ)
    @test Base.IteratorSize(typeof(s)) == Base.HasLength()
    @test length(s) == max_iter
    @test_throws BoundsError s(max_iter + 1)
    @test axes(s) == 1:length(s)
end

@testset "Inv" begin
    λ = rand()
    γ = rand()
    p = rand(1:20)
    s = Inv(λ = λ, p = p, γ = γ)
    @test s == Inv(λ, γ, p)
    @test all(λ / (1 + (t - 1) * γ)^p == s(t) for t in 1:100)
    @test all(p == s(t) for (t, p) in zip(1:100, s))
    @test Base.IteratorEltype(typeof(s)) == Base.HasEltype()
    @test eltype(s) == eltype(λ)
    @test Base.IteratorSize(typeof(s)) == Base.IsInfinite()
    @test axes(s) == (OneToInf(),)
end


module CompScienceMeshes

using DelimitedFiles
using LinearAlgebra
using SparseArrays

using StaticArrays
using Compat
using Requires

import Base.getindex

export getcommonedge

# defaults
export index
export euclidianbasis, point

# default mesh creation
export mesh, readmesh, writemesh
export meshsegment, meshrectangle, meshcircle, meshsphere, meshcuboid
export subdMesh
# mesh interface
export dimension, universedimension, vertextype, coordtype
export numvertices, vertices
export numcells, cells
export boundary, skeleton, vertextocellmap
export connectivity, cellpairs # marked for deprecation

# mesh transforms
export translate, translate!, rotate, rotate!
export fliporientation!, fliporientation
export weld

# mesh refinement
export barycentric_refinement, bisecting_refinement

export  Loop_subdivision

# submesh selection and cell retrieval
export GSubdMesh,submesh
export octree, boundingbox
export overlap_gpredicate, interior_tpredicate, inclosure_gpredicate
export interior_vpredicate

export mapper, restriction

# link to the chart concept
export chart

export domain # marked for deprecation
export dimension, universedimension
export volume
export neighborhood
export quadpoints

export isinside, isinclosure, overlap

# specific to simplicial charts
export simplex, center, vertices
export barytocart, carttobary

export intersection

# the neighborhood concept
export barycentric

#export paramtype
export cartesian, parametric
export jacobian, tangents, normal

export trgauss, sqgauss, legendre

# marked for deprecation
export SegmentedAxis
export minmaxdist, rings, ring

using SparseArrays

Pt{N,T} = StaticArrays.SVector{N,T}

include("defaults.jl")
include("utils.jl")
# include("combinatorics.jl")

# quadrature rules for segements, triangles, and squares
include("quadrature/SegmentGauss.jl")
include("quadrature/TriangleGauss.jl")
include("quadrature/SquareGauss.jl")

# mesh component
include("mesh.jl")
include("scomplex.jl")
include("meshes/flippedmesh.jl")
include("meshes/embedding.jl")
include("subdMesh.jl")

# simplices and related algorithms
include("rectangle.jl")
include("charts.jl")
include("subd_chart.jl")
include("sphere.jl")
include("overlap.jl")
include("intersect.jl")
include("isinside.jl")
include("findchart.jl")
include("neighborhood.jl")
include("subd_neighborhood.jl")
include("quadpoints.jl")

include("submesh.jl")
# include("flipped.jl")
include("timeaxis.jl")
include("subd_shape.jl")
include("gaussquarature.jl")
include("fileio/TRI_mesh.jl")
include("fileio/readmesh.jl")
include("fileio/gmsh.jl")
include("fileio/gid.jl")
include("primitives.jl")
#include("../examples/waveguide_with_post.jl")
include("baryref.jl")
include("subdivision.jl")
include("weld.jl")

include("mapper.jl")
include("restrict.jl")

include("plotlyjs_glue.jl")

end # module


export qr, qr_back, copyltu!, lq, lq_back

"""
		qr(A) -> Tuple{AbstractMatrix, AbstractMatrix}

private QR method, call LinearAlgebra.qr to achieve forward calculation, while
return Tuple type
"""
function qr(A)
	res = LinearAlgebra.qr(A)
	Matrix(res.Q), res.R
end

"""
		qr(A, pivot) -> Tuple{AbstractMatrix, AbstractMatrix, AbstractVector}
"""
function qr(A::AbstractMatrix, pivot::Val{true})
    res = LinearAlgebra.qr(A, pivot)
    Q, R, P = Matrix(res.Q), res.R, res.P
end

"""
		lq(A) -> Tuple{AbstractMatrix, AbstractMatrix}
"""
function lq(A)
		res = LinearAlgebra.lq(A)
    res.L, Matrix(res.Q)
end

trtrs!(c1::Char, c2::Char, c3::Char, r::AbstractMatrix, b::AbstractVecOrMat) = LinearAlgebra.LAPACK.trtrs!(c1, c2, c3, r, b)

"""
    copyltu!(A::AbstractMatrix) -> AbstractMatrix

copy the lower triangular to upper triangular.
"""
function copyltu!(A::AbstractMatrix)
    m, n = size(A)
    for i=1:m
        A[i,i] = real(A[i,i])
        for j=i+1:n
            @inbounds A[i,j] = conj(A[j,i])
        end
    end
    A
end

"""
    qr_back_fullrank(q, r, dq, dr) -> Matrix

backward for QR decomposition, for input matrix (in forward pass) with M > N.

References:
    Seeger, M., Hetzel, A., Dai, Z., Meissner, E., & Lawrence, N. D. (2018). Auto-Differentiating Linear Algebra.
"""
@generated function qr_back_fullrank(q, r, dq, dr)
    dqnot0 = !(dq <: Nothing)
    drnot0 = !(dr <: Nothing)
    (!dqnot0 && !drnot0) && return :(nothing)
    ex = drnot0 && dqnot0 ? :(r*dr' - dq'*q) : (dqnot0 ? :(-dq'*q) : :(r*dr'))
    :(b = $(dqnot0 ? :(dq) : :(ZeroAdder())) + q*copyltu!($ex);
    trtrs!('U', 'N', 'N', r, do_adjoint(b))')
end

do_adjoint(A::Matrix) = Matrix(A')

"""
    qr_back(A, q, r, dq, dr) -> Matrix

backward for QR decomposition, for an arbituary shaped input matrix.

References:
    Seeger, M., Hetzel, A., Dai, Z., Meissner, E., & Lawrence, N. D. (2018). Auto-Differentiating Linear Algebra.
    Differentiable Programming Tensor Networks, Hai-Jun Liao, Jin-Guo Liu, Lei Wang, Tao Xiang
"""
@generated function qr_back(A, q, r, dq, dr)
    dqnot0 = !(dq <: Nothing)
    drnot0 = !(dr <: Nothing)
    (!dqnot0 && !drnot0) && return :(nothing)
    ex = quote
        size(r, 1) == size(r, 2) && return qr_back_fullrank(q, r, dq ,dr)
        M, N = size(r)
        B = view(A,:,M+1:N)
        U = view(r,:,1:M)
        D = view(r,:,M+1:N)
        $(if drnot0
            :(dD = view(dr,:,M+1:N);
            da = qr_back_fullrank(q, U, $(dqnot0 ? :(dq+B*dD') : :(B*dD')), view(dr,:,1:M));
            db = q*dD)
        else
            :(da = qr_back_fullrank(q, U, dq, nothing);
            db = zero(B))
        end)
        hcat(da, db)
    end
end

"""
    lq_back(A, l, q, dl, dq) -> Matrix

backward for LQ decomposition, for an arbituary shaped input matrix.

References:
    Seeger, M., Hetzel, A., Dai, Z., Meissner, E., & Lawrence, N. D. (2018). Auto-Differentiating Linear Algebra.
    Differentiable Programming Tensor Networks, Hai-Jun Liao, Jin-Guo Liu, Lei Wang, Tao Xiang
"""
function lq_back_fullrank(L, Q, dL, dQ)
    M = ZeroAdder()
    dL === nothing || (M += L'*dL)
    dQ === nothing || (M -= dQ*Q')
    C = copyltu!(M)*Q
    if dQ !== nothing
        C += dQ
    end
    #inv(L)' * C
    trtrs!('L', 'C', 'N', L, C)
end

"""
    lq_back(A, L, Q, dL, dQ) -> Matrix

backward for QR decomposition, for an arbituary shaped input matrix.

References:
    Seeger, M., Hetzel, A., Dai, Z., Meissner, E., & Lawrence, N. D. (2018). Auto-Differentiating Linear Algebra.
    HaiJun's paper.
"""
@generated function lq_back(A, L, Q, dL, dQ)
    dunot0 = !(dQ <: Nothing)
    dlnot0 = !(dL <: Nothing)
    (!dunot0 && !dlnot0) && return :(nothing)
    ex = quote
        N, M = size(L)
        M == N && return lq_back_fullrank(L, Q, dL, dQ)
        B = view(A,M+1:N,:)
        U = view(L,1:M,:)
        D = view(L,M+1:N,:)
        $(if dlnot0
            :(dD = view(dL,M+1:N,:);
            da = lq_back_fullrank(U, Q, view(dL,1:M,:), $(dunot0 ? :(dQ+dD'*B) : :(dD'*B)));
            db = dD*Q)
        else
            :(da = lq_back_fullrank(U, Q, nothing, dQ);
            db = zero(B))
        end)
        vcat(da, db)
    end
end


function init_observable_mappings!(m::RealBondMkup)

    observables = OrderedDict{Symbol,Observable}()
    population_mnemonic = get(get_setting(m, :population_mnemonic))

    ############################################################################
    ## 1. GDP
    ############################################################################
    gdp_fwd_transform = function (levels)
        # FROM: Level of nominal GDP (FRED :GDP series)
        # TO:   The cyclical component of log real GDP

        levels[:rGDP] = nominal_to_real(:GDP, levels)
        gdp = oneqtrpctchange(levels[:rGDP])
        _, levels[:GDP_cyc_comp] = hpfilter(gdp, 1600)
        levels[:GDP_cyc_comp]
        #=levels[:lGDP] = log.(levels[:rGDP])
        _, levels[:lGDP_cyc_comp] = hpfilter(levels[:lGDP], 1600)

        levels[:lGDP_cyc_comp]=#
    end

    gdp_rev_transform = logleveltopct_annualized

    observables[:obs_gdp] = Observable(:obs_gdp, [:GDP__FRED, population_mnemonic, :GDPDEF__FRED],
                                       gdp_fwd_transform, gdp_rev_transform,
                                       "Log Real GDP", "")

    ############################################################################
    ## 2. Core PCE Inflation
    ############################################################################

    pce_fwd_transform = function (levels)
        # FROM: Core PCE index
        # INTO: Approximate quarter-to-quarter percent change of Core PCE,
        # i.e. quarterly core pce inflation

        oneqtrpctchange(levels[:PCEPILFE])
    end

    pce_rev_transform = loggrowthtopct_annualized

    observables[:obs_corepce] = Observable(:obs_corepce, [:PCEPILFE__FRED],
                                           pce_fwd_transform, pce_rev_transform,
                                           "Core PCE Inflation",
                                           "Core PCE Inflation")

    ############################################################################
    ## 3. Nominal short-term interest rate (3 months)
    ############################################################################

    nominalrate_fwd_transform = function (levels)
        # FROM: Nominal effective federal funds rate (aggregate daily data at a
        #       quarterly frequency at an annual rate)
        # TO:   Nominal effective fed funds rate, at a quarterly rate

        annualtoquarter(levels[:DFF])
    end

    nominalrate_rev_transform = quartertoannual

    observables[:obs_nominalrate] = Observable(:obs_nominalrate, [:DFF__FRED],
                                               nominalrate_fwd_transform, nominalrate_rev_transform,
                                               "Nominal FFR",
                                               "Nominal Effective Fed Funds Rate")

    m.observable_mappings = observables
end


module Test_conversions

using DescriptorSystems
using LinearAlgebra
using Polynomials
using Test

println("Test_conversions")

@testset "gbilin" begin

s = rtf('s'); # define the complex variable s
Gc = [s^2 s/(s+1); 0 1/s] # define the 2-by-2 improper Gc(s)
@time sysc = dss(Gc);            # build continuous-time descriptor system realization
g = (s+0.01)/(1+0.01*s);

@time syst, = gbilin(sysc,g,atol = 1.e-7,minimal=true);

@test opnorm(evalfr(sysc-syst,1)) < 1.e-7
# compute the ν-gap
@time nugap = gnugap(sysc,syst,atol=1.e-4)[1];
@test nugap < 0.02

z = rtf('z');
Gd = [z^2 z/(z-2); 0 1/z]     # define the 2-by-2 improper Gd(z)
@time syst, = gbilin(dss(Gd),rtfbilin("cayley")[2],atol=1.e-7,minimal = true);
@time sysi, = gbilin(syst,rtfbilin("c2d")[1],atol = 1.e-7,minimal = true);

@test ghinfnorm(gminreal(dss(Gd)-sysi,atol=1.e-7))[1] < 1.e-7

for Ty in (Float64,Complex{Float64})

type = "c2d";
sys = rss(T = Ty, 3,2,3);
@time g, ginv = rtfbilin(type)
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

type = "c2d";
sys = rdss(T = Ty, 3,2,3);
@time g, ginv = rtfbilin(type)
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

type = "cayley";
sys = rss(T = Ty, 3,2,3,disc = true);
@time g, ginv = rtfbilin(type)
@time syst, g1 = gbilin(sys,ginv,atol=1.e-7);
@time sysi, gi1 = gbilin(syst,g,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && g1 == g && ginv == gi1

type = "cayley";
sys = rdss(T = Ty, 3,2,3,disc = true);
@time g, ginv = rtfbilin(type)
@time syst, g1 = gbilin(sys,ginv,atol=1.e-7);
@time sysi, gi1 = gbilin(syst,g,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && g1 == g && ginv == gi1

type = "tustin";
sys = rss(T = Ty, 3,2,3);
@time g, ginv = rtfbilin(type,Ts=0.1)
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

type = "lft";
sys = rss(T = Ty, 3,2,3);
val = rand(Ty,4)
@time g, ginv = rtfbilin(type,a=val[1],b=val[2],c=val[3],d=val[4])
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

type = "lft";
sys = rss(T = Ty, 3,2,3,disc=true);
val = rand(Ty,4)
@time g, ginv = rtfbilin(type,Ts=1, Tsi=1, a=val[1],b=val[2],c=val[3],d=val[4])
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

type = "lft";
sys = rss(T = Ty, 3,2,3,disc=true);
val = rand(Ty,4)
@time g, ginv = rtfbilin(type,Ts=0, Tsi=-1, a=val[1],b=val[2],c=val[3],d=val[4])
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

type = "lft";
sys = rss(T = Ty, 3,2,3,disc=false);
val = rand(Ty,4)
@time g, ginv = rtfbilin(type,Ts=-1, Tsi=0, a=val[1],b=val[2],c=val[3],d=val[4])
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

type = "lft";
sys = rss(T = Ty, 3,2,3,disc=false);
val = rand(Ty,2)
@time g, ginv = rtfbilin(type,Ts=-1, Tsi=0, a=val[1],b=val[2])
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi ≈ ginv && g ≈ g1

type = "lft";
sys = rdss(T = Ty, 3,2,3);
val = rand(Ty,4)
g, ginv = rtfbilin(type,a=val[1],b=val[2],c=val[3],d=val[4])
syst, gi = gbilin(sys,g,atol=1.e-7);
sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

type = "lft";
sys = rdss(T = Ty, 3,2,3);
val = rand(Ty,2)
@time g, ginv = rtfbilin(type,a=val[1],b=val[2])
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi ≈ ginv && g ≈ g1

type = "lft";
sys = rdss(T = Ty, 3,2,3,disc=true);
val = rand(Ty,4)
@time g, ginv = rtfbilin(type,Ts=1, Tsi=1, a=val[1],b=val[2],c=val[3],d=val[4])
@time syst, gi = gbilin(sys,g,atol=1.e-7)
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

type = "lft";
sys = rdss(T = Ty, 3,2,3,disc=true);
val = rand(4)
@time g, ginv = rtfbilin(type,Ts=0, Tsi=-1, a=val[1],b=val[2],c=val[3],d=val[4])
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

type = "lft";
sys = rdss(T = Ty, 3,2,3,disc=false);
val = rand(Ty,4)
@time g, ginv = rtfbilin(type,Ts=-1, Tsi=0, a=val[1],b=val[2],c=val[3],d=val[4])
@time syst, gi = gbilin(sys,g,atol=1.e-7);
@time sysi, g1 = gbilin(syst,ginv,atol=1.e-7);
@test iszero(sys-sysi,atol=1.e-7) && gi == ginv && g == g1

end #Ty
end # gbilin

@testset "c2d - descriptor systems" begin

a = [-4 -2;1 0]; b = [2;0]; c = [0.5 1]; d = [0]; x0 = [1,2]; u0 = [1];
sysc = dss(a,b,c,d);
@time sysd, xd0, Mx, Mu = c2d(sysc,1; x0 = [1,2], u0 = [1], state_mapping = true);
EAt = exp(a);
@test dcgain(sysd) ≈ dcgain(sysc) && sort(gpole(sysd)) ≈ sort(exp.(gpole(sysc))) &&
      sysd.A ≈ EAt && sysd.B ≈ a\(EAt-I)*b && x0 ≈ Mx*xd0+Mu*u0
@time sysd_foh, xd0, Mx, Mu = c2d(sysc,1,"foh"; x0, u0, state_mapping = true);
@test dcgain(sysd_foh) ≈ dcgain(sysc) && x0 ≈ Mx*xd0+Mu*u0
@time sysd_imp, xd0, Mx, Mu = c2d(sysc,1,"impulse"; x0, u0, state_mapping = true);
@test sort(gpole(sysd_imp)) ≈ sort(exp.(gpole(sysc))) && x0 ≈ Mx*xd0+Mu*u0
@time sysd1, xd1, Mx1, Mu1 = c2d(sysc,1,"tustin"; x0, u0, standard = true, state_mapping = true)
@time sysd2, xd2, Mx2, Mu2 = c2d(sysc,1,"tustin"; x0, u0, standard = false, state_mapping = true)
syst = gbilin(sysc,rtfbilin("Tustin"; Ts = 1)[1])[1]
@test iszero(sysd1 - sysd2, atol=1.e-7) && iszero(sysd1-syst, atol=1.e-7) &&
      xd1-xd2 ≈ (sysd2.E-I)*x0 && x0 ≈ [Mx1 Mu1]*[xd1;u0] && x0 ≈ [Mx2 Mu2]*[xd2;u0]

@time sysd1, xd1, Mx1, Mu1 = c2d(sysc,1,"tustin"; prewarp_freq=1, x0, u0, standard = true, state_mapping = true)
@time sysd2, xd2, Mx2, Mu2 = c2d(sysc,1,"tustin"; prewarp_freq=1, x0, u0, standard = false, state_mapping = true)
syst = gbilin(sysc,rtfbilin("Tustin"; Ts = 1, prewarp_freq=1)[1])[1]
@test gnrank(sysd1 - sysd2, atol=1.e-7) == 0 && iszero(sysd1-syst, atol=1.e-7) && xd1-xd2 ≈ (sysd2.E-I)*x0 &&
      evalfr(sysc,fval=1) ≈ evalfr(sysd1,fval=1) && evalfr(sysc,fval=1) ≈ evalfr(sysd2,fval=1)  &&
      x0 ≈ [Mx1 Mu1]*[xd1;u0] && x0 ≈ [Mx2 Mu2]*[xd2;u0]

@test_throws ErrorException c2d(sysc,1,"Euler")

a = [-4 -2;1 0]; b = [2;0]; c = [0.5 1]; d = [0]; e = [1 2; 3 0]; x0 = [1,2]; u0 = [1];
sysc = dss(a,e,b,c,d);
@time sysd, xd0, Mx, Mu = c2d(sysc,1; x0, u0, state_mapping = true);
a1, e1, b1, c1, d1 = dssdata(sysc);
EAt = exp(e1\a1);
@test dcgain(sysd) ≈ dcgain(sysc) && sort(gpole(sysd)) ≈ sort(exp.(gpole(sysc))) &&
      sysd.A ≈ EAt && sysd.B ≈ (e1\a1)\(EAt-I)*(e1\b1) && x0 ≈ Mx*xd0+Mu*u0

a = [-4 -2;1 0]; b = [2;0]; c = [0.5 1]; d = [0]; e = [1 2; 0 0]; x0 = [1,2]; u0 = [1];
sysc = dss(a,e,b,c,d);
@time sysd, xd0, Mx, Mu = c2d(sysc,1; x0, u0, state_mapping = true, simple_infeigs = false);
@test norm(Mx*xd0+Mu*u0 - x0) > 0.001

x0 = Mx*xd0+Mu*u0;
@time sysd, xd1, Mx, Mu = c2d(sysc,1; x0, u0, state_mapping = true, simple_infeigs = false);
@test norm(Mx*xd1+Mu*u0 - x0) < 0.001

@time sysd, xd0, Mx, Mu = c2d(sysc,1; x0, u0, state_mapping = true, simple_infeigs = true);
@test norm(Mx*xd0+Mu*u0 - x0) > 0.001

x0 = Mx*xd0+Mu*u0;
@time sysd, xd1, Mx, Mu = c2d(sysc,1; x0, u0, state_mapping = true, simple_infeigs = true);
@test norm(Mx*xd1+Mu*u0 - x0) < 0.001

a = [-4 -2;1 0]; b = [2;1]; c = [0.5 1]; d = [0]; e = [1 2; 0 0]; x0 = [1,2]; u0 = [1];
sysc = dss(a,e,b,c,d);
@time sysd, xd0, Mx, Mu = c2d(sysc,1; x0, u0, state_mapping = true);
a1, e1, b1, c1, d1 = dssdata(gir(sysc,contr=false,obs=false,noseig=true));
EAt = exp(e1\a1);
@test dcgain(sysd) ≈ dcgain(sysc) && sort(gpole(sysd)) ≈ sort(exp.(gpole(sysc))) &&
      sysd.A ≈ EAt && sysd.B ≈ (e1\a1)\(EAt-I)*(e1\b1) && x0 ≈ Mx*xd0+Mu*u0
@time sysd_foh, xd0, Mx, Mu = c2d(sysc,1,"foh"; x0, u0, state_mapping = true);
@test dcgain(sysd_foh) ≈ dcgain(sysc) && x0 ≈ Mx*xd0+Mu*u0
@time sysd_imp, xd0, Mx, Mu = c2d(sysc,1,"impulse"; x0, u0, state_mapping = true);
@test sort(gpole(sysd_imp)) ≈ sort(exp.(gpole(sysc))) && x0 ≈ Mx*xd0+Mu*u0

@time sysd1, xd1, Mx1, Mu1 = c2d(sysc,1,"tustin"; x0, u0, standard = true, state_mapping = true)
@time sysd2, xd2, Mx2, Mu2 = c2d(sysc,1,"tustin"; x0, u0, standard = false, state_mapping = true)
syst = gbilin(sysc,rtfbilin("Tustin"; Ts = 1)[1])[1]
@test iszero(sysd1 - sysd2, atol=1.e-7) && iszero(sysd1-syst, atol=1.e-7) &&
      xd1-xd2 ≈ (sysd2.E-I)*x0 && x0 ≈ [Mx1 Mu1]*[xd1;u0] && x0 ≈ [Mx2 Mu2]*[xd2;u0]

@time sysd1, xd1, Mx1, Mu1 = c2d(sysc,1,"tustin"; prewarp_freq=1, x0, u0, standard = true, state_mapping = true)
@time sysd2, xd2,Mx2, Mu2 = c2d(sysc,1,"tustin"; prewarp_freq=1, x0, u0, standard = false, state_mapping = true)
syst = gbilin(sysc,rtfbilin("Tustin"; Ts = 1, prewarp_freq=1)[1])[1]
@test iszero(sysd1 - sysd2, atol=1.e-7) && iszero(sysd1-syst, atol=1.e-7) &&
      evalfr(sysc,fval=1) ≈ evalfr(sysd1,fval=1) && evalfr(sysc,fval=1) ≈ evalfr(sysd2,fval=1) &&
      x0 ≈ [Mx1 Mu1]*[xd1;u0] && x0 ≈ [Mx2 Mu2]*[xd2;u0]

a = [-4 -2;1 0]; b = [2;0]; c = [0.5 1]; d = [0]; e = [0 0; 0 0]; x0 = [1,2]; u0 = [1];
sysc = dss(a,e,b,c,d);
@time sysd, xd0, Mx, Mu = c2d(sysc,1; x0, u0, state_mapping = true, simple_infeigs = true);
@test norm(Mx*xd0+Mu*u0 - x0) > 0.001
x0 = Mx*xd0+Mu*u0;
@time sysd, xd1, Mx, Mu = c2d(sysc,1; x0, u0, state_mapping = true, simple_infeigs = true);
@test norm(Mx*xd1+Mu*u0 - x0) < 0.001

end # c2d

@testset "c2d - rational transfer functions" begin

s = rtf('s'); z = rtf('z',Ts=1);
rc = 1/s;

rd1 = 1/(z-1);
@time rd = c2d(rc, 1, "zoh");
@test rd1 ≈ rd

rd2 = 0.5*(z+1)/(z-1);
@time rd = c2d(rc, 1, "foh");
@test rd2 ≈ rd

rd3 = z/(z-1);
@time rd = c2d(rc, 1, "impulse");
@test rd3 ≈ rd

rd4 = 0.5*(z+1)/(z-1);
@time rd = c2d(rc, 1, "tustin");
@test rd4 ≈ rd

rd5 = 1.0001/(z-1);
@time rd = c2d(rc, 1, "matched");
@test norm((rd5-rd).num.coeffs,Inf) < 0.001

rc = 1/(s+1);

z1 = exp(-1);
rd1 = (1-z1)/(z-z1);
@time rd = c2d(rc, 1, "zoh");
@test rd1 ≈ rd

z2 = 1-2*z1;
rd2 = (z1*z+z2)/(z-z1);
@time rd = c2d(rc, 1, "foh");
@test rd2 ≈ rd

rd3 = z/(z-z1);
@time rd = c2d(rc, 1, "impulse");
@test rd3 ≈ rd

rd4 = 1/3*(z+1)/(z-1/3);
@time rd = c2d(rc, 1, "tustin");
@test rd4 ≈ rd

rd5 = (1-z1)/(z-z1);
@time rd = c2d(rc, 1, "matched");
@test rd5 ≈ rd

rc = s+1;

rd4 = (3*z-1)/(z+1);
@time rd = c2d(rc, 1, "tustin");
@test rd4 ≈ rd

z1 = exp(-1);
k1 = 1/(1-z1);
rd5 = k1*(z-z1);
@time rd = c2d(rc, 1, "matched");
@test rd5 ≈ rd

s = rtf('s');
Gc = [s^2 s/(s+1); 0 1/s]
@time Gd = c2d(Gc, 1, "matched");

end

@testset "dss2pm & dss2rm" begin

# Example 3: P = [λ^2 λ; λ 1] DeTeran, Dopico, Mackey, ELA 2009
λ = Polynomial([0,1],:s)
P = [λ^2 λ; λ 1]
@test all(P .≈ dss2pm(dss(P)))
@test all(P .≈ dss2rm(dss(P)))

z = rtf('z')
@test_throws ErrorException dss2pm(dss(z+1/z))

s = Polynomial([0, 1],:s);
R = rtf.([s^2+3*s+3 1; -1 2*s^2+7*s+4] ./ [(s+1)^2 s+2; (s+1)^3 (s+1)*(s+2)]);
@test all(R .≈ dss2rm(dss(R)))

end # dss2pm & dss2rm

@testset "timeresp" begin

a = [-4 -2;1 0]; b = [2 1;0 1]; c = [0.5 1]; d = [0 1]; x0 = [1,2]; u0 = [1, 1]; Ts = 1;
sysc = dss(a,b,c,d);
@time sysd, xd0, = c2d(sysc, Ts; x0, u0);
@time y, tout, x = timeresp(sysd, ones(11,2), Int[], xd0; state_history = true);

@time y1, tout1, x1 = timeresp(sysc, ones(11,2), tout, x0; state_history = true, interpolation = "zoh")
@test norm(y-y1,Inf) < 1.e-7 && norm(x-x1,Inf) < 1.e-7

@time y2, tout2, x2 = timeresp(sysc, ones(11,2), tout, x0; state_history = true, interpolation = "foh")
@test norm(y-y2,Inf) < 1.e-7 && norm(x-x2,Inf) < 1.e-7

ed = rand(2,2);
sysdd = dss(ed*sysd.A,ed,ed*sysd.B,sysd.C,sysd.D,Ts=sysd.Ts)
@time y3, tout3, x3 = timeresp(sysdd, ones(11,2), Int[], xd0; state_history = true)
@test norm(y-y3,Inf) < 1.e-7 && norm(x-x3,Inf) < 1.e-7

u = rand(11,2);
@time sysd, xd0, Mx, Mu = c2d(sysc, Ts, "foh"; x0, u0 = u[1,:], state_mapping = true);

@time y, tout, x = timeresp(sysd, u, Int[], xd0; state_history = true)

@time y2, tout2, x2 = timeresp(sysc, u, tout, x0; state_history = true, interpolation = "foh")
@test norm(y-y2,Inf) < 1.e-7 && norm([Mx Mu]*[x';u']-x2',Inf) < 1.e-7

a = [-4 -2;1 0]; b = [2 1;0 1]; c = [0.5 1]; d = [0 1]; x0 = [1,2]; u0 = [1, 1]; Ts = 1;
e = [1 3; 1 1];
u = ones(11,2);
sysc = dss(a,b,c,d); t = 0:Ts:10;
@time y, tout, x = timeresp(sysc, u, t, x0; state_history = true)

sysc1 = dss(e*a,e,e*b,c,d);
@time y1, tout, x1 = timeresp(sysc1, u, t, x0; state_history = true, interpolation = "zoh")
@test norm(y-y1,Inf) < 1.e-7 && norm(x-x1,Inf) < 1.e-7

@time sysd, xd0, Mx, Mu = c2d(sysc, Ts; state_mapping = true, x0, u0);
@time y2, tout, x2 = timeresp(sysd, u, t, xd0; state_history = true) # OK
@test norm(y-y2,Inf) < 1.e-7 && norm(x-x2,Inf) < 1.e-7

sysdd = dss(e*sysd.A,e,e*sysd.B,sysd.C,sysd.D,Ts = 1);
@time yd, tout, xd = timeresp(sysdd, u, t, xd0; state_history = true) # OK
@test norm(y-yd,Inf) < 1.e-7 && norm(x-xd,Inf) < 1.e-7

@time sysd1, xd1, Mx, Mu = c2d(sysc1, Ts; state_mapping = true, x0, u0);
@time y3, tout3, x3 = timeresp(sysd1, u, t, xd1; state_history = true) # OK
@test norm(y-y3,Inf) < 1.e-7 && norm([Mx Mu]*[x3';u']-x',Inf) < 1.e-7

@time y4, tout, x4 = timeresp(sysc, u, t, x0; state_history = true, interpolation = "foh")
@test norm(y-y4,Inf) < 1.e-7 && norm(x-x4,Inf) < 1.e-7

@time y5, tout, x5 = timeresp(sysc1, u, t, x0; state_history = true, interpolation = "foh")
@test norm(y-y5,Inf) < 1.e-7 && norm(x-x5,Inf) < 1.e-7

@time sysd2, xd2, Mx, Mu = c2d(sysc, Ts, "foh"; state_mapping = true, x0, u0);
@time y6, tout, x6 = timeresp(sysd2, u, t, xd2; state_history = true) # OK
@test norm(y-y6,Inf) < 1.e-7 && norm([Mx Mu]*[x6';u']-x',Inf) < 1.e-7

sysdd2 = dss(e*sysd2.A,e,e*sysd2.B,sysd2.C,sysd2.D,Ts = 1);
@time ydd2, tout, xdd2 = timeresp(sysdd2, u, t, xd2; state_history = true) # OK
@test norm(y-ydd2,Inf) < 1.e-7 && norm([Mx Mu]*[xdd2';u']-x',Inf) < 1.e-7

@time sysd3, xd3, Mx, Mu = c2d(sysc1, Ts, "foh"; state_mapping = true, x0, u0);
@time y7, tout, x7 = timeresp(sysd3, u, t, xd3; state_history = true) # OK
@test norm(y-y7,Inf) < 1.e-7 && norm([Mx Mu]*[x7';u']-x',Inf) < 1.e-7

u = rand(11,2);
@time sysd, xd0, Mx, Mu = c2d(sysc, Ts, "foh"; x0, u0 = u[1,:], state_mapping = true);

@time y, tout, x = timeresp(sysd, u, Int[], xd0; state_history = true)

@time y2, tout2, x2 = timeresp(sysc, u, tout, x0; state_history = true, interpolation = "foh")
@test norm(y-y2,Inf) < 1.e-7 && norm([Mx Mu]*[x';u']-x2',Inf) < 1.e-7

a = [-4 -2;1 0]; b = [2 1;0 1]; c = [0.5 1]; d = [0 1]; x0 = [1,2]; u0 = [1, 1]; Ts = 1;
sysc = dss(a,b,c,d); t = 0:Ts:10;
u = ones(11,2);
@time y, tout, x = timeresp(sysc, u, t, x0; state_history = true);
e = rand(Complex{Float64},2,2);
syscc = dss(e*a,e,e*b,c,d); t = 0:Ts:10;
@time sysdc, xd0, Mx, Mu = c2d(syscc, Ts; x0, u0, state_mapping = true);
@time yc, tout, xc = timeresp(sysdc, u, t, xd0; state_history = true)
@test norm(y-yc,Inf) < 1.e-7 && norm([Mx Mu]*[xc';u']-x',Inf) < 1.e-7

@time yc1, tout, xc1 = timeresp(syscc, u, t, x0; state_history = true)
@test norm(y-yc1,Inf) < 1.e-7 && norm(x-xc1,Inf) < 1.e-7

@time yc2, tout1, xc2 = timeresp(syscc, u, t, x0; state_history = true, interpolation = "foh")
@test norm(y-yc2,Inf) < 1.e-7 && norm(x-xc2,Inf) < 1.e-7

u = exp.(im*rand(11,2));
@time y, tout, x = timeresp(sysc, u, t, x0; state_history = true, interpolation = "foh");

@time sysdc, xd0, Mx, Mu = c2d(syscc, Ts, "foh"; x0, u0 = u[1,:], state_mapping = true);

@time yc1, tout, xc1 = timeresp(sysdc, u, t, xd0; state_history = true)
@test norm(y-yc1,Inf) < 1.e-7 && norm([Mx Mu]*[xc1';u']-x',Inf) < 1.e-7

@time yc2, tout2, xc2 = timeresp(syscc, u, tout, x0; state_history = true, interpolation = "foh")
@test norm(y-yc2,Inf) < 1.e-7 && norm(x-xc2,Inf) < 1.e-7

a = [-4 -2;1 0]; b = [2 1;0 1]; c = [0.5 1]; d = [0 1]; x0 = [1,2]; u0 = [1, 1]; Ts = 1;
e = [1 2; 0 0];
sysc = dss(a,e,b,c,d);
@time sysd, xd0, Mx, Mu = c2d(sysc, Ts; x0, u0, state_mapping = true, atol=1.e-7);

@time y, tout, x = timeresp(sysd, ones(11,2), Int[],xd0; state_history = true, atol=1.e-7)

@time y1, tout1, x1 = timeresp(sysc, ones(11,2), tout, x0; state_history = true, interpolation = "zoh", atol=1.e-7)
@test norm(y-y1,Inf) < 1.e-7

@time y2, tout2, x2 = timeresp(sysc, ones(11,2), tout, x0; state_history = true, interpolation = "foh", atol=1.e-7)
@test norm(y-y2,Inf) < 1.e-7

sysd = rdss(3,2,2,disc=true,stable=true,id=ones(Int,2))
sysdr = gss2ss(sysd)[1]
u = rand(11,2);
x0 = rand(5);

@time y, tout, x = timeresp(sysd, u, Int[], x0; state_history = true, atol=1.e-7)
sys, xt, Mx, Mu = dss2ss(sysd, x0; state_mapping = true)
@time y1, tout1, x1 = timeresp(sys, u, Int[], xt; state_history = true, atol=1.e-7)
@test norm(y-y1,Inf) < 1.e-7 && [Mx Mu]*[x1';u'] ≈ x'

@time y2, tout2, x2 = timeresp(sys, u, Int[]; state_history = false, atol=1.e-7)
@time y3, tout3, x3 = timeresp(sysdr, u, Int[]; state_history = false, atol=1.e-7)
@test norm(y2-y3,Inf) < 1.e-7

end # timeresp

end #module


using CovarianceMatrices
using Statistics
using CSV
using BenchmarkTools

X = randn(10000,20);
k = BartlettKernel(prewhiten=true)
cfg = CovarianceMatrices.HACConfig(X, k)
CovarianceMatrices.variance(X, k, cfg)

@benchmark CovarianceMatrices.variance(X, k, cfg, calculatechol=false)


using StatsFuns
using Test
using InverseFunctions

@testset "inverse" begin
    x = 0.7
    InverseFunctions.test_inverse(norminvcdf, x)
    InverseFunctions.test_inverse(norminvccdf, x)
    InverseFunctions.test_inverse(norminvlogcdf, log(x))
    InverseFunctions.test_inverse(norminvlogccdf, log(x))
end


module GaussianMeanPrecisionTest

using Test
using ForneyLab
using ForneyLab: outboundType, isApplicable, isProper, unsafeMean, unsafeMode, unsafeVar, unsafeCov, unsafeMeanCov, unsafePrecision, unsafeWeightedMean, unsafeWeightedMeanPrecision
using ForneyLab: SPGaussianMeanPrecisionOutNPP, SPGaussianMeanPrecisionMPNP, SPGaussianMeanPrecisionOutNGP, SPGaussianMeanPrecisionMGNP, VBGaussianMeanPrecisionOut, VBGaussianMeanPrecisionM, VBGaussianMeanPrecisionW, SVBGaussianMeanPrecisionOutVGD, SVBGaussianMeanPrecisionMGVD, SVBGaussianMeanPrecisionW, MGaussianMeanPrecisionGGD, MGaussianMeanPrecisionGGN

@testset "dims" begin
    @test dims(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=1.0)) == 1
    @test dims(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=ones(2), w=diageye(2))) == 2
end

@testset "vague" begin
    @test vague(GaussianMeanPrecision) == ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=tiny)
    @test vague(GaussianMeanPrecision, 2) == ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=zeros(2), w=tiny*eye(2))
    @test vague(GaussianMeanPrecision, (2,)) == ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=zeros(2), w=tiny*eye(2))
end

@testset "isProper" begin
    # Univariate
    @test isProper(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=1.0))
    @test !isProper(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=-1.0))

    # Multivariate
    @test isProper(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[0.0], w=mat(1.0)))
    @test isProper(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=ones(2), w=diageye(2)))
    @test !isProper(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[0.0], w=mat(-1.0)))
end

@testset "==" begin
    # Univariate
    @test ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=1.0) == ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=1.0)
    @test ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=1.0) == ProbabilityDistribution(Univariate, GaussianWeightedMeanPrecision, xi=0.0, w=1.0)

    # Multivariate
    @test ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[0.0], w=mat(1.0)) == ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[0.0], w=mat(1.0))
    @test ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[0.0], w=mat(1.0)) == ProbabilityDistribution(Multivariate, GaussianWeightedMeanPrecision, xi=[0.0], w=mat(1.0))
end

@testset "unsafe statistics" begin
    # Univariate
    @test unsafeMean(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=2.0, w=4.0)) == 2.0
    @test unsafeMode(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=2.0, w=4.0)) == 2.0
    @test unsafeVar(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=2.0, w=4.0)) == 0.25
    @test unsafeCov(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=2.0, w=4.0)) == 0.25
    @test unsafeMeanCov(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=2.0, w=4.0)) == (2.0, 0.25)
    @test unsafePrecision(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=2.0, w=4.0)) == 4.0
    @test unsafeWeightedMean(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=2.0, w=4.0)) == 8.0
    @test unsafeWeightedMeanPrecision(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=2.0, w=4.0)) == (8.0, 4.0)

    # Multivariate
    @test unsafeMean(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[2.0], w=mat(4.0))) == [2.0]
    @test unsafeMode(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[2.0], w=mat(4.0))) == [2.0]
    @test unsafeVar(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[2.0], w=mat(4.0))) == [0.25]
    @test unsafeCov(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[2.0], w=mat(4.0))) == mat(0.25)
    @test unsafeMeanCov(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[2.0], w=mat(4.0))) == ([2.0], mat(0.25))
    @test unsafePrecision(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[2.0], w=mat(4.0))) == mat(4.0)
    @test unsafeWeightedMean(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[2.0], w=mat(4.0))) == [8.0]
    @test unsafeWeightedMeanPrecision(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[2.0], w=mat(4.0))) == ([8.0], mat(4.0))
end

@testset "log pdf" begin
    @test isapprox(logPdf(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=1.0, w=2.0), 1.0), -0.5723649429247)
    @test isapprox(logPdf(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[1.0, 1.0], w=[2.0 0.0; 0.0 2.0]), [1.0, 0.0]), -2.1447298858494)
end

@testset "convert" begin
    @test convert(ProbabilityDistribution{Univariate, GaussianMeanPrecision}, ProbabilityDistribution(Univariate, GaussianWeightedMeanPrecision, xi=8.0, w=4.0)) == ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=2.0, w=4.0)
    @test convert(ProbabilityDistribution{Univariate, GaussianMeanPrecision}, ProbabilityDistribution(Univariate, GaussianMeanVariance, m=2.0, v=0.25)) == ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=2.0, w=4.0)
    @test convert(ProbabilityDistribution{Multivariate, GaussianMeanPrecision}, ProbabilityDistribution(Multivariate, GaussianWeightedMeanPrecision, xi=[8.0], w=mat(4.0))) == ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[2.0], w=mat(4.0))
    @test convert(ProbabilityDistribution{Multivariate, GaussianMeanPrecision}, ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[2.0], v=mat(0.25))) == ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[2.0], w=mat(4.0))
    @test convert(ProbabilityDistribution{Multivariate, GaussianMeanPrecision}, ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=1.0, w=2.0)) == ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[1.0], w=mat(2.0))
end

#-------------
# Update rules
#-------------

@testset "SPGaussianMeanPrecisionOutNPP" begin
    @test SPGaussianMeanPrecisionOutNPP <: SumProductRule{GaussianMeanPrecision}
    @test outboundType(SPGaussianMeanPrecisionOutNPP) == Message{GaussianMeanPrecision}
    @test isApplicable(SPGaussianMeanPrecisionOutNPP, [Nothing, Message{PointMass}, Message{PointMass}])
    @test !isApplicable(SPGaussianMeanPrecisionOutNPP, [Message{PointMass}, Nothing, Message{PointMass}])

    @test ruleSPGaussianMeanPrecisionOutNPP(nothing, Message(Univariate, PointMass, m=1.0), Message(Univariate, PointMass, m=2.0)) == Message(Univariate, GaussianMeanPrecision, m=1.0, w=2.0)
    @test ruleSPGaussianMeanPrecisionOutNPP(nothing, Message(Multivariate, PointMass, m=[1.0]), Message(MatrixVariate, PointMass, m=mat(2.0))) == Message(Multivariate, GaussianMeanPrecision, m=[1.0], w=mat(2.0))
end

@testset "SPGaussianMeanPrecisionMPNP" begin
    @test SPGaussianMeanPrecisionMPNP <: SumProductRule{GaussianMeanPrecision}
    @test outboundType(SPGaussianMeanPrecisionMPNP) == Message{GaussianMeanPrecision}
    @test !isApplicable(SPGaussianMeanPrecisionMPNP, [Nothing, Message{PointMass}, Message{PointMass}])
    @test isApplicable(SPGaussianMeanPrecisionMPNP, [Message{PointMass}, Nothing, Message{PointMass}])

    @test ruleSPGaussianMeanPrecisionMPNP(Message(Univariate, PointMass, m=1.0), nothing, Message(Univariate, PointMass, m=2.0)) == Message(Univariate, GaussianMeanPrecision, m=1.0, w=2.0)
    @test ruleSPGaussianMeanPrecisionMPNP(Message(Multivariate, PointMass, m=[1.0]), nothing, Message(MatrixVariate, PointMass, m=mat(2.0))) == Message(Multivariate, GaussianMeanPrecision, m=[1.0], w=mat(2.0))
end

@testset "SPGaussianMeanPrecisionOutNGP" begin
    @test SPGaussianMeanPrecisionOutNGP <: SumProductRule{GaussianMeanPrecision}
    @test outboundType(SPGaussianMeanPrecisionOutNGP) == Message{GaussianMeanVariance}
    @test isApplicable(SPGaussianMeanPrecisionOutNGP, [Nothing, Message{Gaussian}, Message{PointMass}])
    @test !isApplicable(SPGaussianMeanPrecisionOutNGP, [Message{Gaussian}, Nothing, Message{PointMass}])

    @test ruleSPGaussianMeanPrecisionOutNGP(nothing, Message(Univariate, GaussianMeanVariance, m=1.0, v=1.0), Message(Univariate, PointMass, m=2.0)) == Message(Univariate, GaussianMeanVariance, m=1.0, v=1.5)
    @test ruleSPGaussianMeanPrecisionOutNGP(nothing, Message(Multivariate, GaussianMeanVariance, m=[1.0], v=mat(1.0)), Message(MatrixVariate, PointMass, m=mat(2.0))) == Message(Multivariate, GaussianMeanVariance, m=[1.0], v=mat(1.5))
end

@testset "SPGaussianMeanPrecisionMGNP" begin
    @test SPGaussianMeanPrecisionMGNP <: SumProductRule{GaussianMeanPrecision}
    @test outboundType(SPGaussianMeanPrecisionMGNP) == Message{GaussianMeanVariance}
    @test !isApplicable(SPGaussianMeanPrecisionMGNP, [Nothing, Message{Gaussian}, Message{PointMass}])
    @test isApplicable(SPGaussianMeanPrecisionMGNP, [Message{Gaussian}, Nothing, Message{PointMass}])

    @test ruleSPGaussianMeanPrecisionMGNP(Message(Univariate, GaussianMeanVariance, m=1.0, v=1.0), nothing, Message(Univariate, PointMass, m=2.0)) == Message(Univariate, GaussianMeanVariance, m=1.0, v=1.5)
    @test ruleSPGaussianMeanPrecisionMGNP(Message(Multivariate, GaussianMeanVariance, m=[1.0], v=mat(1.0)), nothing, Message(MatrixVariate, PointMass, m=mat(2.0))) == Message(Multivariate, GaussianMeanVariance, m=[1.0], v=mat(1.5))
end

@testset "VBGaussianMeanPrecisionM" begin
    @test VBGaussianMeanPrecisionM <: NaiveVariationalRule{GaussianMeanPrecision}
    @test outboundType(VBGaussianMeanPrecisionM) == Message{GaussianMeanPrecision}
    @test isApplicable(VBGaussianMeanPrecisionM, [ProbabilityDistribution, Nothing, ProbabilityDistribution])
    @test !isApplicable(VBGaussianMeanPrecisionM, [ProbabilityDistribution, ProbabilityDistribution, Nothing])

    @test ruleVBGaussianMeanPrecisionM(ProbabilityDistribution(Univariate, GaussianMeanVariance, m=3.0, v=4.0), nothing, ProbabilityDistribution(Univariate, Gamma, a=1.0, b=2.0)) == Message(Univariate, GaussianMeanPrecision, m=3.0, w=0.5)
    @test ruleVBGaussianMeanPrecisionM(ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[3.0], v=mat(4.0)), nothing, ProbabilityDistribution(MatrixVariate, Wishart, v=mat(0.25), nu=2.0)) == Message(Multivariate, GaussianMeanPrecision, m=[3.0], w=mat(0.5))
end

@testset "VBGaussianMeanPrecisionW" begin
    @test VBGaussianMeanPrecisionW <: NaiveVariationalRule{GaussianMeanPrecision}
    @test outboundType(VBGaussianMeanPrecisionW) == Message{Union{Gamma, Wishart}}
    @test isApplicable(VBGaussianMeanPrecisionW, [ProbabilityDistribution, ProbabilityDistribution, Nothing])

    @test ruleVBGaussianMeanPrecisionW(ProbabilityDistribution(Univariate, GaussianMeanVariance, m=3.0, v=4.0), ProbabilityDistribution(Univariate, GaussianMeanVariance, m=1.0, v=2.0), nothing) == Message(Univariate, Gamma, a=1.5, b=0.5*(2.0 + 4.0 + (3.0 - 1.0)^2))
    @test ruleVBGaussianMeanPrecisionW(ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[3.0], v=mat(4.0)), ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[1.0], v=mat(2.0)), nothing) == Message(MatrixVariate, Wishart, v=mat(1.0/(2.0 + 4.0 + (3.0 - 1.0)^2)), nu=3.0)
end

@testset "VBGaussianMeanPrecisionOut" begin
    @test VBGaussianMeanPrecisionOut <: NaiveVariationalRule{GaussianMeanPrecision}
    @test outboundType(VBGaussianMeanPrecisionOut) == Message{GaussianMeanPrecision}
    @test isApplicable(VBGaussianMeanPrecisionOut, [Nothing, ProbabilityDistribution, ProbabilityDistribution])

    @test ruleVBGaussianMeanPrecisionOut(nothing, ProbabilityDistribution(Univariate, GaussianMeanVariance, m=3.0, v=4.0), ProbabilityDistribution(Univariate, Gamma, a=1.0, b=2.0)) == Message(Univariate, GaussianMeanPrecision, m=3.0, w=0.5)
    @test ruleVBGaussianMeanPrecisionOut(nothing, ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[3.0], v=mat(4.0)), ProbabilityDistribution(MatrixVariate, Wishart, v=mat(0.25), nu=2.0)) == Message(Multivariate, GaussianMeanPrecision, m=[3.0], w=mat(0.5))
end

@testset "SVBGaussianMeanPrecisionMGVD" begin
    @test SVBGaussianMeanPrecisionMGVD <: StructuredVariationalRule{GaussianMeanPrecision}
    @test outboundType(SVBGaussianMeanPrecisionMGVD) == Message{GaussianMeanVariance}
    @test isApplicable(SVBGaussianMeanPrecisionMGVD, [Message{Gaussian}, Nothing, ProbabilityDistribution])

    @test ruleSVBGaussianMeanPrecisionMGVD(Message(Univariate, GaussianMeanVariance, m=3.0, v=4.0), nothing, ProbabilityDistribution(Univariate, Gamma, a=1.0, b=2.0)) == Message(Univariate, GaussianMeanVariance, m=3.0, v=6.0)
    @test ruleSVBGaussianMeanPrecisionMGVD(Message(Multivariate, GaussianMeanVariance, m=[3.0], v=mat(4.0)), nothing, ProbabilityDistribution(MatrixVariate, Wishart, v=mat(0.25), nu=2.0)) == Message(Multivariate, GaussianMeanVariance, m=[3.0], v=mat(6.0))
end

@testset "SVBGaussianMeanPrecisionW" begin
    @test SVBGaussianMeanPrecisionW <: StructuredVariationalRule{GaussianMeanPrecision}
    @test outboundType(SVBGaussianMeanPrecisionW) == Message{Union{Gamma, Wishart}}
    @test isApplicable(SVBGaussianMeanPrecisionW, [ProbabilityDistribution, Nothing])

    @test ruleSVBGaussianMeanPrecisionW(ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[2.0, 3.0], v=[5.0 1.0; 1.0 4.0]), nothing) == Message(Univariate, Gamma, a=1.5, b=0.5*(5.0 - 2*1.0 + 4.0 + (3.0 - 2.0)^2))
    @test ruleSVBGaussianMeanPrecisionW(ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[1.0, 2.0, 3.0, 4.0], v=[5.0 1.0 0.5 0.0; 1.0 4.0 2.0 0.5; 0.5 2.0 3.0 1.0; 0.0 0.5 1.0 2.0]), nothing) == Message(MatrixVariate, Wishart, v=cholinv([5.0 1.0; 1.0 4.0] - [0.5 0.0; 2.0 0.5] - [0.5 2.0; 0.0 0.5] + [3.0 1.0; 1.0 2.0] + ([1.0, 2.0] - [3.0, 4.0])*([1.0, 2.0] - [3.0, 4.0])'), nu=4.0)
end

@testset "SVBGaussianMeanPrecisionOutVGD" begin
    @test SVBGaussianMeanPrecisionOutVGD <: StructuredVariationalRule{GaussianMeanPrecision}
    @test outboundType(SVBGaussianMeanPrecisionOutVGD) == Message{GaussianMeanVariance}
    @test isApplicable(SVBGaussianMeanPrecisionOutVGD, [Nothing, Message{Gaussian}, ProbabilityDistribution])

    @test ruleSVBGaussianMeanPrecisionOutVGD(nothing, Message(Univariate, GaussianMeanVariance, m=3.0, v=4.0), ProbabilityDistribution(Univariate, Gamma, a=1.0, b=2.0)) == Message(Univariate, GaussianMeanVariance, m=3.0, v=6.0)
    @test ruleSVBGaussianMeanPrecisionOutVGD(nothing, Message(Multivariate, GaussianMeanVariance, m=[3.0], v=mat(4.0)), ProbabilityDistribution(MatrixVariate, Wishart, v=mat(0.25), nu=2.0)) == Message(Multivariate, GaussianMeanVariance, m=[3.0], v=mat(6.0))
end

@testset "MGaussianMeanPrecisionGGD" begin
    @test MGaussianMeanPrecisionGGD <: MarginalRule{GaussianMeanPrecision}
    @test isApplicable(MGaussianMeanPrecisionGGD, [Message{Gaussian}, Message{Gaussian}, ProbabilityDistribution])
    @test !isApplicable(MGaussianMeanPrecisionGGD, [Message{Gaussian}, Message{Gaussian}, Nothing])

    @test ruleMGaussianMeanPrecisionGGD(Message(Univariate, GaussianMeanPrecision, m=1.0, w=2.0), Message(Univariate, GaussianMeanPrecision, m=3.0, w=4.0), ProbabilityDistribution(Univariate, Gamma, a=1.0, b=2.0)) == ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[1.3636363636363638, 2.8181818181818175], v=[0.4090909090909091 0.04545454545454545; 0.04545454545454545 0.22727272727272724])
    @test ruleMGaussianMeanPrecisionGGD(Message(Multivariate, GaussianMeanPrecision, m=[1.0], w=mat(2.0)), Message(Multivariate, GaussianMeanPrecision, m=[3.0], w=mat(4.0)), ProbabilityDistribution(MatrixVariate, Wishart, v=mat(0.25), nu=2.0)) == ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[1.3636363636363638, 2.8181818181818175], v=[0.4090909090909091 0.04545454545454545; 0.04545454545454545 0.22727272727272724])
end

@testset "MGaussianMeanPrecisionGGN" begin
    @test MGaussianMeanPrecisionGGN <: MarginalRule{GaussianMeanPrecision}
    @test isApplicable(MGaussianMeanPrecisionGGN, [Message{Gaussian}, Message{Gaussian}, Nothing])
    @test !isApplicable(MGaussianMeanPrecisionGGN, [Message{Gaussian}, Message{Gaussian}, ProbabilityDistribution])

    @test ruleMGaussianMeanPrecisionGGN(Message(Univariate, GaussianMeanPrecision, m=1.0, w=2.0), Message(Univariate, GaussianMeanPrecision, m=3.0, w=4.0), Message(Univariate, PointMass, m=0.5)) == ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[1.3636363636363638, 2.8181818181818175], v=[0.4090909090909091 0.04545454545454545; 0.04545454545454545 0.22727272727272724])
    @test ruleMGaussianMeanPrecisionGGN(Message(Multivariate, GaussianMeanPrecision, m=[1.0], w=mat(2.0)), Message(Multivariate, GaussianMeanPrecision, m=[3.0], w=mat(4.0)), Message(MatrixVariate, PointMass, m=mat(0.5))) == ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[1.3636363636363638, 2.8181818181818175], v=[0.4090909090909091 0.04545454545454545; 0.04545454545454545 0.22727272727272724])
end

@testset "averageEnergy and differentialEntropy" begin
    @test differentialEntropy(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=2.0)) == averageEnergy(GaussianMeanPrecision, ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=2.0), ProbabilityDistribution(Univariate, PointMass, m=0.0), ProbabilityDistribution(Univariate, PointMass, m=2.0))
    @test differentialEntropy(ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=2.0)) == differentialEntropy(ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[0.0], w=mat(2.0)))
    @test averageEnergy(GaussianMeanPrecision, ProbabilityDistribution(Univariate, GaussianMeanPrecision, m=0.0, w=2.0), ProbabilityDistribution(Univariate, PointMass, m=0.0), ProbabilityDistribution(Univariate, PointMass, m=2.0)) == averageEnergy(GaussianMeanPrecision, ProbabilityDistribution(Multivariate, GaussianMeanPrecision, m=[0.0], w=mat(2.0)), ProbabilityDistribution(Multivariate, PointMass, m=[0.0]), ProbabilityDistribution(MatrixVariate, PointMass, m=mat(2.0)))
    @test averageEnergy(GaussianMeanPrecision, ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[0.0, 1.0], v=[3.0 1.0; 1.0 2.0]), ProbabilityDistribution(Univariate, PointMass, m=2.0)) == averageEnergy(GaussianMeanPrecision, ProbabilityDistribution(Multivariate, GaussianMeanVariance, m=[0.0, 1.0], v=[3.0 1.0; 1.0 2.0]), ProbabilityDistribution(MatrixVariate, PointMass, m=mat(2.0)))
end

end #module


# ---
# title: 1648. Sell Diminishing-Valued Colored Balls
# id: problem1648
# author: AquaIndigo
# date: 2020-11-08
# difficulty: Medium
# categories: Math, Greedy, Sort
# link: <https://leetcode.com/problems/sell-diminishing-valued-colored-balls/description/>
# hidden: true
# ---
#
# You have an `inventory` of different colored balls, and there is a customer
# that wants `orders` balls of **any** color.
#
# The customer weirdly values the colored balls. Each colored ball's value is
# the number of balls **of that color  **you currently have in your `inventory`.
# For example, if you own `6` yellow balls, the customer would pay `6` for the
# first yellow ball. After the transaction, there are only `5` yellow balls
# left, so the next yellow ball is then valued at `5` (i.e., the value of the
# balls decreases as you sell more to the customer).
#
# You are given an integer array, `inventory`, where `inventory[i]` represents
# the number of balls of the `ith` color that you initially own. You are also
# given an integer `orders`, which represents the total number of balls that the
# customer wants. You can sell the balls **in any order**.
#
# Return _the **maximum** total value that you can attain after selling
# _`orders` _colored balls_. As the answer may be too large, return it
# **modulo**`109 + 7`.
#
#
#
# **Example 1:**
#
# ![](https://assets.leetcode.com/uploads/2020/11/05/jj.gif)
#
#
#
#     Input: inventory = [2,5], orders = 4
#     Output: 14
#     Explanation: Sell the 1st color 1 time (2) and the 2nd color 3 times (5 + 4 + 3).
#     The maximum total value is 2 + 5 + 4 + 3 = 14.
#
#
# **Example 2:**
#
#
#
#     Input: inventory = [3,5], orders = 6
#     Output: 19
#     Explanation: Sell the 1st color 2 times (3 + 2) and the 2nd color 4 times (5 + 4 + 3 + 2).
#     The maximum total value is 3 + 2 + 5 + 4 + 3 + 2 = 19.
#
#
# **Example 3:**
#
#
#
#     Input: inventory = [2,8,4,10,6], orders = 20
#     Output: 110
#
#
# **Example 4:**
#
#
#
#     Input: inventory = [1000000000], orders = 1000000000
#     Output: 21
#     Explanation: Sell the 1st color 1000000000 times for a total value of 500000000500000000. 500000000500000000 modulo 109 + 7 = 21.
#
#
#
#
# **Constraints:**
#
#   * `1 <= inventory.length <= 105`
#   * `1 <= inventory[i] <= 109`
#   * `1 <= orders <= min(sum(inventory[i]), 109)`
#
#
## @lc code=start
using LeetCode

function max_profit(inventory::Vector{Int}, orders::Int)
    rest = sum(inventory) - orders
    sort!(inventory)
    len = length(inventory)
    for n in inventory
        if rest ÷ len >= n
            len -= 1
            rest -= n
        else
            break
        end
    end
    q = rest ÷ len
    r = rest - q * len
    ret = 0
    for i in (length(inventory) - len + 1):length(inventory)
        ret += sum((q + 2):inventory[i])
    end
    ret += (q + 1) * (len - r)
    return ret % (10^9 + 7)
end
## @lc code=end


#-------------------------------------------------------------# Cross Validation
"""
`StatLearnCV(o::StatLearn, xtest, ytest)`

Automatically tune the regularization parameter λ for `o` by minimizing loss on
test data `xtest`, `ytest`.

```julia
sl = StatLearn(size(x, 2), LassoPenalty(.1))
o = StatLearnCV(sl, xtest, ytest)
fit!(o, x, y)
```
"""
type StatLearnCV{T<:Real, S<:Real, W<:Weight} <: OnlineStat{XYInput}
    o::StatLearn
    burnin::Int
    xtest::AMat{T}
    ytest::AVec{S}
    weight::W
end
function StatLearnCV(o::StatLearn, xtest::AMat, ytest::AVec, burnin = 1000; wgt::LearningRate = LearningRate())
    @assert length(o.β) == size(xtest, 2) "number of predictors doesn't match test data"
    StatLearnCV(o, burnin, xtest, ytest, wgt)
end

function _fit!(o::StatLearnCV, x::AVec, y::Real, γ::Float64)
    if nobs(o) < o.burnin
        _fit!(o.o, x, y, γ)
    else
        tuneλ!(o.o, x, y, γ, o.xtest, o.ytest)
    end
    o
end
# function _fit!(o::StatLearnCV, x::AMat, y::AVec, γ::Float64)
#     if nobs(o) < o.burnin
#         _fit!(o.o, x, y, γ)
#     else
#         tuneλ!(o.o, x, y, γ, o.xtest, o.ytest)
#     end
#     o
# end

function tuneλ!{A<:Algorithm, M<:ModelDefinition}(
        o::StatLearn{A, M, NoPenalty}, x, y, γ, xtest, ytest
    )
    fit!(o, x, y)
end
function tuneλ!(o::StatLearn, x, y, γ, xtest, ytest)
    o_l = copy(o)
    o_h = copy(o)
    o_l.penalty.λ = max(0.0, o_l.penalty.λ - γ)
    o_h.penalty.λ += γ

    fit!(o, x, y)
    fit!(o_l, x, y)
    fit!(o_h, x, y)

    loss_low = loss(o_l, xtest, ytest)
    loss_mid = loss(o, xtest, ytest)
    loss_hi = loss(o_h, xtest, ytest)
    best = min(loss_low, loss_mid, loss_hi)

    if best == loss_low # o_l is winner
        o.β0 = o_l.β0
        o.β = o_l.β
        o.algorithm = o_l.algorithm
        o.penalty.λ = o_l.penalty.λ
    elseif best == loss_hi # o_h is winner
        o.β0 = o_h.β0
        o.β = o_h.β
        o.algorithm = o_h.algorithm
        o.penalty.λ = o_h.penalty.λ
    end
end
nobs(o::StatLearnCV) = nobs(o.o)
nups(o::StatLearnCV) = nups(o.o)
value(o::StatLearnCV) = value(o.o)
coef(o::StatLearnCV) = coef(o.o)
predict(o::StatLearnCV, x) = predict(o.o, x)
loss(o::StatLearnCV, x, y) = loss(o.o, x, y)
loss(o::StatLearnCV) = loss(o, o.xtest, o.ytest)

function Base.show(io::IO, o::StatLearnCV)
    printheader(io, "StatLearnCV")
    print_item(io, "burnin", o.burnin)
    print_item(io, "loss", loss(o))
    show(io, o.o)
end


function knotHash(d)
    d = vcat(map(Int, collect(d)), [17,31,73,47,23])
    l = collect(0:255)
    i = 0
    skip = 0
    for _ in 1:64
        for j in d
            l = circshift(l, -i)
            reverse!(l, 1, j)
            l = circshift(l, i)
            i += j + skip
            skip += 1
        end
    end
    l = mapslices(x -> reduce(⊻, x), reshape(l, (16,16)), 1)

    return mapreduce(x -> hex(x, 2), *, l)
end

input = "ljoxqyyw"

# Part 1
hashes = map(x -> knotHash("$input-$x"), 0:127)
grid = map(h -> vcat(map(x -> reverse!(digits(x, 2, 8)), hex2bytes(h))...)', hashes)
println(mapreduce(sum, +, grid))

# Part 2
n = length(grid)
function visit(i, j)
    if !(1 <= i <= n && 1 <= j <= n) || grid[i][j] != 1
        return 0
    end
    grid[i][j] = 0
    for d in [[1, 0], [0, -1], [-1, 0], [0, 1]]
        visit(i + d[1], j + d[2])
    end
    return 1
end
println(sum(visit(i, j) for i in 1:n, j in 1:n))


@testset "Densities" begin
    @testset "Single functions" begin
        rmin = 0.01
        rmax = 10.0
        δr = 0.05
        N = ceil(Int, rmax/δr)
        k = 7
        Nn = 71

        δrmin=rmin
        δrmax=0.5
        α=0.01

        @testset "$R" for (R,kind,rtol) in [
            (FiniteDifferences(N, δr), :orthogonal_uniform, 1e-14),
            (StaggeredFiniteDifferences(δrmin, δrmax, α, rmax), :orthogonal_non_uniform, 1e-14),
            (FEDVR(range(0, stop=rmax, length=Nn), k), :orthogonal_non_uniform, 1e-14),
            (BSpline(LinearKnotSet(k, 0, rmax, Nn)), :non_orthogonal, 1e-5),
        ]
            f = x -> sin(2π*x)
            g = x -> x*exp(-x)
            h = x -> sin(2π*x)*x*exp(-x)

            r = axes(R,1)

            cf = R \ f.(r)
            cg = R \ g.(r)
            ch = R \ h.(r)

            ρ = Density(applied(*,R,cf), applied(*,R,cg))
            ch2 = ρ.ρ

            @test eltype(ρ) == eltype(cf)

            if kind == :orthogonal_uniform
                @test ρ.LV == I
                @test ρ.RV == I
                @test ρ.C == I
            elseif kind == :orthogonal_non_uniform
                @test ρ.LV == I
                @test ρ.RV == I
            end

            @test ch2 ≈ ch atol=1e-14 rtol=rtol
        end
    end

    @testset "Multiple functions" begin
        L = 6.0
        N = 1000
        ρ = L/(N+1)
        @testset "$R" for R in [
            FiniteDifferences(N, ρ),
            StaggeredFiniteDifferences(0.01, 0.2, 0.1, L),
            FEDVR(range(0, stop=L, length=20), 7)[:,2:end-1],
            BSpline(LinearKnotSet(7, 0, L, 200))
        ]
            r = axes(R,1)
            f = exp.(-(r .- L/2).^2/0.5)
            gs = [
                sin.(1π/L*r),
                sin.(2π/L*r),
                sin.(3π/L*r),
                sin.(4π/L*r),
                sin.(5π/L*r),
                sin.(6π/L*r)
            ]
            w = r -> 1 / r

            cf = R \ f
            cgs = reduce(hcat, [R \ g for g in gs])
            wr = w.(r)

            u = applied(*, R, cf)
            v = applied(*, R, cgs)

            # One times many functions
            ρuv = Density(u, v, w=w)
            uv_refs = reduce(hcat, [R \ (f .* g .* wr) for g in gs])
            @test ρuv.ρ ≈ uv_refs

            # Many times many functions
            ρvv = Density(v, v, w=w)
            vv_refs = reduce(hcat, [R \ (g .* g .* wr) for g in gs])
            @test ρvv.ρ ≈ vv_refs

            @test_throws DimensionMismatch Density(applied(*, R, rand(size(R, 2), 2)), applied(*, R, rand(size(R, 2), 3)))
        end
    end

    @testset "Restricted bases" begin
        rmin = 0.0
        rmax = 10.0
        δr = 0.05
        N = ceil(Int, rmax/δr)
        k = 7
        Nn = 71

        δrmin=0.01
        δrmax=0.5
        α=0.01

        f = x -> sinpi(x/rmax)
        g = x -> cospi(x/rmax)
        @testset "$(label)" for (w,label) in [(one,"Unweighted"),(exp,"Weighted")]
            h = x -> f(x)*w(x)*g(x)

            @testset "$R" for (R,rtol) in [
                (FiniteDifferences(N, δr), 1e-15),
                (StaggeredFiniteDifferences(δrmin, δrmax, α, rmax), 1e-15),
                (FEDVR(range(0, stop=rmax, length=Nn), k), 1e-15),
                (BSpline(LinearKnotSet(k, 0, rmax, Nn)), 1e-14),
            ]
                R̃ = R[:,1:ceil(Int, size(R,2))]

                r = axes(R̃,1)

                u = R̃ \ f.(r)
                v = R̃ \ g.(r)

                ρ = Density(applied(*, R̃, u), applied(*, R̃, v), w=w)
                @test ρ.ρ ≈ (R̃ \ h.(r)) rtol=rtol
            end
        end
    end

    @testset "Pretty-printing" begin
        R = FiniteDifferences(99, 0.1)
        u = R*rand(size(R,2))
        v = R*rand(size(R,2))
        ρ = Density(u, v)
        fp = FunctionProduct{false}(u, v)

        @test string(ρ) == "99 .* 99 -> 99 FunctionProduct Float64, conjugated (<=> Density)"
        @test string(fp) == "99 .* 99 -> 99 FunctionProduct Float64"

        buf = IOBuffer()
        show(buf, MIME"text/plain"(), ρ)
        @test String(take!(buf)) == """
            99 .* 99 -> 99 FunctionProduct Float64, conjugated (<=> Density); L .* R -> R, with
              L: $(string(R))
              R: $(string(R))"""

        buf = IOBuffer()
        show(buf, MIME"text/plain"(), fp)
        @test String(take!(buf)) == """
            99 .* 99 -> 99 FunctionProduct Float64; L .* R -> R, with
              L: $(string(R))
              R: $(string(R))"""

        u = R*rand(size(R,2), 1)
        v = R*rand(size(R,2), 5)
        ρ = Density(u, v)
        @test string(ρ) == "(99, :) .* (99, :) -> (99, 5) FunctionProduct Float64, conjugated (<=> Density)"

        u = R*rand(size(R,2), 5)
        v = R*rand(size(R,2), 5)
        ρ = Density(u, v)
        @test string(ρ) == "(99, :) .* (99, :) -> (99, 5) FunctionProduct Float64, conjugated (<=> Density)"

        R = BSpline(LinearKnotSet(7, 0, 6.0, 200))

        u = R*rand(size(R,2))
        v = R*rand(size(R,2), 5)
        ρ = Density(u, v)
        @test string(ρ) == "206 .* (206, 5) -> (206, 5) FunctionProduct Float64, conjugated (<=> Density)"

        u = R*rand(size(R,2), 5)
        v = R*rand(size(R,2))
        ρ = Density(u, v)
        @test string(ρ) == "(206, 5) .* 206 -> (206, 5) FunctionProduct Float64, conjugated (<=> Density)"

        u = R*rand(size(R,2), 1)
        v = R*rand(size(R,2), 5)
        ρ = Density(u, v)
        @test string(ρ) == "(206, 1) .* (206, 5) -> (206, 5) FunctionProduct Float64, conjugated (<=> Density)"

        u = R*rand(size(R,2), 5)
        v = R*rand(size(R,2), 1)
        ρ = Density(u, v)
        @test string(ρ) == "(206, 5) .* (206, 1) -> (206, 5) FunctionProduct Float64, conjugated (<=> Density)"

        u = R*rand(size(R,2), 5)
        v = R*rand(size(R,2), 5)
        ρ = Density(u, v)
        @test string(ρ) == "(206, 5) .* (206, 5) -> (206, 5) FunctionProduct Float64, conjugated (<=> Density)"
    end
end


"""
ci_pde_aa()
Duplicate part of the data for Figure 4.2 in the book.
"""
function ci_pde_aa()
n=63;
m=10;
pdata=pdeinit(n);
Vstore=zeros(n*n,3*m+3);
VstoreS=zeros(n*n,2*m+4);
aout=PDE_aa(n, m; Vstore=Vstore, pdata=pdata);
aoutS=PDE_aa(n, m; Vstore=VstoreS, pdata=pdata);
# Same results with low storage mode?
alphaS= reldiff(aout.stats.alphanorm,aoutS.stats.alphanorm)
condS= reldiff(aout.stats.condhist,aoutS.stats.condhist)
histS = norm(aoutS.history-aout.history,Inf)
pdeerrS=  condS + alphaS + histS
aout.idid || println("pde solver failed")
(aout.errcode == 0) || println("wrong error code in pde")
(pdeerrS < 1.e-8) || println("different stats   ",condS,"  ",alphaS,"  ",histS)
(length(aout.history)==21) || println("history length wrong")
aa_ok = aout.idid && (aout.errcode==0) && (length(aout.history)==21) && (pdeerrS < 1.e-8)
aa_ok && println("pde succeeds")
return aa_ok
end


export cross_section, radial_sample, punch, bulk, multi_region_sampling

function cross_section(tumor; x = nothing, y = nothing, z = nothing, width = 2.0, reduce_dim = false)
    for (d, coord) in enumerate((x, y, z))
        if !isnothing(coord)
            rows = -width .< getindex.(tumor.position, d) .- coord .< width
            if reduce_dim
                plane = tumor[rows, :]
                plane.position = plane.position .|> p -> reduce_dimension(p, d)
                return plane
            else
                return tumor[rows, :]
            end
        end
    end
end

# this function returns a copy without entries at dimension d
function reduce_dimension(position, d)
    dim = position |> length
    select = mod.((d-1:2:d+1) .- 1, dim) .+ 1 |> unique
    return getindex(position, select)
end

radial_sample(tumor; r = 2.0, width = 2.0) = tumor[map(p -> abs(norm(p) - r) < width, tumor.position), :] |> deepcopy

punch(tumor; pos, r = 10.0) = tumor[map(p -> norm(p .- pos) < r, tumor.position), :] |> deepcopy

bulk(tumor; pos, box) = tumor[map(p -> all(abs.(p .- pos) .< box ./ 2), tumor.position), :] |> deepcopy

struct Square
    anchorpoint
    width
end

Base.in(point, c::Circle) = norm(point .- c.center) < c.r

function triangular_lattice(sq::Square; n = 0, a = sqrt(π / n) * radius)
    v, w = [1.0, 0.0], [0.5, -cosd(30)]

    steps_x = ceil(Int, sq.width / a)
    steps_y = ceil(Int, sq.width / (a * cosd(30)))

    lattice = Matrix{Vector{Float64}}(undef, steps_x, steps_y)
    lattice[:, 1] .= [sq.anchorpoint .+ a * i * v for i = 1:steps_x]
    for j = 2:steps_y
        @. lattice[:, j] = [p + a * (w - iseven(j) * v) for p in lattice[:, j-1]]
    end
    return lattice
end

function multi_region_sampling(tumor; n = 0, a = 0.0, cells_per_sample = 0, sample_r = a / 2)

    cm = mean(tumor.position)
    r = mean(norm.(p .- cm for p in tumor.position))
    density = size(punch(tumor; pos = cm, r = r), 1) / (π * r^2)
    R = sqrt(size(tumor, 1) / (π * density))

    sample_r = iszero(cells_per_sample) ? a / 2 : sqrt(cells_per_sample / (π * density))
    if !iszero(n)
        a = sqrt(π / n) * R
        sample_r = iszero(sample_r) ? a / 2 : sample_r
    elseif iszero(a)
        a = 2 * sample_r
    end
    iszero(sample_r) && error("specify parameters")
    sample_r > a / 2 && error("samples too large for given spacing")

    square = Square(cm .- R .* (1, -1), 2 * R)
    lattice = [triangular_lattice(square; n = n, a = a)...]
    filter!(p -> p in Circle(Point{2}(cm), R - a / 2), lattice)

    samples = lattice .|> p -> punch(tumor; pos = p, r = sample_r)

    return (lattice = lattice, samples = samples, sample_r = sample_r)
end


function dnsol(n, L::TronSparseMatrixCSC, r)
    # Solve L*x = r and store the result in r.

    @inbounds for j=1:n
        temp = r[j]/L.diag_vals[j]
        for k=L.colptr[j]:L.colptr[j+1]-1
            r[L.rowval[k]] = r[L.rowval[k]] - L.tril_vals[k]*temp
        end
        r[j] = temp
    end

    return
end

function dnsol(n, L::TronDenseMatrix, r)
    # Solve L*x = r and store the result in r.

    @inbounds for j=1:n
        temp = r[j]/L.vals[j,j]
        for k=j+1:n
            r[k] = r[k] - L.vals[k,j]*temp
        end
        r[j] = temp
    end
end

@inline function dnsol(n::Int, L::CuDeviceArray{Float64,2},
                       r::CuDeviceArray{Float64,1})
    # Solve L*x = r and store the result in r.

    tx = threadIdx().x
    ty = threadIdx().y

    @inbounds for j=1:n
        if tx == 1 && ty == 1
            r[j] = r[j] / L[j,j]
        end
        CUDA.sync_threads()

        if tx > j && tx <= n && ty == 1
            r[tx] = r[tx] - L[tx,j]*r[j]
        end
        CUDA.sync_threads()
    end

    return
end


if !isdefined(:LombScargle) using LombScargle end

function find_n_peaks_in_pgram(period::Vector, power::Vector; num_peaks::Integer = 1, exclude_period_factor::Real = 1.2 )
  @assert num_peaks>= 1
  peak_periods = zeros(num_peaks)
  peaks_found = 1
  peak_periods[peaks_found] = period[findmax(power)[2]]
  while peaks_found < num_peaks
     idx_active = trues(length(period))
     for j in 1:length(period)
     for i in 1:peaks_found
        if peak_periods[i]/exclude_period_factor <= period[j] <= peak_periods[i]*exclude_period_factor
           idx_active[j] = false
        end # if near a previous peak
     end # for over peaks
     end # for over periods
     peaks_found += 1
     peak_periods[peaks_found] = period[idx_active][findmax(power[idx_active])[2]]
  end # while more peaks to be found
  peak_periods
end

function find_n_peaks(time::Vector, obs::Vector, sigma::Vector; num_peaks::Integer = 1, exclude_period_factor::Real = 1.2 )
  pgram = lombscargle(time, obs, sigma)
  find_n_peaks_in_pgram(period(pgram), power(pgram), num_peaks=num_peaks, exclude_period_factor=exclude_period_factor )
end


const pi_token = Token{:reserved}("pi")

divpi(n::Int) = (pi_token, Token{:reserved}("/"), qasm_int(n))

# Intrinsics
# // 3-parameter 2-pulse single qubit gate
# gate u3(theta,phi,lambda) q { U(theta,phi,lambda) q; }
# // 2-parameter 1-pulse single qubit gate
# gate u2(phi,lambda) q { U(pi/2,phi,lambda) q; }
# // 1-parameter 0-pulse single qubit gate
# gate u1(lambda) q { U(0,0,lambda) q; }
# // Pauli gate: bit-flip
# gate x a { u3(pi,0,pi) a; }
# // Pauli gate: bit and phase flip
# gate y a { u3(pi,pi/2,pi/2) a; }
# // Pauli gate: phase flip
# gate z a { u1(pi) a; }
# // Clifford gate: Hadamard
# gate h a { u2(0,pi) a; }
# // Clifford gate: sqrt(Z) phase gate
# gate s a { u1(pi/2) a; }
# // Clifford gate: conjugate of sqrt(Z)
# gate sdg a { u1(-pi/2) a; }
# // C3 gate: sqrt(S) phase gate
# gate t a { u1(pi/4) a; }
# // C3 gate: conjugate of sqrt(S)
# gate tdg a { u1(-pi/4) a; }

qasm_gate_name(::Type{Intrinsics.XGate}) = "x"
qasm_gate_name(::Type{Intrinsics.YGate}) = "y"
qasm_gate_name(::Type{Intrinsics.ZGate}) = "z"
qasm_gate_name(::Type{Intrinsics.HGate}) = "h"
qasm_gate_name(::Type{Intrinsics.SGate}) = "s"
qasm_gate_name(::Type{Intrinsics.TGate}) = "t"
qasm_gate_name(::Type{<:Intrinsics.Rx}) = "rx"
qasm_gate_name(::Type{<:Intrinsics.Ry}) = "ry"
qasm_gate_name(::Type{<:Intrinsics.Rz}) = "rz"

function intrinsic_qasm(::Type{Intrinsics.XGate}, qargs)
    UGate(pi_token, qasm_int(0), pi_token, qargs[1])
end

function intrinsic_qasm(::Type{Intrinsics.YGate}, qargs)
    UGate(pi_token, divpi(2), divpi(2), qargs[1])
end

function intrinsic_qasm(::Type{Intrinsics.ZGate}, qargs)
    UGate(qasm_int(0), qasm_int(0), pi_token, qargs[1])
end

function intrinsic_qasm(::Type{Intrinsics.HGate}, qargs)
    UGate(divpi(2), qasm_int(0), pi_token, qargs[1])
end

function intrinsic_qasm(::Type{Intrinsics.SGate}, qargs)
    UGate(qasm_int(0), qasm_int(0), divpi(2), qargs[1])
end

function intrinsic_qasm(::Type{Intrinsics.TGate}, qargs)
    UGate(qasm_int(0),qasm_int(0),divpi(4), qargs[1])
end

# // Rotation around X-axis
# gate rx(theta) a { u3(theta,-pi/2,pi/2) a; }
# // rotation around Y-axis
# gate ry(theta) a { u3(theta,0,0) a; }
# // rotation around Z axis
# gate rz(phi) a { u1(phi) a; }
# NOTE: theta can be a variable or exp
function intrinsic_qasm(::Type{<:Intrinsics.Rx}, theta, qargs)
    UGate(theta, Neg(divpi(2)), divpi(2), qargs[1])
end

function intrinsic_qasm(::Type{<:Intrinsics.Ry}, theta, qargs)
    UGate(qasm_int(0), theta, qasm_int(0), qargs[1])
end

function intrinsic_qasm(::Type{<:Intrinsics.Rz}, theta, qargs)
    UGate(qasm_int(0), qasm_int(0), theta, qargs[1])
end


abstract AbstractLineSegment
abstract AbstractLineSegment2D <: AbstractLineSegment

immutable LineSegment2D{T<:Real} <: AbstractLineSegment2D

    p1::Point2D{T}
    p2::Point2D{T}

end

Base.isless(s1::LineSegment2D, s2::LineSegment2D) = isless(s1.p1, s2.p1) || (s1.p1 == s2.p1 && isless(s1.p2, s2.p2))

direction(p, q, r) = det([1 getx(p) gety(p); 1 getx(q) gety(q); 1 getx(r) gety(r)])

direction{T <: Real}(s::LineSegment2D{T}, r::Point2D{T}) = direction(s.p1,s.p2,r)

direction{T <: Real}(s::Array{T,2}, r::Array{T,1}) = direction(getpoint(s,1),getpoint(s,2),r)

function intersection(s1, s2)
    # Do line segments s1 and s2 intersect
    function onsegment{T <: Real}(s::LineSegment2D{T}, r::Point2D{T})
        # Is a point r that is known to be collinear on segment s?
        p, q = s.p1, s.p2
        min(p.x,q.x) <= r.x <= max(p.x,q.x) && min(p.y,q.y) <= r.y <= max(p.y,q.y)
    end

    function onsegement{T <: Real}(s1::Array{T,2}, r::Array{T,1})
        p, q = getpoint(s1,1), getpoint(s1,2)
        min(getx(p),getx(q)) <= getx(r) <= max(getx(p),getx(q)) &&
                                           min(gety(p),gety(q)) <= r.y <= max(gety(p),gety(q))
    end

    d1 = direction(s2, s1.p1)
    d2 = direction(s2, s1.p2)
    d3 = direction(s1, s2.p1)
    d4 = direction(s1, s2.p2)

    if ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))
        true
    elseif d1 == 0 && onsegment(s2, s1.p1)
        true
    elseif d2 == 0 && onsegment(s2, s1.p2)
        true
    elseif d3 == 0 && onsegment(s1, s2.p1)
        true
    elseif d4 == 0 && onsegment(s1, s2.p2)
        true
    else
        false
    end

end


"""
Atmosphere structure to hold all values related to the meteorology / atmoshpere.

# Arguments

- `T` (°C): air temperature
- `Wind` (m s-1): wind speed
- `P` (kPa): air pressure
- `Rh = rh_from_vpd(VPD,eₛ)` (0-1): relative humidity
- `Cₐ` (ppm): air CO₂ concentration
- `e = vapor_pressure(T,Rh)` (kPa): vapor pressure
- `eₛ = e_sat(T)` (kPa): saturated vapor pressure
- `VPD = eₛ - e` (kPa): vapor pressure deficit
- `ρ = air_density(T, P, constants.Rd, constants.K₀)` (kg m-3): air density
- `λ = latent_heat_vaporization(T, constants.λ₀)` (J kg-1): latent heat of vaporization
- `γ = psychrometer_constant(P, λ, constants.Cₚ, constants.ε)` (kPa K−1): psychrometer "constant"
- `ε = atmosphere_emissivity(T,e,constants.K₀)` (0-1): atmosphere emissivity
- `Δ = e_sat_slope(meteo.T)` (0-1): slope of the saturation vapor pressure at air temperature

# Notes

The structure can be built using only `T`, `Rh`, `Wind` and `P`. All other variables are oprional
and can be automatically computed using the functions given in `Arguments`.

# Examples

```julia
Atmosphere(T = 20.0, Wind = 1.0, P = 101.3, Rh = 0.65)
```
"""
Base.@kwdef struct Atmosphere{A}
    T::A
    Wind::A
    P::A
    Rh::A
    Cₐ::A = 400.0
    e::A = vapor_pressure(T,Rh)
    eₛ::A = e_sat(T)
    VPD::A = eₛ - e
    ρ::A = air_density(T, P) # in kg m-3
    λ::A = latent_heat_vaporization(T)
    γ::A = psychrometer_constant(P, λ) # in kPa K−1
    ε::A = atmosphere_emissivity(T,e)
    Δ::A = e_sat_slope(T)
end

"""

```julia
w = Weather(
    [
        Atmosphere(T = 20.0, Wind = 1.0, P = 101.3, Rh = 0.65),
        Atmosphere(T = 23.0, Wind = 1.5, P = 101.3, Rh = 0.60),
        Atmosphere(T = 25.0, Wind = 3.0, P = 101.3, Rh = 0.55)
    ],
    "Test site"
    )
```
"""
struct Weather{D <: AbstractArray,S <: AbstractString}
    data::D
    site::S
end

function Weather(df::DataFrame)
    @assert findfirst(x -> x == "site", names(df)) "The input `DataFrame` should have a column called site"
    Weather(df[!, DataFrames.Not(:site)],unique(df.site))
end

function Base.show(io::IO, n::Weather)
    print(io,"Wheather data from `$(n.site)`:\n")
    print(DataFrame(n))
    return nothing
end

"""
    DataFrame(data::Weather)

Transform a Weather type into a DataFrame.

See also [`Weather`](@Ref) to make the reverse.
"""
function DataFrame(data::Weather)
    df = DataFrame(data.data)
    # df.site .= data.site
    df[!,:site] .= data.site
    return df
end

"""
    vapor_pressure(Tₐ, rh)
Vapor pressure (kPa) at given temperature (°C) and relative hunidity (0-1).
"""
function vapor_pressure(Tₐ, rh)
    rh * e_sat(Tₐ)
end

"""
    e_sat(T)

Saturated water vapour pressure (es, in kPa) at given temperature `T` (°C).
See Jones (1992) p. 110 for the equation.
"""
function e_sat(T)
  0.61375 * exp((17.502 * T) / (T + 240.97))
end

"""
    e_sat_slope(T)

Slope of the vapor pressure saturation curve at a given temperature `T` (°C).
"""
function e_sat_slope(T)
  (e_sat(T + 0.1) - e_sat(T)) / 0.1
end

"""
    air_density(Tₐ, P)
    air_density(Tₐ, P, Rd, K₀)

ρ, the air density (kg m-3).

# Arguments

- `Tₐ` (Celsius degree): air temperature
- `P` (kPa): air pressure
- `Rd` (J kg-1 K-1): gas constant of dry air (see Foken p. 245, or R bigleaf package).
- `K₀` (Celsius degree): temperature in Celsius degree at 0 Kelvin

# Note

Rd and K₀ are Taken from [`Constants`](@ref) if not provided.

# References

Foken, T, 2008: Micrometeorology. Springer, Berlin, Germany.
"""
function air_density(Tₐ, P, Rd, K₀)
    (P * 1000) / (Rd * (Tₐ - K₀))
end

function air_density(Tₐ, P)
    constants = Constants()
    air_density(Tₐ, P, constants.Rd, constants.K₀)
end

"""
    psychrometer_constant(P, λ, Cₚ, ε)
    psychrometer_constant(P, λ)

γ, the psychrometer constant, also called psychrometric constant (kPa K−1). See Monteith and
Unsworth (2013), p. 222.

# Arguments

- `P` (kPa): air pressure
- `λ` (``J\\ kg^{-1}``): latent heat of vaporization for water (see [`latent_heat_vaporization`](@ref))
- `Cₚ` (J kg-1 K-1): specific heat of air at constant pressure (``J\\ K^{-1}\\ kg^{-1}``)
- `ε` (Celsius degree): temperature in Celsius degree at 0 Kelvin

# Note

Cₚ, ε and λ₀ are taken from [`Constants`](@ref) if not provided.

```julia
Tₐ = 20.0

λ = latent_heat_vaporization(Tₐ, λ₀)
psychrometer_constant(100.0, λ)
```

# References

Monteith, John L., et Mike H. Unsworth. 2013. « Chapter 13 - Steady-State Heat Balance: (i)
Water Surfaces, Soil, and Vegetation ». In Principles of Environmental Physics (Fourth Edition),
edited by John L. Monteith et Mike H. Unsworth, 217‑47. Boston: Academic Press.

"""
function psychrometer_constant(P, λ, Cₚ, ε)
    γ = (Cₚ * P)/(ε * λ)
    return γ
end

function psychrometer_constant(P, λ)
    constant = Constants()
    γ = (constant.Cₚ * P) / (constant.ε * λ)
    return γ
end

"""
γ_star(γ, a_sh, a_s, rbv, Rsᵥ, Rbₕ)

γ∗, the apparent value of psychrometer constant (kPa K−1).

# Arguments

- `γ` (kPa K−1): psychrometer constant
- `aₛₕ` (1,2): number of faces exchanging heat fluxes (see Schymanski et al., 2017)
- `aₛᵥ` (1,2): number of faces exchanging water fluxes (see Schymanski et al., 2017)
- `Rbᵥ` (s m-1): boundary layer resistance to water vapor
- `Rsᵥ` (s m-1): stomatal resistance to water vapor
- `Rbₕ` (s m-1): boundary layer resistance to heat

# Note

Using the corrigendum from Schymanski et al. (2017) in here so the definition of
[`latent_heat`](@ref) remains generic.

Not to be confused with [`Γ_star`](@ref) in FcVB model, which is the CO₂ compensation point.

# References

Monteith, John L., et Mike H. Unsworth. 2013. « Chapter 13 - Steady-State Heat Balance: (i)
Water Surfaces, Soil, and Vegetation ». In Principles of Environmental Physics (Fourth Edition),
edited by John L. Monteith et Mike H. Unsworth, 217‑47. Boston: Academic Press.

Schymanski, Stanislaus J., et Dani Or. 2017. Leaf-Scale Experiments Reveal an Important
Omission in the Penman–Monteith Equation ». Hydrology and Earth System Sciences 21 (2): 685‑706.
https://doi.org/10.5194/hess-21-685-2017.
"""
function γ_star(γ, aₛₕ, aₛᵥ, Rbᵥ, Rsᵥ, Rbₕ)
    γ * aₛₕ / aₛᵥ * (Rbᵥ + Rsᵥ) / Rbₕ # rv + Rsᵥ= Boundary + stomatal conductance to water vapour
end

"""
    latent_heat_vaporization(Tₐ,λ₀)
    latent_heat_vaporization(Tₐ)

λ, the latent heat of vaporization for water (J kg-1).

# Arguments

- `Tₐ` (°C): air temperature
- `λ₀`: latent heat of vaporization for water at 0 degree Celsius. Taken from `Constants().λ₀`
if not provided (see [`Constants`](@ref)).

"""
function latent_heat_vaporization(Tₐ,λ₀)
  λ₀ - 2.365e3 * Tₐ
end

function latent_heat_vaporization(Tₐ)
    latent_heat_vaporization(Tₐ,Constants().λ₀)
end


"""
    GraphsOptim

A package for graph-related optimization algorithms that rely on Linear Programming.

The package JuMP.jl and one of its supported solvers are required.
"""
module GraphsOptim

using Graphs
using JuMP
using SimpleTraits
using SparseArrays

include("utils.jl")
include("minimum_cost_flow.jl")
include("maximum_weight_matching.jl")

export minimum_cost_flow
export maximum_weight_matching, maximum_weight_maximal_matching

end


@info "Visualization: [PoissonMatrix (5x5) (λ=5)]"
display(PoissonMatrix(5,5,rank=3,λ=5))
@info "Visualization: [GaussianMatrix (5x5) (μ=0,σ=1)]"
display(GaussianMatrix(5,5,rank=3,σ=1,μ=0))
@info "Visualization: [BinomialMatrix (5x5) (p=0.5)]"
display(BernoulliMatrix(5,5,rank=3,p=0.5))
@info "Visualization: [GammaMatrix (5,5) (α=5,θ=0.5)]"
display(GammaMatrix(5,5,rank=3,α=5,θ=0.5))


using Gadfly

set_default_plot_size(6inch, 3inch)

n = 10
plot(x=rand(n), y=rand(n), color=sinh.(rand(n)), Scale.color_asinh)


using Plots
using Test
using StaticArrays
using LinearAlgebra

mutable struct Moon
  pos::SVector{3, Int32}
  vel::SVector{3, Int32}
  pot::Int32
  kin::Int32
end

function updateEnergy!(m::Moon)
  m.pot = sum(abs.(m.pos))
  m.kin = sum(abs.(m.vel))
end
function totalEnergy(m::Moon)
  return m.pot * m.kin
end

function updatePosition!(moon::Moon)
  moon.pos += moon.vel
  updateEnergy!(moon)
end
function updateVelocity!(moon1::Moon, moon2::Moon)
  diff_pos = moon2.pos .- moon1.pos
  normalized_diff = map(x-> pseudoNormalize(x), diff_pos)
  moon1.vel += normalized_diff
  moon2.vel -= normalized_diff
  return
end

function step!(arr::Array{Moon, 1})
  num_moons = length(arr)
  for i = 1:num_moons
    for j = 1:num_moons
      if j > i
        updateVelocity!(arr[i], arr[j])
      end
    end
  end
  updatePosition!.(arr)
end

function test_step()
  moon1 = Moon(SVector(1,2,3), SVector(4,5,6), 0,0)
  moon2 = Moon(SVector(11,22,3), SVector(44,55,66), 0,0)
  step!([moon1, moon2])
  @test moon1.vel == SVector(5,6,6)
  @test moon1.pos == SVector(6,8,9)
  @test moon2.vel == SVector(43,54,66)
  @test moon2.pos == SVector(54,76,69)
  @test moon1.pot == sum([6,8,9])
  @test moon1.kin == sum([5,6,6])

end

test_step()

function initializeMoon(x, y, z)
  moon = Moon(SVector(x,y,z), SVector(0,0,0), 0,0)
  updateEnergy!(moon)
  return moon
end

function test_simulation(steps)
  moon1 = initializeMoon(-1,0,2)
  moon2 = initializeMoon(2,-10,-7)
  moon3 = initializeMoon(4,-8,8)
  moon4 = initializeMoon(3,5,-1)
  arr = [moon1, moon2, moon3, moon4]

  for i in 1:steps
    step!(arr)
    total_energies = sum(totalEnergy.(arr))
    for moon in arr
      println(moon)
    end
    println(total_energies)
  end
end
function run_moon_simulation(steps=200)
  moon1 = initializeMoon(-10,-13,7)
  moon2 = initializeMoon(1,2,1)
  moon3 = initializeMoon(-15,-3,13)
  moon4 = initializeMoon(3, 7,-4)
  arr = [moon1, moon2, moon3, moon4]

  for i in 1:steps
    step!(arr)
    total_energies = sum(totalEnergy.(arr))
    println(total_energies)
  end
end
#run_moon_simulation(1000)
#test_simulation(10)

function get_frequency(_arr::Array{Moon, 1}, dimension)
  arr = deepcopy(_arr)

  steps = 0
  begin
    @label repeat
    step!(arr)
    steps += 1
    for (i, moon) in enumerate(arr)
      if moon.vel[dimension] != 0 || moon.pos[dimension] != _arr[i].pos[dimension]
        @goto repeat
      end
    end
  end
  return steps
end

#moon1 = initializeMoon(-1,0,2)
#moon2 = initializeMoon(2,-10,-7)
#moon3 = initializeMoon(4,-8,8)
#moon4 = initializeMoon(3,5,-1)

#moon1 = initializeMoon(-8,-10,0)
#moon2 = initializeMoon(5,5,10)
#moon3 = initializeMoon(2,-7,3)
#moon4 = initializeMoon(9,-8,-3)
moon1 = initializeMoon(-10,-13,7)
moon2 = initializeMoon(1,2,1)
moon3 = initializeMoon(-15,-3,13)
moon4 = initializeMoon(3, 7,-4)
arr = [moon1, moon2, moon3, moon4]
x_freq = get_frequency(arr, 1)
y_freq = get_frequency(arr, 2)
z_freq = get_frequency(arr, 3)
println(lcm(x_freq,y_freq,z_freq))