let
	# read a sample .XDSL BN file
	# NOTE: these are generated by Genie / Smile
	#       See Smile.jl
	# Success -> Forecast

	bn = readxdsl(Pkg.dir("BayesNets", "test", "sample_bn.xdsl"))

	@test sort!(names(bn)) == [:Forecast, :Success]
	@test isempty(parents(bn, :Success))
	@test sort!(parents(bn, :Forecast)) == [:Success]

	@test isapprox(pdf(bn, :Success=>1, :Forecast=>1), 0.2*0.4)
	@test isapprox(pdf(bn, :Success=>1, :Forecast=>2), 0.2*0.4)
	@test isapprox(pdf(bn, :Success=>1, :Forecast=>3), 0.2*0.2)
	@test isapprox(pdf(bn, :Success=>2, :Forecast=>1), 0.8*0.1)
	@test isapprox(pdf(bn, :Success=>2, :Forecast=>2), 0.8*0.3)
	@test isapprox(pdf(bn, :Success=>2, :Forecast=>3), 0.8*0.6)

    # test output to text
    push!(bn, DiscreteCPD(:Test, [:Forecast], [3], [Categorical([1.0, 0.0]),
                                                    Categorical([0.0, 1.0]),
                                                    Categorical([0.1234569, 1-0.1234569])]))

    filename = tempname()
    open(filename, "w") do io
        write(io, MIME"text/plain"(), bn)
    end

    lines = readlines(filename)
    @test strip(lines[1]) == "Success Forecast Test"
    @test strip(lines[2]) == "010"
    @test strip(lines[3]) == "001"
    @test strip(lines[4]) == "000"
    @test strip(lines[5]) == "2 3 2"
    @test strip(lines[6]) == "0.2 0.4 0.4 0.1 0.3 1 0 0.1234569"
    @test length(lines) == 6

    bn2 = open(filename, "r") do io
        read(io, MIME"text/plain"(), DiscreteBayesNet)
    end
    rm(filename)

    @test isapprox(pdf(bn2, :Success=>1, :Forecast=>1, :Test=>1), 0.2*0.4*1.0)
    @test isapprox(pdf(bn2, :Success=>1, :Forecast=>1, :Test=>2), 0.2*0.4*0.0)
    @test isapprox(pdf(bn2, :Success=>1, :Forecast=>2, :Test=>1), 0.2*0.4*0.0)
    @test isapprox(pdf(bn2, :Success=>1, :Forecast=>2, :Test=>2), 0.2*0.4*1.0)
    @test isapprox(pdf(bn2, :Success=>1, :Forecast=>3, :Test=>1), 0.2*0.2*0.1234569)
    @test isapprox(pdf(bn2, :Success=>1, :Forecast=>3, :Test=>2), 0.2*0.2*(1-0.1234569))
    @test isapprox(pdf(bn2, :Success=>2, :Forecast=>1, :Test=>1), 0.8*0.1*1.0)
    @test isapprox(pdf(bn2, :Success=>2, :Forecast=>1, :Test=>2), 0.8*0.1*0.0)
    @test isapprox(pdf(bn2, :Success=>2, :Forecast=>2, :Test=>1), 0.8*0.3*0.0)
    @test isapprox(pdf(bn2, :Success=>2, :Forecast=>2, :Test=>2), 0.8*0.3*1.0)
    @test isapprox(pdf(bn2, :Success=>2, :Forecast=>3, :Test=>1), 0.8*0.6*0.1234569)
    @test isapprox(pdf(bn2, :Success=>2, :Forecast=>3, :Test=>2), 0.8*0.6*(1-0.1234569))
end


using WAVI, Plots
"""
Produce a plot of the melt rate in MISMIP for specified melt rate parametrization (c.f. figure 4 in Favier 2019 10.5194/gmd-12-2255-2019)
***Options***
    - PICO_nboxP_zQ    : PICO with P boxes, ambient temperature taken at Qm depth for P = 10, 8, 5, 2 and Q = 500, 700
                        (e.g. "PICO_nbox2_z700")
    - PME              : Plume model emulator (using Lazeroms 2018 algorithm for grounding line and basal slope -- doi:10.5194/tc-12-49-2018)
    - MISMIP_1r        : Melt rate according to the MISMIP+ ice 1r experiment (doi: 10.5194/tc-14-2283-2020) scaled to match the mean melt rate
    - QuadL            : Quadratic formulation of melting with local dependency on thermal driving
    - QuadNL           : Quadratic formulation of melting with non-local dependency on thermal driving
"""

melt_rate_model = "QuadL"

function Favier2019_4km_init(melt_model)
# Grid and boundary conditions
nx = 160
ny = 20
nσ = 4
x0 = 0.0
y0 = -40000.0
dx = 4000.0
dy = 4000.0
h_mask = trues(nx, ny)
u_iszero = falses(nx + 1, ny); u_iszero[1,:] .= true
v_iszero = falses(nx, ny + 1); v_iszero[:,1] .= true; v_iszero[:,end] .= true
grid = Grid(nx=nx,
            ny=ny,
            nσ=nσ,
            x0=x0,
            y0=y0,
            dx=dx,
            dy=dy,
            h_mask=h_mask,
            u_iszero=u_iszero,
            v_iszero=v_iszero)

# Bed
bed = WAVI.mismip_plus_bed # function definition

# Inputing thickness profile, reading from binary file
#fname = "examples\\Favier2019_melt_params\\data\\MISMIP_ice0_2km_SteadyThickness.bin";
#fname = joinpath(dirname(@__FILE__), "data",  "WAVI_ice0_4km_thick_interpolated.bin")
fname = joinpath(dirname(@__FILE__), "data",  "WAVI_ice0_4km_thickness.bin")
h = Array{Float64,2}(undef, nx, ny)
read!(fname, h)
h = ntoh.(h)

# make the model
initial_conditions = InitialConditions(initial_thickness=h) # set thickness
model = Model(grid=grid,
            bed_elevation=bed,
            melt_rate = melt_model,
            initial_conditions=initial_conditions,
            solver_params=SolverParams(maxiter_picard=1)
            ) ;

# update the configuration
update_state!(model)

#contour plot melt rate
m = deepcopy(model.fields.gh.basal_melt)
m[model.fields.gh.grounded_fraction .== 1.] .= NaN
msat = deepcopy(m)
msat[msat .> 50] .= 50
#msat[zb .> -300] .= NaN;
x =  model.grid.xxh[:,1]; y = model.grid.yyh[1,:];
plt = heatmap(x ./ 1e3,y / 1e3, msat',
                fill=true,
                linewidth=0,
                colorbar=true,
                colorbar_title="melt rate (m/yr)",
                framestyle=:box)

xlims!((420, 640))
xlabel!("x (km)")
ylabel!("y (km)")

return model, plt
end

#Need these for PICO
nx = 160; ny = 20;
ice_front_mask = zeros(nx,ny);
ice_front_mask[end,:] .= 1;

if melt_rate_model == "PICO_nbox10_z700"
    melt_model = PICO(ice_front_mask = ice_front_mask,
                T0 =1.2,
                S0 = 34.6,
                use_box_mean_depth = true,
                γT = 0.94e-5,
                nbox = 10);

elseif melt_rate_model == "PICO_nbox8_z700"
    melt_model = PICO(ice_front_mask = ice_front_mask,
                T0 =1.2,
                S0 = 34.6,
                use_box_mean_depth = true,
                γT = 0.91e-5,
                nbox = 8);

elseif melt_rate_model == "PICO_nbox5_z700"
    melt_model = PICO(ice_front_mask = ice_front_mask,
                T0 = 1.2,
                S0 = 34.6,
                use_box_mean_depth = true,
                γT = 0.87e-5,
                nbox = 5);

elseif melt_rate_model == "PICO_nbox2_z700"
    melt_model = PICO(ice_front_mask = ice_front_mask,
                T0 = 1.2,
                S0 = 34.6,
                use_box_mean_depth = true,
                γT = 0.85e-5,
                nbox = 2);

elseif melt_rate_model == "PME"
    melt_model = PlumeEmulator(α=1.49)

elseif melt_rate_model == "MISMIP_1r"
    melt_model = MISMIPMeltRateOne(α = 0.184)

elseif melt_rate_model == "QuadL"
    melt_model = QuadraticMeltRate(γT = 0.745*1e-3)
    #melt_model = QuadraticMeltRate(γT = 0.745*1e-3, melt_partial_cell = true)

elseif melt_rate_model == "QuadNL"
    melt_model = QuadraticMeltRate(γT = 0.85*1e-3, flocal = false)
    #melt_model = QuadraticMeltRate(γT = 0.85*1e-3, flocal = false, melt_partial_cell = true)

else
    throw(ArgumentError("Specified melt rate model not found"))
end

model, plt = Favier2019_4km_init(melt_model);

m = deepcopy(model.fields.gh.basal_melt);
zb = model.fields.gh.b .* (model.fields.gh.grounded_fraction .== 1) + -  918.0 / 1028.0  .* model.fields.gh.h .* (model.fields.gh.grounded_fraction .< 1)
idx = (model.fields.gh.grounded_fraction .== 0)
mean_melt = sum(m[idx])./length(m[idx])
println("mean melt rate for shelf points is $mean_melt")


export Ellipsoid

struct Ellipsoid{K,K²} <: AbstractSimpleShape{K}
    c::SFloat{K}  # center of ellipsoid
    r::SFloat{K}  # semiaxes ("radii") in axis directions
    p::S²Float{K,K²}  # projection matrix to Ellipsoid coordinates; must be orthonormal (see surfpt_nearby)
    Ellipsoid{K,K²}(c,r,p) where {K,K²} = new(c,r,p)  # suppress default outer constructor
end

function Ellipsoid(c::AbsVecReal,  # center of ellipsoid
                   r::AbsVecReal,  # semiaxes ("radii")
                   axes::AbsMatReal=S²Float{length(c)}(I))  # columns are axes vector; assumed orthogonal
    K = length(c)
    l_ax = SVec(ntuple(k->norm(@view(axes[:,k])), Val(K)))
    p = axes' ./ l_ax

    return Ellipsoid{K,K*K}(c,r,p)
end

# Ellipsoid(s::Cuboid{K,K²}) where {K,K²} = Ellipsoid{K,K²}(s.c, (s.r).^-2, s.p)

Base.:(==)(s1::Ellipsoid, s2::Ellipsoid) = s1.c==s2.c && s1.r==s2.r && s1.p==s2.p
Base.isapprox(s1::Ellipsoid, s2::Ellipsoid) = s1.c≈s2.c && s1.r≈s2.r && s1.p≈s2.p
Base.hash(s::Ellipsoid, h::UInt) = hash(s.c, hash(s.r, hash(s.p, hash(:Ellipsoid, h))))

level(x::SReal{K}, s::Ellipsoid{K}) where {K} = norm((s.p * (x-s.c)) ./ s.r) - 1.0
# level(x::SReal{K}, s::Ellipsoid{K}) where {K} = √sum(abs2,((s.p * (x-s.c)) ./ s.r)) - 1.0
center(s::Ellipsoid) = s.c


"""
    ProductOperation <: AbstractGroupOperation

Direct product group operation.
"""
struct ProductOperation <: AbstractGroupOperation end

const ProductGroup{𝔽,T} = GroupManifold{𝔽,ProductManifold{𝔽,T},ProductOperation}

"""
    ProductGroup{𝔽,T} <: GroupManifold{𝔽,ProductManifold{T},ProductOperation}

Decorate a product manifold with a [`ProductOperation`](@ref).

Each submanifold must also be an [`AbstractGroupManifold`](@ref) or a decorated instance of
one. This type is mostly useful for equipping the direct product of group manifolds with an
[`Identity`](@ref) element.

# Constructor
    ProductGroup(manifold::ProductManifold)
"""
function ProductGroup(manifold::ProductManifold{𝔽}) where {𝔽}
    if !all(is_group_decorator, manifold.manifolds)
        error("All submanifolds of product manifold must be or decorate groups.")
    end
    op = ProductOperation()
    return GroupManifold(manifold, op)
end

function decorator_transparent_dispatch(::typeof(group_exp!), M::ProductGroup, q, X)
    return Val(:transparent)
end
function decorator_transparent_dispatch(::typeof(group_log!), M::ProductGroup, X, q)
    return Val(:transparent)
end

function Base.show(io::IO, ::MIME"text/plain", G::ProductGroup)
    print(
        io,
        "ProductGroup with $(length(base_manifold(G).manifolds)) subgroup$(length(base_manifold(G).manifolds) == 1 ? "" : "s"):",
    )
    return _show_product_manifold_no_header(io, base_manifold(G))
end

function Base.show(io::IO, G::ProductGroup)
    M = base_manifold(G)
    return print(io, "ProductGroup(", join(M.manifolds, ", "), ")")
end

submanifold(G::ProductGroup, i) = submanifold(base_manifold(G), i)

function submanifold_component(
    e::Identity{GT},
    ::Val{I},
) where {I,MT<:ProductManifold,𝔽,GT<:GroupManifold{𝔽,MT}}
    return Identity(submanifold(e.group, I), submanifold_component(e.p, I))
end

function submanifold_components(
    e::Identity{GT},
) where {MT<:ProductManifold,𝔽,GT<:GroupManifold{𝔽,MT}}
    M = base_manifold(e.group)
    return map(Identity, M.manifolds, submanifold_components(e.group, e.p))
end

Base.inv(G::ProductGroup, p) = inv(G.manifold, p)
Base.inv(::GT, e::Identity{GT}) where {GT<:ProductGroup} = e
function Base.inv(M::ProductManifold, x::ProductRepr)
    return ProductRepr(map(inv, M.manifolds, submanifold_components(M, x))...)
end
function Base.inv(M::ProductManifold, p)
    q = allocate_result(M, inv, p)
    return inv!(M, q, p)
end

inv!(G::ProductGroup, q, p) = inv!(G.manifold, q, p)
function inv!(M::ProductManifold, q, p)
    map(inv!, M.manifolds, submanifold_components(M, q), submanifold_components(M, p))
    return q
end

Base.identity(G::ProductGroup, p) = identity(G.manifold, p)
Base.identity(::GT, e::Identity{GT}) where {GT<:ProductGroup} = e
function Base.identity(M::ProductManifold, p::ProductRepr)
    return ProductRepr(map(identity, M.manifolds, submanifold_components(M, p))...)
end
function Base.identity(M::ProductManifold, p)
    q = allocate_result(M, identity, p)
    return identity!(M, q, p)
end

identity!(G::ProductGroup, q, p) = identity!(G.manifold, q, p)
function identity!(M::ProductManifold, q, p)
    map(identity!, M.manifolds, submanifold_components(M, q), submanifold_components(M, p))
    return q
end

compose(G::ProductGroup, p, q) = compose(G.manifold, p, q)
compose(G::GT, ::Identity{GT}, p) where {GT<:ProductGroup} = p
compose(G::GT, p, ::Identity{GT}) where {GT<:ProductGroup} = p
compose(G::GT, e::E, ::E) where {GT<:ProductGroup,E<:Identity{GT}} = e
function compose(M::ProductManifold, p::ProductRepr, q::ProductRepr)
    return ProductRepr(
        map(
            compose,
            M.manifolds,
            submanifold_components(M, p),
            submanifold_components(M, q),
        )...,
    )
end
function compose(M::ProductManifold, p, q)
    x = allocate_result(M, compose, p, q)
    return compose!(M, x, p, q)
end

compose!(G::ProductGroup, x, p, q) = compose!(G.manifold, x, p, q)
function compose!(M::ProductManifold, x, p, q)
    map(
        compose!,
        M.manifolds,
        submanifold_components(M, x),
        submanifold_components(M, p),
        submanifold_components(M, q),
    )
    return x
end

translate(G::ProductGroup, p, q, conv::ActionDirection) = translate(G.manifold, p, q, conv)
function translate(
    M::ProductManifold,
    p::ProductRepr,
    q::ProductRepr,
    conv::ActionDirection,
)
    return ProductRepr(
        map(
            translate,
            M.manifolds,
            submanifold_components(M, p),
            submanifold_components(M, q),
            repeated(conv),
        )...,
    )
end
function translate(M::ProductManifold, p, q, conv::ActionDirection)
    x = allocate_result(M, translate, p, q)
    return translate!(M, x, p, q, conv)
end

function translate!(G::ProductGroup, x, p, q, conv::ActionDirection)
    return translate!(G.manifold, x, p, q, conv)
end
function translate!(M::ProductManifold, x, p, q, conv::ActionDirection)
    map(
        translate!,
        M.manifolds,
        submanifold_components(M, x),
        submanifold_components(M, p),
        submanifold_components(M, q),
        repeated(conv),
    )
    return x
end

function inverse_translate(G::ProductGroup, p, q, conv::ActionDirection)
    return inverse_translate(G.manifold, p, q, conv)
end
function inverse_translate(
    M::ProductManifold,
    p::ProductRepr,
    q::ProductRepr,
    conv::ActionDirection,
)
    return ProductRepr(
        map(
            inverse_translate,
            M.manifolds,
            submanifold_components(M, p),
            submanifold_components(M, q),
            repeated(conv),
        )...,
    )
end
function inverse_translate(M::ProductManifold, p, q, conv::ActionDirection)
    x = allocate_result(M, inverse_translate, p, q)
    return inverse_translate!(M, x, p, q, conv)
end

function inverse_translate!(G::ProductGroup, x, p, q, conv::ActionDirection)
    return inverse_translate!(G.manifold, x, p, q, conv)
end
function inverse_translate!(M::ProductManifold, x, p, q, conv::ActionDirection)
    map(
        inverse_translate!,
        M.manifolds,
        submanifold_components(M, x),
        submanifold_components(M, p),
        submanifold_components(M, q),
        repeated(conv),
    )
    return x
end

function translate_diff(G::ProductGroup, p, q, X, conv::ActionDirection)
    return translate_diff(G.manifold, p, q, X, conv)
end
function translate_diff(
    M::ProductManifold,
    p::ProductRepr,
    q::ProductRepr,
    X::ProductRepr,
    conv::ActionDirection,
)
    return ProductRepr(
        map(
            translate_diff,
            M.manifolds,
            submanifold_components(M, p),
            submanifold_components(M, q),
            submanifold_components(M, X),
            repeated(conv),
        )...,
    )
end
function translate_diff(M::ProductManifold, p, q, X, conv::ActionDirection)
    Y = allocate_result(M, translate_diff, X, p, q)
    return translate_diff!(M, Y, p, q, X, conv)
end

function translate_diff!(G::ProductGroup, Y, p, q, X, conv::ActionDirection)
    return translate_diff!(G.manifold, Y, p, q, X, conv)
end
function translate_diff!(M::ProductManifold, Y, p, q, X, conv::ActionDirection)
    map(
        translate_diff!,
        M.manifolds,
        submanifold_components(M, Y),
        submanifold_components(M, p),
        submanifold_components(M, q),
        submanifold_components(M, X),
        repeated(conv),
    )
    return Y
end

function inverse_translate_diff(G::ProductGroup, p, q, X, conv::ActionDirection)
    return inverse_translate_diff(G.manifold, p, q, X, conv)
end
function inverse_translate_diff(
    M::ProductManifold,
    p::ProductRepr,
    q::ProductRepr,
    X::ProductRepr,
    conv::ActionDirection,
)
    return ProductRepr(
        map(
            inverse_translate_diff,
            M.manifolds,
            submanifold_components(M, p),
            submanifold_components(M, q),
            submanifold_components(M, X),
            repeated(conv),
        )...,
    )
end
function inverse_translate_diff(M::ProductManifold, p, q, X, conv::ActionDirection)
    Y = allocate_result(M, inverse_translate_diff, X, p, q)
    return inverse_translate_diff!(M, Y, p, q, X, conv)
end

function inverse_translate_diff!(G::ProductGroup, Y, p, q, X, conv::ActionDirection)
    return inverse_translate_diff!(G.manifold, Y, p, q, X, conv)
end
function inverse_translate_diff!(M::ProductManifold, Y, p, q, X, conv::ActionDirection)
    map(
        inverse_translate_diff!,
        M.manifolds,
        submanifold_components(M, Y),
        submanifold_components(M, p),
        submanifold_components(M, q),
        submanifold_components(M, X),
        repeated(conv),
    )
    return Y
end

function group_exp(M::ProductManifold, X::ProductRepr)
    return ProductRepr(map(group_exp, M.manifolds, submanifold_components(M, X))...)
end
function group_exp(M::ProductManifold, X)
    q = allocate_result(M, group_exp, X)
    return group_exp!(M, q, X)
end

function group_exp!(M::ProductManifold, q, X)
    map(group_exp!, M.manifolds, submanifold_components(M, q), submanifold_components(M, X))
    return q
end

function group_log(M::ProductManifold, q::ProductRepr)
    return ProductRepr(map(group_log, M.manifolds, submanifold_components(M, q))...)
end
function group_log(M::ProductManifold, q)
    X = allocate_result(M, group_log, q)
    return group_log!(M, X, q)
end

function group_log!(M::ProductManifold, X, q)
    map(group_log!, M.manifolds, submanifold_components(M, X), submanifold_components(M, q))
    return X
end


module TestWiringDiagramAlgorithms

using Test
using Catlab.Theories, Catlab.WiringDiagrams

A, B, C, D = [ Ports([sym]) for sym in [:A, :B, :C, :D] ]
I = munit(Ports)
f = singleton_diagram(Box(:f, [:A], [:B]))
g = singleton_diagram(Box(:g, [:B], [:C]))
h = singleton_diagram(Box(:h, [:C], [:D]))

# Normalization
###############

# Normalize copies.
d = compose(mcopy(A), otimes(f,f))
normalized = compose(f, mcopy(B))
@test normalize_copy!(d) == normalized

d = compose(f, mcopy(B), otimes(g,g))
normalize_copy!(d)
normalized = compose(f, g, mcopy(C))
perm = sortperm(boxes(d); by=box->box.value)
@test is_permuted_equal(d, normalized, perm)

d = compose(mcopy(A), otimes(f,f), otimes(g,g))
normalize_copy!(d)
perm = sortperm(boxes(d); by=box->box.value)
@test is_permuted_equal(d, normalized, perm)

# Normalize deletions.
@test normalize_delete!(copy(f)) == f

d = WiringDiagram(I,I)
add_box!(d, first(boxes(f)))
@test normalize_delete!(d) == WiringDiagram(I,I)

d = WiringDiagram(A, B)
fv = add_box!(d, first(boxes(f)))
gv = add_box!(d, first(boxes(g)))
hv = add_box!(d, first(boxes(h)))
add_wires!(d, [
  (input_id(d),1) => (fv,1),
  (fv,1) => (gv,1),
  (gv,1) => (hv,1),
  (fv,1) => (output_id(d),1),
])
@test normalize_delete!(d) == f

# Normalize wiring diagrams representing morphisms in a cartesian category.
d = compose(
  mcopy(A),
  otimes(id(A),mcopy(A)),
  otimes(f,f,f),
  otimes(id(B), id(B), compose(g, delete(C)))
)
normalized = compose(f, mcopy(B))
@test normalize_cartesian!(d) == normalized

# Layout
########

@test crossing_minimization_by_sort(f, box_ids(f), sources=[input_id(d)]) == [1]

d = otimes(f,g)
@test crossing_minimization_by_sort(d, box_ids(d), sources=[input_id(d)]) == [1,2]

d = WiringDiagram(I,I)
fv1, fv2 = add_box!(d, first(boxes(f))), add_box!(d, first(boxes(f)))
gv1, gv2 = add_box!(d, first(boxes(g))), add_box!(d, first(boxes(g)))
add_wires!(d, [ Wire((fv1,1) => (gv1,1)), Wire((fv2,1),(gv2,1)) ])
@test crossing_minimization_by_sort(d, [gv1,gv2], sources=[fv1,fv2]) == [1,2]
@test crossing_minimization_by_sort(d, [fv1,fv2], targets=[gv1,gv2]) == [1,2]

end


using LinearAlgebra: checksquare
using LazySets.Arrays: isinvertible

# ==========================
# Exponentiation functions
# ==========================

"""
    AbstractExpAlg

Abstract supertype for all exponentiation algorithms.
"""
abstract type AbstractExpAlg end

# FIXME use _alias_exp to distinguish from setops
# no-op
_alias(alg::AbstractExpAlg) = alg

# alias defined on value types
_alias(alg::Symbol) = _alias(Val(alg))

"""
    BaseExpAlg <: AbstractExpAlg

Matrix exponential using the scaling and squaring algorithm implemented
in Julia Base.

### Notes

If the array is static, the method implemented in `StaticArrays.jl` is applied.
This algorithm admits the alias `:base`.
"""
struct BaseExpAlg <: AbstractExpAlg end

const BaseExp = BaseExpAlg()
_alias(alg::Val{:base}) = BaseExp

"""
    LazyExpAlg <: AbstractExpAlg

Lazy wrapper for the matrix exponential, operations defined in `LazySets.jl`.

### Fields

- `m`   -- (optional, default: `30`) size of the Krylov subspace
- `tol` -- (optional, default: `1e-10`) tolerance

### Notes

This algorithm admits the alias `:lazy` and also `:krylov`(using default options).
"""
struct LazyExpAlg <: AbstractExpAlg
    m::Int        # size of the Krylov subspace
    tol::Float64  # tolerance
end

# default constructor
LazyExpAlg() = LazyExpAlg(30, 1e-10)

const LazyExp = LazyExpAlg()
_alias(alg::Union{Val{:lazy}, Val{:krylov}}) = LazyExp

"""
    IntervalExpAlg <: AbstractExpAlg

Matrix exponential using an interval enclosure of the Taylor series remainder.

### Fields

- `order` -- order of the Taylor series expansion of the matrix exponential

### Notes

This method allows to overapproximate ``exp(Aδ)`` with an interval matrix.
It also accepts an interval matrix ``A`` given as input.
This method admits the alias `:interval` and `:taylor`.
"""
struct IntervalExpAlg <: AbstractExpAlg
    order::Int
end

# default constructor
IntervalExpAlg() = IntervalExpAlg(10)

const IntervalExp = IntervalExpAlg()
_alias(alg::Val{:interval}) = IntervalExp
_alias(alg::Val{:taylor}) = IntervalExp

"""
    PadeExpAlg <: AbstractExpAlg

Matrix exponential for sparse matrices using Pade approximants.

### Notes

Requires `Expokit.jl`. This algorithm admits the alias `:pade`.
"""
struct PadeExpAlg <: AbstractExpAlg end

const PadeExp = PadeExpAlg()
_alias(alg::Val{:pade}) = PadeExp

# general case: convert to Matrix
@inline _exp(A::AbstractMatrix, ::BaseExpAlg) = exp(Matrix(A))
@inline _exp(A::Matrix, ::BaseExpAlg) = exp(A)

# static arrays have their own exp method
@inline _exp(A::StaticArray, ::BaseExpAlg) = exp(A)

# exponential of an identity multiple, defined in MathematicalSystems.jl
@inline _exp(A::IdentityMultiple, ::BaseExpAlg) = exp(A)

# lazy wrapper of the matrix exponential
@inline _exp(A::AbstractMatrix, ::LazyExpAlg) = SparseMatrixExp(A)

# pade approximants (requires Expokit.jl)
@inline _exp(A::SparseMatrixCSC, ::PadeExpAlg) = padm(A)

function _exp(A::AbstractMatrix, alg::IntervalExpAlg)
    return exp_overapproximation(A, one(eltype(A)), alg.order)
end

function _exp(A::AbstractMatrix, δ, alg::IntervalExpAlg)
    return exp_overapproximation(A, δ, alg.order)
end

"""
    _exp(A::AbstractMatrix, δ, [alg]::AbstractExpAlg=BaseExp)

Compute the matrix exponential ``e^{Aδ}``.

### Input

- `A`    -- matrix
- `δ`    -- step size
- `alg`  -- (optional, default: `BaseExp`) the algorithm used to take the matrix
            exponential of `Aδ`, possible options are `BaseExp`, `LazyExp`, `PadeExp` and `IntervalExp`
            see details in the *Algorithm* section below

### Output

A matrix or a lazy wrapper of the matrix exponential, depending on `alg`.

### Algorithm

- `BaseExp` -- (alias: `:base`) use Higham's scaling and squaring method implemented
               in Julia standard library; see `?exp` for details; if `A` is a static array,
               uses the implementation in `StaticArrays.jl`

- `LazyExp` -- (alias: `:lazy`) return a lazy wrapper type around the matrix exponential
               using the implementation `LazySets.SparseMatrixExp`

- `PadeExp` -- (alias: `pade`) apply Pade approximant method to compute the matrix
               exponential of a sparse matrix (requires `Expokit.jl`)

- `IntervalExp` -- (alias: `interval`, `taylor`) apply the Taylor series expansion of the matrix
                    exponential with an interval remainder; works if `A` is an interval matrix

### Notes

If the algorithm `LazyExp` is used, evaluations of the action of the matrix
exponential are done with the `expmv` implementation from `Expokit`
(but see `LazySets#1312` for the planned generalization to other backends).
"""
function _exp(A::AbstractMatrix, δ, alg::AbstractExpAlg=BaseExp)
    n = checksquare(A)
    return _exp(A * δ, alg)
end

function _exp(A::AbstractMatrix, δ, alg::PadeExpAlg)
    n = checksquare(A)
    @requires Expokit
    return _exp(A * δ, alg)
end

@inline function _P_2n(A::AbstractMatrix{N}, δ, n) where {N}
    return [Matrix(A*δ) Matrix(δ*I, n, n);
            zeros(n, 2*n)]::Matrix{N}
end

@inline function _P_2n(A::SparseMatrixCSC{N, M}, δ, n) where {N, M}
    return [sparse(A*δ) sparse(δ*I, n, n);
            spzeros(n, 2*n)]::SparseMatrixCSC{N, M}
end

@inline function _P_3n(A::AbstractMatrix{N}, δ, n) where {N}
    return [Matrix(A*δ)     Matrix(δ*I, n, n)  zeros(n, n)    ;
            zeros(n, 2*n          )  Matrix(δ*I, n, n)        ;
            zeros(n, 3*n          )                           ]::Matrix{N}
end

@inline function _P_3n(A::SparseMatrixCSC{N, M}, δ, n) where {N, M}
    return [sparse(A*δ)     sparse(δ*I, n, n)  spzeros(n, n) ;
            spzeros(n, 2*n          )  sparse(δ*I, n, n)     ;
            spzeros(n, 3*n          )                        ]::SparseMatrixCSC{N, M}
end

"""
    Φ₁(A::AbstractMatrix, δ, [alg]::AbstractExpAlg=BaseExp, [isinv]::Bool=false, [Φ]=nothing)

Evaluate the series

```math
Φ₁(A, δ) = ∑_{i=0}^∞ \\dfrac{δ^{i+1}}{(i+1)!}A^i,
```
where ``A`` is a square matrix of order ``n`` and ``δ > 0``.

### Input

- `A`      -- coefficients matrix
- `δ`      -- step size
- `alg`    -- (optional, default: `BaseExp`) the method used to take the matrix
               exponential of the coefficient matrix; see the documentation of
               [`_exp`](@ref) for available options
- `isinv`  -- (optional, default: `false`) if `true`, assume that the coefficients
              matrix is invertible and compute ``A^{-1}``
- `Φ`      -- (optional, default: `nothing`) optionally pass the matrix exponential ``e^{Aδ}``

### Output

A matrix.

### Algorithm

If ``A`` is invertible, ``Φ₁`` can be computed as

```math
Φ₁(A, δ) = A^{-1}(e^{Aδ} - I_n),
```
where ``I_n`` is the identity matrix of order ``n``.

In the general case, implemented in this function, it can be
computed as submatrices of the block matrix

```math
P_{2n} = \\exp \\begin{pmatrix}
Aδ && δI_n \\\\
0 && 0
\\end{pmatrix}.
```
It can be shown that
```math
\\exp(P_{2n}) = \\begin{pmatrix}
Φ(A, δ) && Φ₁(A, δ) \\\\
0 &&  δI_n
\\end{pmatrix}.
```
where ``Φ(A, δ) = e^{Aδ}``. In particular, `Φ₁(A, δ) = P[1:n, (n+1):2*n]`.
This method can be found in [[FRE11]](@ref).
"""
function Φ₁(A::AbstractMatrix, δ, alg::AbstractExpAlg=BaseExp, isinv=false, Φ=nothing)
    return _Φ₁(A, δ, alg, Val(isinv), Φ)
end

# dispatch for discretization methods
_Φ₁(A, δ, alg, isinv::Val{:false}, Φ) = _Φ₁_blk(A, δ, alg)
_Φ₁(A, δ, alg, isinv::Val{:true}, Φ) = _Φ₁_inv(A, δ, alg, Φ)

# evaluate the series Φ₁(A, δ) = ∑_{i=0}^∞ \\dfrac{δ^{i+1}}{(i+1)!}A^i
# without assuming invertibility of A, by taking the exponential of a 2n x 2n matrix
function _Φ₁_blk(A, δ, alg)
    n = checksquare(A)
    P_2n = _P_2n(A, δ, n)
    Q = _exp(P_2n, alg)
    return _P₁_blk(Q, n, alg)
end

@inline _P₁_blk(P, n::Int, ::AbstractExpAlg) = P[1:n, (n+1):2*n]
@inline _P₁_blk(P, n::Int, ::LazyExpAlg) = sparse(get_columns(P, (n+1):2*n)[1:n, :])

# compute the matrix Φ₁(A, δ) = A^{-1}(exp(Aδ) - I), assuming that A is invertible
# and explicitly computing inv(A); this function optionally receives the matrix Φ = exp(Aδ)
function _Φ₁_inv(A::AbstractMatrix, δ, alg, Φ=nothing)
    if isnothing(Φ)
        Φ = _exp(A, δ, alg)
    end
    n = size(A, 1)
    N = eltype(A)
    In = Matrix(one(N)*I, n, n)
    Ainv = inv(A)
    return Ainv * (Φ - In)
end

# special case when A is a multiple of the identity
function _Φ₁_inv(A::IdentityMultiple, δ, alg, Φ=nothing)
    λ = A.M.λ
    @assert !iszero(λ) "the given identity multiple is not invertible"
    α = (1/λ) * (exp(δ*λ) - 1)
    return IdentityMultiple(α, size(A, 1))
end

# method to compute Φ₁ * u, where u is a vector
function _Φ₁_u(A, δ, alg, isinv::Val{:true}, u::AbstractVector, Φ=nothing)
    return _Φ₁_u(A, δ, alg, Φ, u)
end

function _Φ₁_u(A, δ, alg, isinv::Val{:false}, u::AbstractVector, Φ=nothing)
    M = _Φ₁_blk(A, δ, alg)
    return M * u
end

# compute the vector Φ₁(A, δ)u = A^{-1}(exp(Aδ) - I) u
# assuming that A is invertible and solving the associated linear system
# (the inverse of A is not explicitly computed)
function _Φ₁_u(A, δ, alg, u::AbstractVector, Φ=nothing)
    if isnothing(Φ)
        Φ = _exp(A, δ, alg)
    end
    x = Φ * u - u
    return A \ x
end

function load_Φ₁_krylov()
return quote

# compute Φ₁(A, δ)u = A^{-1}(exp(Aδ) - I) u without explicitly computing exp(Aδ)
# and assuming that A is invertible
function _Φ₁_u(A, δ, alg::LazyExpAlg, u::AbstractVector, ::Nothing)
    w = expv(1.0 * δ, A, u, m=alg.m, tol=alg.tol)
    x = w - u
    return A \ x
end

end end  # quote / load_Φ₁_krylov()

"""
    Φ₂(A::AbstractMatrix, δ, [alg]::AbstractExpAlg=BaseExp, [isinv]::Bool=false, [Φ]=nothing)

Evaluate the series

```math
Φ₂(A, δ) = ∑_{i=0}^∞ \\dfrac{δ^{i+2}}{(i+2)!}A^i,
```
where ``A`` is a square matrix of order ``n`` and ``δ > 0``.

### Input

- `A`      -- coefficients matrix
- `δ`      -- step size
- `alg`    -- (optional, default: `BaseExp`) the method used to take the matrix
               exponential of the coefficient matrix; see the documentation of
               [`_exp`](@ref) for available options
- `isinv`  -- (optional, default: `false`) if `true`, assume that the coefficients
              matrix is invertible and compute ``A^{-1}``
- `Φ`      -- (optional, default: `nothing`) optionally pass the matrix exponential ``e^{Aδ}``

### Output

A matrix.

### Algorithm

If ``A`` is invertible, ``Φ₂`` can be computed as

```math
Φ₂(A, δ) = A^{-2}(e^{δA} - I_n - δA).
```

In the general case, implemented in this function, it can be computed as
submatrices of the block matrix

```math
P_{3n} = \\exp \\begin{pmatrix}
Aδ && δI_n && 0 \\\\
0 && 0 && δI_n \\\\
0 && 0 && 0
\\end{pmatrix}.
```
It can be shown that
```math
\\exp(P_{3n}) = \\begin{pmatrix}
Φ(A, δ) && Φ₁(A, δ) && Φ₂(A, δ) \\\\
0 && I_n     && δI_n \\\\
0 && 0     && I_n
\\end{pmatrix}.
```
where ``Φ(A, δ) = e^{Aδ}``. In particular, `Φ₂ = P_{3n}[1:n, (2*n+1):3*n]`.
This method can be found in [[FRE11]](@ref).
"""
function Φ₂(A::AbstractMatrix, δ, alg::AbstractExpAlg=BaseExp, isinv=false, Φ=nothing)
    return _Φ₂(A, δ, alg, Val(isinv), Φ)
end

# dispatch for discretization methods
_Φ₂(A, δ, alg, isinv::Val{:false}, Φ) = _Φ₂_blk(A, δ, alg)
_Φ₂(A, δ, alg, isinv::Val{:true}, Φ) = _Φ₂_inv(A, δ, alg, Φ)

@inline _P₂_blk(P, n, ::AbstractExpAlg) = P[1:n, (2*n+1):3*n]
@inline _P₂_blk(P, n, ::LazyExpAlg) = sparse(get_columns(P, (2*n+1):3*n)[1:n, :])

# evaluate the series Φ₂(A, δ) = ∑_{i=0}^∞ \\dfrac{δ^{i+2}}{(i+2)!}A^i
# without assuming invertibility of A, by taking the exponential of a 3n x 3n matrix
function _Φ₂_blk(A, δ, alg)
    n = checksquare(A)
    B = _P_3n(A, δ, n)
    P = _exp(B, alg)
    return _P₂_blk(P, n, alg)
end

# compute the matrix Φ₂ = A^{-2} (exp(A*δ) - I - A*δ) assuming that A is invertible
# and explicitly computing inv(A); this function optionally receives Φ = exp(Aδ)
# FIXME don't pass algorithm since it is ignore if Φ is given
#@time Φ2 = ReachabilityAnalysis._Φ₂_inv(abs.(A), δ, ReachabilityAnalysis.BaseExp, Φ);
function _Φ₂_inv(A::AbstractMatrix, δ, alg, Φ=nothing)
    Aδ = A * δ
    if isnothing(Φ)
        Φ = _exp(Aδ, alg)
    end
    n = size(A, 1)
    N = eltype(A)
    In = Matrix(one(N)*I, n, n)
    B = Φ - In - Aδ
    Ainv = inv(Matrix(A))
    Ainvsqr = Ainv^2
    return Ainvsqr * B
end

# special case when A is a multiple of the identity
function _Φ₂_inv(A::IdentityMultiple, δ, alg, Φ=nothing)
    λ = A.M.λ
    @assert !iszero(λ) "the given identity multiple is not invertible"
    δλ = δ * λ
    α = (1/λ)^2 * (exp(δλ) - 1 - δλ)
    return IdentityMultiple(α, size(A, 1))
end

# ================
# Absolute values
# ================

@inline _elementwise_abs(A::AbstractMatrix) = abs.(A)
@inline _elementwise_abs(A::SparseMatrixCSC) = SparseMatrixCSC(A.m, A.n, A.colptr, A.rowval, abs.(nonzeros(A)))
@inline _elementwise_abs(A::IdentityMultiple) = IdentityMultiple(abs(A.M.λ), size(A, 1))

# ====================================
# Exponentiation of interval matrices
# ====================================

# TODO: outsource to IntervalMatrices.jl

# compute Iδ + 1/2 * δ^2 * A + 1/6 * δ^3 * A² + ... + 1/(η+1)! * δ^(η+1) * A^η + E(δ) * δ
function _Cδ(A, δ, order)
    n = size(A, 1)
    A² = A * A
    if isa(A, IntervalMatrix)
        Iδ = IntervalMatrix(Diagonal(fill(IA.Interval(δ), n)))
    else
        Iδ = Matrix(δ * I, n, n)
    end

    IδW = Iδ + 1/2 * δ^2 * A + 1/6 * δ^3 * A²
    M = IδW

    if order > 2
        # i = 2
        αᵢ₊₁ = 6 # factorial of (i+1)
        Aⁱ = A²
        δⁱ⁺¹ = δ^3
        @inbounds for i in 3:order
            αᵢ₊₁ *= i+1
            δⁱ⁺¹ *= δ
            Aⁱ *= A
            M += (δⁱ⁺¹/αᵢ₊₁) * Aⁱ
        end
    end
    E = IntervalMatrices._exp_remainder(A, δ, order; n=n)
    return M + E * δ
end


include("update.jl")

function randomwalk(nsteps::Integer, prob::Real=0.5)
    result = zeros(Int, nsteps+1)
    x = 0
    result[1] = x
    for t in 1:nsteps
        x = update(x,prob)
        result[t+1] = x
    end
    return result
end


export spqr_pinv

"""
    SPQR_PINV approx pseudoinverse solution to min(norm(B-A*X))

usage: x,stats,N,NT = spqr_pinv(A, B, opts)
This function returns an approximate pseudoinverse solution to
                 min || B - A x ||                     (1)
for rank deficient matrices A.  The pseudoinverse solution is the minimum
norm solution to the least squares problem (1).  Also, optionally, the
routine returns statistics including the numerical rank of the matrix A for
tolerance tol (i.e. the number of singular values > tol) and other statistics
(see below), as well as, if requested, an orthonormal basis for the numerical
null space to A and an orthonormal basis for the null space of A transpose.
The pseudoinverse solution is approximate since the algorithm allows small
perturbations in A (columns of A may be changed by no more than a user
defined value in tol).

Input:  A -- an m by n matrix
        B -- an m by p right hand side matrix
        opts (optional) -- see below
Output: x -- this n by p matrix contains pseudoinverse solutions to (1).
             If B is empty then x will also be empty.
        stats (optional) -- statistics including:
"""
function spqr_pinv(A::AbstractMatrix{T}, B::AbstractMatrix{T}; tol::T, nargout=4, get_details::Int=0) where T
#=
 Examples:
    A = sparse(gallery('kahan',100))
    B = randn(100,1) ; B = B / norm(B)
    x = spqr_pinv(A,B)
    x_pinv = pinv(full(A))*B
    rel_error_in_x = norm(x - x_pinv) / norm(x_pinv)
    # or
    [x,stats,N,NT] = spqr_pinv(A,B)
    norm_A_times_N = norm(full(spqr_null_mult(N,A,3)))
    norm_N_transpose_times_A = norm(full(spqr_null_mult(NT,A,0)))
    # or
    opts = struct('tol',1.e-5)
    [x,stats] = spqr_pinv(A,B,opts)
    stats

See also spqr_basic, spqr_null, spqr_pinv, spqr_cod.

Copyright 2012, Leslie Foster and Timothy A Davis.

Algorithm:  a basic solution is calculated using spqr_basic. Following
   this an orthogonal basis, stored in N, for the numerical null space
   of A is calculated using spqr_null. The psuedoinverse solution is
   then x - N*(N'*x) which is calculated using the routine spqr_null_mult.
=#

#-------------------------------------------------------------------------------
# set tolerance and number of singular values to estimate
#-------------------------------------------------------------------------------
start_tic = time_ns()
stats = SpqrStats()

if nargout > 4
    error("usage: x, stats, N, NT = spqr_pinv(A, B, opts...)")
end

# get the options
get_details = opts.get_details

if get_details == 1
    stats.opts_used = opts
end

# set the order of the stats fields
#     stats.flag, stats.rank, stats.rank_spqr, stats.rank_spqr(if get_details
#     >= 1), stats.tol, stats.tol_alt, stats.normest_A (if calculated),
#     stats.est_sval_upper_bounds, stats.est_sval_lower_bounds, and
#     stats.sval_numbers_for_bounds already initialized in spqr_rank_get_inputs
if nargout >= 3
    stats.est_norm_A_times_N = -1
end
if nargout == 4
   stats.est_norm_A_transpose_times_NT = -1
end
# order for the additional stats fields needed when get_details is 1 will be
#     set using spqr_rank_order_fields at the end of the routine

#-------------------------------------------------------------------------------
# find basic solution
#-------------------------------------------------------------------------------

if get_details == 0
    # opts2 used to include a few extra statistics in stats_ssi
    opts2 = opts
    opts2.get_details = 2
else
    opts2 = opts
end
if nargout == 4
    # save basis for null space of A', if there are four output
    #   parameters.  This will require more memory.
    x, stats_spqr_basic, NT = spqr_basic(A, B, nargout=3, tol=tol, get_details=2)
else
    # do not save the basis for the null space of A'. Saves memory.
    x, stats_spqr_basic = spqr_basic(A, B, nargout=2, tol=tol, get_details=2)
end

stats.rank = stats_spqr_basic.rank

# save the stats
if get_details == 1 || get_details == 2
    stats.rank_spqr = stats_spqr_basic.rank_spqr
end

#-------------------------------------------------------------------------------
# check for early return
#-------------------------------------------------------------------------------

if stats_spqr_basic.flag == 4
    # overflow in spqr_ssi, called by spqr_basic.
    # spqr_basic has already issued a warning regarding overflow
    return spqr_failure(4, stats, get_details, start_tic)
end

#-------------------------------------------------------------------------------
# calculate basis for the null space of A
#-------------------------------------------------------------------------------

# spqr_null calls spqr_ssi.  Set the block size in spqr_ssi based on the
# difference in the numerical rank calculated by spqr_basic and by spqr.  Note
# that this overwrites the user-provided value of opts.ssi_min_block, if any.
opts.ssi_min_block = max(2, stats_spqr_basic.rank_spqr - stats.rank + 1)

opts.ssi_min_block = min(opts.ssi_min_block, stats_spqr_basic.stats_ssi.ssi_max_block_used)

N, stats_spqr_null = spqr_null(A, opts2)

if get_details == 1
    stats.stats_spqr_basic = stats_spqr_basic
    stats.stats_spqr_null = stats_spqr_null
end

#-------------------------------------------------------------------------------
# check for early return
#-------------------------------------------------------------------------------

# spqr_basic and spqr_null both return error flags.  Since both must be correct
# to calculate the psuedoinverse solution, choose the largest (worst) error.

stats.flag = max(stats_spqr_basic.flag, stats_spqr_null.flag)

if stats_spqr_basic.flag == 0 && stats_spqr_null.flag == 0  && stats_spqr_basic.rank ~= stats_spqr_null.rank
    # Rank from spqr_basic and spqr_null differ.  This is so rare that we know
    # of no matrices that trigger this condition.  This block of code is thus
    # untested.
    error("spqr_rank:inconsistent", "inconsistent rank estimates") ; # untested
    # an alternative, which would cause a return in the code below.
    #   stats.flag = 5
end

if stats.flag >= 4
    # early return: overflow in inverse power method in ssi,
    # or inconsistent rank estimates by spqr_basic and spqr_null.
    return spqr_failure(stats.flag, stats, get_details, start_tic)
end

#-------------------------------------------------------------------------------
# calculate the psuedoinverse solution
#-------------------------------------------------------------------------------

x = x - spqr_null_mult(N, spqr_null_mult(N, x, 0), 1)

#-------------------------------------------------------------------------------
# select from two estimates of numerical rank and two sets of bounds
#-------------------------------------------------------------------------------

# Strategy -- the choice of stats.flag above was the
#     max(stats_spqr_basic.flag, stats_spqr_null.flag)
# We will choose the bounds and rank corresponding to this choice of flag.
# When the flags are the same we will use spqr_basic results for
# the bounds, except when the flags are both 1 we will choose the bounds
# corresponding to the maximum of stats_spqr_basic.tol_alt and
# stats_spqr_null.tol_alt (the conservative choice).

if stats_spqr_basic.flag == 1 && stats_spqr_null.flag == 1
    if stats_spqr_basic.tol_alt >= stats_spqr_null.tol_alt
        st = stats_spqr_basic
    else
        st = stats_spqr_null
    end
elseif stats_spqr_basic.flag >= stats_spqr_null.flag
        st = stats_spqr_basic
else
        st = stats_spqr_null
end
stats.rank = st.rank
stats.tol_alt = st.tol_alt
stats.sval_numbers_for_bounds = st.sval_numbers_for_bounds
stats.est_sval_upper_bounds  = st.est_sval_upper_bounds
stats.est_sval_lower_bounds  = st.est_sval_lower_bounds

#-------------------------------------------------------------------------------
# return statistics
#-------------------------------------------------------------------------------

if get_details == 1 || nargout >= 3
   stats.est_norm_A_times_N = stats_spqr_null.est_norm_A_times_N
end
if get_details == 1
    stats.est_err_bound_norm_A_times_N = stats_spqr_null.est_err_bound_norm_A_times_N
end
if nargout == 4
    stats.est_norm_A_transpose_times_NT = stats_spqr_basic.est_norm_A_transpose_times_NT
    if get_details == 1
        stats.est_err_bound_norm_A_transpose_times_NT = stats_spqr_basic.est_err_bound_norm_A_transpose_times_NT
    end
end

if get_details == 1
    stats.time_svd = stats_spqr_basic.time_svd + stats_spqr_basic.stats_ssi.time_svd + stats_spqr_null.time_svd + stats_spqr_null.stats_ssi.time_svd + stats_spqr_null.stats_ssp_N.time_svd
    if nargout == 4
        stats.time_svd = stats.time_svd+stats_spqr_basic.stats_ssp_NT.time_svd
    end
    stats.time_basis = stats_spqr_basic.time_basis + stats_spqr_null.time_basis
end

if get_details == 1
    stats.time = time_ns() - start_tic
end

return x, stats, N, NT
end


using LinearAlgebra
using SparseArrays
include("../src/ExactDiagonalization.jl")
include("deprecate/ExactDiagonalization.jl")
using .ExactDiagonalization
import .OldED as OED
using Test
#-------------------------------------------------------------------------------------------------------------------------
# Pauli matrices
#-------------------------------------------------------------------------------------------------------------------------
const X = [0 1; 1 0]
const Y = [0 -1; 1 0] * 1im
const Z = [1 0; 0 -1]
@testset "PauliKron" begin
    XII = operator([X], [[1]], 3)
    @test Array(XII) ≈ kron(X, I(4))
    IYI = operator([Y], [[2]], 3)
    @test Array(IYI) ≈ kron(I(2), Y, I(2))
    IIZ = operator([Z], [[3]], 3)
    @test Array(IIZ) ≈ kron(I(4), Z)
    # Multiplication
    V = rand(8)
    M = rand(ComplexF64, 8, 2)
    @test (XII * V) ≈ (kron(X, I(4)) * V)
    @test (IYI * M) ≈ (kron(I(2), Y, I(2)) * M)
end

#-------------------------------------------------------------------------------------------------------------------------
# Randon Matrices
#-------------------------------------------------------------------------------------------------------------------------
@testset "Real Random Matrices" begin
    L = 12
    println("\nL = $L System:")
    mats = [rand(4, 4) |> Hermitian |> Array for i=1:L]
    inds = [mod.([i-1, i], L) .+ 1 for i=1:L]
    print("New :")
    @time op1 = operator(mats, inds, L) |> Array
    print("Old :")
    @time op2 = OED.operation(mats, inds, L) |> Array
    @test op1 ≈ op2
end

@testset "ComplexF64 Random Matrices" begin
    L = 13
    println("\nL = $L System:")
    mats = [rand(ComplexF64, 4, 4) |> Hermitian |> Array for i=1:L]
    inds = [mod.([i-1, i], L) .+ 1 for i=1:L]
    print("New :")
    @time op1 = operator(mats, inds, L) |> Array
    print("Old :")
    @time op2 = OED.operation(mats, inds, L) |> Array
    @test op1 ≈ op2
end

@testset "Benchmark Sparse Matrix" begin
    L = 14
    println("\nBenchmark Sparse Matrix for L = $L System:")
    mat = kron(I(2), X, Y, Z)
    print("New :")
    @time op1 = trans_inv_operator(mat, 4, L) |> Array
    print("Old :")
    @time op2 = OED.trans_inv_operation(mat, 1:4, L) |> Array
end

@testset "Benchmark Matrix Multiplication" begin
    L = 14
    println("\nBenchmark Matrix Multiplication for L = $L System:")
    mat = kron(I(2), X, Y, Z)
    v = rand(2^L)
    opt = trans_inv_operator(mat, 4, L)
    print("Operator : ")
    @time v2 = opt * v
    print("Dense    : ")
    optm = Array(opt)
    @time v3 = optm * v
    @test v2 ≈ v3
end


### A Pluto.jl notebook ###
# v0.12.17

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
        el
    end
end

# ╔═╡ b88b727a-3f3e-11eb-3e3c-bb62b380a257
begin
	using HDF5
	using Plots
	using PlutoUI
	using LaTeXStrings
	using Printf
	import Pda
end

# ╔═╡ 92ae91ea-3f52-11eb-12f9-497e9140820d
@bind frame PlutoUI.Slider(0:99, show_value=true)

# ╔═╡ a6760a18-3f5d-11eb-3cac-152259f7d4c8
file = "U1.000tauR1.000disk.h5"

# ╔═╡ c2d093dc-3f3e-11eb-187e-195e52e1fd31
begin
	x = Pda.get_h5data(file, "config/$(frame)/x")
	y = Pda.get_h5data(file, "config/$(frame)/y")
end

# ╔═╡ d2bd59f4-3f5c-11eb-3201-9fdd56f3174b
function average_radial_density(file)
	bins =100
	loc = zeros(bins)
	n = zeros(bins)
	frames = 100
	for frame in 0:frames-1
		x = Pda.get_h5data(file, "config/$(frame)/x")
		y = Pda.get_h5data(file, "config/$(frame)/y")
		r = sqrt.(x.^2 + y.^2)
		loc, n_ = Pda.density1d(r, 0, maximum(r), bins=100, normalize=false)
		n += n_
	end
	return loc, n/frames
end

# ╔═╡ 13220fe6-3f5d-11eb-3b9a-3d925df3f57c
loc,n = average_radial_density(file)

# ╔═╡ 18248b9a-3f3f-11eb-018f-cdf6768c2ac4
scatter(loc[2:end-1], (n./loc)[2:end-1], framestyle=:box, label=nothing)

# ╔═╡ Cell order:
# ╠═b88b727a-3f3e-11eb-3e3c-bb62b380a257
# ╠═92ae91ea-3f52-11eb-12f9-497e9140820d
# ╠═a6760a18-3f5d-11eb-3cac-152259f7d4c8
# ╠═c2d093dc-3f3e-11eb-187e-195e52e1fd31
# ╠═d2bd59f4-3f5c-11eb-3201-9fdd56f3174b
# ╠═13220fe6-3f5d-11eb-3b9a-3d925df3f57c
# ╠═18248b9a-3f3f-11eb-018f-cdf6768c2ac4


using CovarianceMatrices
using Base.Test

############################################################
## HAC
############################################################

X = randn(100, 5);

@time vcov(X, TruncatedKernel(2.))
@time vcov(X, BartlettKernel(2.))
@time vcov(X, ParzenKernel(2.))
@time vcov(X, QuadraticSpectralKernel(2.))
@time vcov(X, TruncatedKernel())
@time vcov(X, BartlettKernel())
@time vcov(X, ParzenKernel())
@time vcov(X, QuadraticSpectralKernel())

############################################################
## HC
############################################################

# A Gamma example, from McCullagh & Nelder (1989, pp. 300-2)
clotting = DataFrame(
    u    = log([5,10,15,20,30,40,60,80,100]),
    lot1 = [118,58,42,35,27,25,21,19,18],
    lot2 = [69,35,26,21,18,16,13,12,12],
    w    = [1/8, 1/9, 1/25, 1/6, 1/14, 1/25, 1/15, 1/13, 0.3022039]
)

## Unweighted OLS though GLM interface
OLS = fit(GeneralizedLinearModel, lot1~u,clotting, Normal(), IdentityLink())
S0 = vcov(OLS, HC0())
S1 = vcov(OLS, HC1())
S2 = vcov(OLS, HC2())
S3 = vcov(OLS, HC3())
S4 = vcov(OLS, HC4())
S4m = vcov(OLS, HC4m())
S5 = vcov(OLS, HC5())

St0 = [720.6213 -190.0645; -190.0645 51.16333]
St1 = [926.5131 -244.3687; -244.3687 65.78143]
St2 = [1300.896 -343.3307; -343.3307 91.99719]
St3 = [2384.504 -628.975 ; -628.975 167.7898]
St4 = [2538.746 -667.9597; -667.9597 177.2631]
St4m= [3221.095 -849.648 ; -849.648 226.1705]
St5 = St4

@test abs(maximum(S0 .- St0)) < 1e-04
@test abs(maximum(S1 .- St1)) < 1e-04
@test abs(maximum(S2 .- St2)) < 1e-04
@test abs(maximum(S3 .- St3)) < 1e-04
@test abs(maximum(S4 .- St4)) < 1e-03
@test abs(maximum(S4m .- St4m)) < 1e-03
@test abs(maximum(S5 .- St5)) < 1e-03

M0 = meat(OLS.model, HC0())
M1 = meat(OLS.model, HC1())
M2 = meat(OLS.model, HC2())
M3 = meat(OLS.model, HC3())
M4 = meat(OLS.model, HC4())
M4m = meat(OLS.model, HC4m())
M5 = meat(OLS.model, HC5())

Mt0 = [206.6103 518.7871; 518.7871 1531.173]
Mt1 = [265.6418 667.012;  667.012 1968.651]
Mt2 = [323.993 763.0424;  763.0424 2149.767]
Mt3 = [531.478 1172.751;  1172.751 3122.79]
Mt4 = [531.1047 1110.762; 1110.762 2783.269]
Mt4m= [669.8647 1412.227; 1412.227 3603.247]
Mt5 = Mt4

@test abs(maximum(M0 .- Mt0)) < 1e-04
@test abs(maximum(M1 .- Mt1)) < 1e-04
@test abs(maximum(M2 .- Mt2)) < 1e-04
@test abs(maximum(M3 .- Mt3)) < 1e-03
@test abs(maximum(M4 .- Mt4)) < 1e-03
@test abs(maximum(M4m .- Mt4m)) < 1e-03
@test abs(maximum(M5 .- Mt5)) < 1e-03

## Unweighted
OLS = glm(lot1~u,clotting, Normal(), IdentityLink())
S0 = vcov(OLS, HC0())
S1 = vcov(OLS, HC1())
S2 = vcov(OLS, HC2())
S3 = vcov(OLS, HC3())
S4 = vcov(OLS, HC4())

## Weighted OLS though GLM interface
wOLS = fit(GeneralizedLinearModel, lot1~u, clotting, Normal(),
           IdentityLink(), wts = Vector{Float64}(clotting[:w]))
S0 = vcov(wOLS, HC0())
S1 = vcov(wOLS, HC1())
S2 = vcov(wOLS, HC2())
S3 = vcov(wOLS, HC3())
S4 = vcov(wOLS, HC4())
S4m= vcov(wOLS, HC4m())
S5 = vcov(wOLS, HC5())

St0 = [717.7362 -178.4043; -178.4043 45.82273]
St1 = [922.8037 -229.3769; -229.3769 58.91494]
St2 = [1412.94 -361.33; -361.33 95.91252]
St3 = [2869.531 -756.2976; -756.2976 208.2344]
St4 = [3969.913 -1131.358; -1131.358 342.2859]
St4m= [4111.626 -1103.174; -1103.174   310.194]
St5 = St4

@test abs(maximum(S0 .- St0)) < 1e-04
@test abs(maximum(S1 .- St1)) < 1e-04
@test abs(maximum(S2 .- St2)) < 1e-03
@test abs(maximum(S3 .- St3)) < 1e-03
@test abs(maximum(S4 .- St4)) < 1e-03
@test abs(maximum(S4m .- St4m)) < 1e-03
@test abs(maximum(S5 .- St5)) < 1e-03

## Unweighted GLM - Gamma
GAMMA = glm(lot1~u, clotting, Gamma(),InverseLink())

S0 = vcov(GAMMA, HC0())
S1 = vcov(GAMMA, HC1())
S2 = vcov(GAMMA, HC2())
S3 = vcov(GAMMA, HC3())
S4 = vcov(GAMMA, HC4())
S4m = vcov(GAMMA, HC4m())
S5 = vcov(GAMMA, HC5())

St0 = [4.504287921232951e-07 -1.700020601541489e-07;
       -1.700020601541490e-07  8.203697048568913e-08]

St1 = [5.791227327299548e-07 -2.185740773410504e-07;
       -2.185740773410510e-07  1.054761049101728e-07]

St2 = [3.192633083111232e-06 -9.942484630848573e-07;
       -9.942484630848578e-07  3.329973305723091e-07]

St3 = [2.982697811926944e-05 -8.948137019946751e-06;
       -8.948137019946738e-06  2.712024459305714e-06]

St4 = [0.002840158946368653 -0.0008474436578800430;
       -0.000847443657880045  0.0002528819761961959]

St4m= [8.49306e-05 -2.43618e-05; -2.43618e-05  7.04210e-06]

St5 = St4

@test abs(maximum(S0 .- St0)) < 1e-06
@test abs(maximum(S1 .- St1)) < 1e-06
@test abs(maximum(S2 .- St2)) < 1e-06
@test abs(maximum(S3 .- St3)) < 1e-06
@test abs(maximum(S4 .- St4)) < 1e-06
@test abs(maximum(S4m .- St4m)) < 1e-05
@test abs(maximum(S5 .- St5)) < 1e-05

## Weighted Gamma

GAMMA = glm(lot1~u, clotting, Gamma(),InverseLink(), wts = convert(Array, clotting[:w]))

S0 = vcov(GAMMA, HC0())
S1 = vcov(GAMMA, HC1())
S2 = vcov(GAMMA, HC2())
S3 = vcov(GAMMA, HC3())
S4 = vcov(GAMMA, HC4())
S4m = vcov(GAMMA, HC4m())
S5 = vcov(GAMMA, HC5())

St0 = [4.015104e-07 -1.615094e-07;
       -1.615094e-07  8.378363e-08]

St1 = [5.162277e-07 -2.076549e-07;
       -2.076549e-07  1.077218e-07]

St2 = [2.720127e-06 -8.490977e-07;
       -8.490977e-07  2.963563e-07]

St3 = [2.638128e-05 -7.639883e-06;
       -7.639883e-06  2.259590e-06]

St4 = [0.0029025754 -0.0008275858;
       -0.0008275858  0.0002360053]

St4m = [8.493064e-05 -2.436180e-05; -2.436180e-05  7.042101e-06]

St5 = St4

@test abs(maximum(S0 .- St0)) < 1e-06
@test abs(maximum(S1 .- St1)) < 1e-06
@test abs(maximum(S2 .- St2)) < 1e-06
@test abs(maximum(S3 .- St3)) < 1e-06
@test abs(maximum(S4 .- St4)) < 1e-06
@test abs(maximum(S4m .- St4m)) < 1e-05
@test abs(maximum(S5 .- St5)) < 1e-05

### Cluster basic interface

## Construct Fake ols data

## srand(1)

## df = DataFrame( Y = randn(500),
##                X1 = randn(500),
##                X2 = randn(500),
##                X3 = randn(500),
##                X4 = randn(500),
##                X5 = randn(500),
##                w  = rand(500),
##                cl = repmat(collect(1:25), 20))

df = readtable("wols_test.csv")

OLS = fit(GeneralizedLinearModel, Y~X1+X2+X3+X4+X5, df,
          Normal(), IdentityLink())

S0 = vcov(OLS, HC0())
S1 = vcov(OLS, HC1())
S2 = vcov(OLS, HC2())
S3 = vcov(OLS, HC3())
S4 = vcov(OLS, HC4())
S5 = vcov(OLS, HC5())

cl = convert(Array, df[:cl])
S0 = stderr(OLS, CRHC0(cl))
S1 = stderr(OLS, CRHC1(cl))
S2 = stderr(OLS, CRHC2(cl))
S3 = stderr(OLS, CRHC3(cl))

## STATA
St1 = [.0374668, .0497666, .0472636, .0437952, .0513613, .0435369]

@test maximum(abs(S0 .- St1)) < 1e-02
@test maximum(abs(S1 .- St1)) < 1e-04
@test maximum(abs(S2 .- St1)) < 1e-02
@test maximum(abs(S3 .- St1)) < 1e-02

wOLS = fit(GeneralizedLinearModel, Y~X1+X2+X3+X4+X5, df,
          Normal(), IdentityLink(), wts = convert(Array, df[:w]))

S0 = stderr(wOLS, CRHC0(cl))
S1 = stderr(wOLS, CRHC1(cl))
S2 = stderr(wOLS, CRHC2(cl))
S3 = stderr(wOLS, CRHC3(cl))

St1 = [0.042839848169137905,0.04927285387211425,
       0.05229519531359171,0.041417170723876025,
       0.04748115282615204,0.04758615959662984]

@test maximum(abs(S1 .- St1)) < 1e-10

############################################################
## Linear Model
############################################################
srand(1)
y = randn(100);
x = randn(100, 5);

lm1 = lm(x, y)
@test_approx_eq_eps stderr(lm1, HC0()) [0.0941998, 0.0946132, 0.0961678, 0.0960445, 0.101651] 1e-06
@test_approx_eq_eps diag(vcov(lm1, HC0())) [0.0941998, 0.0946132, 0.0961678, 0.0960445, 0.101651].^2 1e-06

############################################################
## HAC
############################################################

include("ols_hac.jl")


# Parameters:
#	N::Int
#	- the problem size
#	K::Int
#	- the problem is homogenous at N-K
# Return Values
#	YOR::Array{Array{Array{SparseMatrixCSC, 1}, 1}, 1} (Young's Orthogonal Representations)
#	- YOR[n][p][k] is Young's Orthogonal Representation for the Adjacent Transposition (K, K + 1) for the pth Partition of n that is needed for the bandlimited functionality
#	- length(YOR[n]) = 0 for n = 1:(N - K - 1)
#	- if p < ZFI[n], length(YOR[n][p] = 1) and YOR[n][p][1,1] contains the dimension of the full Young's Orthogonal Representation
#	PT::Array{Array{Array{Int, 1}, 1}, 1}
#	- output1 of partition_tree_bl()
#	ZFI::Array{Int, 1}
#	- output2 for paritions_bl()
function yor_bl(N::Int, K::Int)
	P, ZFI, WI = partitions_bl(N, K)
	PT = partition_tree_bl(N, K, P, ZFI, WI)
	YS = ys_symbols_bl(N, K, P, ZFI, PT)
	YOR = Array(Array{Array{SparseMatrixCSC, 1}, 1}, N)
	for n = 1:(N - K - 1)
		YOR[n] = Array(Array{SparseMatrixCSC, 1}, 0)
	end
	for n = (N - K):N
		YSn = YS[n]
		Pn = P[n]
		Pn_L = length(Pn)
		YORn = Array(Array{SparseMatrixCSC, 1}, Pn_L)
		for p = 1:ZFI[n]
			YORnp_sparse = spzeros(1, 1)
			YORnp_sparse[1,1] = length(YSn[p])
			YORnp = Array(SparseMatrixCSC, 1)
			YORnp[1] = YORnp_sparse
			YORn[p] = YORnp
		end
		for p = (ZFI[n] + 1):Pn_L
			YSnp = YSn[p]
			Pnp = Pn[p]
			R = length(Pnp)
			YORn[p] = yor_p(n, R, YSnp)
		end
		YOR[n] = YORn
	end
	return YOR, PT, ZFI
end


module DatagenCopulaBased
  using Distributions
  using Combinatorics
  using HypothesisTests
  using HCubature
  using StatsBase
  using QuadGK
  using Roots
  using SpecialFunctions
  using Random
  using LinearAlgebra
  using Distributed
  using SharedArrays
  if VERSION >= v"1.3"
    using CompilerSupportLibraries_jll
  end

  include("simulate_copula.jl")
  include("sampleunivdists.jl")
  include("corgen.jl")
  include("marshallolkincopcor.jl")
  include("archcopcorrelations.jl")

  # particular copulas famillies
  include("eliptic_fr_mo_copulas.jl")
  include("archcopulagendat.jl")
  include("nestedarchcopulagendat.jl")
  include("chaincopulagendat.jl")

  # change Gaussian data by adding higher order cross-correlations
  include("add_higher_order_cors.jl")

  export Gaussian_cop, Student_cop, Frechet_cop, Marshall_Olkin_cop
  export Gumbel_cop, Gumbel_cop_rev, Clayton_cop, Clayton_cop_rev, AMH_cop, AMH_cop_rev, Frank_cop
  export Nested_Clayton_cop, Nested_AMH_cop, Nested_Frank_cop, Nested_Gumbel_cop
  export Double_Nested_Gumbel_cop, Hierarchical_Gumbel_cop
  export Chain_of_Archimedeans, Chain_of_Frechet
  export SpearmanCorrelation, KendallCorrelation, CorrelationType
  export cormatgen, cormatgen_constant, cormatgen_toeplitz, convertmarg!
  export cormatgen_constant_noised, cormatgen_toeplitz_noised, cormatgen_rand
  export cormatgen_two_constant, cormatgen_two_constant_noised

  export gcop2tstudent, gcop2frechet, gcop2marshallolkin, gcop2arch

  export simulate_copula, simulate_copula!

end


function solve()
    n, m = [parse(Int, x) for x in split(readline())]
    as = [parse(Int, x) for x in split(readline())]
    ret = maximum(as) + 1

    pos = [[0] for _ in 1:ret]
    for (i, a) in enumerate(as)
        push!(pos[a+1], i)
    end
    for v in values(pos)
        push!(v, n+1)
    end

    for (k, v) in pairs(pos)
        for i in 1:length(v)-1
            if v[i+1] - v[i] > m
                ret = min(ret, k-1)
            end
        end
    end
    ret
end

println(solve())


# code for Video 5.5:
julia> using RDatasets
julia> iris = dataset("datasets", "iris")
julia> labels = iris[:Species]
150-element DataArrays.PooledDataArray{ASCIIString,UInt8,1}:
 "setosa"
 "setosa"
 "setosa"
 ...
 julia> features = convert(Array, iris[:, 1:4])
150x4 Array{Float64,2}:
 5.1  3.5  1.4  0.2
 4.9  3.0  1.4  0.2
 4.7  3.2  1.3  0.2
 4.6  3.1  1.5  0.2
 5.0  3.6  1.4  0.2

julia> Pkg.add("LIBSVM")
julia> using LIBSVM

features_train, features_test = features[1:2:end, :], features[2:2:end, :]
(
75x4 Array{Float64,2}:
 5.1  3.5  1.4  0.2
 4.7  3.2  1.3  0.2
 5.0  3.6  1.4  0.2
 4.6  3.4  1.4  0.3
 4.4  2.9  1.4  0.2
 5.4  3.7  1.5  0.2
 4.8  3.0  1.4  0.1
 5.8  4.0  1.2  0.2
 5.4  3.9  1.3  0.4
 ...
 )
75x4 Array{Float64,2}:
 4.9  3.0  1.4  0.2
 4.6  3.1  1.5  0.2
 5.4  3.9  1.7  0.4
 5.0  3.4  1.5  0.2
 4.9  3.1  1.5  0.1
 4.8  3.4  1.6  0.2
 4.3  3.0  1.1  0.1
 ...
 )

julia> labels_train, labels_test = labels[1:2:end], labels[2:2:end]
(ASCIIString["setosa","setosa","setosa","setosa","setosa","setosa","setosa","seto
sa","setosa","setosa"  …  "virginica","virginica","virginica","virginica","virgin
ica","virginica","virginica","virginica","virginica","virginica"],ASCIIString["se
tosa","setosa","setosa","setosa","setosa","setosa","setosa","setosa","setosa","se
tosa"  …  "virginica","virginica","virginica","virginica","virginica","virginica"
,"virginica","virginica","virginica","virginica"])

julia> features_train' # we need a transposed matrix for our SVM model
4x75 Array{Float64,2}:
 5.1  4.7  5.0  4.6  4.4  5.4  4.8  5.8  5.4  5.7  …  7.4  6.4  6.1  6.3  6.0  6.7  5.8  6.7  6.3  6.2
 3.5  3.2  3.6  3.4  2.9  3.7  3.0  4.0  3.9  3.8     2.8  2.8  2.6  3.4  3.0  3.1  2.7  3.3  2.5  3.4
 1.4  1.3  1.4  1.4  1.4  1.5  1.4  1.2  1.3  1.7     6.1  5.6  5.6  5.6  4.8  5.6  5.1  5.7  5.0  5.4
 0.2  0.2  0.2  0.3  0.2  0.2  0.1  0.2  0.4  0.3     1.9  2.2  1.4  2.4  1.8  2.4  1.9  2.5  1.9  2.3
# Rows: 1) SepalLength, 2) SepalWidth, 3) PetalLength, 4) PetalWidth

julia> model = svmtrain(labels_train, features_train')
LIBSVM.SVMModel{ASCIIString}(Ptr{Void} @0x00000000044e9620,[LIBSVM.SVMParameter(0,2,3,0.25,0.0,100.0,0.001,1.0,0,Ptr{Int32} @0x00007ef9f467d800,Ptr{Float64} @0x00007ef9f467d840,0.5,0.1,1,0)],[LIBSVM.SVMProblem(75,Ptr{Float64} @0x00007ef9f3f37a00,Ptr{Ptr{LIBSVM.SVMNode}} @0x00007ef9f4bc0040)],5x75 Array{LIBSVM.SVMNode,2}:
 LIBSVM.SVMNode(1,5.1)   LIBSVM.SVMNode(1,4.7)   …  LIBSVM.SVMNode(1,6.2)
 LIBSVM.SVMNode(2,3.5)   LIBSVM.SVMNode(2,3.2)      LIBSVM.SVMNode(2,3.4)
 LIBSVM.SVMNode(3,1.4)   LIBSVM.SVMNode(3,1.3)      LIBSVM.SVMNode(3,5.4)
 LIBSVM.SVMNode(4,0.2)   LIBSVM.SVMNode(4,0.2)      LIBSVM.SVMNode(4,2.3)
 LIBSVM.SVMNode(-1,NaN)  LIBSVM.SVMNode(-1,NaN)     LIBSVM.SVMNode(-1,NaN),[Ptr{LIBSVM.SVMNode} @0x00000000044e7710,Ptr{LIBSVM.SVMNode} @0x00000000044e7760,Ptr{LIBSVM.SVMNode} @0x00000000044e77b0,Ptr{LIBSVM.SVMNode} @0x00000000044e7800,Ptr{LIBSVM.SVMNode} @0x00000000044e7850,Ptr{LIBSVM.SVMNode} @0x00000000044e78a0,Ptr{LIBSVM.SVMNode} @0x00000000044e78f0,Ptr{LIBSVM.SVMNode} @0x00000000044e7940,Ptr{LIBSVM.SVMNode} @0x00000000044e7990,Ptr{LIBSVM.SVMNode} @0x00000000044e79e0  …  Ptr{LIBSVM.SVMNode} @0x00000000044e8b60,Ptr{LIBSVM.SVMNode} @0x00000000044e8bb0,Ptr{LIBSVM.SVMNode} @0x00000000044e8c00,Ptr{LIBSVM.SVMNode} @0x00000000044e8c50,Ptr{LIBSVM.SVMNode} @0x00000000044e8ca0,Ptr{LIBSVM.SVMNode} @0x00000000044e8cf0,Ptr{LIBSVM.SVMNode} @0x00000000044e8d40,Ptr{LIBSVM.SVMNode} @0x00000000044e8d90,Ptr{LIBSVM.SVMNode} @0x00000000044e8de0,Ptr{LIBSVM.SVMNode} @0x00000000044e8e30],ASCIIString["setosa","versicolor","virginica"],Int32[],Float64[],4,false)

julia> (predicted_labels, decision_values) = svmpredict(model, features_test')

(ASCIIString["setosa","setosa","setosa","setosa","setosa","setosa","setosa","setosa","setosa","setosa"  …  "virginica","versicolor","virginica","virginica","virginica","virginica","virginica","virginica","virginica","virginica"],
3x75 Array{Float64,2}:
 1.07954   1.07629   1.05125   1.16964   …  -0.809238  -0.897749  -0.936477
 1.10042   1.07671   1.03685   1.1533       -1.00333   -1.0383    -1.07345
 0.263692  0.254304  0.358478  0.282262     -0.810783  -0.571849  -0.315681)

julia> @printf "Accuracy: %.2f%%\n" mean((predicted_labels .== labels_test))*100
Accuracy: 93.33%

julia> using DecisionTree

julia> confusion_matrix(convert(Array, labels_test), predicted_labels)
Classes:  ASCIIString["setosa","versicolor","virginica"]
Matrix:   3x3 Array{Int64,2}:
 25   0   0
  0  24   1
  0   4  21

Accuracy: 0.9333333333333333
Kappa:    0.9

### Take 80% of records (120) for training and 20% of records (30) for testing
julia> julia> n_test = Int(length(labels) * 0.2) # 30

julia> collect(1:length(labels)) .> n_test
150-element BitArray{1}:
 false   # first 30 boolean values are false
 false
 false
 false
 false
 false
 false
 false
 false
 false
 false
 false
     ⋮
  true
  true

julia> train_rows = shuffle(collect(1:length(labels)) .> n_test) # shuffle randomizes the true and false values
150-element BitArray{1}:
  true  # shuffle randomizes the true and false values
  true
  true
  true
  true
 false
  true
  ...

julia> features_train, features_test = features[train_rows, :], features[!train_rows, :]
julia> labels_train, labels_test = labels[train_rows], labels[!train_rows]

julia> model = svmtrain(labels_train, features_train')
julia> (predictions, decision_values) = svmpredict(model, features_test')

julia> confusion_matrix(labels_test, predictions)
Classes:  ASCIIString["setosa","versicolor","virginica"]
Matrix:   3x3 Array{Int64,2}:
 15  0  0
  0  9  1
  0  0  5
Accuracy: 0.9666666666666667
Kappa:    0.9459459459459458


"""
Package: Forecast

    ar(x::DataFrame, or, constant = true;
                     alpha = 1.0, dΦ0 = nothing, dΦ = nothing)

    ar(x::AbstractArray, or, constant = true;
                         alpha = false, dΦ0 = nothing, dΦ = nothing, varnames = nothing)

Fit a multivariate autoregressive series model.

The fitting is done via Ordinary Least Squared and implements the following model:

```math
Xt = \\Phi_0 + \\sum_{i=1}^p \\Phi_i \\cdot X_{t-i} + \\mathcal{N}(\\vec{0},\\Sigma)
```

# Arguments
- `x`: Multivariate series each column containing a dimension and ordered by time ascending rows.
- `or`: Number of parameters Φ to be estimated.
- `constant`: If `true` `ar` estimates Φ0 otherwise it is assume to be zero.
- `alpha`: fixes to zero all coefficients which significance is below its value. It defaults to one.
- `dΦ0`: Tuple containing two Φ0 objects, the first one will act as an original reference to the second one and different values will be fixed in the fitting process to the values in the second Φ0.
- `dΦ`:  Equivalent to dΦ0 but for Φ.
- `varnames`: Names of the dimensions (by default xi where `i` is an integer)

# Returns
An AR object containing the model coefficients, the error sigma matrix, residuals and a collection of information criteria

# Examples
```julia-repl
julia> ar(rand(100,2),2)
AR([...])
"""
function ar(df::DataFrame,
            order::Integer = 1,
            constant::Bool = true;
            alpha::Real = 1.0,
            dΦ0::Tuple = d_ar_dΦ0(size(df,2)-1,constant,Float64),
            dΦ::Tuple = d_ar_dΦ(size(df,2)-1,order,Float64))

    #TODO promote dΦ0 and dΦ types based on df and control for Integer values on x
    ttype = [Date,DateTime,Day,Month,Week]
    ar_df = eltype(df[:,1]) in ttype ? Array{Float64}(df[:,2:end]) : Array{Float64}(df)
    names_df = eltype(df[:,1]) in ttype ? names(df[:,2:end]) : names(df)
    xar = ar(ar_df, order, constant; alpha, dΦ0, dΦ, varnames = names_df)
    xar.x = df

    return xar

end

# Default ar values functions
d_ar_dΦ0(m::Integer,constant::Bool,T::Type) = (repeat([T(1)],m),  repeat([T(constant ? 1 : 0)],m))
d_ar_dΦ(m::Integer,order::Integer,T::Type) =
    (r11=reshape(repeat([T(1)],m*m*order),m,m,order); (r11,r11))
d_ar_varnames(m::Integer)::Vector{String} = ["x"*string(i) for i in 1:m]

function ar(x::AbstractArray{T},
            order::Integer = 1,
            constant::Bool = true;
            alpha::Real = 1.0,
            dΦ::Tuple = d_ar_dΦ(size(x,2),order,T),
            dΦ0::Tuple = d_ar_dΦ0(size(x,2),constant,T),
            varnames::AbstractVector{String} = d_ar_varnames(size(x,2))) where T<:Real

    @assert 0.0 < alpha <= 1.0

    m = size(x,2)
    np = order

    dΦ  = ( expand(dΦ[1],(m,m,np)), expand(dΦ[2],(m,m,np)) )
    dΦ0 = ( expand(dΦ0[1],(m,)),    expand(dΦ0[2],(m,)) )

    xar = ar_ols(x, order, constant; dΦ0, dΦ, varnames)

    alpha == 1.0 && return xar

    m, np = xar.ndims, xar.order

    Φ = expand(xar.Φ,(m,m,np))
    Φ0 = expand(xar.Φ0,(m,))
    Φpv = expand(xar.Φpv,(m,m,np))
    Φ0pv = expand(xar.Φ0pv,(m,))

    dΦs = (Φpv .<= alpha)
    dΦ0s = (Φ0pv .<= alpha)

    all(dΦs) && all(dΦ0s) && return xar

    if isnothing(dΦ)
        dΦ = (Φ, Φ .* dΦs)
        dΦ0 = (Φ0, Φ0 .* dΦ0s)
    else
        dΦ = (dΦ[1], dΦ[2] .* dΦs)
        dΦ0 = (dΦ0[1], dΦ0[2] .* dΦ0s)
    end

    return ar_ols(x, order, constant; dΦ0, dΦ, varnames)

end

function ar_ols(x::AbstractArray{T},
                or::Integer,
                constant::Bool;
                dΦ::Tuple,
                dΦ0::Tuple,
                varnames::AbstractVector{String}) where T<:Real

    @assert 1 <= ndims(x) <= 2
    @assert 1 <= or < size(x,1)-1

    n = size(x,1)
    m = size(x,2)
    np = m*m*or + m

    x = x[end:-1:1,:]
    nx = x
    for i in 1:or
        nx = hcat(x,nx[vcat(2:end,1),:])
    end
    nx = nx[1:end-or,:]

    Y = nx[:,1:m]
    X = nx[:,m+1:end]
    X = hcat(repeat([1.0],size(X,1)),X)

    # Fixing parameters
    W = Array{T,2}(undef,(or*m+1,m))
    for i in 1:m
        Xi,Yi = fixΦ(X,Y,i,dΦ0,dΦ)
        dW = Xi\Yi
        W[:,i] = fixW(dW,i,dΦ0,dΦ)
    end

    # Fitted values
    fitted = X*W

    # Prediction error
    e = Y - fitted

    Φ0 = W[1,:]
    Φ = W[2:end,:]
    Φ = Array(reshape(Φ',m,m,or))

    # Maximum Likelihood noise covariance
    k = or*m*m
    @assert n-k > 0 "Insufficient data for the model"
    Σ2 = 1/(n-k)*e'*e

    # ML parameters std. error.
    Φse = sqrt.(abs.(diag(kron(Σ2, (X'*X)^-1))))
    Φse = fixΦse(Φse,dΦ0,dΦ)

    rΦse = reshape(Φse,:,m)'
    Φ0se = rΦse[:,1]
    Φse  = reshape(rΦse[:,2:end],m,m,:)

    #fix p
    np = fixnp(dΦ0,dΦ)

    # TODO check weher to use 'or' or 'np' and if 'or' then a fixor function is needed
    # Information Criteria
    lΣ2   = log(norm(Σ2))
    ic = Dict([("AIC",  lΣ2 + 2*np*m^2/n),
               ("AICC", lΣ2 + 2*(np*m^2+1)/(n-(np*m^2+2))),
               ("BIC",  n*log(norm(Σ2)+m)+(m^2*np+m*(m+1)/2)*log(n)),
               ("H&Q",  lΣ2 + 2*log(log(n))*np*m^2/n)])

    # Statistics
    SStot = sum((Y .- mean(Y,dims=1)).^2,dims=1)
    SSres = sum(e .^ 2,dims=1)
    R2 = vec(1 .- (SSres ./ SStot))

    pvf(mu,se) = se == 0 ? 1.0 : cdf(Normal(abs(mu),se),0) #1 to make log(1) = 0
    Φpv = pvf.(Φ,Φse)
    Φ0pv = pvf.(Φ0,Φ0se)
    slpv = vec(reshape(sum(log.(vcat(reshape(Φ0pv,:,m),reshape(Φpv,:,m))),dims=1),:,1))

    stats = Dict([(" Variable", varnames),
                  ("R2",    R2),
                  ("R2adj", 1 .- (1 .- R2) * (n-1)/(n-(np-1)/m-1)),
                  ("Fisher's p-test", 1 .- cdf(Chisq(2*np/m),-2*slpv))])

    Σ = sqrt.(diag(Σ2))

    call = "ar(X, order="*string(or)*
        ", constant="*string(constant)*")"

    AR(varnames,
       or, m,
       compact.([Φ,Φ0,Σ2,Σ,x[end:-1:1,:],fitted,e])...,
       ic,stats,
       compact.([Φse,Φ0se,Φpv,Φ0pv])...,
       call)
end

"""
Package: Forecast

    fixΦ(X,Y,dΦ0,dΦ)

For a given X and Y OLS matrices returns the X and Y resulting from fixing parameters given dΦ0 and dΦ
"""
function fixΦ(X::AbstractMatrix{T},
              Y::AbstractMatrix{T},
              i::Integer,
              dΦ0::Tuple{AbstractArray{T},AbstractArray{T}},
              dΦ::Tuple{AbstractArray{T},AbstractArray{T}}) where T<:Real

    Φ0, fΦ0 = dΦ0
    Φ, fΦ = dΦ
    (Φ0 == fΦ0) & (Φ == fΦ) && return((X,Y[:,i]))

    rΦ0 = reshape(Φ0,:,1)
    rΦ  = reshape(Φ,size(Φ,1),:)
    rΦ  = hcat(rΦ0,rΦ)

    rfΦ0 = reshape(fΦ0,:,1)
    rfΦ = reshape(fΦ,size(fΦ,1),:)
    rfΦ = hcat(rfΦ0,rfΦ)

    dc = findall(rΦ[i,:] .!== rfΦ[i,:])

    @assert length(rΦ) > length(dc) "No degrees of freedom"

    # Create new Yi
    Yi = Y[:,i]
    for c in dc
        Yi = Yi .- rfΦ[i,c]*X[:,c]
    end

    # Create new X
    Xi = drop(X,c=dc)

    (Xi,Yi)

end

"""
Package: Forecast

    fixW(W,dΦ0,dΦ)

For a given Weight matrix returns a version with fixed values based on dΦ0 and dΦ
"""
function fixW(dWi::AbstractVector{T},
              i::Integer,
              dΦ0::Tuple{AbstractArray{T},AbstractArray{T}},
              dΦ::Tuple{AbstractArray{T},AbstractArray{T}}) where T<:Real

    Φ0, fΦ0 = dΦ0
    Φ, fΦ = dΦ
    (Φ0 == fΦ0) & (Φ == fΦ) && return(dWi)

    rΦ0 = reshape(Φ0,:,1)
    rΦ  = reshape(Φ,size(Φ,1),:)
    rΦ  = hcat(rΦ0,rΦ)

    rfΦ0 = reshape(fΦ0,:,1)
    rfΦ = reshape(fΦ,size(fΦ,1),:)
    rfΦ = hcat(rfΦ0,rfΦ)

    dc = findall(rΦ[i,:] .!== rfΦ[i,:])

    for c in dc
        insert!(dWi,c,rfΦ[i,c])
    end

    dWi

end

"""
Package: Forecast

    fixnp(dΦ0,dΦ)

return the number of free parameters
"""
function fixnp(dΦ0::Tuple{AbstractArray{T},AbstractArray{T}},
               dΦ::Tuple{AbstractArray{T},AbstractArray{T}}) where T<:Real

    Φ0, fΦ0 = dΦ0
    Φ, fΦ = dΦ
    (Φ0 == fΦ0) & (Φ == fΦ) && return(length(Φ) + length(Φ0))

    rΦ0 = reshape(Φ0,:,1)
    rΦ  = reshape(Φ,size(Φ,1),:)
    rΦ  = hcat(rΦ0,rΦ)

    rfΦ0 = reshape(fΦ0,:,1)
    rfΦ = reshape(fΦ,size(fΦ,1),:)
    rfΦ = hcat(rfΦ0,rfΦ)

    dc = findall(rΦ .!== rfΦ)

    length(Φ) + length(Φ0) - length(dc)
end

"""
Package: Forecast

    fixΦse(M,dΦ0,dΦ)

For a given `se` matrix returns an version with zeroes based on dΦ0 and dΦ
"""
function fixΦse(Φse::AbstractVector{T},
                dΦ0::Tuple{AbstractArray{T},AbstractArray{T}},
                dΦ::Tuple{AbstractArray{T},AbstractArray{T}}) where T<:Real

    Φ0, fΦ0 = dΦ0
    Φ, fΦ = dΦ
    (Φ0 == fΦ0) & (Φ == fΦ) && return(Φse)

    _,m,np = size(Φ)

    rΦ  = reshape(hcat(Φ0 ,reshape(Φ ,m,m*np))',m*(m*np+1),1)
    rfΦ = reshape(hcat(fΦ0,reshape(fΦ,m,m*np))',m*(m*np+1),1)

    dc = findall(rΦ .!== rfΦ)

    fΦse = Vector{T}(undef, length(Φse))
    fΦse[:] = Φse
    for ci in dc
        c = Tuple(ci)
        fΦse[c[1]] = 0.0
    end

    fΦse

end


using XGBoost

##
#  this script demonstrate how to fit generalized linear model in xgboost
#  basically, we are using linear model, instead of tree for our boosters
##

dtrain = DMatrix("../data/agaricus.txt.train")
dtest = DMatrix("../data/agaricus.txt.test")

# change booster to gblinear, so that we are fitting a linear model
# alpha is the L1 regularizer
# lambda is the L2 regularizer
# you can also set lambda_bias which is L2 regularizer on the bias term

param = ["booster"=>"gblinear", "eta"=>1, "silent"=>0,
         "objective"=>"binary:logistic", "alpha"=>0.0001, "lambda"=>1]
# normally, you do not need to set eta (step_size)
# XGBoost uses a parallel coordinate descent algorithm (shotgun),
# there could be affection on convergence with parallelization on certain cases
# setting eta to be smaller value, e.g 0.5 can make the optimization more stable

##
# the rest of settings are the same
#

watchlist  = [(dtest,"eval"), (dtrain,"train")]
num_round = 4

bst = xgboost(dtrain, num_round, param=param, watchlist=watchlist)

preds = predict(bst, dtest)
labels = get_info(dtest, "label")

print("test-error=", sum((preds .> 0.5) .!= labels) / float(size(preds)[1]), "\n")


@testset "utils" begin
    @testset "center_predictors" begin
        @testset "NamedTuples" begin
            f = @formula(y_int ~ x_float + x_cat)
            X = T.data_fixed_effects(f, nt_str)
            μ_X, X_centered = T.center_predictors(X)
            @test μ_X ≈ [2.547, 0.25, 0.25, 0.25] atol = 0.01
            @test X_centered ≈ [
                -1.447 -0.25 -0.25 -0.25
                -0.247 0.75 -0.25 -0.25
                0.592 -0.25 0.75 -0.25
                1.102 -0.25 -0.25 0.75
            ] atol = 0.01
            @test mapslices(mean, X_centered; dims=1) ≈ [0 0 0 0] atol = 0.01
        end

        @testset "DataFrames" begin
            f = @formula(y_int ~ x_float + x_cat)
            X = T.data_fixed_effects(f, df_str)
            μ_X, X_centered = T.center_predictors(X)
            @test μ_X ≈ [2.547, 0.25, 0.25, 0.25] atol = 0.01
            @test X_centered ≈ [
                -1.447 -0.25 -0.25 -0.25
                -0.247 0.75 -0.25 -0.25
                0.592 -0.25 0.75 -0.25
                1.102 -0.25 -0.25 0.75
            ] atol = 0.01
            @test mapslices(mean, X_centered; dims=1) ≈ [0 0 0 0] atol = 0.01
        end
    end

    @testset "standardize_predictors" begin
        @testset "NamedTuples" begin
            f = @formula(y_int ~ x_float + x_cat)
            y = T.data_response(f, nt_str)
            μ_y, σ_y, y_std = T.standardize_predictors(y)
            X = T.data_fixed_effects(f, nt_str)
            μ_X, σ_X, X_std = T.standardize_predictors(X)
            @test y_std ≈ [-1.16, -0.39, 0.38, 1.16] atol = 0.01
            @test μ_X ≈ [2.547, 0.25, 0.25, 0.25] atol = 0.01
            @test σ_X ≈ [1.114, 0.5, 0.5, 0.5] atol = 0.01
            @test X_std ≈ [
                -1.299 -0.5 -0.5 -0.5
                -0.222 1.5 -0.5 -0.5
                0.531 -0.5 1.5 -0.5
                0.989 -0.5 -0.5 1.5
            ] atol = 0.01
            @test mapslices(mean, X_std; dims=1) ≈ [0 0 0 0] atol = 0.01
            @test mapslices(std, X_std; dims=1) ≈ [1 1 1 1] atol = 0.01
        end

        @testset "DataFrames" begin
            f = @formula(y_int ~ x_float + x_cat)
            X = T.data_fixed_effects(f, df_str)
            μ_X, σ_X, X_std = T.standardize_predictors(X)
            @test μ_X ≈ [2.547, 0.25, 0.25, 0.25] atol = 0.01
            @test σ_X ≈ [1.114, 0.5, 0.5, 0.5] atol = 0.01
            @test X_std ≈ [
                -1.299 -0.5 -0.5 -0.5
                -0.222 1.5 -0.5 -0.5
                0.531 -0.5 1.5 -0.5
                0.989 -0.5 -0.5 1.5
            ] atol = 0.01
            @test mapslices(mean, X_std; dims=1) ≈ [0 0 0 0] atol = 0.01
            @test mapslices(std, X_std; dims=1) ≈ [1 1 1 1] atol = 0.01
        end
    end
end


#=
array3d_to_lar:
- Julia version:
- Author: miros
- Date: 2019-01-14
=#
include("../src/LarSurf.jl")
# include("../src/plasm.jl")
# include("../src/read.jl")
# include("../src/surface.jl")
# include("../src/representation.jl")
# include("../src/import3d.jl")
# LarSurf.

## Artifical data
data3d = zeros(Int16,5,4,3)
data3d[2:4,2:3,2] .= 10
# data3d

voxelsize_mm = [0.5, 0.9, 0.8]
threshold=0

# Get vertices and count the voxesl (*4 face number)
verts, nvoxels = LarSurf.vertices_and_count_voxels_as_the_square_face_preprocessing(data3d, voxelsize_mm)

# Create square faces

faces = LarSurf.create_square_faces(data3d, nvoxels, threshold)

new_verts, new_faces = LarSurf.keep_surface_faces(verts, faces)

verts = new_verts
faces = new_faces

# Triangulation

trifaces = LarSurf.triangulation(faces)

# Visualization
LarSurf.check_vf(verts, trifaces)

V, EV, FE = LarSurf.to_lar(verts, trifaces)
VV = [i for i=1:length(V)]

EVc = Lar.cop2lar(EV)
Vnew = convert(Array, transpose(V))
# LarSurf.createVVll()

Plasm.view((Vnew, [EVc]))

# Plasm.visualize_numbers
# this does not work yet
# LarSurf.visualize_numbers((V, EV, FE), 0.5)


using COSMO
using Logging

DEFAULT_SOLVER = optimizer_with_attributes(
    COSMO.Optimizer, "verbose" => false, "max_iter" => 900000
)

"""
simulate returns normal
"""
function returns_montecarlo(Σ, r̄, numS)
    d = MvNormal(r̄, Σ)
    r = rand(d, numS)
    P = pdf(d, r)
    return r', P / sum(P)
end

## Prep data
function compute_solution(model::JuMP.Model, w; solver=DEFAULT_SOLVER)
    set_optimizer(model, solver)
    optimize!(model)
    status = termination_status(model)
    status !== MOI.OPTIMAL && @warn "Did not find an optimal solution: status=$status"

    w_values = value.(w)
    if sum(w_values) > 1.0
        w_values = w_values / sum(w_values)
    end
    r = sum(r̄'w_values)
    return w_values, objective_value(model), r
end

function compute_solution_dual(model::JuMP.Model, w; solver=DEFAULT_SOLVER)
    set_optimizer(model, solver)
    optimize!(model)
    status = termination_status(model)
    status !== MOI.OPTIMAL && @warn "Did not find an optimal solution: status=$status"

    w_values = value.(w)
    if sum(w_values) > 1.0
        w_values = w_values / sum(w_values)
    end
    return w_values, objective_value(model), value(model[:E])
end

function compute_solution_stoc(model::JuMP.Model, w; solver=DEFAULT_SOLVER)
    set_optimizer(model, solver)
    optimize!(model)
    status = termination_status(model)
    status !== MOI.OPTIMAL && @warn "Did not find an optimal solution: status=$status"

    w_values = value.(w)
    if sum(w_values) > 1.0
        w_values = w_values / sum(w_values)
    end
    Cvar = -objective_value(model)
    r = sum(r̄'w_values)
    q1_α = value.(model[:z])
    return w_values, r, Cvar, q1_α
end

function compute_solution_stoc_2(model::JuMP.Model, w; solver=DEFAULT_SOLVER)
    set_optimizer(model, solver)
    optimize!(model)
    status = termination_status(model)
    status !== MOI.OPTIMAL && @warn "Did not find an optimal solution: status=$status"

    w_values = value.(w)
    if sum(w_values) > 1.0
        w_values = w_values / sum(w_values)
    end
    r = objective_value(model)
    q1_α = value.(model[:z])
    return w_values, r, q1_α
end


using Revise
using AdFem
using PyCall
using LinearAlgebra
using ADCME
using MAT
using JLD2
using PyPlot
np = pyimport("numpy")

reset_default_graph()
function nnlaw(σ, ε)
    x = [σ ε]
    ae(x, [20,20,20,20,3], "law")
end

# end
mode = "data"
# Domain information
NT = 20
Δt = 1/NT
n = 10
m = 2*n
h = 1.0/n
bdnode = bcnode("lower", m, n, h)

b = 1.0
H_ = Variable(diagm(0=>ones(3)))
H = H_'*H_
H = H .* [1.0 1.0 0.0
                    1.0 1.0 0.0
                0.0 0.0 1.0]

bd = bcedge("upper", m, n, h)
Q, Prhs = compute_fvm_tpfa_matrix(ones(4*m*n), bd, zeros(size(bd,1)),m, n, h)
Q = SparseTensor(Q)
K = compute_fem_stiffness_matrix(H, m, n, h)
L = SparseTensor(compute_interaction_matrix(m, n, h))
M = SparseTensor(compute_fvm_mass_matrix(m, n, h))
A = [K -b*L'
b*L/Δt 1/Δt*M-Q]
A, Abd = fem_impose_coupled_Dirichlet_boundary_condition(A, bdnode, m, n, h)

U = zeros(m*n+2(m+1)*(n+1), NT+1)
x = Float64[]; y = Float64[]
for j = 1:n+1
    for i = 1:m+1
        push!(x, (i-1)*h)
        push!(y, (j-1)*h)
    end
end
injection = (div(n,2)-1)*m + 3
production = (div(n,2)-1)*m + m-3

function get_disp(ipval)

    function condition(i, tas...)
        i<=NT
    end

    function body(i, tas...)
        ta_u, ta_ε, ta_σ = tas
        u = read(ta_u, i)
        σ0 = read(ta_σ, i)
        ε0 = read(ta_ε, i)
        G = nnlaw(σ0, ε0)
        rhs1 = compute_strain_energy_term(G, m, n, h)

        rhs1 = scatter_update(rhs1, [bdnode; bdnode .+ (m+1)*(n+1)], zeros(2length(bdnode)))
        rhs2 = zeros(m*n)
        rhs2[injection] += 1.0
        rhs2[production] -= 1.0
        rhs2 = rhs2 * ipval + b*L*u[1:2(m+1)*(n+1)]/Δt +
                M * u[2(m+1)*(n+1)+1:end]/Δt + Prhs

        rhs = [rhs1;rhs2]
        o = A\rhs

        ε = eval_strain_on_gauss_pts(o, m, n, h)
        σ = G + ε*H

        ta_u = write(ta_u, i+1, o)
        ta_ε = write(ta_ε, i+1, ε)
        ta_σ = write(ta_σ, i+1, σ)
        i+1, ta_u, ta_ε, ta_σ
    end

    i = constant(1, dtype=Int32)
    ta_u = TensorArray(NT+1); ta_u = write(ta_u, 1, constant(zeros(2(m+1)*(n+1)+m*n)))
    ta_ε = TensorArray(NT+1); ta_ε = write(ta_ε, 1, constant(zeros(4*m*n, 3)))
    ta_σ = TensorArray(NT+1); ta_σ = write(ta_σ, 1, constant(zeros(4*m*n, 3)))
    _, u_out, ε_out, σ_out = while_loop(condition, body, [i, ta_u, ta_ε, ta_σ])
    u_out = stack(u_out)
    u_out.set_shape((NT+1, size(u_out,2)))
    σ_out = stack(σ_out)
    ε_out = stack(ε_out)

    upper_idx = Int64[]
    for i = 1:m+1
        push!(upper_idx, (div(n,3)-1)*(m+1)+i)
        push!(upper_idx, (div(n,3)-1)*(m+1)+i + (m+1)*(n+1))
    end
    for i = 1:m
        push!(upper_idx, (div(n,3)-1)*m+i+2(m+1)*(n+1))
    end

    u_out, σ_out
end

Us = Array{PyObject}(undef, 6)
Ss = Array{PyObject}(undef, 6)

for i = 1:5
    Us[i], Ss[i] = get_disp(0.2*i)
end
Us[6], Ss[6] = get_disp(0.5)

if isfile("invdata.mat")
    d = matread("invdata.mat")
    Us_ = d["U"]
    Ss_ = d["S"]
    global Sigma0 = Ss_[6]
    global U0 = Us_[6]
    global loss = sum([sum((Us[i][:,1:m+1] - Us_[i][:,1:m+1])^2) for i = 1:5])
end
sess = Session(); init(sess)

function visualize(i)
    visualize_von_mises_stress(Sigma_, m, n, h, name="_nn$i")
    visualize_displacement(Array(U_'), m, n, h, name="_nn0$i",
                    xlim_=[-3h, m*h+2h], ylim_=[-2h, n*h+2h])
    close("all")
    figure(figsize=(13,4))
    subplot(121)
    plot(LinRange(0, 20, NT+1), U0[:,1], "r--")
    plot(LinRange(0, 20, NT+1), U_[:,1], "r")
    plot(LinRange(0, 20, NT+1), U0[:,1+(n+1)*(m+1)], "g--")
    plot(LinRange(0, 20, NT+1), U_[:,1+(n+1)*(m+1)], "g")
    xlabel("Time")
    ylabel("Displacement")
    subplot(122)
    plot(LinRange(0, 20, NT+1), mean(Sigma0[:,1:4,1], dims=2)[:],"r--", label="\$\\sigma_{xx}\$")
    plot(LinRange(0, 20, NT+1), mean(Sigma0[:,1:4,2], dims=2)[:],"b--", label="\$\\sigma_{yy}\$")
    plot(LinRange(0, 20, NT+1), mean(Sigma0[:,1:4,3], dims=2)[:],"g--", label="\$\\sigma_{xy}\$")
    plot(LinRange(0, 20, NT+1), mean(Sigma_[:,1:4,1], dims=2)[:],"r-")
    plot(LinRange(0, 20, NT+1), mean(Sigma_[:,1:4,2], dims=2)[:],"b-")
    plot(LinRange(0, 20, NT+1), mean(Sigma_[:,1:4,3], dims=2)[:],"g-")
    legend()
    legend()
    xlabel("Time")
    ylabel("Stress")
    savefig("disp$i.png")
    savefig("disp$i.pdf")
    matwrite("nn$i.mat", Dict("U"=>U_, "S"=>Sigma_))
end

# run(sess, loss)
# gradview(sess, H_, loss, diagm(0=>ones(3))[:])
# savefig("line.png")

loss = loss*1e10
U_, Sigma_ = run(sess, [Us[6], Ss[6]])
visualize(0)
for iter = 1:5000
    BFGS!(sess, loss, 500)
    global U_, Sigma_ = run(sess, [Us[6], Ss[6]])
    visualize(iter)
end


### A Pluto.jl notebook ###
# v0.12.21

using Markdown
using InteractiveUtils

# ╔═╡ e2d165ea-8efe-11eb-0505-65311ef915fa
using Pkg

# ╔═╡ c41ad652-8efe-11eb-2bec-45044027da71
using CSV, DataFrames, Plots, StatsPlots, EmpiricalCDFs, StatsBase, ShiftedArrays, Statistics, Distributions, Dates

# ╔═╡ 70a19b4e-8f05-11eb-224b-5df1b5308b30
Pkg.add("ShiftedArrays")

# ╔═╡ 84c098cc-8f06-11eb-29b3-e11faff4f67e
pyplot()

# ╔═╡ 1b2f8ff0-8eff-11eb-181e-3542d87b88fd
btcdata = DataFrame(
	CSV.File("BTC-USD.csv",
		# types=Dict("Open" => Float16,
		# 	 "High" => Float16,
		# 	 "Low" => Float16,
		# 	 "Close" => Float16,
		# 	 "Adj Close" => Float16,
		# 	 "Volume" => Int)
	)
)

# ╔═╡ cd0240d6-8f10-11eb-30c0-f72bc355d709
describe(btcdata)

# ╔═╡ e6ea09ea-8eff-11eb-2e0c-7d3e532fef5b
@df btcdata plot(:Date, :Close)

# ╔═╡ 67ad1e5e-8f01-11eb-239a-877da6996b3a
@df btcdata plot(:Date, :Close, yaxis=:log)

# ╔═╡ 841df9f2-8f01-11eb-10af-fde225b0ed04
@df btcdata histogram(:Close)

# ╔═╡ 0818e5bc-8f07-11eb-3b0f-4712461dfc62
returns = [btcdata[i, :Close] - btcdata[i-1,:Close] for i=2:length(btcdata[:Close]) ]

# ╔═╡ 21c6e3fc-8f06-11eb-3e81-652aad55e40e
logreturns = [ log(btcdata[i, :Close] / btcdata[i-1,:Close]) for i=2:length(btcdata[:Close]) ]

# ╔═╡ 1293b38c-8f07-11eb-3fac-814f22d8ecd8
plot(
	histogram(returns),
	histogram(rand(Normal(mean(returns), std(returns)), length(returns)), xlims=(minimum(returns),maximum(returns))),
	layout=(2,1)
)

# ╔═╡ fbad7c0c-8f10-11eb-2eac-f3588fdce9fb
histogram([returns, rand(Normal(mean(returns), std(returns)), length(returns))], bins=-10^4:200:10^4)

# ╔═╡ 37abb3d4-8f06-11eb-1f55-098daa698f59
histogram(logreturns)

# ╔═╡ 75b8244c-8f08-11eb-2413-f9e77b9c0cec
function empiricalcdf(v)
	empcdf = EmpiricalCDF()
	append!(empcdf, v)
	sort!(empcdf)
	return empcdf
end

# ╔═╡ b5023930-8f03-11eb-3e0d-c598e1526937
begin
	lr_close_empcdf = empiricalcdf(logreturns)
	r_close_empcdf = empiricalcdf(returns)
	close_empcdf = empiricalcdf(btcdata[:Close])
end

# ╔═╡ 2c76370e-8f07-11eb-1b20-41861bebfe81
plot(
	plot(returns, r_close_empcdf(returns), seriestype=:scatter, label="returns"),
	plot(logreturns, lr_close_empcdf(logreturns), seriestype=:scatter, label="logreturns"),
	plot(btcdata[:Close], close_empcdf(btcdata[:Close]), seriestype=:scatter, label="close"),
	layout=(1,3)
)

# ╔═╡ 39c023b6-8f07-11eb-3b5c-7341e02b7e90
plot(
	returns,
	r_close_empcdf(returns),
	seriestype=:scatter,
	yaxis=:log,
	xaxis=:log,
	xlims=(1,10^5)
)

# ╔═╡ 11458512-8f04-11eb-08c4-733a2ae79b86
plot(
	logreturns,
	lr_close_empcdf(logreturns),
	seriestype=:scatter,
	yaxis=:log,
	xaxis=:log,
	xlims=(0.01,0.5)
)

# ╔═╡ Cell order:
# ╠═e2d165ea-8efe-11eb-0505-65311ef915fa
# ╠═70a19b4e-8f05-11eb-224b-5df1b5308b30
# ╠═c41ad652-8efe-11eb-2bec-45044027da71
# ╠═84c098cc-8f06-11eb-29b3-e11faff4f67e
# ╠═1b2f8ff0-8eff-11eb-181e-3542d87b88fd
# ╠═cd0240d6-8f10-11eb-30c0-f72bc355d709
# ╠═e6ea09ea-8eff-11eb-2e0c-7d3e532fef5b
# ╠═67ad1e5e-8f01-11eb-239a-877da6996b3a
# ╠═841df9f2-8f01-11eb-10af-fde225b0ed04
# ╠═0818e5bc-8f07-11eb-3b0f-4712461dfc62
# ╠═21c6e3fc-8f06-11eb-3e81-652aad55e40e
# ╠═1293b38c-8f07-11eb-3fac-814f22d8ecd8
# ╠═fbad7c0c-8f10-11eb-2eac-f3588fdce9fb
# ╠═37abb3d4-8f06-11eb-1f55-098daa698f59
# ╠═75b8244c-8f08-11eb-2413-f9e77b9c0cec
# ╠═b5023930-8f03-11eb-3e0d-c598e1526937
# ╠═2c76370e-8f07-11eb-1b20-41861bebfe81
# ╠═39c023b6-8f07-11eb-3b5c-7341e02b7e90
# ╠═11458512-8f04-11eb-08c4-733a2ae79b86


### A Pluto.jl notebook ###
# v0.15.1

using Markdown
using InteractiveUtils

# ╔═╡ d1c4498d-4ca9-4eea-a0d1-5bd60eff8821
begin
	using Pkg; Pkg.add("PyPlot")
	using Plots;gr()
	using LaTeXStrings
	using PlutoUI
end

# ╔═╡ dad84aa1-2705-40d0-87a0-fea5b49c68ee
md"""
## $A_n$ as a function of $a$
"""

# ╔═╡ c35d5180-e19d-11eb-337f-4309ec908881
md"""

$A_n = \dfrac{2bL^2}{a (L - a) n^2 \pi^2} \sin \left(\dfrac{n \pi a}{L} \right)$
"""

# ╔═╡ 32036e45-edf1-4887-b778-ddffdfed715b
md"""
**Constants used:**
"""

# ╔═╡ 5c478b04-857c-4346-93e9-1acd7de93f65
begin
	L = 650
	b = 4
	size = 10
	y(a) = 0
end

# ╔═╡ 75fb5ad4-f4db-4a8c-b663-00e6bfaed677
md"""
#### Graph 1: isolated harmonics
"""

# ╔═╡ d8aa9c96-60cc-47f6-b78d-66c70bedd166
md"""
**Functions to be plotted:**
"""

# ╔═╡ 633628c9-efea-4118-a5a4-8ca55fd19832
begin
	n1(a) = (2*b*L^2)*sin(π*a/L)/(a*(L - a)*π^2)
	n2(a) = (2*b*L^2)*sin(2*π*a/L)/(a*(L - a)*2^2*π^2)
	n3(a) = (2*b*L^2)*sin(3*π*a/L)/(a*(L - a)*3^2*π^2)
end

# ╔═╡ d726ada1-3f5e-4d6b-87fa-acc9b2487356
begin
	p1 = plot(n1,
		xlims = (0,651),
		ylims = (-2, 4),
		xlabel = "a (mm)",
		ylabel = "An(a) (mm)",
		fontfamily = "Helvetica Bold",
		xticks = 0:50:650,
		yticks = -1:1:3,
		w=2,
		label = "n=1",
		size=(600,500),
		framestyle = :box,
		xtickfontsize=size,
		ytickfontsize=size,
		xguidefontsize=18,
		yguidefontsize=18,
		legendfontsize=13,
		legend = (0.15,0.2),
		foreground_color_legend = nothing,
		dpi=350,
		palette = :tab10,
		linestyle = :solid,
		grid = false
		)
	plot!(n2,
		w=2,
		label="n=2",
		linestyle = :dash
	)
	plot!(n3,
		w=3,
		label="n=3",
		linestyle = :dashdot
	)
	plot!(y,
		label= false,
		linestyle = :dash,
		colour = "black"
	)
end

# ╔═╡ 5ffd711b-6e36-445a-a870-c766e41fa8f2
md"""
#### Graph 2: sums of n harmonics, for different values of n
"""

# ╔═╡ 25cdb346-dbab-4079-b46a-829e025050e5
md"""
**Functions to be plotted:**
"""

# ╔═╡ 8e544e29-4457-47d5-a19a-e72211f738a0
begin
	s3(a) = sum((2*b*L^2)*sin(n*π*a/L)/(a*(L - a)*n^2*π^2) for n in 1:3)
	s5(a) = sum((2*b*L^2)*sin(n*π*a/L)/(a*(L - a)*n^2*π^2) for n in 1:5)
	s10(a) = sum((2*b*L^2)*sin(n*π*a/L)/(a*(L - a)*n^2*π^2) for n in 1:10)
	s50(a) = sum((2*b*L^2)*sin(n*π*a/L)/(a*(L - a)*n^2*π^2) for n in 1:50)
end

# ╔═╡ 2a87c736-a093-4a47-98cd-23ec6f4f35bf
begin
	p2 = plot(s3,
		xlims = (0,651),
		ylims = (0, 12.01),
		xlabel = "a (mm)",
		ylabel = "An(a) (mm)",
		fontfamily = "Helvetica Bold",
		xticks = 50:50:650,
		yticks = 0:3:12,
		w=2,
		label = "until 3rd",
		size=(600,500),
		framestyle = :box,
		xtickfontsize=size,
		ytickfontsize=size,
		xguidefontsize=18,
		yguidefontsize=18,
		legendfontsize=13,
		legend = (0.15,0.25),
		foreground_color_legend = nothing,
		dpi=350,
		palette = :tab10,
		linestyle = :solid,
		grid = false

	)
	plot!(s5, w=2, label = "until 5th", linestyle = :dash)
	plot!(s10, w=2,  label = "until 10th", linestyle = :dashdot)
	plot!(s50, w=3, label = "until 50th", linestyle = :dot)
end

# ╔═╡ 2c434bcf-b8f2-42b0-b145-3226a541a5d2
md"""
## Graphs of $f(x,t)$
"""

# ╔═╡ c26b4924-73c2-4d70-9fef-c48a40f8567c
md"""

$f(x,t) = \sum_{n=1}^\infty A_n \sin \left( \frac{n \pi x}{L}\right) \cos (\omega_n t - \phi_n)$
"""

# ╔═╡ 9edb2569-44df-4845-bd5e-8f64cc38baf6
md"""
**Constants used:**
"""

# ╔═╡ 59f23614-e7af-466b-bbc0-00586b30567c
begin
	a = 60
	A_1 = (2*b*L^2)*sin(π*a/L)/(a*(L - a)*π^2)
	ω_1 = π/L
end

# ╔═╡ 9d8b21bb-51b1-46ff-ab17-a79dffd227ef
md"""
#### Graph 3: first harmonic, for different values of t
"""

# ╔═╡ 6d11a2ac-2b41-4b42-a5f0-afbe91d0bbbf
md"""
**Functions to be plotted:**
"""

# ╔═╡ e37811ba-c806-4ff0-954e-e041b687b94c
begin
	f1(x) =  A_1*sin(π*x/L)*cos(0)
	f2(x) =  A_1*sin(π*x/L)*cos(2*π*0.25)
	f3(x) =  A_1*sin(π*x/L)*cos(2*π*3/8)
	f4(x) =  A_1*sin(π*x/L)*cos(2*π*0.5)
end

# ╔═╡ 43dec0ff-a7c8-4b86-9fa6-0dbac92d3be9
begin
	p3 = plot(f1,
		xlims = (0,651),
		ylims = (-4, 4),
		xlabel = "x (mm)",
		ylabel = "f(x,t) (mm)",
		fontfamily = "Helvetica Bold",
		xticks = 0:50:650,
		yticks = -3:1:3,
		w=2,
		label = "t = 0",
		size=(600,500),
		framestyle = :box,
		xtickfontsize=size,
		ytickfontsize=size,
		xguidefontsize=18,
		yguidefontsize=18,
		legendfontsize=10,
		legend = (0.1,0.2),
		foreground_color_legend = nothing,
		dpi=350,
		palette = :tab10,
		grid = false,
		linestyle = :solid
		)
	plot!(f2,
		w=2,
		label="t = L/2c",
		linestyle = :dash
	)
	plot!(f3,
		w=2,
		label="t = 3L/4c",
		linestyle = :dashdot
	)
	plot!(f4,
		w=3,
		label="t = L/c",
		linestyle = :dot
	)
	#plot!(y, label=false, colour="black", linestyle=:dash)
end

# ╔═╡ a4dddba9-b21a-4edb-a26b-36e88bf99f7a
md"""
#### Graph 4: sums of N harmonics with t=0, for different values of n
"""

# ╔═╡ ff2fd560-c536-4dc7-a719-e85421377c1c
md"""
**Functions to be plotted:**
"""

# ╔═╡ 8213670d-c5f9-4ca4-9c33-a4ecc8ed4834
begin
	g3(x) = sum((2*b*L^2)*sin(n*π*a/L)/(a*(L - a)*n^2*π^2)*sin(n*π*x/L) for n in 1:3)
	g5(x) = sum((2*b*L^2)*sin(n*π*a/L)/(a*(L - a)*n^2*π^2)*sin(n*π*x/L) for n in 1:5)
	g10(x) = sum((2*b*L^2)*sin(n*π*a/L)/(a*(L - a)*n^2*π^2)*sin(n*π*x/L) for n in 1:10)
	g50(x) = sum((2*b*L^2)*sin(n*π*a/L)/(a*(L - a)*n^2*π^2)*sin(n*π*x/L) for n in 1:50)
end

# ╔═╡ 2779cb1e-f6ac-4e4a-b73d-8f2999c4b2bd
begin
	p4 = plot(g3,
			xlims = (0,651),
			ylims = (-2, 5),
			xlabel = "x (mm)",
			ylabel = "f(x,t) (mm)",
			fontfamily = "Helvetica Bold",
			xticks = 0:50:650,
			yticks = -0:1:4,
			w=2,
			label = "until 3rd",
			size=(600,500),
			framestyle = :box,
			tickfontsize=size,
			guidefontsize=18,
			legendfontsize=14,
			legend = (0.80,0.9),
			foreground_color_legend = nothing,
			dpi=350,
			palette = :tab10,
			grid = false,
			linestyle = :solid

		)
		plot!(g5,
			w=2,
			label="until 5th",
			linestyle = :dash
		)
		plot!(g10,
			w=2,
			label="until 10th",
			linestyle = :dashdot
		)
		plot!(g50,
			w=3,
			label="until 50th",
			linestyle = :dot
		)
		plot!(y, label=false, colour="black", linestyle=:dash)
end

# ╔═╡ c37553de-f3e0-428c-b07f-b6aaf46e33c5
md"""
**Saving figures**
"""

# ╔═╡ aad18a7b-825d-4321-8c87-44bcb22942f2
begin
	savefig(p1, "an_n_separados.png")
	savefig(p2, "an_n_somas.png")
	savefig(p3, "solucao_n1.png")
	savefig(p4, "solucao_somas.png")
 end

# ╔═╡ 1f1e8574-1ce3-4dbe-af5c-8f7cf3e83b31
md"""
## Simulation of $f(x,t)$ with 50 harmonics
"""

# ╔═╡ 35015f75-7887-4f18-b4f8-ef8c5a4103fc
begin
	j = 240
	s = 50L

	p5 = @animate for i in range(0, stop = s, length = j)
	    g(x) = sum((2*b*L^2)*sin(n*π*a/L)* ℯ^(-i*n^3/10000)/(a*(L - a)*n^2*π^2)*sin(n*π*x/L)*cos((n*π/L) *i) for n in 1:50)
	    plot(g,
			xlims = (0,650),
			ylims = (-40, 40),
			xlabel = "x (mm)",
			ylabel = "f(x,t) (mm)",
			fontfamily = "Helvetica Bold",
			xticks = 0:50:600,
			yticks = -60:30:60,
			w=2,
			palette = :tab10,
			legend = false
		)
	end
	gif(p5, "oscilacao.gif", fps = 30)
end

# ╔═╡ Cell order:
# ╠═d1c4498d-4ca9-4eea-a0d1-5bd60eff8821
# ╟─dad84aa1-2705-40d0-87a0-fea5b49c68ee
# ╟─c35d5180-e19d-11eb-337f-4309ec908881
# ╟─32036e45-edf1-4887-b778-ddffdfed715b
# ╟─5c478b04-857c-4346-93e9-1acd7de93f65
# ╟─75fb5ad4-f4db-4a8c-b663-00e6bfaed677
# ╟─d8aa9c96-60cc-47f6-b78d-66c70bedd166
# ╟─633628c9-efea-4118-a5a4-8ca55fd19832
# ╟─d726ada1-3f5e-4d6b-87fa-acc9b2487356
# ╟─5ffd711b-6e36-445a-a870-c766e41fa8f2
# ╟─25cdb346-dbab-4079-b46a-829e025050e5
# ╟─8e544e29-4457-47d5-a19a-e72211f738a0
# ╟─2a87c736-a093-4a47-98cd-23ec6f4f35bf
# ╟─2c434bcf-b8f2-42b0-b145-3226a541a5d2
# ╟─c26b4924-73c2-4d70-9fef-c48a40f8567c
# ╟─9edb2569-44df-4845-bd5e-8f64cc38baf6
# ╟─59f23614-e7af-466b-bbc0-00586b30567c
# ╟─9d8b21bb-51b1-46ff-ab17-a79dffd227ef
# ╟─6d11a2ac-2b41-4b42-a5f0-afbe91d0bbbf
# ╟─e37811ba-c806-4ff0-954e-e041b687b94c
# ╟─43dec0ff-a7c8-4b86-9fa6-0dbac92d3be9
# ╟─a4dddba9-b21a-4edb-a26b-36e88bf99f7a
# ╟─ff2fd560-c536-4dc7-a719-e85421377c1c
# ╟─8213670d-c5f9-4ca4-9c33-a4ecc8ed4834
# ╟─2779cb1e-f6ac-4e4a-b73d-8f2999c4b2bd
# ╟─c37553de-f3e0-428c-b07f-b6aaf46e33c5
# ╟─aad18a7b-825d-4321-8c87-44bcb22942f2
# ╟─1f1e8574-1ce3-4dbe-af5c-8f7cf3e83b31
# ╟─35015f75-7887-4f18-b4f8-ef8c5a4103fc


function transform_density!(img::AbstractArray,edges::AbstractArray, cdf::AbstractArray, minval::Union{Real,AbstractGray}, maxval::Union{Real,AbstractGray})
    first_edge = first(edges)
    inv_step_size = 1/step(edges)
    scale = (maxval - minval) / (cdf[end] - first(cdf))
    function transform(val)
        if val >= edges[end]
            newval = cdf[end]
        elseif val < first_edge
            newval = first(cdf)
        else
            index = floor(Int, (val-first_edge)*inv_step_size) + 1
            newval = cdf[index]
        end
        # Scale the new intensity value to so that it lies in the range [minval, maxval].
        newval = minval + (newval - first(cdf)) * scale
    end
    if eltype(img) <: Integer
        map!(val->ceil(transform(val)), img, img)
    else
        map!(transform, img, img)
    end
end

function construct_pdfs(img::AbstractArray, targetimg::AbstractArray, edges::AbstractRange)
    _, histogram = build_histogram(img, edges)
    _, target_histogram = build_histogram(targetimg, edges)
    return edges, histogram / sum(histogram), target_histogram / sum(target_histogram)
end

function construct_pdfs(img::AbstractArray, targetimg::AbstractArray, nbins::Integer = 256)
    if eltype(img) <: AbstractGray
        imin, imax = 0, 1
    else
        imin, imax = min(minfinite(img), minfinite(targetimg)), max(maxfinite(img), maxfinite(targetimg))
    end
    edges, histogram = build_histogram(img, nbins, minval = imin, maxval = imax)
    _, target_histogram = build_histogram(targetimg, edges)
    return edges, histogram / sum(histogram), target_histogram / sum(target_histogram)
end

function lookup_icdf(cdf::AbstractArray, targetcdf::AbstractArray)
    lookup_table = zeros(Int, length(cdf))
    i = 1
    for j = 1:length(cdf)
        p = cdf[j]
        while i < length(targetcdf) && targetcdf[i+1] <= p
            i += 1
        end
        lookup_table[j] = i
    end
    lookup_table
end

function cdf2pdf!(pdf::AbstractArray, cdf::AbstractArray)
    pdf[1] = cdf[1]
    for i = length(cdf)-1:-1:2
        pdf[i] = cdf[i] - cdf[i-1]
    end
end


@testset "whiten/whiten!" begin
    x = rand(1000)
    x[1:100] .*= 30
    LyceumAI.whiten!(x)
    @test std(x) ≈ 1
    @test abs(mean(x)) < sqrt(eps(eltype(x)))
end

@testset "TimeFeatures" begin
    dimobs = 3
    N = 100
    let o = [1, 2, 3], c = [2, 4, 6], dt = 0.01
        A = rand(dimobs, N)
        op = LyceumAI.TimeFeatures(o, c, dt)
        B = op(A)
        x = (0:(N - 1)) .* dt
        @test B[1:dimobs, :] == A
        @test all(Iterators.product(1:dimobs, 1:N)) do (k, j)
            i = k + dimobs
            B[i,j] == c[k] * x[j]^o[k]
        end
    end
    let o = [1, 2, 3], c = [2, 4, 6], dt = 0.01
        op = LyceumAI.TimeFeatures([1,2,3], [1,1,1], 0.01)
        A1 = [rand(3, 5), rand(3, 4)]
        A2 = hcat(A1...)
        B = op(A1)
        @test op(A2[:, 1:5]) == B[:, 1:5]
        @test op(A2[:, 6:end]) == B[:, 6:end]
    end
    let o = [1, 2, 3], c = [2, 4, 6], dt = 0.01
        op = LyceumAI.TimeFeatures([1,2,3], [1,1,1], 0.01)
        A1 = rand(3, 2)
        B1 = op(A1, [5, 10])
        A2 = [rand(3, 5), rand(3, 10)]
        B2 = op(A2)
        @test B1[(dimobs + 1):end, 1] == B2[(dimobs + 1):end, 5]
        @test B1[(dimobs + 1):end, 2] == B2[(dimobs + 1):end, end]
    end
end