import Base: eltype, size, length, getindex, setindex!, similar, copy

import SPTK

### Generic interface ###

abstract type SpectralParam end

function estimate(s::SpectralParam, x::AbstractArray)
    error("should provide an estimator")
end

function params(s::SpectralParam)
    error("should provide get access to parameters")
end

# subtypes of SpectralParam should have field `order`
param_order(s::SpectralParam) = s.order

### Types that characterize spectral parameters ###

abstract type FrequencyForm end
struct MelFrequency <: FrequencyForm
end
struct LinearFrequency <: FrequencyForm
end

abstract type LogForm end
struct GeneralizedLog <: LogForm
end
struct StandardLog <: LogForm
end
struct AllPoleLog <: LogForm
end

### Definitions of spectral envelope parameters ###

struct MelGeneralizedCepstrum{F<:FrequencyForm,L<:LogForm} <: SpectralParam
    order::Int
    α::Real
    γ::Real
    function (::Type{MelGeneralizedCepstrum{F,L}}){F<:FrequencyForm,L<:LogForm}(
            order::Int, α::Real, γ::Real)
        order > 0 || throw(ArgumentError("order should be larger than 0"))
        abs(α) < 1 || throw(ArgumentError("|α| < 1 is supported"))
        (-1 <= γ <= 0) || throw(ArgumentError("-1 <= γ <= 0 is supported"))
        new{F,L}(order, α, γ)
    end
end

const MelFrequencyCepstrum{L} = MelGeneralizedCepstrum{MelFrequency,L}
const LinearFrequencyCepstrum{L} = MelGeneralizedCepstrum{LinearFrequency,L}
const GeneralizedLogCepstrum{F} = MelGeneralizedCepstrum{F,GeneralizedLog}
const StandardLogCepstrum{F} = MelGeneralizedCepstrum{F,StandardLog}
const AllPoleLogCepstrum{F} = MelGeneralizedCepstrum{F,AllPoleLog}

const GeneralizedCepstrum = MelGeneralizedCepstrum{LinearFrequency,GeneralizedLog}
const MelCepstrum = MelGeneralizedCepstrum{MelFrequency,StandardLog}
const LinearCepstrum = MelGeneralizedCepstrum{LinearFrequency,StandardLog}
const AllPoleCepstrum = MelGeneralizedCepstrum{LinearFrequency,AllPoleLog}
const MelAllPoleCepstrum = MelGeneralizedCepstrum{MelFrequency,AllPoleLog}

freq_form{F<:FrequencyForm,L<:LogForm}(::Type{MelGeneralizedCepstrum{F,L}}) = F
freq_form{T<:MelGeneralizedCepstrum}(::Type{T}) = freq_form(super(T))

log_form{F<:FrequencyForm,L<:LogForm}(::Type{MelGeneralizedCepstrum{F,L}}) = L
log_form{T<:MelGeneralizedCepstrum}(::Type{T}) = freq_form(super(T))

# Generic fallback constructor, which determines type parmaters from α and γ
function MelGeneralizedCepstrum(order::Int, α::Real, γ::Real)
    F = (α == zero(α)) ? LinearFrequency : MelFrequency

    L = GeneralizedLog
    if γ == zero(γ)
        L = StandardLog
    elseif γ == -one(γ)
        L = AllPoleLog
    end

    MelGeneralizedCepstrum{F,L}(order, α, γ)
end

function MelGeneralizedCepstrum(mgc::MelGeneralizedCepstrum)
    order = param_order(mgc)
    α = allpass_alpha(mgc)
    γ = glog_gamma(mgc)
    MelGeneralizedCepstrum(order, α, γ)
end

function (::Type{GeneralizedCepstrum})(order::Int, γ::Real)
    MelGeneralizedCepstrum{LinearFrequency,GeneralizedLog}(order, 0.0, γ)
end

function (::Type{MelCepstrum})(order::Int, α::Real)
    MelGeneralizedCepstrum{MelFrequency,StandardLog}(order, α, 0.0)
end

function (::Type{LinearCepstrum})(order::Int)
    MelGeneralizedCepstrum{LinearFrequency,StandardLog}(order, 0.0, 0.0)
end

function (::Type{AllPoleCepstrum})(order::Int)
    MelGeneralizedCepstrum{LinearFrequency,AllPoleLog}(order, 0.0, -1.0)
end

function (::Type{MelAllPoleCepstrum})(order::Int, α::Real)
    MelGeneralizedCepstrum{MelFrequency,AllPoleLog}(order, α, -1.0)
end

params(c::MelGeneralizedCepstrum) = (c.order, c.α, c.γ)
params(c::GeneralizedCepstrum) = (c.order, c.γ)
params(c::MelCepstrum) = (c.order, c.α)
params(c::LinearCepstrum) = (c.order,)
params(c::AllPoleCepstrum) = (c.order,)
params(c::MelAllPoleCepstrum) = (c.order, c.α)
allpass_alpha(c::MelGeneralizedCepstrum) = c.α
glog_gamma(c::MelGeneralizedCepstrum) = c.γ

### Definitions of LPC variants ###

abstract type LinearPredictionCoefVariants <: SpectralParam end

# LPC, LSP and PARCOR
struct LinearPredictionCoef <: LinearPredictionCoefVariants
    order
end

struct LineSpectralPair <: LinearPredictionCoefVariants
    order
end

struct PartialAutoCorrelation <: LinearPredictionCoefVariants
    order
end

params(s::LinearPredictionCoefVariants) = (s.order,)

### State, which keeps actual computation results in arrays ###

abstract type AbstractParamState{T,N} <: AbstractArray{T,N} end

function paramdef(s::AbstractParamState)
    error("should provide get access for parameter definition")
end

function rawdata(s::AbstractParamState)
    error("should provide get access for raw data")
end

mutable struct SpectralParamState{S<:SpectralParam,T,N} <: AbstractParamState{T,N}
    def::S
    data::Array{T,N}

    has_loggain::Bool
    gain_normalized::Bool
    ready_to_filt::Bool  # state is explicitly converted to filter coef or not.

    function (::Type{SpectralParamState{S,T,N}}){S,T,N}(s::S, data::Array{T,N},
                                has_loggain::Bool,
                                gain_normalized::Bool,
                                ready_to_filt::Bool)
        new{S,T,N}(s, data, has_loggain, gain_normalized, ready_to_filt)
    end
end

const SpectralParamStateVector{S,T} = SpectralParamState{S,T,1}
const SpectralParamStateMatrix{S,T} = SpectralParamState{S,T,2}

function SpectralParamState{S<:SpectralParam,T,N}(s::S, data::Array{T,N},
                                                  has_loggain::Bool,
                                                  gain_normalized::Bool;
                                                  ready_to_filt::Bool=false)
    SpectralParamState{S,T,N}(s, data, has_loggain, gain_normalized,
                              ready_to_filt)
end

eltype(s::SpectralParamState) = eltype(s.data)
size(s::SpectralParamState) = size(s.data)
length(s::SpectralParamState) = length(s.data)
getindex(s::SpectralParamState, i::Real) = getindex(s.data, i)
getindex(s::SpectralParamState, i::Real...) = getindex(s.data, i...)

# colon indexing like s[:,1]
function getindex(s::SpectralParamState, i::Colon, j...)
    SpectralParamState(paramdef(s), getindex(s.data, i, j...),
                       has_loggain(s), gain_normalized(s),
                       ready_to_filt=ready_to_filt(s))
end

setindex!(s::SpectralParamState, i::Real) = setindex!(s.data, i)
setindex!(s::SpectralParamState, i::Real...) = setindex!(s.data, i...)
setindex!(s::SpectralParamState, i::Colon, j...) = setindex!(s.data, i, j...)

paramdef(s::SpectralParamState) = s.def
rawdata(s::SpectralParamState) = s.data
has_loggain(s::SpectralParamState) = s.has_loggain
gain_normalized(s::SpectralParamState) = s.gain_normalized
ready_to_filt(s::SpectralParamState) = s.ready_to_filt

function loggain!(s::SpectralParamStateVector)
    has_loggain(s) && return s
    data = rawdata(s)
    data[1] = log(data[1])
    s.has_loggain = true
    s
end

function loggain!(s::SpectralParamStateMatrix)
    has_loggain(s) && return s
    data = rawdata(s)
    for i in 1:size(s, 2)
        data[1,i] = log(data[1,i])
    end
    s.has_loggain = true
    s
end

function unloggain!(s::SpectralParamStateVector)
    !has_loggain(s) && return s
    data = rawdata(s)
    data[1] = exp(data[1])
    s.has_loggain = false
    s
end

function unloggain!(s::SpectralParamStateMatrix)
    !has_loggain(s) && return s
    data = rawdata(s)
    for i in 1:size(s, 2)
        data[1,i] = exp(data[1,i])
    end
    s.has_loggain = false
    s
end

function similar(s::SpectralParamState)
    def = paramdef(s)
    data = rawdata(s)
    SpectralParamState(def, similar(data), has_loggain(s), gain_normalized(s),
                       ready_to_filt=ready_to_filt(s))
end

function copy(s::SpectralParamState)
    def = paramdef(s)
    data = rawdata(s)
    SpectralParamState(def, copy(data), has_loggain(s), gain_normalized(s),
                       ready_to_filt=ready_to_filt(s))
end

# Force re-type mel-generalized cepstrums by its parameter values
# needed?
function retype{T<:MelGeneralizedCepstrum}(s::SpectralParamState{T})
    def = paramdef(s)
    newdef = MelGeneralizedCepstrum(def)
    SpectralParamState(newdef, rawdata(s), has_loggain(s), gain_normalized(s),
                       ready_to_filt=ready_to_filt(s))
end

### Asserts ###

function assert_ready_to_filt(state::SpectralParamState)
    ready_to_filt(state) && return nothing
    throw(ArgumentError("""ready_to_filt = $(ready_to_filt(state))
                        filter coefficient input is expected"""))
end

function assert_not_ready_to_filt(state::SpectralParamState)
    !ready_to_filt(state) && return nothing
    throw(ArgumentError("""ready_to_filt = $(ready_to_filt(state))
                        filter coefficient input (can be obtain by `mgc2b` or `mc2b`) is not allowed"""))
end

function assert_gain_normalized(state::SpectralParamState)
    gain_normalized(state) && return nothing
    throw(ArgumentError("""gain_normalized: $(gain_normalized(state))
                        gain is assumed to be normalized"""))
end

function assert_gain_unnormalized(state::SpectralParamState)
    gain_normalized(state) || return nothing
    throw(ArgumentError("""gain_normalized: $(gain_normalized(state))
                        gain is assumed to be un-normalized"""))
end


# just show body, not standalone
function Plots._show(io::IO, ::MIME"text/html", plt::Plots.Plot{Plots.PlotlyBackend})
    write(io, Plots.html_body(plt))
end

## -----

export plotif, trimplot, signchart,
       plot_polar, plot_polar!,
       plot_parametric,   plot_parametric!,
       vectorfieldplot,   vectorfieldplot!,
       vectorfieldplot3d, vectorfieldplot3d,
       arrow, arrow!

export newton_vis

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

"""
   trimplot(f, a, b, c=20; kwargs...)

Plot f over [a,b] but break graph if it exceeds c in absolute value.
"""
function trimplot(f, a, b, c=20; color=:black, legend=false, kwargs...)
    F = (a,b) -> begin
        fa, fb = f(a), f(b)
        M = max(fa, fb)
        m = min(fa, fb)
        m < -c && return false
        M > c && return false
        true
    end
    xs = range(a, b, length=251)
    cols = find_colors(F, xs, (color, :transparent, :red))
    Plots.plot(xs, f.(xs), colors=cols, legend=legend, kwargs...)
end

"""
    plotif(f, g, a, b)

Plot f colored depending on g >= 0 or not.
"""
function plotif(f, g, a, b)
    xs = range(a, b, length=251)
    cols = identify_colors(g, xs)
    Plots.plot(xs, f.(xs), color=cols, legend=false)
end

"""
   signchart(f, a, b)

Plot f over a,b with different color when negative.
"""
function signchart(f, a, b)
    p = plotif(f, f, a, b)
    Plots.plot!(p, zero)
    p
end

## --- parametric plots

adapted_grid = CalculusWithJulia.PlotUtils.adapted_grid
function xyzs(ab::ClosedInterval, r)
    a, b = abs = extrema(ab)
    xs = sort(union([adapted_grid(t -> r(t)[i], abs)[1] for i in eachindex(r(a))]...))
    unzip(r.(xs))
end

"""
    plot_parametric(ab, r; kwargs...)
    plot_parametric!(ab, r; kwargs...)
    plot_parametric(u, v, F; kwargs...)
    plot_parametric!(u, v, F; kwargs...)

Make a parametric plot of a space curve or parametrized surface

The intervals to plot over are specifed using `a..b` notation, from IntervalSets
"""
plot_parametric(ab::ClosedInterval, r; kwargs...) = Plots.plot(xyzs(ab, r)...; kwargs...)
plot_parametric!(ab::ClosedInterval, r; kwargs...) = Plots.plot!(xyzs(ab, r)...; kwargs...)

plot_polar(ab::ClosedInterval, r; kwargs...) =
    plot_parametric(ab, t->[r(t)*cos(t), r(t)*sin(t)]; kwargs...)
plot_polar!(ab::ClosedInterval, r; kwargs...) =
    plot_parametric!(ab, t->[r(t)*cos(t), r(t)*sin(t)]; kwargs...)

## ----

function XYZs(u::ClosedInterval, v::ClosedInterval, r; n=51)
    us = range(extrema(u)..., length=n)
    vs = range(extrema(v)..., length=n)
    unzip(r.(us', vs))  # Plots.jl style; flip for Makie
end

# needs plotly(); not gr()
plot_parametric(u::ClosedInterval, v::ClosedInterval, F;  kwargs...) =
    Plots.surface(XYZs(u,v,F)...;  kwargs..., )
plot_parametric!(u::ClosedInterval, v::ClosedInterval, F; kwargs...) =
    Plots.surface!(XYZs(u,v,F)...;  kwargs...)

import Contour

"""
    Visualize `F(x,y,z) = c` by plotting assorted contour lines

This graphic makes slices in the `x`, `y`, and/or `z` direction of the 3-D level surface and plots them accordingly. Which slices (and their colors) are specified through a dictionary.

Examples:
```
F(x,y,z) = x^2 + y^2 + x^2
plot_implicit_surface(F, 20)  # 20 slices in z direction
plot_implicit_surface(F, 20, slices=Dict(:x=>:blue, :y=>:red, :z=>:green), nlevels=6) # all 3 shown

# A heart
a,b = 1,3
F(x,y,z) = (x^2+((1+b)*y)^2+z^2-1)^3-x^2*z^3-a*y^2*z^3
plot_implicit_surface(F, xlims=-2..2,ylims=-1..1,zlims=-1..2)
```

Note: Idea [from](https://stackoverflow.com/questions/4680525/plotting-implicit-equations-in-3d).

Not exported.
"""
function plot_implicit_surface(F, c=0;
                       xlim=(-5,5), ylim=xlim, zlim=xlim,
                       nlevels=25,         # number of levels in a direction
                       slices=Dict(:z => :blue), # Dict(:x => :color, :y=>:color, :z=>:color)
                       kwargs...          # passed to initial `plot` call
                       )

    _linspace(rng, n=150) = range(extrema(rng)[1], stop=extrema(rng)[2], length=n)

    X1, Y1, Z1 = _linspace(xlim), _linspace(ylim), _linspace(zlim)

    p = Plots.plot(;legend=false,kwargs...)

    if :x ∈ keys(slices)
        for x in _linspace(xlim, nlevels)
            local X1 = [F(x,y,z) for y in Y1, z in Z1]
            cnt = Contour.contours(Y1,Z1,X1, [c])
            for line in Contour.lines(Contour.levels(cnt)[1])
                ys, zs = Contour.coordinates(line) # coordinates of this line segment
                Plots.plot!(p, x .+ 0 * ys, ys, zs, color=slices[:x])
          end
        end
    end

    if :y ∈ keys(slices)
        for y in _linspace(ylim, nlevels)
            local Y1 = [F(x,y,z) for x in X1, z in Z1]
            cnt = Contour.contours(Z1,X1,Y1, [c])
            for line in Contour.lines(Contour.levels(cnt)[1])
                xs, zs = Contour.coordinates(line) # coordinates of this line segment
                Plots.plot!(p, xs, y .+ 0 * xs, zs, color=slices[:y])
            end
        end
    end

    if :z ∈ keys(slices)
        for z in _linspace(zlim, nlevels)
            local Z1 = [F(x, y, z) for x in X1, y in Y1]
            cnt = Contour.contours(X1, Y1, Z1, [c])
            for line in Contour.lines(Contour.levels(cnt)[1])
                xs, ys = Contour.coordinates(line) # coordinates of this line segment
                Plots.plot!(p, xs, ys, z .+ 0 * xs, color=slices[:z])
            end
        end
    end

    p
end

## ----

"""
   `arrow!(p, v)`

Add the vector `v` to the plot anchored at `p`.

This would just be a call to `quiver`, but there is no 3-D version of that. As well, the syntax for quiver is a bit awkward for plotting just a single arrow. (Though efficient if plotting many).

```
using Plots
r(t) = [sin(t), cos(t), t]
rp(t) = [cos(t), -sin(t), 1]
plot(unzip(r, 0, 2pi)...)
t0 = 1
arrow!(r(t0), r'(t0))
```
"""
function arrow!(plt, p, v; kwargs...)
  if length(p) == 2
      Plots.quiver!(plt, unzip([p])..., quiver=Tuple(unzip([v])); kwargs...)
  elseif length(p) == 3
    # 3d quiver needs support
    # https://github.com/JuliaPlots/Plots.jl/issues/319#issue-159652535
    # headless arrow instead
      Plots.plot!(plt, unzip(p, p+v)...; kwargs...)
	end
end
arrow!(p,v;kwargs...) = arrow!(Plots.current(), p, v; kwargs...)

"""

    vectorfieldplot(V; xlim=(-5,5), ylim=(-5,5), n=10; kwargs...)

V is a function that takes a point and returns a vector (2D dimensions), such as `V(x) = x[1]^2 + x[2]^2`.

The grid `xlim × ylim` is paritioned into (n+1) × (n+1) points. At each point, `pt`, a vector proportional to `V(pt)` is drawn.

This is written to add to an existing plot.

```
plot()  # make a plot
V(x,y) = [x, y-x]
vectorfield_plot!(p, V)
p
```
"""
function vectorfieldplot!(plt, V; xlim=(-5,5), ylim=(-5,5), n=10, kwargs...)

    dx, dy = (xlim[2]-xlim[1])/n, (ylim[2]-ylim[1])/n
    xs, ys = xlim[1]:dx:xlim[2], ylim[1]:dy:ylim[2]

    ps = [[x,y] for x in xs for y in ys]
    vs = V.(ps)
    λ = 0.9 * min(dx, dy) /maximum(norm.(vs))

    Plots.quiver!(plt, unzip(ps)..., quiver=unzip(λ * vs))

end
vectorfieldplot!(V; kwargs...) = vectorfieldplot!(Plots.current(), V; kwargs...)

##
# visualize newtons method
function newton_vis(f, x0, a=Inf,b=-Inf; steps=5, kwargs...)
    xs = Float64[x0]
    for i in 1:steps
        push!(xs, xs[end] - f(xs[end]) / f'(xs[end]))
    end

    m,M = extrema(xs)
    m = min(m, a)
    M = max(M, b)

    p = Plots.plot(f, m, M; linewidth=3, legend=false, kwargs...)
    Plots.plot!(p, zero)
    for i in 1:steps
        Plots.plot!(p, [xs[i],xs[i],xs[i+1]], [0,f(xs[i]), 0])
        Plots.scatter!(p, xs[i:i],[0])
    end
    Plots.scatter!(p, [xs[steps+1]], [0])
    p
end


#using Lint

function readSMAT(filename::AbstractString)
    (rows,header) = readdlm(filename;header=true)
    A = sparse(
               convert(Array{Int64,1},rows[1:parse(Int,header[3]),1])+1,
               convert(Array{Int64,1},rows[1:parse(Int,header[3]),2])+1,
               convert(Array{Int64,1},rows[1:parse(Int,header[3]),3]),
               parse(Int,header[1]),
               parse(Int,header[2])
               )
    return A
end

function load_matrix_network(name::AbstractString)
    pathname = joinpath(Pkg.dir("MatrixNetworks"),"data")
    smatfile = joinpath(pathname,"$(name).smat")
    if isfile(smatfile)
        return readSMAT(smatfile)
    else
        error(@sprintf "The example datafile '%s' does not seem to exist where it should\n" name)
    end
end

function load_matrix_network_metadata(name::AbstractString)
    pathname = joinpath(Pkg.dir("MatrixNetworks"),"data")
    smatfile = joinpath(pathname,"$(name).smat")
    meta_xy = joinpath(pathname,"$(name).xy")
    meta_labels = joinpath(pathname,"$(name).labels")
    if isfile(smatfile)
        if isfile(meta_xy) && isfile(meta_labels)
            xy = readdlm(meta_xy)
            labels = readdlm(meta_labels)
            return (readSMAT(smatfile),xy,labels)
        end
    else
        error(@sprintf "The example datafile '%s' does not seem to exist where it should\n" name)
    end
end

function matrix_network_datasets()
    datasets_location = joinpath(Pkg.dir("MatrixNetworks"),"data")
    content = readdir(datasets_location)
    smat_files = filter(x->contains(x,".smat"),content)
    for i = 1:length(smat_files)
        # no need for this lintpragma anymore
        # @lintpragma( "Ignore use of undeclared variable end")
        smat_files[i] = smat_files[i][1:end-5]
    end
    return smat_files
end


using Gaston
g = Gaston;
set_terminal("x11");

c = g.CurveConf();
c.legend = "Random";
c.plotstyle = "errorbars";

y = exp(-(1:.1:4.9));
ylow = y - 0.05*rand(40);
yhigh = y + 0.05*rand(40);
g.addcoords(1:40,y,c);
g.adderror(0.1*rand(40));

a = g.AxesConf();
a.title = "Error bars (ydelta)";
g.addconf(a);
g.llplot();


# # Filtered Oscillator

# ## Model

module filtered_oscillator      #jl

using ReachabilityAnalysis

one_loop_iteration = false
n0 = 4
n1 = (one_loop_iteration ? n0 + 1 : n0)
n = n1 + 2
z = zeros(n1)

## common flow
A = zeros(n, n)
A[1,1], A[2,2] = -2., -1.
A[3,1], A[3,3] = 5., -5.
for i = 4 : n-1
    A[i,i-1], A[i,i] = 5., -5.
end

function mode1(z)
    b = [1.4; -0.7; z]
    X = HPolyhedron([HalfSpace([-0.714286; -1.0; z], 0.0),  # 0.714286*x + y >= 0
                     HalfSpace([1.0; 0.0; z], 0.0)])  # x <= 0
    @system(x' = Ax + b, x ∈ X)
end

function mode2(z)
    b = [-1.4; 0.7; z]
    X = HPolyhedron([HalfSpace([1.0; 0.0; z], 0.0),  # x <= 0
                     HalfSpace([0.714286; 1.0; z], 0.0)])  # 0.714286*x + y <= 0
    @system(x' = Ax + b, x ∈ X)
end

function mode3(z)
    b = [1.4; -0.7; z]
    X = HPolyhedron([HalfSpace([-1.0; 0.0; z], 0.0),  # x >= 0
                     HalfSpace([-0.714286; -1.0; z], 0.0)])  # 0.714286*x + y >= 0
    @system(x' = Ax + b, x ∈ X)
end

function mode4(z, one_loop_iteration)
    b = [-1.4; 0.7; z]
    X = HPolyhedron([HalfSpace([0.714286; 1.0; z], 0.0),  # 0.714286*x + y <= 0
                     HalfSpace([-1.0; 0.0; z], 0.0)])  # x >= 0

    if one_loop_iteration
        ## k <= 2 (2.1 to avoid numerical issues)
        addconstraint!(X, HalfSpace([zeros(n-1); 1.], 2.1))
    end
    @system(x' = Ax + b, x ∈ X)
end

function filtered_oscillator_hybrid(n0, one_loop_iteration)

    n1 = (one_loop_iteration ? n0 + 1 : n0)
    n = n1 + 2
    z = zeros(n1)

    ## transition graph (automaton)
    a = LightAutomaton(4)
    add_transition!(a, 3, 4, 1)
    add_transition!(a, 4, 2, 2)
    add_transition!(a, 2, 1, 3)
    add_transition!(a, 1, 3, 4)

    mode1 = mode1(z)
    mode2 = mode2(z)
    mode3 = mode3(z)
    mode4 = mode4(z, one_loop_iteration)
    m = [mode1, mode2, mode3, mode4]

    ## transitions

    ## transition l3 -> l4
    X_l3l4 = HPolyhedron([HalfSpace([-1.0; 0.0; z], 0.0),  # x >= 0
                          HalfSpace([-0.714286; -1.0; z], 0.0),  # 0.714286*x + y >= 0
                          HalfSpace([0.714286; 1.0; z], 0.0)])  # 0.714286*x + y <= 0
    if one_loop_iteration
        A_trans_34 = Matrix(1.0I, n, n)
        A_trans_34[n, n] = 2.  # k' = k * 2
        r1 = ConstrainedLinearMap(A_trans_34, X_l3l4)
    else
        r1 = ConstrainedIdentityMap(n, X_l3l4)
    end

    ## transition l4 -> l2
    X_l4l2 = HPolyhedron([HalfSpace([0.714286; 1.0; z], 0.0),  # 0.714286*x + y <= 0
                          HalfSpace([-1.0; 0.0; z], 0.0),  # x >= 0
                          HalfSpace([1.0; 0.0; z], 0.0)])  # x <= 0
    r2 = ConstrainedIdentityMap(n, X_l4l2)

    ## transition l2 -> l1
    X_l2l1 = HPolyhedron([HalfSpace([1.0; 0.0; z], 0.0),  # x <= 0
                          HalfSpace([-0.714286; -1.0; z], 0.0),  # 0.714286*x + y >= 0
                          HalfSpace([0.714286; 1.0; z], 0.0)])  # 0.714286*x + y <= 0
    r3 = ConstrainedIdentityMap(n, X_l2l1)

    ## transition l1 -> l3
    X_l1l3 = HPolyhedron([HalfSpace([-0.714286; -1.0; z], 0.0),  # 0.714286*x + y >= 0
                          HalfSpace([-1.0; 0.0; z], 0.0),  # x >= 0
                          HalfSpace([1.0; 0.0; z], 0.0)])  # x <= 0
    r4 = ConstrainedIdentityMap(n, X_l1l3)

    r = [r1, r2, r3, r4]

    ## switchings
    s = [HybridSystems.AutonomousSwitching()]

    return HybridSystem(a, m, r, s)
end

function model(X0; n0::Int=4, one_loop_iteration::Bool=false)
    H = filtered_oscillator_hybrid(n0, one_loop_iteration)
    return IVP(H, [(1, X0)])
end

end #jl

# ## References


import Plots
#import PyPlot

"""
Generates data for plotting f on [a, b]
"""
function plt_data(f, a, b, numpts=100)
    h = (b-a)/(numpts-1)
    y = zeros(1,numpts)
    x = collect(a:h:b)
    y = map(f, x)
    return (x, y)
end

"""
Displays graph of f on interval [a, b]
"""
function plt(f, a, b, numpts=100; label = "y", title="")
    h = (b-a)/(numpts-1)
    y = zeros(1,numpts)
    x = collect(a:h:b)
    y = map(f, x)
    display(Plots.plot(x, y, label = label, title = title))
    return (x, y)
end

function plt!(f, a, b, numpts=100; label = "y", title = "")
    h = (b-a)/(numpts-1)
    y = zeros(1,numpts)
    x = collect(a:h:b)
    y = map(f, x)
    display(Plots.plot!(x, y, label = label, title = title))
    return(x, y)
end

function plt!(point::Float64; label = "", title = "")
    display(Plots.plot!([point], seriestype = :vline))
end

function plot_multiple(a, b, c, r1=.001, r2=1, npts = 100)
    plt(a, r1, r2, npts, label = "pdf")
    plt!(b, r1, r2, npts, label = "cdf")
    plt!(c, r1, r2, npts, label = "dpdf")
end

"""
plot pdf, median, mode and credible interval
"""
function plot_distribution_single(pdf, median, mode, CI, z0_true, quadtype)
    # plot pdf
    b = ceil(2.5 * mode)
    z_grid = range(1e-5, stop = b, step = 0.01)
    p_grid = pdf.(z_grid);
    PyPlot.plot(z_grid, p_grid, label = "probability density function")
    # PyPlot.vlines(median, 0, pdf0(mean), label = "mean", colors = "k")
    PyPlot.vlines(median, 0, pdf(median), label = "median",  colors = "b")
    PyPlot.vlines(mode, 0, pdf(mode), label = "mode")
    PyPlot.vlines(z0_true, 0, pdf(z0_true), label = "true value",  colors = "r")
    # plot CI
    CI_x_range = range(CI[1], stop = CI[2], step = 0.01)
    CI_y_range = pdf.(CI_x_range)
    PyPlot.fill_between(CI_x_range, 0, CI_y_range, alpha = 0.3, label = "95% confidence interval")
    PyPlot.legend(fontsize=8)
    PyPlot.grid()
    PyPlot.title(quadtype, fontsize=10)
#     PyPlot.savefig("Figure/Prediction")
end


using Test

using Random
rng = MersenneTwister(1234)

using GR
GR.__init__()

mutable struct Example
  title::AbstractString
  code::Vector{Expr}
end

const _examples = Example[

Example("Simple line plot", [:(begin
        x = 0:π/100:2π
        y = sin.(x)
        plot(x, y)
    end)]),
Example("Scatter plot", [:(begin
        x = LinRange(0, 1, 51)
        y = x .- x.^2
        scatter(x, y)
    end)]),

Example("Colored scatter plot", [:(begin
        sz = LinRange(50, 300, length(x))
        c = LinRange(0, 255, length(x))
        scatter(x, y, sz, c)
    end)]),

Example("Stem plot", [:(begin
        stem(x, y)
    end)]),

Example("Histogram plot", [:(begin
        histogram(randn(rng, 10000))
    end)]),

Example("Multi-line plot", [:(begin
        plot(randn(rng, 50))
    end)]),

Example("Overlay plot", [:(begin
        oplot(randn(rng, 50, 3))
    end)]),

Example("3-d line plot", [:(begin
        x = LinRange(0, 30, 1000)
        y = cos.(x) .* x
        z = sin.(x) .* x
        plot3(x, y, z)
    end)]),

Example("Polar plot", [:(begin
        angles = LinRange(0, 2pi, 40)
        radii = LinRange(0, 2, 40)
        polar(angles, radii)
    end)]),

Example("3-d point plot", [:(begin
        x = 2 .* rand(rng, 100) .- 1
        y = 2 .* rand(rng, 100) .- 1
        z = 2 .* rand(rng, 100) .- 1
        scatter3(x, y, z)
    end)]),

Example("Colored 3-d point plot", [:(begin
        c = 999 .* rand(rng, 100) .+ 1
        scatter3(x, y, z, c)
    end)]),

Example("Hexbin plot", [:(begin
        x = randn(rng, 100000)
        y = randn(rng, 100000)
        hexbin(x, y)
    end)]),

Example("Contour plot", [:(begin
        x = 8 .* rand(rng, 100) .- 4
        y = 8 .* rand(rng, 100) .- 4
        z = sin.(x) .+ cos.(y)
        contour(x, y, z)
    end)]),

Example("Contour plot of matrix", [:(begin
        X = LinRange(-2, 2, 40)
        Y = LinRange(0, pi, 20)
        x, y = meshgrid(X, Y)
        z = sin.(x) .+ cos.(y)
        contour(x, y, z)
    end)]),

Example("Filled contour plot", [:(begin
        x = 8 .* rand(rng, 100) .- 4
        y = 8 .* rand(rng, 100) .- 4
        z = sin.(x) .+ cos.(y)
        contourf(x, y, z)
    end)]),

Example("Filled contour plot of matrix", [:(begin
        X = LinRange(-2, 2, 40)
        Y = LinRange(0, pi, 20)
        x, y = meshgrid(X, Y)
        z = sin.(x) .+ cos.(y)
        contourf(x, y, z)
    end)]),

Example("Filled contour plot on a triangular mesh", [:(begin
        x = 8 .* rand(rng, 100) .- 4
        y = 8 .* rand(rng, 100) .- 4
        z = sin.(x) + cos.(y)
        tricont(x, y, z)
    end)]),

Example("Surface plot", [:(begin
        x = 8 .* rand(rng, 100) .- 4
        y = 8 .* rand(rng, 100) .- 4
        z = sin.(x) .+ cos.(y)
        surface(x, y, z)
    end)]),

Example("Surface plot of matrix", [:(begin
        X = LinRange(-2, 2, 40)
        Y = LinRange(0, pi, 20)
        x, y = meshgrid(X, Y)
        z = sin.(x) .+ cos.(y)
        surface(x, y, z)
    end)]),

Example("Surface plot on a triangular mesh", [:(begin
        x = 8 .* rand(rng, 100) .- 4
        y = 8 .* rand(rng, 100) .- 4
        z = sin.(x) .+ cos.(y)
        trisurf(x, y, z)
    end)]),

Example("Wireframe plot", [:(begin
        x = 8 .* rand(rng, 100) .- 4
        y = 8 .* rand(rng, 100) .- 4
        z = sin.(x) .+ cos.(y)
        wireframe(x, y, z)
    end)]),

Example("Wireframe plot of matrix", [:(begin
        X = LinRange(-2, 2, 40)
        Y = LinRange(0, pi, 20)
        x, y = meshgrid(X, Y)
        z = sin.(x) .+ cos.(y)
        wireframe(x, y, z)
    end)]),

Example("Heatmap plot", [:(begin
        X = LinRange(-2, 2, 40)
        Y = LinRange(0, pi, 20)
        x, y = meshgrid(X, Y)
        z = sin.(x) .+ cos.(y)
        heatmap(z)
    end)]),

Example("Image plot", [:(begin
        imshow(z)
    end)]),

Example("Isosurface plot", [:(begin
        s = LinRange(-1, 1, 40)
        x, y, z = meshgrid(s, s, s)
        v = 1 .- (x .^ 2 .+ y .^ 2 .+ z .^ 2) .^ 0.5
        isosurface(v, isovalue=0.2)
    end)]),

Example("Shade points", [:(begin
        GR.GR3.terminate()

        volume(randn(rng, 50, 50, 50))

        N = 1_000_000
        x = randn(rng, N)
        y = randn(rng, N)
        shade(x, y)
    end)]),

Example("Discrete plot", [:(begin
        clearws()
        xd = -2 .+ 4 * rand(rng, 100)
        yd = -2 .+ 4 * rand(rng, 100)
        zd = [xd[i] * exp(-xd[i]^2 - yd[i]^2) for i = 1:100]

        setviewport(0.1, 0.95, 0.1, 0.95)
        setwindow(-2, 2, -2, 2)
        setspace(-0.5, 0.5, 0, 90)
        setcolormap(0)
        setlinecolorind(1)
        setmarkersize(1)
        setmarkertype(-1)
        setmarkercolorind(1)
        setcharheight(0.024)
        settextalign(2, 0)
        settextfontprec(3, 0)

        x, y, z = gridit(xd, yd, zd, 200, 200)
        h = -0.5:0.05:0.5
        surface(x, y, z, 5)
        contour(x, y, h, z, 0)
        polymarker(xd, yd)
        GR.axes(0.25, 0.25, -2, -2, 2, 2, 0.01)

        updatews()
        emergencyclosegks()
    end)])
]

function basic_tests()
    @test GR.tick(1.2,3.14) == 0.5

    for ex in _examples
        @info("Testing plot: $(ex.title)")

        GR.inline("pdf")
        file_path = ENV["GKS_FILEPATH"]

        map(eval, ex.code)

        @test isfile(file_path)
        rm(file_path)
    end
end

@time basic_tests()


# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .jl
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.5.0
#   kernelspec:
#     display_name: Julia 1.4.2
#     language: julia
#     name: julia-1.4
# ---

# # Seaborn

# +
using Seaborn
const sns = Seaborn
using Pandas
using PyCall: pyimport, @py_str

# Refer these issues
# https://stackoverflow.com/questions/27835619/urllib-and-ssl-certificate-verify-failed-error/42334357#42334357
# https://github.com/ytdl-org/youtube-dl/issues/4816#issuecomment-455840010
certifi = pyimport("certifi")
ENV["SSL_CERT_FILE"]=certifi.where();
# -

# ## Scatter plot
#
# - https://seaborn.pydata.org/examples/scatterplot_matrix.html
#
# ```python
# # Python
# import seaborn as sns
# sns.set(style="ticks")
#
# df = sns.load_dataset("iris")
# sns.pairplot(df, hue="species")
# ```

# ### Julia implementation

df = sns.load_dataset("iris")
head(df, 10)

pairplot(df, hue="species")

# # KDE
#
# https://seaborn.pydata.org/examples/multiple_joint_kde.html
#
# ```python
# import seaborn as sns
# import matplotlib.pyplot as plt
#
# sns.set(style="darkgrid")
# iris = sns.load_dataset("iris")
#
# # Subset the iris dataset by species
# setosa = iris.query("species == 'setosa'")
# virginica = iris.query("species == 'virginica'")
#
# # Set up the figure
# f, ax = plt.subplots(figsize=(8, 8))
# ax.set_aspect("equal")
#
# # Draw the two density plots
# ax = sns.kdeplot(setosa.sepal_width, setosa.sepal_length,
#                  cmap="Reds", shade=True, shade_lowest=False)
# ax = sns.kdeplot(virginica.sepal_width, virginica.sepal_length,
#                  cmap="Blues", shade=True, shade_lowest=False)
#
# # Add labels to the plot
# red = sns.color_palette("Reds")[-2]
# blue = sns.color_palette("Blues")[-2]
# ax.text(2.5, 8.2, "virginica", size=16, color=blue)
# ax.text(3.8, 4.5, "setosa", size=16, color=red)
# ```

# ### Julia implementation

sns.set(style="darkgrid")
iris = sns.load_dataset("iris")
setosa = query(iris, :(species=="setosa"))
virginica = query(iris, :(species=="virginica"))
fig, ax = sns.subplots(figsize=(8,8))
ax = sns.kdeplot(setosa.sepal_width, setosa.sepal_length,
            cmap="Reds", shade=true, shade_lowest=false)
ax = sns.kdeplot(virginica.sepal_width, virginica.sepal_length,
                 cmap="Blues", shade=true, shade_lowest=false)
# Add labels to the plot
red = sns.seaborn.color_palette("Reds")[end-2]
blue = sns.seaborn.color_palette("Blues")[end-2]
ax.text(2.5, 8.2, "virginica", size=16, color=blue)
ax.text(3.8, 4.5, "setosa", size=16, color=red)


using Test
using SparseArrays
using LinearAlgebraicRepresentation
Lar = LinearAlgebraicRepresentation

@testset "interface.jl file Tests" begin
	@testset "characteristicMatrix Tests" begin
		V,(VV,EV,FV,CV) = Lar.cuboid([1.,1.,1.], true);
		@test Matrix(Lar.characteristicMatrix(FV)) == [
		 1  1  1  1  0  0  0  0;
		 0  0  0  0  1  1  1  1;
		 1  1  0  0  1  1  0  0;
		 0  0  1  1  0  0  1  1;
		 1  0  1  0  1  0  1  0;
		 0  1  0  1  0  1  0  1]
		@test size(Lar.characteristicMatrix(CV))==(1,8)
		@test typeof(Lar.characteristicMatrix(CV))==SparseMatrixCSC{Int8,Int64}
		@test Matrix(Lar.characteristicMatrix(EV)) == [
		 1  1  0  0  0  0  0  0;
		 0  0  1  1  0  0  0  0;
		 0  0  0  0  1  1  0  0;
		 0  0  0  0  0  0  1  1;
		 1  0  1  0  0  0  0  0;
		 0  1  0  1  0  0  0  0;
		 0  0  0  0  1  0  1  0;
		 0  0  0  0  0  1  0  1;
		 1  0  0  0  1  0  0  0;
		 0  1  0  0  0  1  0  0;
		 0  0  1  0  0  0  1  0;
		 0  0  0  1  0  0  0  1]
		@test size(Lar.characteristicMatrix(EV))==(12,8)
		@test typeof(Lar.characteristicMatrix(EV))==SparseMatrixCSC{Int8,Int64}
	end

	@testset "signed_boundary_1 Tests" begin
		V,(VV,EV,FV,CV) = Lar.cuboid([1.,1.,1.], true);
		signed_boundary_1 = Lar.boundary_1( EV::Lar.Cells )

		@test EV==[[1, 2], [3, 4], [5, 6], [7, 8], [1, 3], [2, 4], [5, 7],
			[6, 8], [1, 5], [2, 6], [3, 7], [4, 8]]
		@test length(EV)==12
		@test typeof(EV)==Array{Array{Int64,1},1}
		@test size(signed_boundary_1)==(8,12)
		@test typeof(signed_boundary_1)==SparseMatrixCSC{Int8,Int64}
		@test nnz(signed_boundary_1)==24

		@test Matrix(Lar.boundary_1(EV::Lar.Cells))==[
		 -1   0   0   0  -1   0   0   0  -1   0   0   0;
		  1   0   0   0   0  -1   0   0   0  -1   0   0;
		  0  -1   0   0   1   0   0   0   0   0  -1   0;
		  0   1   0   0   0   1   0   0   0   0   0  -1;
		  0   0  -1   0   0   0  -1   0   1   0   0   0;
		  0   0   1   0   0   0   0  -1   0   1   0   0;
		  0   0   0  -1   0   0   1   0   0   0   1   0;
		  0   0   0   1   0   0   0   1   0   0   0   1]
	end

	@testset "unsigned_coboundary_1 Tests" begin
		V,(VV,EV,FV,CV) = Lar.cuboid([1.,1.,1.], true);
		unsigned_coboundary_1 = Lar.u_coboundary_1(FV,EV)

		@test size(unsigned_coboundary_1)==(6,12)
		@test nnz(unsigned_coboundary_1)==24
		@test typeof(unsigned_coboundary_1)==SparseMatrixCSC{Int8,Int64}

		@test Matrix(unsigned_coboundary_1)==[
		 1  1  0  0  1  1  0  0  0  0  0  0;
		 0  0  1  1  0  0  1  1  0  0  0  0;
		 1  0  1  0  0  0  0  0  1  1  0  0;
		 0  1  0  1  0  0  0  0  0  0  1  1;
		 0  0  0  0  1  0  1  0  1  0  1  0;
		 0  0  0  0  0  1  0  1  0  1  0  1]
	 end

	@testset "signed_coboundary_1 Tests" begin
		FV = [[1,2,3,4,5,17,16,12],
		[1,2,3,4,6,7,8,9,10,11,12,13,14,15],
		[4,5,9,11,12,13,14,15,16,17],
		[2,3,6,7], [8,9,10,11]]

		EV = [[1,2],[2,3],[3,4],[4,5],[1,12],[2,6],[3,7],[4,9],[5,17],[6,7],[8,9],
		[8,10],[9,11],[10,11],[11,15],[12,13],[12,16],[13,14],[14,15],[16,17]]

		V = [0   2   5   7  10   2   5   3   7  3  7  0  3  3  7  0  10;
			16  16  16  16  16  13  13  11  11  8  8  5  5  2  2  0   0]
		signed_coboundary_1 = Lar.coboundary_1( V,FV,EV,false,true );

		@test size(Matrix(signed_coboundary_1))==(5,20)
		@test typeof(signed_coboundary_1)==SparseArrays.SparseMatrixCSC{Int8,Int64}
		@test nnz(signed_coboundary_1)==40
		@test signed_coboundary_1[1,1]==-1
		@test signed_coboundary_1[3,20]==-1
		@test Matrix(signed_coboundary_1)==
		[-1  -1  -1  -1   1   0   0   0  -1   0   0   0   0   0   0   0   1   0   0   1;
		  1   0   1   0  -1   1  -1   1   0   1  -1   1   0   1   1  -1   0  -1  -1   0;
		  0   0   0   1   0   0   0  -1   1   0   0   0  -1   0  -1   1  -1   1   1  -1;
		  0   1   0   0   0  -1   1   0   0  -1   0   0   0   0   0   0   0   0   0   0;
		  0   0   0   0   0   0   0   0   0   0   1  -1   1  -1   0   0   0   0   0   0]
	end

	@testset "coboundary_1 with non-convex cells Tests" begin
		V = [5  0  6  9  5  3  0  5  8  10  10  10  0  0  3  9  8  6  5;
			 9  9  5  5  5  6  6  0  0   9   5   0  0  3  3  8  3  8  3]

		EV = [[1,2],[3,4],[3,5],[1,10],[16,18],[7,14],[6,15],[8,13],[13,14],[10,11],
		[11,12],[8,19],[9,17],[1,5],[17,19],[6,7],[14,15],[4,11],[3,18],[4,16],[8,9],[9,12],[2,7]]

		FV = [[1,2,7,6,15,14,13,8,19,17,9,12,11,4,3,5],
		[3,4,16,18],[6,7,14,15],[1,3,4,5,10,11,16,18],
		[8,9,17,19],[1,2,7,14,13,8,9,12,11,10]]

		copFE = Lar.coboundary_1( V::Lar.Points, FV::Lar.Cells, EV::Lar.Cells, false,true );

		@test Matrix(copFE) ==
		[-1  0  0  1  0 -1  0  1  1  1  1  0  0  0  0  0  0  0  0  0 -1 -1 -1;
		  1 -1  1  0  0  0  1 -1 -1  0 -1  1 -1 -1 -1 -1 -1 -1  0  0  0  1  1;
		  0  1  0  0  1  0  0  0  0  0  0  0  0  0  0  0  0  0 -1  1  0  0  0;
		  0  0  0  0  0  1 -1  0  0  0  0  0  0  0  0  1  1  0  0  0  0  0  0;
		  0  0 -1 -1 -1  0  0  0  0 -1  0  0  0  1  0  0  0  1  1 -1  0  0  0;
		  0  0  0  0  0  0  0  0  0  0  0 -1  1  0  1  0  0  0  0  0  1  0  0]
	end

	@testset "chaincomplex 2D Tests" begin
		W =
		 [0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0  2.0  2.0  2.0  2.0  3.0  3.0  3.0  3.0;
		  0.0  1.0  2.0  3.0  0.0  1.0  2.0  3.0  0.0  1.0  2.0  3.0  0.0  1.0  2.0  3.0]
		EW =
		[[1, 2],[2, 3],[3, 4],[5, 6],[6, 7],[7, 8],[9, 10],[10, 11],[11, 12],[13, 14],
		 [14, 15],[15, 16],[1, 5],[2, 6],[3, 7],[4, 8],[5, 9],[6, 10],[7, 11],[8, 12],
		 [9, 13],[10, 14],[11, 15],[12, 16]]
		V,bases,coboundaries = Lar.chaincomplex(W,EW)

		@test length(bases[1])==24	# edges
		@test typeof(bases[1])==Array{Array{Int64,1},1}	# edges

		@test length(bases[2])==9 # faces -- previously unknown !!
		@test typeof(bases[2])==Array{Array{Int64,1},1} # faces

		@test size(coboundaries[1])==(24, 16) # coboundary_1
		@test typeof(coboundaries[1])==SparseArrays.SparseMatrixCSC{Int8,Int64}
		# coboundary_1
		@test SparseArrays.nnz(coboundaries[1])==48 # coboundary_1

		@test size(coboundaries[2])==(9,24) # coboundary_2: oriented 2-cycles of faces
		@test typeof(coboundaries[2])==SparseArrays.SparseMatrixCSC{Int8,Int64}
		@test SparseArrays.nnz(coboundaries[2])==36
		# coboundary_2: oriented 2-cycles of faces
		@test Matrix(coboundaries[2]) ==
		[-1  0  0  1  0  0  0  0  0  0  0  0  1 -1  0  0  0  0  0  0  0  0  0  0;
		  0 -1  0  0  1  0  0  0  0  0  0  0  0  1 -1  0  0  0  0  0  0  0  0  0;
		  0  0 -1  0  0  1  0  0  0  0  0  0  0  0  1 -1  0  0  0  0  0  0  0  0;
		  0  0  0 -1  0  0  1  0  0  0  0  0  0  0  0  0  1 -1  0  0  0  0  0  0;
		  0  0  0  0 -1  0  0  1  0  0  0  0  0  0  0  0  0  1 -1  0  0  0  0  0;
		  0  0  0  0  0 -1  0  0  1  0  0  0  0  0  0  0  0  0  1 -1  0  0  0  0;
		  0  0  0  0  0  0  0 -1  0  0  1  0  0  0  0  0  0  0  0  0  0  1 -1  0;
		  0  0  0  0  0  0 -1  0  0  1  0  0  0  0  0  0  0  0  0  0  1 -1  0  0;
		  0  0  0  0  0  0  0  0 -1  0  0  1  0  0  0  0  0  0  0  0  0  0  1 -1]
	end

#	@testset "chaincomplex 3D Tests" begin
#		cube_1 = ([0 0 0 0 1 1 1 1; 0 0 1 1 0 0 1 1; 0 1 0 1 0 1 0 1],
#		[[1,2,3,4],[5,6,7,8],[1,2,5,6],[3,4,7,8],[1,3,5,7],[2,4,6,8]],
#		[[1,2],[3,4],[5,6],[7,8],[1,3],[2,4],[5,7],[6,8],[1,5],[2,6],[3,7],[4,8]] )
#
#		cube_2 = Lar.Struct(
#			[Lar.t(
#			0,0,0.5), Lar.r(0,0,pi/3), cube_1])
#		V,FV,EV = Lar.struct2lar(
#			Lar.Struct([ cube_1, cube_2 ]))
#
#		V,bases,coboundaries = Lar.chaincomplex(V,FV,EV)
#		(EV, FV, CV), (cscEV, cscFE, cscCF) = bases,coboundaries
#
#		@test typeof(FV)==Array{Array{Int64,1},1} # bases[2]
#		@test length(FV)==18 # bases[2]
#		@test FV==[[1,3,4,6],[2,3,5,6],[7,8,9,10],[1,2,3,7,8],[4,6,9,10,11,12],[5,6,11,12],
#		[1,4,7,9],[2,5,11,13],[2,8,10,11,13],[2,3,14,15,16],[11,12,13,17],
#		[11,12,13,18,19,20],[2,3,13,17],[2,13,14,18],[15,16,19,20],[3,6,12,15,19],
#		[3,6,12,17],[14,16,18,20]]
#		@test typeof(CV)==Array{Array{Int64,1},1} # bases[3]
#		@test length(CV)==3 # bases[3]
#		@test CV==[[2, 3, 5, 6, 11, 12, 13, 14, 15, 16, 18, 19, 20],
#		[2, 3, 5, 6, 11, 12, 13, 17],
#		[1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 17]]
#
#		@test typeof(cscEV)==SparseArrays.SparseMatrixCSC{Int8,Int64} # coboundaries[1]
#		@test size(cscEV)==(34,20) # coboundaries[1]
#		@test nnz(cscEV)==68 # coboundaries[1]
#
#		@test typeof(cscFE)==SparseArrays.SparseMatrixCSC{Int8,Int64} # coboundaries[2]
#		@test size(cscFE)==(18,34) # coboundaries[2]
#		@test nnz(cscFE)==80 # coboundaries[2]
#
#		@test typeof(cscCF)==SparseArrays.SparseMatrixCSC{Int8,Int64} # coboundaries[3]
#		@test size(cscCF)==(4,18) # coboundaries[3]
#		@test nnz(cscCF)==36 # coboundaries[3]
#	end
end


# ---
# title: 207. Course Schedule
# id: problem207
# author: Indigo
# date: 2021-01-20
# difficulty: Medium
# categories: Depth-first Search, Breadth-first Search, Graph, Topological Sort
# link: <https://leetcode.com/problems/course-schedule/description/>
# hidden: true
# ---
#
# There are a total of `numCourses` courses you have to take, labeled from `0`
# to `numCourses-1`.
#
# Some courses may have prerequisites, for example to take course 0 you have to
# first take course 1, which is expressed as a pair: `[0,1]`
#
# Given the total number of courses and a list of prerequisite **pairs** , is it
# possible for you to finish all courses?
#
#
#
# **Example 1:**
#
#
#
#     Input: numCourses = 2, prerequisites = [[1,0]]
#     Output: true
#     Explanation:  There are a total of 2 courses to take.
#                  To take course 1 you should have finished course 0. So it is possible.
#
#
# **Example 2:**
#
#
#
#     Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
#     Output: false
#     Explanation:  There are a total of 2 courses to take.
#                  To take course 1 you should have finished course 0, and to take course 0 you should
#                  also have finished course 1. So it is impossible.
#
#
#
#
# **Constraints:**
#
#   * The input prerequisites is a graph represented by **a list of edges** , not adjacency matrices. Read more about [how a graph is represented](https://www.khanacademy.org/computing/computer-science/algorithms/graph-representation/a/representing-graphs).
#   * You may assume that there are no duplicate edges in the input prerequisites.
#   * `1 <= numCourses <= 10^5`
#
#
## @lc code=start
using LeetCode
function can_finish_courses(num_courses, prerequisites::Vector{Vector{Int}})
    in_deg = fill(0, num_courses)
    point_to = [Int[] for _ in 1:num_courses] # fill(Int[], num_courses) is not proper!
    for edge in prerequisites
        in_deg[edge[1]] += 1
        push!(point_to[edge[2]], edge[1])
    end
    q = Queue{Int}()
    for idx in 1:length(in_deg)
        (in_deg[idx] == 0) && (enqueue!(q, idx))
    end
    while !isempty(q)
        frt = dequeue!(q)
        for pt in point_to[frt]
            (in_deg[pt] -= 1) == 0 && enqueue!(q, pt)
        end
    end
    return all(==(0), in_deg)
end
## @lc code=end


function _find_sparse(s,eta)
    # Relative "eta" based sparsity parameter
    s ./= sqrt(sum(s.^2))
    goodies = abs.(s) .- eta*maximum(abs.(s))
    s = goodies .* sign.(s)
    goodies = findall(goodies .> 0)

    return(s,goodies)

end

function sparsify(s,eta,method="relative")

    if method == "relative"
        (s,goodies) = _find_sparse(s,eta)
    elseif method == "absolute"
        (s,goodies) = _find_sparse_l(s,eta)
    else
        throw(DomainError("Methods accepted are `relative` or `absolute`"))
    end

    elimvars = setdiff(1:length(s),goodies)
    s[elimvars] .= 0

    return((s,goodies,elimvars))

end

function _find_sparse_l(s,lambda)

    s ./= sqrt(sum(s.^2))
    goodies = abs.(s) .- lambda/2
    s = goodies .* sign.(s)
    goodies = findall(goodies .> 0)

    return(s,goodies)

end

@doc """

    Autoscale a data matrix

    Either pass on functions of location and scale estimators,
    or strings (e.g. Statistics.median or "median") or "none"
    if no centring and/or scaling needed.

    Output is a NamedTuple containing the autoscaled data, and the location
    and scale estimates.

    """ ->
function autoscale(X::Matrix,locest,scalest)

    if typeof(locest)==String
        if locest in ["mean","median"]
            locest = getfield(Statistics,Symbol(locest))
        else
            # other location estimators to be included
            if locest != "none"
                @warn("Only supported strings for median:" * "\n" *
                    "'mean', 'median', 'none'" * "\n" *
                    "Alternatively pass a function")
            end
        end
        pre_estimated_loc = false
    else
        if typeof(locest) in [Array{Float64,1},Array{Int64,1},Array{Float32,1},Array{Int32,1},
                              Array{Float64,2},Array{Int64,2},Array{Float32,2},Array{Int32,2}]
            pre_estimated_loc = true
        else
            pre_estimated_loc = false
        end
    end

    if typeof(scalest)==String
        if scalest == "std"
            scalest = getfield(Statistics,Symbol(scalest))
        else
            # Further scale estimates to be included
            if scalest != "none"
                # Further scale estimates to be included
                @warn("Only supported strings for scale:" * "\n" *
                    "'std', 'none'" * "\n" *
                    "Alternatively pass a function")
            end
        end
        pre_estimated_sca = false
    else
        if typeof(scalest) in [Array{Float64,1},Array{Int64,1},Array{Float32,1},Array{Int32,1},
                            Array{Float64,2},Array{Int64,2},Array{Float32,2},Array{Int32,2}]
            pre_estimated_sca = true
        else
            pre_estimated_sca = false
        end
    end

    (n,p) = size(X)

    if pre_estimated_loc
        x_loc_ = locest
        if length(size(x_loc_))==1
            x_loc_ = x_loc_'
        end
    else
        if locest == "none"
            x_loc_ = zeros(1,p)
        else
            x_loc_ = mapslices(x -> locest(x),X,dims=1)
        end
    end

    if pre_estimated_sca
        x_sca_ = scalest
        if length(size(x_sca_))==1
            x_sca_ = x_sca_'
        end
    else
        if scalest == "none"
            x_sca_ = ones(1,p)
        else
            x_sca_ = mapslices(x -> scalest(x),X,dims=1)
        end
    end

    if (locest == "none" && scalest == "none")
        X_as_ = X
    else
        X_as_ = (X - ones(n,1)*x_loc_) ./ (ones(n,1)*x_sca_)
    end
    return(X_as_ = X_as_, col_loc_ = x_loc_, col_sca_ = x_sca_)

end

@doc """

    Autoscale a data frame

    Either pass on functions of location and scale estimators,
    or strings (e.g. Statistics.median or "median") or "none"
    if no centring and/or scaling needed.

    Output is a NamedTuple containing the autoscaled data, and the location
    and scale estimates.

    """ ->
function autoscale(X::DataFrame,locest,scalest)

    if typeof(locest)==String
        if locest in ["mean","median"]
            locest = getfield(Statistics,Symbol(locest))
        else
            # other location estimators can be included
            if locest != "none"
                @warn("Only supported strings for median:" * "\n" *
                    "'mean', 'median', 'none'" * "\n" *
                    "Alternatively pass a function")
            end
        end
        pre_estimated_loc = false
    else
        if typeof(locest) in [Array{Float64,1},Array{Int64,1},Array{Float32,1},Array{Int32,1},
                            Array{Float64,2},Array{Int64,2},Array{Float32,2},Array{Int32,2}]
            pre_estimated_loc = true
        else
            pre_estimated_loc = false
        end
    end

    if typeof(scalest)==String
        if scalest == "std"
            scalest = getfield(Statistics,Symbol(scalest))
        else
            if scalest != "none"
                # Further scale estimates to be included
                @warn("Only supported strings for scale:" * "\n" *
                    "'std', 'none'" * "\n" *
                    "Alternatively pass a function")
            end
        end
        pre_estimated_sca = false
    else
        if typeof(scalest) in [Array{Float64,1},Array{Int64,1},Array{Float32,1},Array{Int32,1},
                            Array{Float64,2},Array{Int64,2},Array{Float32,2},Array{Int32,2}]
            pre_estimated_sca = true
        else
            pre_estimated_sca = false
        end
    end

    (n,p) = size(X)
    x_names = names(X)

    if pre_estimated_loc
        x_loc_ = locest
        if length(size(x_loc_))>1
            x_loc_ = x_loc_'
        end
    else
        if locest == "none"
            x_loc_ = zeros(p,1)
        else
            x_loc_ = colwise(locest,X)
        end
    end

    if pre_estimated_sca
        x_sca_ = scalest
        if length(size(x_sca_))>1
            x_sca_ = x_sca_'
        end
    else
        if scalest == "none"
            x_sca_ = ones(p,1)
        else
            x_sca_ = colwise(scalest,X)
        end
    end
    if (locest == "none" && scalest == "none")
        X_as_ = X
    else
        X_as_ = (Matrix(X) - ones(n,1)*x_loc_') ./ (ones(n,1)*x_sca_')
        X_as_ = DataFrame(X_as_)
    end
    names!(X_as_,x_names)
    return(X_as_ = X_as_, col_loc_ = x_loc_, col_sca_ = x_sca_)

end

@doc """

    Autoscale a data vector

    Either pass on functions of location and scale estimators,
    or strings (e.g. Statistics.median or "median") or "none"
    if no centring and/or scaling needed.

    Output is a NamedTuple containing the autoscaled data, and the location
    and scale estimates.

    """ ->
function autoscale(X::Vector,locest,scalest)

    if typeof(locest)==String
        if locest in ["mean","median"]
            locest = getfield(Statistics,Symbol(locest))
        else
            # other location estimators to be included
            if locest != "none"
                @warn("Only supported strings for median:" * "\n" *
                    "'mean', 'median', 'none'" * "\n" *
                    "Alternatively pass a function")
            end
        end
        pre_estimated_loc = false
    else
        if typeof(locest) in [Array{Float64,1},Array{Int64,1},Array{Float32,1},Array{Int32,1},Float64,Int64,Float32,Int32]
            pre_estimated_loc = true
        else
            pre_estimated_loc = false
        end
    end

    if typeof(scalest)==String
        if scalest == "std"
            scalest = getfield(Statistics,Symbol(scalest))
        else
            if scalest != "none"
                # Further scale estimates to be included
                @warn("Only supported strings for scale:" * "\n" *
                    "'std', 'none'" * "\n" *
                    "Alternatively pass a function")
            end
        end
        pre_estimated_sca = false
    else
        if typeof(scalest) in [Array{Float64,1},Array{Int64,1},Array{Float32,1},Array{Int32,1},Float64,Int64,Float32,Int32]
            pre_estimated_sca = true
        else
            pre_estimated_sca = false
        end
    end

    if pre_estimated_loc
        x_loc_ = locest
    else
        if locest == "none"
            x_loc_ = 0
        else
            x_loc_ = locest(X)
        end
    end

    if pre_estimated_sca
        x_sca_ = scalest
    else
        if scalest == "none"
            x_sca_ = 1
        else
            x_sca_ = scalest(X)
        end
    end
    if (locest == "none" && scalest == "none")
        X_as_ = X
    else
        X_as_ = (X .- x_loc_) ./ x_sca_
    end
    return(X_as_ = X_as_, col_loc_ = x_loc_, col_sca_ = x_sca_)

end


using Roots
function Yfind(Q,P,H)

    Hamil(XX,YY,QQ,PP)=( (QQ-XX)^2+(PP-YY)^2 )*( (QQ+XX)^2+(PP+YY)^2 )/((PP^4+2*PP^2*(XX^2-1)+( 1+XX^2)^2 )*(QQ^4+2*QQ^2*(YY^2+1)+(YY^2-1)^2 ))
    Y_find(y)=Hamil(0,y,Q,P)-H
    try
        Y=find_zero(Y_find,.01,maxeval=100,maxfnevals=300,tol=1e-15)
    catch
        Y=zeros(0)
    end
end
# @everywhere function Yfind(Q,P,H)
#     Y_find1(y)=Hamil(0,y,Q,P)-H
#         try
#             Y=find_zeros(Y_find1,0,6, maxeval=100,maxfnevals=300,tol=1e-12)
#          catch
#             Y=zeros(0)
#         end
#     end


function _make_dense_block(
    num_layers,
    bn_size,
    growth_rate,
    dropout,
    stage_index,
)
    result = Sequential()
    for _ = 1:num_layers
        add(result, _make_dense_layer(growth_rate, bn_size, dropout))
    end
    return result
end

function _make_dense_layer(growth_rate, bn_size, dropout)
    new_features = Sequential()
    add(new_features, BatchNorm(relu))
    add(new_features, Conv2D(bn_size * growth_rate, (1, 1), use_bias = false))
    add(new_features, BatchNorm(relu))
    add(
        new_features,
        Conv2D(growth_rate, (3, 3), padding = 1, use_bias = false),
    )
    if dropout > 0
        add(new_features, Dropout(dropout))
    end
    out = Concurrent()
    add(out, identity)
    add(out, new_features)
    return out
end

function _make_transition(num_output_features)
    out = Sequential()
    add(out, BatchNorm(relu))
    add(out, Conv2D(num_output_features, (1, 1), use_bias = false))
    add(out, AvgPool2D())
    return out
end

mutable struct DenseNet <: Photon.Layer
    features
    output
end

function DenseNet(
    num_init_features,
    growth_rate,
    block_config;
    bn_size = 4,
    dropout = 0,
    classes = 1000,
)

    features = Sequential()
    add(
        features,
        Conv2D(
            num_init_features,
            (7, 7),
            strides = 2,
            padding = 3,
            use_bias = false,
        ),
    )
    add(features, BatchNorm(relu))
    add(features, MaxPool2D(3, strides = 2, padding = 1))
    # Add dense blocks
    num_features = num_init_features
    for (i, num_layers) in enumerate(block_config)
        add(
            features,
            _make_dense_block(num_layers, bn_size, growth_rate, dropout, i + 1),
        )
        num_features = num_features + num_layers * growth_rate
        if i != length(block_config) - 1
            num_features = floor(Int, num_features / 2)
            add(features, _make_transition(num_features))
        end
    end
    add(features, BatchNorm(relu))
    add(features, AvgPool2D(7))
    add(features, Flatten())

    output = Dense(classes)
    DenseNet(features, output)
end

function (dn::DenseNet)(x)
    x = dn.features(x)
    dn.output(x)
end

densenet_spec = Dict(
    121 => (64, 32, [6, 12, 24, 16]),
    161 => (96, 48, [6, 12, 36, 24]),
    169 => (64, 32, [6, 12, 32, 32]),
    201 => (64, 32, [6, 12, 48, 32]),
)

DenseNet121(; bn_size = 4, dropout = 0, classes = 1000) =
    DenseNet(densenet_spec[121]..., bn_size = bn_size, dropout=dropout, classes=classes)

DenseNet161(; bn_size = 4, dropout = 0, classes = 1000) =
    DenseNet(densenet_spec[161]..., bn_size = bn_size, dropout=dropout, classes=classes)

DenseNet169(; bn_size = 4, dropout = 0, classes = 1000) =
    DenseNet(densenet_spec[169]..., bn_size = bn_size, dropout=dropout, classes=classes)

DenseNet201(; bn_size = 4, dropout = 0, classes = 1000) =
    DenseNet(densenet_spec[201]..., bn_size = bn_size, dropout=dropout, classes=classes)


function get_number_in_each_fold(num_samples::Integer, num_folds::Integer)::Vector{Int}
    if num_samples < num_folds
        throw(ArgumentError("`num_samples` must be greater than or equal to `num_folds`"))
    end
    result::Vector{Int} = zeros(Int, num_folds)
    if num_samples % num_folds == 0
        result .= Int(num_samples//num_folds)
    else
        min_number_per_fold = round(Int, floor(num_samples/num_folds))
        remaining_number = num_samples - min_number_per_fold*num_folds
        result[1:remaining_number] .= min_number_per_fold + 1
        result[(remaining_number+1):end] .= min_number_per_fold
    end
    @assert sum(result) == num_samples
    return result
end

function get_indices_in_each_fold(all_indices::AbstractVector{<:Integer}, num_folds::Integer)::Vector{Vector{Int}}
    all_indices_deepcopy = deepcopy(all_indices)
    unique!(all_indices_deepcopy)
    sort!(all_indices_deepcopy)
    num_samples = length(all_indices_deepcopy)
    number_in_each_fold = get_number_in_each_fold(num_samples, num_folds)
    result::Vector{Vector{Int}} = Vector{Vector{Int}}(undef, num_folds)
    lower::Int = 1
    upper::Int = lower + number_in_each_fold[1] - 1
    result[1] = all_indices_deepcopy[lower:upper]
    @assert length(result[1]) == length(unique(result[1]))
    @assert length(result[1]) == number_in_each_fold[1]
    for fold = 2:num_folds
        lower = upper + 1
        upper = lower + number_in_each_fold[fold] - 1
        result[fold] = all_indices_deepcopy[lower:upper]
        # @assert length(result[fold]) == length(unique(result[fold]))
        # @assert length(result[fold]) == number_in_each_fold[fold]
    end
    @assert all(sort(unique(all_indices_deepcopy)) .== sort(unique(vcat(result...))))
    return result
end

function get_leavein_indices(all_indices::AbstractVector{<:Integer}, num_folds::Integer, fold_choice::Integer)::Vector{Int}
    indices_in_each_fold::Vector{Vector{Int}} = get_indices_in_each_fold(all_indices, num_folds)
    result::Vector{Int} = reduce(vcat, indices_in_each_fold[(1:num_folds).!=fold_choice])
    return result
end

function get_leaveout_indices(all_indices::AbstractVector{<:Integer}, num_folds::Integer, fold_choice::Integer)::Vector{Int}
    indices_in_each_fold::Vector{Vector{Int}} = get_indices_in_each_fold(all_indices, num_folds)
    result::Vector{Int} = indices_in_each_fold[fold_choice]
    return result
end

function vector_to_ranges(vector::T1)::Vector{UnitRange{Int}} where
        T1 <: AbstractVector{T2} where
        T2 <: Integer
    result::Vector{UnitRange{Int}} = Vector{UnitRange{Int}}(undef, 0)
    vector_deepcopy = deepcopy(vector)
    unique!(vector_deepcopy)
    sort!(vector_deepcopy)
    if !isempty(vector_deepcopy)
        lower::Int = vector_deepcopy[1]
        upper::Int = vector_deepcopy[1]
        for x in vector_deepcopy
            if (x) > (upper + 1)
                push!(result, lower:upper)
                lower = x
                upper = x
            else
                upper = x
            end
        end
        push!(result, lower:upper)
    end
    unique!(result)
    sort!(result)
    return result
end

function ranges_to_vector(ranges::T1)::Vector{Int} where
        T1 <: AbstractVector{T2} where
        T2 <: AbstractRange{T3} where
        T3 <: Integer
    ranges_deepcopy = deepcopy(ranges)
    unique!(ranges_deepcopy)
    sort!(ranges_deepcopy)
    result = Vector{Int}(undef, 0)
    for r in ranges_deepcopy
        append!(result, collect(r))
    end
    unique!(result)
    sort!(result)
    return result
end

function Base.:(==)(x::CrossValidation{T}, y::CrossValidation{T})::Bool where T
   result::Bool = (x.leaveout == y.leaveout) && (x.leavein == y.leavein)
   return result
end

function CrossValidation{T}(
        ;
        num_folds_per_level,
        all_indices,
        )::CrossValidation{T} where T
    all_indices_deepcopy = deepcopy(all_indices)
    unique!(all_indices_deepcopy)
    sort!(all_indices_deepcopy)
    if length(num_folds_per_level) < 1
        leaveout = Vector{Vector{T}}(undef, 1)
        leaveout[1] = all_indices_deepcopy
        leavein = Vector{CrossValidation{T}}(undef, 0)
    else
        num_folds_this_level = num_folds_per_level[1]
        remaining_num_folds_per_level = num_folds_per_level[2:end]
        leaveout = Vector{Vector{T}}(undef, num_folds_this_level)
        leavein = Vector{CrossValidation{T}}(undef, num_folds_this_level)
        for fold_choice = 1:num_folds_this_level
            leaveout[fold_choice] = get_leaveout_indices(all_indices, num_folds_this_level, fold_choice)
            leavein[fold_choice] = CrossValidation{T}(
                ;
                num_folds_per_level = remaining_num_folds_per_level,
                all_indices = get_leavein_indices(all_indices, num_folds_this_level, fold_choice)
                )
        end
    end
    result::CrossValidation{T} = CrossValidation{T}(leavein, leaveout)
    return result
end

function CrossValidation{T}(
        cv::CrossValidation{R},
        )::CrossValidation{T} where
        T where
        R <: AbstractUnitRange{S} where
        S <: Integer
    if isleaf(cv)
        leaveout = Vector{Vector{T}}(undef, 1)
        leaveout[1] = ranges_to_vector(get_leavein_indices(cv))
        leavein = Vector{CrossValidation{T}}(undef, 0)
    else
        num_folds = get_top_level_num_folds(cv)
        leaveout = Vector{Vector{T}}(undef, num_folds)
        leavein = Vector{CrossValidation{T}}(undef, num_folds)
        for fold_choice = 1:num_folds
            leaveout[fold_choice] = ranges_to_vector(get_leaveout_indices(cv, fold_choice))
            leavein[fold_choice] = CrossValidation{T}(get_leavein_cv(cv, fold_choice))
        end
    end
    result::CrossValidation{T} = CrossValidation{T}(leavein, leaveout)
    return result
end

function CrossValidation{T}(
        cv::CrossValidation{<:Integer},
        )::CrossValidation{T} where T
    if isleaf(cv)
        leaveout = Vector{Vector{T}}(undef, 1)
        leaveout[1] = vector_to_ranges(get_leavein_indices(cv))
        leavein = Vector{CrossValidation{T}}(undef, 0)
    else
        num_folds = get_top_level_num_folds(cv)
        leaveout = Vector{Vector{T}}(undef, num_folds)
        leavein = Vector{CrossValidation{T}}(undef, num_folds)
        for fold_choice = 1:num_folds
            leaveout[fold_choice] = vector_to_ranges(get_leaveout_indices(cv, fold_choice))
            leavein[fold_choice] = CrossValidation{T}(get_leavein_cv(cv, fold_choice))
        end
    end
    result::CrossValidation{T} = CrossValidation{T}(leavein, leaveout)
    return result
end

function isleaf(cv::CrossValidation{T})::Bool where T
    result::Bool = length(cv.leavein) < 1
    return result
end

function get_leavein_cv(cv::CrossValidation{T}, fold_choice)::CrossValidation{T} where T
    if isleaf(cv)
        throw(ArgumentError("`cv` is a leaf, so `get_leavein_cv(cv, fold_choice)` is not defined"))
    end
    result::CrossValidation{T} = cv.leavein[fold_choice]
    return result
end

function get_leavein_indices(cv::CrossValidation{T})::Vector{T} where T
    if !isleaf(cv)
        throw(ArgumentError("`cv` is not a leaf, so `get_leavein_indices(cv)` is not defined"))
    end
    result::Vector{T} = cv.leaveout[1]
    return result
end

function get_leaveout_indices(cv::CrossValidation{T}, fold_choice)::Vector{T} where T
    if isleaf(cv)
        throw(ArgumentError("`cv` is a leaf, so `get_leaveout_indices(cv, fold_choice)` is not defined"))
    end
    result::Vector{T} = cv.leaveout[fold_choice]
    return result
end

function get_all_indices(cv::CrossValidation{T})::Vector{T} where T
    result::Vector{T} = sort(
        unique(
            reduce(vcat, cv.leaveout)
            )
        )
    return result
end

function get_top_level_num_folds(cv::CrossValidation{T})::Int where T
    if isleaf(cv)
        result = 0
    else
        if length(cv.leaveout) != length(cv.leavein)
            throw(DimensionMismatch("length(cv.leaveout) != length(cv.leavein)"))
        end
        result = length(cv.leaveout)
    end
    return result
end


# Submodule to compute the coefficients of shearlet in serial in 2D and its reconstruction

# Type for the shearlet serial prepared filters

struct Serialpreparedfilters
		Xfreq
		Xrec
		preparedFilters
		dualFrameWeightsCurr
		shearletIdxs
end

##############################################################################
## Function that prepare the serial shearlet transform

"""
...
prepareserial2D(X,nScales,shearLevels = ceil.((1:nScales)/2), full = 0,
directionalFilter = filt_gen("directional_shearlet") , quadratureMirrorFilter = filt_gen("scaling_shearlet"))
...
"""
function prepareserial2D(X,nScales,shearLevels = ceil.((1:nScales)/2), full = 0,
directionalFilter = filt_gen("directional_shearlet") , quadratureMirrorFilter = filt_gen("scaling_shearlet"))
		shearLeveles = map(Int,shearLevels)
		# Compute the fft transform
		Xfreq = fftshift(fft(ifftshift(X)));
		# Compute prepared filters
		preparedFilters = preparefilters2D(size(X,1),size(X,2),nScales,shearLevels,directionalFilter,quadratureMirrorFilter)
		# Compute dual frames
		dualFrameWeightsCurr = zeros(size(X));
		Xrec = zeros(size(X));
		# Compute the indices
		shearletIdxs = getshearletidxs2D(shearLevels,full);

		# Assign values to the type
		return Serialpreparedfilters(Xfreq, Xrec, preparedFilters, dualFrameWeightsCurr, shearletIdxs)
end #prepareserial2D

# Type for the 2D serial sheralet decompostion
struct Sheardecserial2D
		coeffs
		shearlet
		dualFrameWeightsNew
		RMS
end

##############################################################################
## Function that decompose a signal in frequency domain in serial
"""
...
sheardecserial2D(Xfreq, ShearletIdx, preparedFilters, dualFrameWeightsCurr) decompose a signal in frequency domain in serial
...
"""
function sheardecserial2D(Xfreq, shearletIdx, preparedFilters, dualFrameWeightsCurr)
		# Compute the serial decomposition
		Shearlets2D = Shearlab.getshearlets2D(preparedFilters,shearletIdx);
		shearlet = Shearlets2D.shearlets;
		RMS = Shearlets2D.RMS;
		dualFrameWeights = Shearlets2D.dualFrameWeights;
		dualFrameWeightsNew = dualFrameWeightsCurr+dualFrameWeights;
		coeffs = fftshift(ifft(ifftshift(conj(shearlet).*Xfreq)));
		return Sheardecserial2D(coeffs, shearlet, dualFrameWeightsNew, RMS)
end # sheardecserial2D

##############################################################################
## Function that makes the serial recovery
"""
...
shearrecserial2D(coeffs, shearlet, Xrec) serial recovery of coefficients with certain shearlet
...
"""
function shearrecserial2D(coeffs, shearlet, Xrec)
		return Xrec+fftshift(fft(ifftshift(coeffs))).*shearlet;
end # shearrecserial2D

##############################################################################
## Function that finishes the serial recovery
"""
...
finishserial2D(Xrec,dualFrameWeightsCurr) serial recovery of coefficients with certain shearlet
...
"""
function finishserial2D(Xcurr, dualFrameWeights)
		return fftshift(ifft(ifftshift((1 ./dualFrameWeights).*Xcurr)))
end # shearrecserial2D


# sample from weights

struct AliasingScalarSampler
  domain::Vector{Float64}
  weights::StatsBase.ProbabilityWeights
  AliasingScalarSampler() = new()
  AliasingScalarSampler(x::Vector{<:Real}, p_x::Vector{<:Real}; SNRfloor::Float64=0.0) = begin
    pxf = Float64.(p_x)
    pxf .-= quantile(pxf, SNRfloor)
    pxf[pxf .< 0.0] = 0.0
    pxf ./= norm(pxf)
    wim = StatsBase.ProbabilityWeights(pxf)
    new(x, wim)
  end
end

function rand!(ass::AliasingScalarSampler, smpls::Array{Float64})
    StatsBase.alias_sample!(ass.domain, ass.weights, smpls)
    nothing
end

function rand(ass::AliasingScalarSampler, N::Int=1)
  smpls = zeros(N)
  rand!(ass,smpls)
  smpls
end


export AbstractModel, Heisenberg
export heisenberg_ij, hamiltonian, heisenberg_term, ground_state, energy, energy_exact, get_bonds, energy, heisenberg_2d, nspins

abstract type AbstractModel{D} end
abstract type AbstractHeisenberg{D} <: AbstractModel{D} end

nspins(model::AbstractModel) = prod(size(model))

"""
    energy_exact(tc, model::AbstractModel) -> Float64

Exact ground state energy.
"""
function energy_exact(tc, model::AbstractModel)
    nbit = nspins(tc)
    expect(hamiltonian(model), state_exact(tc)) |> real
end

"""
    ground_state(model::AbstractModel) -> DefaultRegister

Get the exact ground state of a model.
"""
function ground_state(model::AbstractModel)
    # get the ground state
    hami = hamiltonian(model)
    E, v = eigsolve(mat(hami), 1, :SR)
    E[1], ArrayReg(v[1])
end

"""
    get_bonds(model::AbstractHeisenberg) -> Vector

Get the weighted bonds of a Heisenberg model in the form Tuple(i,j,w_ij).
"""
function get_bonds end

include("Heisenberg.jl")
include("J1J2.jl")


using LinearMaps
using Magnus

using PyCall
pygui(:qt)
using PyPlot

include("plot_convergence.jl")

type ExpEulerExponentiator <: Magnus.Exponentiator
end

function (SE::ExpEulerExponentiator)(Ω::LinearMap{T}, τ::Number,
                                     v, w) where T<:Number
    a = Ω(v)
    w[:] = v + τ*a
end

function test_stiff(A,y0,y_exact,tmax,N,name,
                    Exps = [:scalar, :exp_euler],
                    Na = 0, Nb = 3)
    t = linspace(0,tmax,N+1)
    y_exact = y_exact(t)

    figure(name)
    clf()
    subplot(311)
    plot(t, y_exact, "r", linewidth = 2.0, label="Exact")

    Exp_objs = Dict(:scalar => (ScalarExponentiator(y0),"Exact exp"),
                    :lanczos => (LanczosExponentiator(2,y0),"Lanczos"),
                    :exp_euler => (ExpEulerExponentiator(),"Explicit Euler"))
    Exps = [Exp_objs[e] for e in Exps]

    for (Exp,label) in Exps
        prop = MidpointPropagator(t -> LinearMap(A), Exp)

        y = vec(integrate(y0, tmax, N, prop, save_intermediate = true))

        subplot(311)
        plot(t,y,label=label)

        plot_convergence(y_exact[end], y0, tmax, Na, Nb,
                         prop, [312,313], label)
    end
    subplot(311)
    margins(0,0.1)
    legend(framealpha=0.75)

    subplot(312)
    xscale("log")
    yscale("log")
    xlabel(L"N")
    ylabel("Error")
    margins(0,1)

    subplot(313)
    xscale("log")
    yscale("log")
    xlabel("Error")
    ylabel("Execution time [s]")
    margins(0,1)

    tight_layout()
end

test_stiff(-15., [1.0], t -> exp(-15.0t), 1.0, 10, "stiff")

if Pkg.installed("LambertW") != nothing
    using LambertW
    δ = 0.01
    a = 1/δ - 1
    y = t -> 1./(lambertw(a*exp(a-t)) + 1)
    # Technically not a linear operator, but, hey.
    # See http://mathworks.com/company/newsletters/articles/stiff-differential-equations.html
    fireball = LinearMap{Float64}((y,x,α,β) -> y[:] = α*(x.^2 - x.^3))
    test_stiff(fireball, [δ], y, 1./δ, 200, "Fireball, half interval",
               [:exp_euler], 0, 4)
    test_stiff(fireball, [δ], y, 2./δ, 200, "Fireball, full interval",
               [:exp_euler], 0, 4)
end


__precompile__(true)

module PlanarMaps

import Random,
       AsyPlots,
       DataStructures,
       SpecialFunctions

export NeighborCycle, CyclicRange, to,
       PlanarMap,
       Face,
       rotate, over,
       cw, ccw, ccwrange, insertccw, insertcw,
       isedge, add_edge!, delete_edge!,
       edges, face, faces, neighbors,
       interiorfaces, outerface,
       triangulation,
       RootedTree, root,
       isblue, isred, isgreen,
       vertices, interiorvertices,
       WoodedTriangulation, PartialWoodedTriangulation, shell!, shell,
       outerface, blueroot, redroot, greenroot,
       schnyderwood, allwoods,
       schnyder_blue, schnyder_red, schnyder_green, schnyder_coords,
       EmbeddedMap, draw, GraphPenSet,
       UWT

"""
    NeighborCycle(elements::Vector)

A circular permutation of distinct integers, representing
the (counterclockwise) neighbors of a vertex in a `PlanarMap`
"""
mutable struct NeighborCycle{T<:Integer}
    elements::Vector{T}
    lookup::Dict{T,T}
end

function remap!(N::NeighborCycle)
    N.lookup = Dict(map(reverse,enumerate(N.elements)));
end

function NeighborCycle(elements::Vector)
    NeighborCycle(elements,Dict(map(reverse,enumerate(elements))))
end

import Base.position
function position(N::NeighborCycle{T}, u::T) where T<:Integer
    N.lookup[u]
end

"""
    insertccw(N::NeighborCycle{T},u::T,v::T)

Return the `NeighborCycle` obtained by inserting the vertex
`v` counterclockwise of the vertex `u` in `N`
"""
function insertccw(N::NeighborCycle{T}, u::T, v::T) where T<:Integer
    nbs = copy(N.elements)
    k = position(N, u)
    insert!(nbs, k+1, v)
    NeighborCycle(nbs)
end

function insertcw(N::NeighborCycle{T}, u::T, v::T) where T<:Integer
    nbs = copy(N.elements)
    k = position(N, u)
    insert!(nbs, k, v)
    NeighborCycle(nbs)
end

"""
    PlanarMap(nbs::Vector{NeighborCycle})

A planar map with vertex set `1:length(nbs)` and
for which the neighbors of vertex `i` listed in
counterclockwise order are `nbs[i]`
"""
struct PlanarMap{T<:Integer}
    nbs::Vector{NeighborCycle{T}}
end

function PlanarMap(a::Vector{<:Vector{<:Integer}})
    return PlanarMap(map(NeighborCycle,a))
end

vertices(P::PlanarMap) = 1:length(P)

function neighbors(P::PlanarMap)
    P.nbs
end

function neighbors(P::PlanarMap,i::Integer)
    P.nbs[i]
end

"""
    edges(P::PlanarMap)

Return a vector containing both `(a,b)` and `(b,a)`
for every edge `(a,b)` in `P`
"""
function edges(P::PlanarMap)
    vcat([[(u,v) for v in N.elements]
                for (u,N) in enumerate(neighbors(P))]...)
end

struct Face{T<:Integer}
    elements::Vector{T}
end

Face(args...) = Face([args...])

function Base.show(io::IO,F::Face)
    print(io,"Face(")
    show(io,F.elements)
    print(io,")")
end

function isedge(P::PlanarMap{T},
                i::T,
                j::T) where T<:Integer
    j in neighbors(P,i)
end

"""
Find a face which has (i,j) as a diagonal
"""
function containing_face(P::PlanarMap{T},
                         i::T,
                         j::T) where T<:Integer
    for v in neighbors(P,i)
        F = Face(P,i,v)
        k = findfirst(isequal(j), F)
        if k ≠ 0
            return F
        end
    end
    error("No face found which has a diagonal ($i,$j)")
end

"""
    add_edge!(P::PlanarMap,i::Integer,j::Integer,F::Face)
    add_edge!(P::PlanarMap,i::Integer,j::Integer)

Add edge from `i` to `j` through the face `F`
in the planar map `P`. If `F` is not given, the
face discovered first in the `NeighborCycle` of `i`
will be used
"""
function add_edge!(P::PlanarMap{T},
                   i::T,
                   j::T) where T<:Integer
    if isedge(P,i,j)
        error("($i,$j) already an edge of $P")
    end
    for v in neighbors(P,i)
        F = face(P,i,v)
        k = findfirst(isequal(j), F)
        if !isa(k, Nothing)
            P.nbs[i] = insertccw(P.nbs[i], F[2], j)
            P.nbs[j] = insertccw(P.nbs[j], F[k+1], i)
            return
        end
    end
end

function add_edge!(P::PlanarMap{T},
                   i::T,
                   j::T,
                   F::Face) where T<:Integer
    if isedge(P,i,j)
        error("($i,$j) already an edge of $P")
    end
    k = findfirst(isequal(i), F)
    l = findfirst(isequal(j), F)
    if k == 0 || l == 0
        error("$i and $j are not both in $F")
    end
    P.nbs[i] = insertccw(P.nbs[i],F[k+1],j)
    P.nbs[j] = insertccw(P.nbs[j],F[l+1],i);
end

import Base: filter, filter!
Base.filter!(f::Function,N::NeighborCycle) = (Base.filter!(f,N.elements); remap!(N))
Base.filter(f::Function,N::NeighborCycle) = NeighborCycle(filter(f,N.elements))
Base.filter!(f::Function,F::Face) = Base.filter!(f,F.elements)
Base.filter(f::Function,F::Face) = Base.filter(f,F.elements)

function delete_edge!(P::PlanarMap{T},
                      i::T,
                      j::T) where T<:Integer
    if ~(i in neighbors(P,j) || j in neighbors(P,i))
        error("No edge to delete")
    end
    filter!(x->x≠j,P.nbs[i])
    filter!(x->x≠i,P.nbs[j])
    foreach(remap!,(P.nbs[i],P.nbs[j]))
end
"""
    OrientedPlanarMap(nbs::Vector{NeighborCycle{T}},
                      outgoing::Vector{Set{T}}) where T<:Integer

An oriented planar map with vertex set `1:length(nbs)` and
for which the neighbors of vertex `i` listed in counterclockwise order
are `nbs[i]` and for which the outneighbors are `outgoing[i]`
"""
struct OrientedPlanarMap{T<:Integer}
    nbs::Vector{NeighborCycle{T}}
    outneighbors::Vector{Set{T}}
end

function neighbors(O::OrientedPlanarMap)
    return O.nbs
end

function outneighbors(O::OrientedPlanarMap)
    return O.outneighbors
end

function PlanarMap(O::OrientedPlanarMap)
    return PlanarMap(neighbors(O))
end

#-----DISPLAY-----------------------------------
import Base.show
function show(io::IO,N::NeighborCycle)
    print(io,"NeighborCycle(")
    print(io,string(N.elements))
    print(io,")")
end

function show(io::IO,M::Union{PlanarMap,OrientedPlanarMap})
    s = isa(M,PlanarMap) ? "" : "Oriented"
    print(io,"$(s)PlanarMap(")
    print(io,string(length(M)))
    print(io,")")
end
#-------------------------------------------------

import Base.length
length(N::NeighborCycle) = length(N.elements)
length(P::PlanarMap) = length(P.nbs)
length(O::OrientedPlanarMap) = length(O.nbs)

#--- CYCLE FUNCTIONS -----------------------------
"""
    over(N::NeighborCycle,u::T,k::T) where T<: Integer

Return the element which is `k` positions
to the right of `u` in the neighbor cycle `N`
"""
function over(N::NeighborCycle,u::T,k::T) where T<: Integer
    return N.elements[mod1(position(N,u)+k,length(N))]
end

"""
    rotate(N::NeighborCycle,k::Integer)

Return a vector containing the elements of `N`
in order beginning with position k
"""
function rotate(N::NeighborCycle{T},u::T) where T
    k = position(N,u)
    return [N.elements[k:end]; N.elements[1:k-1]]
end

prv(N::NeighborCycle,k::Integer) = over(N,k,-1)
nxt(N::NeighborCycle,k::Integer) = over(N,k,1)

import Base.==
function ==(N::NeighborCycle,M::NeighborCycle)
    if length(N) == 0 || length(M) == 0
        return length(N) == 0 && length(M) == 0
    end
    a = first(N.elements)
    N.elements == rotate(M,a)
end

function pairs(N::NeighborCycle)
    return zip(N.elements,rotate(N,2))
end

cw(P::PlanarMap{T},u::T,v::T) where T<:Integer =
                        over(neighbors(P,u),v,-1)
ccw(P::PlanarMap{T},u::T,v::T) where T<:Integer =
                        over(neighbors(P,u),v,1)

struct CyclicRange
    start::Int64
    done::Int64
end

to(a,b) = CyclicRange(a,b)

length(F::Face) = length(F.elements)
import Base: getindex, iterate, lastindex
Base.getindex(F::Face,i::Integer) = Base.getindex(F.elements,mod1(i,length(F)))
Base.getindex(F::Face,r::UnitRange) = Base.getindex(F.elements,r)
function Base.getindex(F::Face,C::CyclicRange)
    a,b = mod1.((C.start,C.done),length(F))
    if a ≤ b
        F[a:b]
    else
        vcat(F[a:end], F[1:b])
    end
end
function Base.getindex(N::NeighborCycle,C::CyclicRange)
    a,b = map(mod1,(C.start, C.done),(length(N),length(N)))
    if a ≤ b
        N[a:b]
    else
        vcat(N[a:end], N[1:b])
    end
end
Base.getindex(N::PlanarMaps.NeighborCycle{Int64},
              r::UnitRange{Int64}) = Base.getindex(N.elements,r)
Base.iterate(F::Face) = Base.iterate(F.elements)
Base.iterate(F::Face,i::Integer) = Base.iterate(F.elements,i)
Base.lastindex(F::Face) = Base.lastindex(F.elements)
Base.getindex(N::NeighborCycle,i::Integer) = Base.getindex(N.elements,mod1(i,length(N)))
Base.iterate(N::NeighborCycle) = Base.iterate(N.elements)
Base.iterate(N::NeighborCycle,i::Integer) = Base.iterate(N.elements, i)
Base.lastindex(N::NeighborCycle) = Base.lastindex(N.elements)

"""
    face(P::PlanarMap,u::Int64,v::Int64,rotate=cw)

Return the face of the planar map P which includes the
edge (u,v) and finds each new edge by rotating in the
specified direction about the current vertex. Note
that this traces out the face in the direction *opposite*
to `rotate`
"""
function face(P::PlanarMap,u::Int64,v::Int64;rotate=cw)
    f = [u,v]
    while length(f) == 2 || (f[end-1],f[end]) ≠ (u,v)
        push!(f,rotate(P,f[end],f[end-1]))
    end
    return Face(f[1:end-2])
end

function edges(F::Face)
    ((F[i],F[mod1(i+1,length(F))]) for i=1:length(F))
end

function rotate(F::Face,k::Integer)
    return circshift(F.elements,-(k-1))
end

import Base.keys
keys(F::Face) = keys(F.elements)

function ==(F::Face,G::Face)
    a = first(F)
    places = findall(isequal(a), G)
    any(rotate(G,p) == F.elements for p in places)
end

struct FaceList
    interiorfaces::Vector{Face}
    outerface::Face
end

"""
    faces(P::PlanarMap)

Return a vector of faces of `P`
"""
function faces(RNG::Random.AbstractRNG,
               P::PlanarMap;
               outeredge=(1,neighbors(P,1)[1]),
               rotate=cw)
    rem_edges = Set(edges(P))
    int_faces = Face[]
    outerface = face(P,pop!(rem_edges,outeredge)...;rotate=rotate)
    for e in edges(outerface)
        delete!(rem_edges,e)
    end
    while length(rem_edges) > 0
        e = rand(RNG,rem_edges)
        f = face(P,pop!(rem_edges,e)...;rotate=rotate)
        for e in edges(f)
	        delete!(rem_edges,e)
        end
        push!(int_faces,f)
    end
    return FaceList(int_faces,outerface)
end

faces(P::PlanarMap;kwargs...) = faces(Random.MersenneTwister(0),P;kwargs...)

function allfaces(RNG::Random.AbstractRNG,
                  P::PlanarMap;
                  outeredge=(1,neighbors(P,1)[1]),
                  rotate=cw)
    L = faces(RNG,P;outeredge=outeredge,rotate=rotate)
    return vcat(interiorfaces(L),[outerface(L)])
end

allfaces(P::PlanarMap;kwargs...) =
            allfaces(Random.MersenneTwister(0),P;kwargs...)

function interiorfaces(L::FaceList)
    L.interiorfaces
end

function outerface(L::FaceList)
    L.outerface
end

function find_drawable_edge(P::PlanarMap,F::Face)
    if length(F) == 3
        error("No drawable edge; $F is a triangle")
    end
    for (i,v) in enumerate(F)
        j = findfirst(x-> x≠v && x∉neighbors(P,v), F)
        !isa(j,Nothing) && return (i,j)
    end
    error("No drawable edge found")
end

function poprand!(G::Random.AbstractRNG,collection)
    splice!(collection,rand(G,1:length(collection)))
end

function facewithcorners(P::PlanarMap{T},blue::T,green::T,red::T) where T<:Integer
    for v in neighbors(P,blue)
        F = face(P,blue,v)
        k = findfirst(isequal(green), F.elements)
        # look for a red *after* the green already found:
        l = k+findfirst(isequal(red), F.elements[k+1:end])
        if 1 < k < l
            return F
        end
    end
    error("No face containing $blue, $green, $red")
end

poprand!(collection) = poprand(Random.GLOBAL_RNG,collection)

"""
    triangulation(P::PlanarMap)

Find a simple plane triangulation obtained from P
by adding edges
"""
function triangulation(RNG::Random.AbstractRNG,
                       P::PlanarMap;
                       outeredge=(1,neighbors(P,1)[1]),
                       corners=nothing)
    T = deepcopy(P)
    if corners ≠ nothing
        F = PlanarMaps.facewithcorners(T,corners...)
        for (a,b) in edges(Face(corners...))
            if !isedge(T,a,b)
                add_edge!(T,a,b,F)
            end
        end
        outeredge=corners[1:2]
    end
    facelist = [F for F in allfaces(T;outeredge=outeredge) if length(F) > 3]
    while !isempty(facelist)
        F = poprand!(RNG,facelist)
        (i,j) = find_drawable_edge(T,F)
        T.nbs[F[i]] = insertccw(T.nbs[F[i]],F[i+1],F[j])
        T.nbs[F[j]] = insertccw(T.nbs[F[j]],F[j+1],F[i])
        newfaces = [f for f in map(Face,(F[to(i,j)],F[to(j,i)])) if length(f) > 3]
        append!(facelist,newfaces)
    end
    return T
end

function triangulation(P::PlanarMap;kwargs...)
    triangulation(Random.MersenneTwister(0),P;kwargs...)
end

import Base.in
Base.in(a,N::NeighborCycle) = Base.in(a,keys(N.lookup))

pairs(F::Face) = zip(F.elements,circshift(F.elements,-1))

include("woodedtriangulations.jl")
include("uniformwoodedtriangulations.jl")
include("graphing.jl")

end # module


struct SBM
    w::Matrix{Float64}      # Matrix of probabilities
    dist::String            # Distribution (either "poisson" or "bernoulli")
    n_communities::Int      # Number of communities/clusters
    variant::String

    # Constructor function for struct SBM
    function SBM(w::Matrix{Float64}, dist::String, variant::String)
        # Check if w is a square matrix
        if size(w,1) != size(w,2)
            throw(ArgumentError("Matrix w must be a square matrix."))
        end
        # Check if elements in w are strictly positive
        if any(w .< 0)
            throw(DomainError("All elements in matrix of probabilities w must be strictly positive."))
        end
        # Check if matrix is symmetric
        if ~(w ≈ w')
            throw(ArgumentError("Matrix w must be symmetric."))
        end
        q = size(w,1)

        # Check input argument 'distribution'
        if ~(dist in ["poisson","bernoulli"])
            throw(ArgumentError(string("Invalid distribution: ", dist)))
        end
        if dist == "bernoulli"
            # Check if w is a matrix of 0-1 values
            if any(w .> 1)
                throw(DomainError("When using a bernoulli distribution all elements in matrix of probabilities w must be between 0 and 1."))
            end
        end
        if ~(variant in ["standard","degree-corrected"])
            throw(ArgumentError("Invalid SBM variant: $variant"))
        end
        return new(w, dist, q, variant)
    end

    function SBM(w::Matrix{Float64}, dist::String)
        variant = "standard"
        return SBM(w, dist, variant)
    end
end

function generate(sbm::SBM, n_per_community::Vector{Int}, seed::Union{Nothing,Int}=nothing)::Dataset
    """
    Generates a graph from the Stochastic Block Model.

    Parameters
    ----------
    sbm : SBM
        An SBM structure.
    n_per_community : Vector{Int}
        Number of nodes in each community.
    seed : Union{Nothing,Int}
        Seed for the random number generator.

    Returns
    -------
    adj_matrix : Matrix{Int}
        Adjacency matrix of the generated graph
    """
    if sbm.variant != "standard"
        throw("not implemented")
    end

    # Check input argument 'n_per_community'
    if any(n_per_community .<= 0)
        throw(DomainError(string("Invalid value in argument n_per_community: ",
                                 "the number of nodes in each community must be strictly positive.")))
    end
    # Check if n_per_community matches the dimensions of w
    if ~(length(n_per_community) == sbm.n_communities)
        throw(ArgumentError("Number of communities in n_per_community must match the number of communities in SBM"))
    end

    # Set the random seed if one was provided
    if seed != nothing
        Random.seed!(seed)
    end
    # Define number of vertices, initialize adjacency matrix and community labels
    n_vertices = sum(n_per_community);
    adj_matrix = zeros(Int, n_vertices,n_vertices);
    community_labels = convert(Array{Int}, vcat([i*ones(ni) for (i,ni) in enumerate(n_per_community)]...));
    # Construct adjacency matrix
    for i=1:n_vertices
        for j=1:i
            proba = sbm.w[community_labels[i], community_labels[j]];
            if sbm.dist == "poisson"
                if i == j
                    proba = proba / 2 # "The factor of half is included solely because it makes the algebra easier"
                end
                pois = Distributions.Poisson(proba);
                adj_matrix[i,j] = rand(pois);
            elseif sbm.dist == "bernoulli"
                if i == j
                    adj_matrix[i,j] = 0;
                else
                    bernoulli = Bernoulli(proba);
                    adj_matrix[i,j] = rand(bernoulli);
                end
            end
        end
    end
    # "We have adopted the common convention that a self-edge is represented by A[i,j] = 2
    # (and not 1 as one might at first imagine)"
    adj_matrix += transpose(adj_matrix) #- Diagonal(adj_matrix)

    # Instantiate a dataset
    n_communities = length(n_per_community)
    dataset = Dataset(adj_matrix, n_communities)
    return dataset
end

function generate(probability_matrix::Matrix{Float64}, n_per_community::Vector{Int};
                            distribution::String="poisson", seed::Union{Nothing, Int}=nothing)::Dataset
    """
    Generates a graph from the Stochastic Block Model.

    Parameters
    ----------
    probability_matrix : Matrix{Float64}
        Matrix of expected number of edges under Poisson distribution.
    n_per_community : Vector{Int}
        Number of nodes in each community.
    distribution : String
        Distribution to be used for generating edges (either "poisson" or "bernoulli").
    seed : Union{Nothing,Int}
        Seed for the random number generator.

    Returns
    -------
    dataset : Dataset
        Dataset representing a graph.
    """
    sbm = SBM(probability_matrix, distribution)
    return generate(sbm, n_per_community, seed)
end


#=
Copyright (c) 2015, Intel Corporation
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
=#

using DocOpt
using Images

function blur(img, iterations)
    w, h = size(img)
    for i = 1:iterations
      img[3:w-2,3:h-2] =
           img[3-2:w-4,3-2:h-4] * 0.003  + img[3-1:w-3,3-2:h-4] * 0.0133 + img[3:w-2,3-2:h-4] * 0.0219 + img[3+1:w-1,3-2:h-4] * 0.0133 + img[3+2:w,3-2:h-4] * 0.0030 +
           img[3-2:w-4,3-1:h-3] * 0.0133 + img[3-1:w-3,3-1:h-3] * 0.0596 + img[3:w-2,3-1:h-3] * 0.0983 + img[3+1:w-1,3-1:h-3] * 0.0596 + img[3+2:w,3-1:h-3] * 0.0133 +
           img[3-2:w-4,3+0:h-2] * 0.0219 + img[3-1:w-3,3+0:h-2] * 0.0983 + img[3:w-2,3+0:h-2] * 0.1621 + img[3+1:w-1,3+0:h-2] * 0.0983 + img[3+2:w,3+0:h-2] * 0.0219 +
           img[3-2:w-4,3+1:h-1] * 0.0133 + img[3-1:w-3,3+1:h-1] * 0.0596 + img[3:w-2,3+1:h-1] * 0.0983 + img[3+1:w-1,3+1:h-1] * 0.0596 + img[3+2:w,3+1:h-1] * 0.0133 +
           img[3-2:w-4,3+2:h-0] * 0.003  + img[3-1:w-3,3+2:h-0] * 0.0133 + img[3:w-2,3+2:h-0] * 0.0219 + img[3+1:w-1,3+2:h-0] * 0.0133 + img[3+2:w,3+2:h-0] * 0.0030
    end
    return img
end

function main()
    doc = """gaussian-blur.jl

Gaussian blur image processing.

Usage:
  gaussian-blur.jl -h | --help
  gaussian-blur.jl [--img-file=<img-file>] [--iterations=<iterations>]

Options:
  -h --help                  Show this screen.
  --img-file=<img-file>      Specify a path to an input image file [default: ../../example.jpg].
  --iterations=<iterations>  Specify a number of iterations [default: 100].
"""
    arguments = docopt(doc)

    dir = dirname(@__FILE__)
    img_file = joinpath(dir, arguments["--img-file"])
    iterations = parse(Int, arguments["--iterations"])

    (fname, ext) = splitext(img_file)
    out_file = string(fname, "-blur", ".jpg")

    println("input file = ", img_file)
    println("iterations = ", iterations)
    println("output file = ", out_file)

    function gaussian_blur(input_fname, output_fname, iterations)
        local img = convert(Array{Float32, 2}, load(input_fname))
        tic()
        blur(img, 0)
        println("SELFPRIMED ", toq())

        tic()
        img = blur(img, iterations)
        println("SELFTIMED ", toq())
        save(output_fname, ufixed8sc(convert(Image, img)))
    end

    gaussian_blur(img_file, out_file, iterations)

end

main()


using Decimals
using Compat.Test

@testset "Normalization" begin

@test Decimal(1, 151100, -4) == Decimal(1, 1511, -2)
@test Decimal(0, 100100, -5) == Decimal(0, 1001, -3)
@test normalize(Decimal(1, 151100, -4)) == Decimal(1, 1511, -2)
@test normalize(Decimal(0, 100100, -5)) == Decimal(0, 1001, -3)
@test parse(Decimal, "3.0") == Decimal(0, 3, 0)
@test parse(Decimal, "3.0") == Decimal(0, 30, -1)
@test parse(Decimal, "3.1400") == Decimal(0, 314, -2)
@test parse(Decimal, "1234") == Decimal(0, 1234, 0)

end