# common functors

abstract Functor{N}  # N is the number of arguments

typealias UnaryFunctor Functor{1}
typealias BinaryFunctor Functor{2}
typealias TernaryFunctor Functor{3}

#######################################
#
#  Macros to define functors
#
#######################################

macro functor1(F, T, f)
    quote
        global evaluate
        type $F <: Functor{1} end
        evaluate(::($F), x::($T)) = ($f)(x)
    end
end

macro functor2(F, T, f)
    quote
        global evaluate
        type $F <: Functor{2} end
        evaluate(::($F), x::($T), y::($T)) = ($f)(x, y)
    end
end

macro functor3(F, T, f)
    quote
        global evaluate
        type $F <: Functor{3} end
        evaluate(::($F), x::($T), y::($T), z::($T)) = ($f)(x, y, z)
    end
end

#######################################
#
#  Functors for operators
#
#######################################

export
    Negate, Add, Subtract, Multiply, Divide, Pow, Modulo,
    Greater, GreaterEqual, Less, LessEqual, Equal, NotEqual,
    Not, And, Or,
    BitwiseNot, BitwiseAnd, BitwiseOr, BitwiseXor

# arithmetic operators

type Negate <: Functor{1} end
evaluate(::Negate, x::Number) = -x

type Add <: Functor{2} end
evaluate(::Add, x::Number, y::Number) = x + y

type Subtract <: Functor{2} end
evaluate(::Subtract, x::Number, y::Number) = x - y

type Multiply <: Functor{2} end
evaluate(::Multiply, x::Number, y::Number) = x * y

type Divide <: Functor{2} end
evaluate(::Divide, x::Number, y::Number) = x / y

type Pow <: Functor{2} end
evaluate(::Pow, x::Number, y::Number) = x ^ y

type Modulo <: Functor{2} end
evaluate(::Modulo, x::Real, y::Real) = x % y

# comparison operators

type Greater <: Functor{2} end
evaluate(::Greater, x::Real, y::Real) = x > y

type GreaterEqual <: Functor{2} end
evaluate(::GreaterEqual, x::Real, y::Real) = x >= y

type Less <: Functor{2} end
evaluate(::Less, x::Real, y::Real) = x < y

type LessEqual <: Functor{2} end
evaluate(::LessEqual, x::Real, y::Real) = x <= y

type Equal <: Functor{2} end
evaluate(::Equal, x::Number, y::Number) = x == y

type NotEqual <: Functor{2} end
evaluate(::NotEqual, x::Number, y::Number) = x != y

# logical operators

type Not <: Functor{1} end
evaluate(::Not, x::Bool) = !x

type And <: Functor{2} end
evaluate(::And, x::Bool, y::Bool) = (x && y)

type Or <: Functor{2} end
evaluate(::Or, x::Bool, y::Bool) = (x || y)

type BitwiseNot <: Functor{1} end
evaluate(::BitwiseNot, x::Real) = (~x)

type BitwiseAnd <: Functor{2} end
evaluate(::BitwiseAnd, x::Real, y::Real) = (x & y)

type BitwiseOr <: Functor{2} end
evaluate(::BitwiseOr, x::Real, y::Real) = (x | y)

type BitwiseXor <: Functor{2} end
evaluate(::BitwiseXor, x::Real, y::Real) = (x $ y)

#######################################
#
#  Functors for elementary math
#
#######################################

export
    MaxFun, MinFun,
    AbsFun, Abs2Fun, SqrFun, RcpFun, SqrtFun, RsqrtFun,
    CbrtFun, RcbrtFun, HypotFun, FixPow, FixAbsPow,
    IsfiniteFun, IsinfFun, IsnanFun, IsequalFun,
    FloorFun, CeilFun, RoundFun, TruncFun,
    IfloorFun, IceilFun, IroundFun, ItruncFun,
    ModFun, FldFun, RemFun, DivFun,
    ExpFun, Exp2Fun, Exp10Fun, Expm1Fun,
    LogFun, Log2Fun, Log10Fun, Log1pFun, LdexpFun,
    SinFun, CosFun, TanFun, CotFun, SecFun, CscFun,
    SindFun, CosdFun, TandFun, CotdFun, SecdFun, CscdFun,
    SinhFun, CoshFun, TanhFun, CothFun, SechFun, CschFun,
    AsinFun, AcosFun, AtanFun, AcotFun, AsecFun, AcscFun,
    AsindFun, AcosdFun, AtandFun, AcotdFun, AsecdFun, AcscdFun,
    AsinhFun, AcoshFun, AtanhFun, AcothFun, AsechFun, AcschFun,
    Atan2Fun

# basic functors

@functor2 MaxFun Real max
@functor2 MinFun Real min

type _Max <: Functor{2} end
evaluate(::_Max, x::Real, y::Real) = ifelse(y > x, y, x)

type NonnegMax <: Functor{2} end
evaluate(::NonnegMax, x::Real, y::Real) = ifelse(y > x, y, x)

type _Min <: Functor{2} end
evaluate(::_Min, x::Real, y::Real) = ifelse(y < x, y, x)

# absolute value & power

@functor1 AbsFun   Number abs
@functor1 Abs2Fun  Number abs2
@functor1 SqrFun   Number sqr
@functor1 RcpFun   Number rcp
@functor1 SqrtFun  Number sqrt
@functor1 RsqrtFun Number rsqrt
@functor1 CbrtFun  Real   cbrt
@functor1 RcbrtFun Real   rcbrt
@functor2 HypotFun Real   hypot

immutable FixPow{T<:Real} <: Functor{1}
    p::T
end
evaluate(f::FixPow, x::Real) = (x ^ f.p)

immutable FixAbsPow{T<:Real} <: Functor{1}
    p::T
end
evaluate(f::FixAbsPow, x::Real) = (abs(x) ^ f.p)

# number classifying functors

@functor1 IsfiniteFun Real   isfinite
@functor1 IsnanFun    Real   isnan
@functor1 IsinfFun    Real   isinf
@functor2 IsequalFun  Number isequal

# rounding functors

@functor1 FloorFun Real floor
@functor1 CeilFun  Real ceil
@functor1 RoundFun Real round
@functor1 TruncFun Real trunc

@functor1 IfloorFun Real ifloor
@functor1 IceilFun  Real iceil
@functor1 IroundFun Real iround
@functor1 ItruncFun Real itrunc

# division & modulus functors

@functor2 ModFun Real mod
@functor2 FldFun Real fld
@functor2 RemFun Real rem
@functor2 DivFun Real div

# exponentiation & logarithm

@functor1 ExpFun   Number exp
@functor1 Exp2Fun  Number exp2
@functor1 Exp10Fun Number exp10

@functor1 LogFun   Number log
@functor1 Log2Fun  Number log2
@functor1 Log10Fun Number log10

@functor1 Expm1Fun Real expm1
@functor1 Log1pFun Real log1p

type LdexpFun <: Functor{2} end
evaluate(::LdexpFun, x::Real, n::Integer) = ldexp(x, n)

# trigonometric & hyperbolic

@functor1 SinFun Number sin
@functor1 CosFun Number cos
@functor1 TanFun Number tan
@functor1 CotFun Number cot
@functor1 SecFun Number sec
@functor1 CscFun Number csc

@functor1 SindFun Real sind
@functor1 CosdFun Real cosd
@functor1 TandFun Real tand
@functor1 CotdFun Real cotd
@functor1 SecdFun Real secd
@functor1 CscdFun Real cscd

@functor1 SinhFun Number sinh
@functor1 CoshFun Number cosh
@functor1 TanhFun Number tanh
@functor1 CothFun Number coth
@functor1 SechFun Number sech
@functor1 CschFun Number csch

@functor1 AsinFun Number asin
@functor1 AcosFun Number acos
@functor1 AtanFun Number atan
@functor1 AcotFun Number acot
@functor1 AsecFun Number asec
@functor1 AcscFun Number acsc

@functor1 AsindFun Real asind
@functor1 AcosdFun Real acosd
@functor1 AtandFun Real atand
@functor1 AcotdFun Real acotd
@functor1 AsecdFun Real asecd
@functor1 AcscdFun Real acscd

@functor1 AsinhFun Number asinh
@functor1 AcoshFun Number acosh
@functor1 AtanhFun Number atanh
@functor1 AcothFun Number acoth
@functor1 AsechFun Number asech
@functor1 AcschFun Number acsch

@functor2 Atan2Fun Real atan2

#######################################
#
#  Functors for special functions
#
#######################################

export ErfFun, ErfcFun, ErfInvFun, ErfcInvFun,
    GammaFun, LgammaFun, LfactFun, DigammaFun,
    BetaFun, LbetaFun, ZetaFun,
    AiryFun, AiryprimeFun, AiryaiFun, AiryaiprimeFun,
    AirybiFun, AirybiprimeFun,
    BesseljFun, Besselj0Fun, Besselj1Fun, BesseliFun, BesselkFun,
    LogitFun, LogisticFun, InvLogisticFun,
    XlogxFun, XlogyFun

# error functors

@functor1 ErfFun     Real erf
@functor1 ErfcFun    Real erfc
@functor1 ErfInvFun  Real erfinv
@functor1 ErfcInvFun Real erfcinv

# gamma functors

@functor1 GammaFun   Real gamma
@functor1 LgammaFun  Real lgamma
@functor1 LfactFun   Real lfact
@functor1 DigammaFun Real digamma

# beta functors

@functor2 BetaFun  Real beta
@functor2 LbetaFun Real lbeta
@functor1 EtaFun   Real eta
@functor1 ZetaFun  Real zeta

# airy functors

@functor1 AiryFun        Real airy
@functor1 AiryprimeFun   Real airyprime
@functor1 AiryaiFun      Real airyai
@functor1 AiryaiprimeFun Real airyaiprime
@functor1 AirybiFun      Real airybi
@functor1 AirybiprimeFun Real airybiprime

# bessel functors

@functor2 BesseljFun  Real besselj
@functor1 Besselj0Fun Real besselj0
@functor1 Besselj1Fun Real besselj1
@functor2 BesseliFun  Real besseli
@functor2 BesselkFun  Real besselk

# stats-related functors

@functor1 LogitFun       Real logit
@functor1 LogisticFun    Real logistic
@functor1 InvLogisticFun Real invlogistic

@functor1 XlogxFun Real xlogx
@functor2 XlogyFun Real xlogy

#######################################
#
#  Ternary functors
#
#######################################

export FMA, IfelseFun

type FMA <: Functor{3} end
evaluate(::FMA, x::Number, y::Number, z::Number) = (x + y * z)

type IfelseFun <: Functor{3} end
evaluate{T<:Number}(::IfelseFun, c::Bool, x::T, y::T) = ifelse(c, x, y)

#######################################
#
#  type inference facility
#
#######################################

fptype{T<:FloatingPoint}(::Type{T}) = T
fptype{T<:Integer}(::Type{T}) = Float64
fptype{T<:Union(Bool,Uint8,Int8,Uint16,Int16)}(::Type{T}) = Float32

typealias BasicBinaryFuns Union(Add,Subtract,Multiply,MaxFun,MinFun,_Max,_Min,NonnegMax)

result_type{T}(f::BasicBinaryFuns, ::Type{T}, ::Type{T}) = promote_type(T)
result_type{T1,T2}(f::BasicBinaryFuns, ::Type{T1}, ::Type{T2}) = promote_type(T1, T2)

result_type(f::Functor{1}, t1::Type) = typeof(evaluate(f, one(t1)))
result_type(f::Functor{2}, t1::Type, t2::Type) = typeof(evaluate(f, one(t1), one(t2)))
result_type(f::Functor{3}, t1::Type, t2::Type, t3::Type) = typeof(evaluate(f, one(t1), one(t2), one(t3)))

typealias BlasFP Union(Float32,Float64)


"""
```julia
p3p(points, pdn_pixels::AbstractVector{SVector{3, T}}, K)
```

Recover pose using P3P algorithm.

# Arguments:

- `points: Vector of 3D points in `(x, y, z)` format.
- `pdn_pixels::AbstractVector{SVector{3, Float64}}`:
    Corresponding projections of `points` onto image plane,
    predivided by `K` intrinsics and normalized.
    E.g.: `Ki = inv(K); p = Ki [x, y, 1]; p /= norm(p)`.
- `K::SMatrix{3, 3, Float64}`: Camera intrinsics.

# Returns:

`Vector{SMatrix{3, 4, Float64}}` vector of up to 4 possible solutions.
Each element is a projection matrix `P = K * [R|t]`.
To get pure transformation matrix, multiply `P` by `inv(K)`.

# References:

```
Link: https://cmp.felk.cvut.cz/~pajdla/gvg/GVG-2016-Lecture.pdf
chapter: 7.3 Calibrated camera pose computation.
pages: 51-59
```
"""
function p3p(
    points, pdn_pixels::AbstractVector{SVector{3, T}}, K,
) where T <: Real
    models = SMatrix{3, 4, Float64}[]
    ϵ = 1e-4

    d12 = norm(points[1] .- points[2])
    d13 = norm(points[1] .- points[3])
    d23 = norm(points[2] .- points[3])
    (d12 < ϵ || d13 < ϵ || d23 < ϵ) && return models

    sd12, sd13, sd23 = d12^2, d13^2, d23^2

    α12 = pdn_pixels[1] ⋅ pdn_pixels[2]
    α13 = pdn_pixels[1] ⋅ pdn_pixels[3]
    α23 = pdn_pixels[2] ⋅ pdn_pixels[3]

    # Compute coefficients of a polynomial [7.80].
    m1, m2 = sd12, sd13 - sd23
    p1 = PPolynomial{Float64, :x}([0.0, -2.0 * α23 * sd12])
    p2 = PPolynomial{Float64, :x}([2.0 * α13 * sd23, - 2.0 * α23 * sd13])
    q1 = sd23 * PPolynomial{Float64, :x}([1.0, - 2.0 * α12, 1.0]) +
        PPolynomial{Float64, :x}([0.0, 0.0, -sd12])
    q2 = PPolynomial{Float64, :x}([sd23, 0.0, -sd13])

    P::PPolynomial{Float64, :x} =
        (m1 * q2 - m2 * q1)^2 - (m1 * p2 - m2 * p1) * (q1 * p2 - q2 * p1)
    P_roots::Vector{ComplexF64} = P |> Polynomials.roots

    for η12r in P_roots
        isreal(η12r) || continue
        η12 = real(η12r)
        η12 ≤ 0 && continue
        # Evaluate [7.89] at η12 to get η13.
        η13 = (m1 * q2 - m2 * q1)(η12) / (m1 * p2 - m2 * p1)(η12)
        # [7.91 - 7.93].
        denom = 1.0 + η12^2 - 2.0 * η12 * α12
        denom ≤ 0 && continue
        η1 = d12 / √denom
        η2 = η1 * η12
        η3 = η1 * η13
        (η1 ≤ 0 || η2 ≤ 0 || η3 ≤ 0) && continue
        # Compute and check errors.
        (
            abs((√(η1^2 + η2^2 - 2 * η1 * η2 * α12) - d12) / d12) > ϵ ||
            abs((√(η1^2 + η3^2 - 2 * η1 * η3 * α13) - d13) / d13) > ϵ ||
            abs((√(η2^2 + η3^2 - 2 * η2 * η3 * α23) - d23) / d23) > ϵ
        ) && continue

        nx1 = η1 * pdn_pixels[1]
        z2 = η2 * pdn_pixels[2] - nx1; z2 /= norm(z2)
        z3 = η3 * pdn_pixels[3] - nx1; z3 /= norm(z3)
        z1 = z2 × z3; z1 /= norm(z1)
        z3z1 = z3 × z1

        zw2 = @. (points[2] - points[1]) / d12
        zw3 = @. (points[3] - points[1]) / d13
        zw1 = zw2 × zw3; zw1 /= norm(zw1)
        zw3zw1 = zw3 × zw1

        # Recover rotation R [7.130 - 7.134].
        Z = SMatrix{3, 3, Float64}(z1..., z2..., z3z1...)
        ZW = SMatrix{3, 3, Float64}(zw1..., zw2..., zw3zw1...)
        R = Z * inv(ZW)
        KR = K * R
        Kt = -KR * (points[1] - R' * nx1)
        push!(models, SMatrix{3, 4, Float64}(KR..., Kt...))
    end
    models
end

"""
```julia
p3p_select_model(models, points, pixels; threshold = 1.0)
```

Select best pose from `models`.

# Arguments:

- `models`: Projection matrices from which to select best one.
- `points`: 3D points in `(x, y, z)`
- `pixels`: Corresponding projections onto image plane in `(x, y)` format.
- `threshold`: Maximum distance in pixels between projected point
    and its target pixel, for the point to be considered inlier.
    Default value is `1.0`.

# Returns:

`n_inliers, (projection, inliers, error)`.

- `n_inliers`: Number of inliers for the `projection`.
- `projection`: `K * P` projection matrix that projects `points`
    onto image plane.
- `inliers`: Boolean vector indicating which point is inlier.
- `error`: Average reprojection error for the `pose`.
"""
function p3p_select_model(models, points, pixels; threshold = 1.0)
    best_n_inliers = 0
    best_error = maxintfloat()
    best_projection = nothing
    best_inliers = nothing

    for P in models
        avg_error = 0.0
        inliers = fill(false, length(points))
        n_inliers = 0
        for (i, (pixel, point)) in enumerate(zip(pixels, points))
            projected = P * SVector{4}(point..., 1.0)
            projected = projected[[1, 2]] ./ projected[3]
            error = norm(pixel .- projected)

            avg_error += error
            error ≥ threshold && continue

            inliers[i] = true
            n_inliers += 1
        end
        avg_error /= length(points)
        n_inliers < 3 && continue

        if avg_error < best_error
            best_projection = P
            best_inliers = inliers
            best_n_inliers = n_inliers
            best_error = avg_error
        end
    end

    best_n_inliers, (best_projection, best_inliers, best_error)
end

"""
```julia
pre_divide_normalize(pixels, K)
```

Divide `pixels` by `K` intrinsic matrix and normalize them.
`pixels` in `(x, y)` format.
"""
function pre_divide_normalize(pixels, K)
    res = Vector{SVector{3, Float64}}(undef, length(pixels))
    @inbounds for (i, px) in enumerate(pixels)
        p = SVector{3, Float64}(
            (px[1] - K[1, 3]) / K[1, 1], (px[2] - K[2, 3]) / K[2, 2], 1)
        res[i] = normalize(p)
    end
    res
end

function p3p(points, pixels::AbstractVector{SVector{2, T}}, K) where T <: Real
    p3p(points, pre_divide_normalize(pixels, K), K)
end

"""
```julia
p3p_ransac(points, pixels, pdn_pixels, K; threshold = 1.0, ransac_kwargs...)
```

Recover pose `K*[R|t]` using P3P Ransac algorithm.

# Arguments:

- `points`: 3D points in `(x, y, z)`
- `pixels`: Corresponding projections onto image plane in `(x, y)` format.
- `pdn_pixels`: Corresponding projections onto image plane,
    predivided by `K` intrinsics and normalized.
    E.g.: `Ki = inv(K); p = Ki [x, y, 1]; p /= norm(p)`.
- `K`: Camera intrinsics.
- `threshold`: Maximum distance in pixels between projected point
    and its target pixel, for the point to be considered inlier.
    Default value is `1.0`.
- `ransac_kwargs...`: Keyword arguments passed to [`ransac`](@ref).

# Returns:

`n_inliers, (projection, inliers, error)`.

- `n_inliers`: Number of inliers for the `projection`.
- `projection`: `K * P` projection matrix that projects `points`
    onto image plane.
- `inliers`: Boolean vector indicating which point is inlier.
- `error`: Average reprojection error for the `pose`.

# References:

```
Link: https://cmp.felk.cvut.cz/~pajdla/gvg/GVG-2016-Lecture.pdf
chapter: 7.3 Calibrated camera pose computation.
pages: 51-59
```
"""
function p3p_ransac(
    points, pixels, pdn_pixels, K; threshold = 1.0, ransac_kwargs...,
)
    sample_selection(sample_ids) = (points[sample_ids], pdn_pixels[sample_ids], K)
    rank(models; sample_ids) = p3p_select_model(models, points, pixels; threshold)
    ransac(sample_selection, p3p, rank, length(points), 3; ransac_kwargs...)
end

"""
```julia
p3p_ransac(points, pixels, K; threshold = 1.0, ransac_kwargs...)
```

Recover pose `K*[R|t]` using P3P Ransac algorithm.

# Arguments:

- `points`: 3D points in `(x, y, z)`
- `pixels`: Corresponding projections onto image plane in `(x, y)` format.
    These values, **WILL** be predivided and normalized by the intrinsic matrix.
    E.g.: `Ki = inv(K); p = Ki [x, y, 1]; p /= norm(p)`.
- `K`: Camera intrinsics.
- `threshold::Real`:
    Maximum distance in pixels between projected point and its target pixel,
    for the point to be considered inlier. Default value is `1.0`.
- `ransac_kwargs...`: Keyword arguments passed to [`ransac`](@ref).

# Returns:

`Vector{SMatrix{3, 4, Float64}}` vector of up to 4 possible solutions.
Each element is a projection matrix `P = K * [R|t]`.
To get pure transformation matrix, multiply `P` by `inv(K)`.

# References:

```
Link: https://cmp.felk.cvut.cz/~pajdla/gvg/GVG-2016-Lecture.pdf
chapter: 7.3 Calibrated camera pose computation.
pages: 51-59
```
"""
function p3p_ransac(points, pixels, K; threshold = 1.0, ransac_kwargs...)
    pdn_pixels = pre_divide_normalize(pixels, K)
    p3p_ransac(points, pixels, pdn_pixels, K; threshold, ransac_kwargs...)
end


# target a univariate measure μ(dx) ∝ (x-\mu)^2/2\sigma^2 + δ_0 (x) dx
using ZigZagBoomerang
const ZZB = ZigZagBoomerang
function freezing_time(x, θ)
    if θ*x > 0
        return Inf
    else
        return -x/θ
    end
end

# k determines the weight on the Dirac measure. The smaller k, the higher the wegiht
function ss_pdmp(∇ϕ, x, θ, T, c, k, Flow::ZZB.ContinuousDynamics; adapt=false, factor=2.0)
    t = zero(T)
    Ξ = [(t, x, θ)]
    num = acc = 0
    a, b = ZZB.ab(x, θ, c, Flow)
    t_ref = t + ZZB.waiting_time_ref(Flow)
    t′ =  t + poisson_time(a, b, rand())
    tˣ = t + freezing_time(x, θ)
    while t < T
        if  tˣ < min(t_ref, t′)
            t, x, θ = ZZB.move_forward(tˣ - t, t, x, θ, Flow) # go to 0
            @assert -0.0001 < x < 0.0001 #check
            #t, x , θ = tˣ, 0.0, θ #go to 0
            push!(Ξ, (t, x, 0.0))
            t = t - log(rand())/k #wait exponential time
            push!(Ξ, (t, x, θ))
            tˣ  = Inf
        elseif t_ref < t′
            t, x, θ = ZZB.move_forward(t_ref - t, t, x, θ, Flow)
            θ = sqrt(Flow.Σ)*randn()
            t_ref = t +  ZZB.waiting_time_ref(Flow)
            tˣ = t + freezing_time(x, θ)
            push!(Ξ, (t, x, θ))
        else
            τ = t′ - t
            t, x, θ = ZZB.move_forward(τ, t, x, θ, Flow)
            l, lb = ZZB.λ(∇ϕ, x, θ, Flow), ZZB.λ_bar(τ, a, b)
            num += 1
            if rand()*lb < l
                acc += 1
                if l >= lb
                    !adapt && error("Tuning parameter `c` too small.")
                    c *= factor
                end
                θ = -θ  # In multi dimensions the change of velocity is different:
                        # reflection symmetric on the normal vector of the contour
                tˣ = t + freezing_time(x, θ)
                push!(Ξ, (t, x, θ))
            end
        end
        a, b = ZZB.ab(x, θ, c, Flow)
        t′ = t + poisson_time(a, b, rand())
    end
    return Ξ, acc/num
end

ϕ(x) = x^2/2 #not used
∇ϕ(x) = x

# Example: ZigZag
x0, θ0 = -1.0, 1.0
T = 300.0
c = 1.0
k = 1.0
out1, acc = ss_pdmp(∇ϕ, x0, θ0, T, c, k, ZigZag1d())

# Boomerang
function freezing_time(x, θ)
    if θ*x > 0
        return Inf
    else
        return -x/θ
    end
end
out2, acc = ss_pdmp(∇ϕ, x0, θ0, T, c, k, Boomerang1d(0.1))
@show acc
using Makie, CairoMakie
p1 = lines(eventtime.(out1), eventposition.(out1))

## TEST probability of x being 0 euqal to total time divided by time spent on 0
function prob_of_zero(out1)
    k = 0
    freeze =false
    for event in out1
        if freeze == true
            k += event[1]
        end
        if event[3] == 0.0
            freeze = true
            k -= event[1]
        else
            freeze = false
        end
    end
    k/out1[end][1]
end

const μ = 1.0
ϕ(x) = (x-μ)^2/2 #not used
∇ϕ(x) = x - μ
# Example: ZigZag
x0, θ0 = -1.0, 1.0
T = 2000.0
c = 10.01
k = 0.1
@show acc
out1, acc = ss_pdmp(∇ϕ, x0, θ0, T, c, k, ZigZag1d())
using Makie, CairoMakie
p1 = lines(eventtime.(out1), eventposition.(out1), linewidth = 0.3)
prob_of_zero(out1)

f0 = 1/(sqrt(2*pi))*exp(-1/2*μ^2)
1/(k/f0 + 1)


struct Point3D{T} <: FieldVector{3,T}
	r :: T
	θ :: T
	ϕ :: T
end
Point3D(x...) = Point3D(promote(x...)...)
struct Point2D{T} <: FieldVector{2,T}
	θ :: T
	ϕ :: T
end
Point3D(x::Tuple) = Point3D(x...)
Point2D(x::Tuple) = Point2D(x...)
Point2D(x::T...) where {T} = throw(ArgumentError("Point2D requires two angular coordinates"))
Point3D(x::T...) where {T} = throw(ArgumentError("Point2D requires three angular coordinates"))
Point2D(x...) = Point2D(promote(x...)...)

Point3D(r, p::Point2D) = Point3D(r, p.θ, p.ϕ)
Point3D(p::Point2D) = Point3D(1, p)
Point2D(p::Point3D) = Point2D(p.θ, p.ϕ)
Point2D(p::Point2D) = p
Point3D(p::Point3D) = p

radiusvector(θ, ϕ) = SVector{3}(sin(θ)cos(ϕ), sin(θ)sin(ϕ), cos(θ))
radiusvector(r, θ, ϕ) = SVector{3}(sin(θ)cos(ϕ), sin(θ)sin(ϕ), cos(θ)) .* r
radiusvector(p::Point3D) = radiusvector(p.r, p.θ, p.ϕ)
radiusvector(p::Point2D) = radiusvector(p.θ, p.ϕ)

Point3D(x::AbstractVector) = Point3D(SVector{3}(x))
Point2D(x::AbstractVector) = Point2D(SVector{3}(x))
function polarangles(v)
    x, y, z = v
    θ = acos(z)
    if abs(z) == 1
        ϕ = 0.0
    else
        ϕ = atan(x, y)
    end
    promote(θ, ϕ)
end
Point3D(v::SVector{3}) = Point3D(norm(v), polarangles(v)...)
Point2D(v::SVector{3}) = Point2D(polarangles(normalize(v))...)

const SphericalPoint = Union{Point2D,Point3D}

Base.broadcastable(p::SphericalPoint) = Ref(p)

for f in [:cosχ, :∂θ₁cosχ, :∂θ₂cosχ, :∂ϕ₁cosχ, :∂ϕ₂cosχ]
    @eval $f(n1, n2) = $f(Point2D(n1), Point2D(n2))
end
cosχ(n1::SphericalPoint, n2::SphericalPoint) = radiusvector(Point2D(n1)) ⋅ radiusvector(Point2D(n2))
∂θ₁cosχ(n1::SphericalPoint, n2::SphericalPoint) = epsilon(cosχ(Point2D(Dual(n1.θ,1), n1.ϕ), n2))
∂θ₂cosχ(n1::SphericalPoint, n2::SphericalPoint) = epsilon(cosχ(n1, Point2D(Dual(n2.θ,1), n2.ϕ)))
∂ϕ₁cosχ(n1::SphericalPoint, n2::SphericalPoint) = epsilon(cosχ(Point2D(n1.θ, Dual(n1.ϕ,1)), n2))
∂ϕ₂cosχ(n1::SphericalPoint, n2::SphericalPoint) = epsilon(cosχ(n1, Point2D(n2.θ, Dual(n2.ϕ, 1))))

shiftϕ(p::Point2D, Δϕ) = Point2D(p.θ, p.ϕ + Δϕ)
shiftϕ(p::Point3D, Δϕ) = Point3D(p.r, p.θ, p.ϕ + Δϕ)


cd(@__DIR__); include("setups/rgc100.jl")
gr(dpi = 200)

plt = plot(0:(N - 1), 𝛌, c = :black, lw = 1, ylims = [0, 5], legend = false,
        shape = :circle, frame = :box, xlab = latexstring("l"),
        ylab = latexstring("\\tilde{\\lambda}_l"), size = (500, 400))
savefig(plt, "../figs/RGC100_eigenvalues.png")


"""
    RectangularCluster <: NeutralLandscapeMaker

    RectangularCluster(; minimum=2, maximum=4)
    RectangularCluster(minimum, [maximum=4])

Fills the landscape with rectangles containing a random value. The size of each
rectangle/patch is between `minimum` and `maximum` (the two can be equal for a
fixed size rectangle).
"""
@kwdef struct RectangularCluster <: NeutralLandscapeMaker
    minimum::Integer = 2
    maximum::Integer = 4
    function RectangularCluster(minimum::T, maximum::T=4) where {T <: Integer}
        @assert 0 < minimum <= maximum
        new(minimum, maximum)
    end
end

function _landscape!(mat, alg::RectangularCluster)
    mat .= -1.0
    while minimum(mat) == -1.0
        width, height = rand(alg.minimum:alg.maximum, 2)
        row = rand(1:(size(mat,1)-(width-1)))
        col = rand(1:(size(mat,2)-(height-1)))
        mat[row:(row+(width-1)) , col:(col+(height-1))] .= rand()
    end
    return mat
end


module TrialFESpacesTests

using Test
using Gridap.Arrays
using Gridap.TensorValues
using Gridap.ReferenceFEs
using Gridap.Geometry
using Gridap.Integration
using Gridap.Fields
using Gridap.FESpaces
using Gridap.CellData

domain =(0,1,0,1,0,1)
partition = (3,3,3)
model = CartesianDiscreteModel(domain,partition)

order = 2
reffe = ReferenceFE(lagrangian,Float64,order)
V = FESpace(model,reffe,dirichlet_tags=["tag_01","tag_10"])

U = TrialFESpace(V,[4,3])
@test U.dirichlet_values == compute_dirichlet_values_for_tags(V,[4,3])
v = copy(U.dirichlet_values)
v .= 0
isa(V,SingleFieldFESpace)
ud = compute_dirichlet_values_for_tags!(v,copy(v),V,[4,3])
@test all(ud .== v)
test_single_field_fe_space(U)
U = TrialFESpace!(v,V,[4,3])

matvecdata = ([],[],[])
matdata = ([],[],[])
vecdata = ([],[])
test_single_field_fe_space(U,matvecdata,matdata,vecdata)

@test get_dirichlet_values(U) == [4.0, 3.0, 3.0, 3.0, 3.0, 3.0]
TrialFESpace!(U,[1,2])
@test get_dirichlet_values(U) == [1.0, 2.0, 2.0, 2.0, 2.0, 2.0]

U0 = HomogeneousTrialFESpace(V)
@test get_dirichlet_values(U0) == zeros(6)

U0 = HomogeneousTrialFESpace!(v,V)
@test v === get_dirichlet_values(U0)
@test v == zeros(6)
@test get_dirichlet_values(U0) == zeros(6)

u(x) = x[1]
U = TrialFESpace(V,u)
uh = interpolate(u,U)
e = u - uh
trian = Triangulation(model)
quad = CellQuadrature(trian,order)

el2 = sqrt(sum(integrate(inner(e,e),quad)))
@test el2 < 1.0e-10

uh = zero(U)
cellidsL = [4,2,1,3]
cellidsR = [2,4,3,1]
cellidsS = SkeletonPair(cellidsL,cellidsR)
cell_vals = get_cell_dof_values(uh,cellidsS)
@test isa(cell_vals[1],BlockArrayCoo)

cell_dofs = get_cell_dof_ids(U,cellidsS)
@test isa(cell_dofs[1],BlockArrayCoo)

U0 = HomogeneousTrialFESpace(U)
@test get_dirichlet_values(U0) == zeros(6)

#trian = get_triangulation(model)
#
#using Gridap.Visualization
#
#writevtk(trian,"trian",cellfields=["uh"=>uh])

end # module


function Phi_s(CellData,s,z,Def)
    """
    Solid Potential Transfer Function
    # Add Ins and Outs
        # Cell Data
        # Frequency Vector
        # Discretisation Locations
        # Electrode Definition
    """

 if Def == "Pos"
    Electrode = CellData.Pos #Electrode Length
 else
    Electrode = CellData.Neg #Electrode Length
 end

as = 3*Electrode.ϵ_s/Electrode.Rs # Specific interfacial surf. area
κ_eff = CellData.Const.κ*Electrode.ϵ_e^Electrode.κ_brug #Effective Electrolyte Conductivity
σ_eff = Electrode.σ*Electrode.ϵ_s^Electrode.σ_brug #Effective Electrode Conductivity
comb_cond_eff = κ_eff+σ_eff #combining into single variable

#Defining SOC
θ = CellData.Const.SOC * (Electrode.θ_100-Electrode.θ_0) + Electrode.θ_0

#Beta's
β = @. Electrode.Rs*sqrt(s/Electrode.Ds)

#Prepare for j0
cs0 = Electrode.cs_max * θ

#Current Flux Density
if CellData.Const.CellTyp == "Doyle_94"
   κ = Electrode.k_norm/Electrode.cs_max/CellData.Const.ce0^(1-Electrode.α)
   j0 = κ*(CellData.Const.ce0*(Electrode.cs_max-cs0))^(1-Electrode.α)*cs0^Electrode.α
else
   j0 = Electrode.k_norm*(CellData.Const.ce0*(cs0/Electrode.cs_max*(1-cs0/Electrode.cs_max)))^(1-Electrode.α)
end

#Resistance
Rtot = R*CellData.Const.T/(j0*F^2) + Electrode.RFilm

#∂Uocp_Def
∂Uocp_elc = CellData.Const.∂Uocp(Def,θ)/Electrode.cs_max

ν = @. Electrode.L*sqrt((as/σ_eff+as/κ_eff)/(Rtot+∂Uocp_elc*(Electrode.Rs/(F*Electrode.Ds))*(tanh(β)/(tanh(β)-β)))) #Condensing Variable - eq. 4.13
ν_∞ = @. Electrode.L*sqrt((as/κ_eff+as/σ_eff)/(Rtot))

ϕ_tf = @. (-Electrode.L*(κ_eff*(cosh(ν)-cosh((z-1)*ν)))-Electrode.L*(σ_eff*(1-cosh(z*ν)+z*ν*sinh(ν))))/(CellData.Const.CC_A*σ_eff*(comb_cond_eff)*ν*sinh(ν)) #Transfer Function - eq. 4.19
D = @. (-Electrode.L*(κ_eff*(cosh(ν_∞)-cosh(z-1)*ν_∞))-Electrode.L*(σ_eff*(1-cosh(z*ν_∞)+z*ν_∞*sinh(ν_∞))))/(CellData.Const.CC_A*σ_eff*(comb_cond_eff)*ν_∞*sinh(ν_∞)) # Contribution to D as G->∞
D_term = "@. -$(Electrode.L)*($κ_eff*(cosh($ν_∞)-cosh($z-1)*$ν_∞))/($(CellData.Const.CC_A)*$σ_eff*($comb_cond_eff)*$ν_∞*sinh($ν_∞))-$(Electrode.L)*($σ_eff*(1-cosh($z*$ν_∞)+$z*$ν_∞*sinh($ν_∞)))/($(CellData.Const.CC_A)*$σ_eff*($comb_cond_eff)*$ν_∞*sinh($ν_∞))"
zero_tf = @. Electrode.L*(z-2)*z/(2*CellData.Const.CC_A*σ_eff)
ϕ_tf[:,findall(s.==0)] .= zero_tf[:,findall(s.==0)]
res0 = zeros(length(z))

if Def == "Pos"
   ϕ_tf = -ϕ_tf
   D = -D
   D_term = "@. $(Electrode.L)*($κ_eff*(cosh($ν_∞)-cosh($z-1)*$ν_∞))/($(CellData.Const.CC_A)*$σ_eff*($comb_cond_eff)*$ν_∞*sinh($ν_∞))-$(Electrode.L)*($σ_eff*(1-cosh($z*$ν_∞)+$z*$ν_∞*sinh($ν_∞)))/($(CellData.Const.CC_A)*$σ_eff*($comb_cond_eff)*$ν_∞*sinh($ν_∞))"
   if Debug == 1
      println("D:Phi_s:Pos:",D)
      println("zero_tf:Phi_s:Pos:",zero_tf)
      println("zero_tf:Phi_s:Pos:",ϕ_tf[1:5])
      println("z:Phi_s:Pos:",z)
      println("ν_∞:Phi_s:Pos:",ν_∞)
      println("as:Phi_s:Pos:",as)
      println("L:Phi_s:Pos:",Electrode.L)
      println("ν:Phi_s:Pos:",ν[1:5])
      println("θ:Phi_s:Pos:",θ)
      println("κ:Phi_s:Pos:",κ)
      println("Rtot:Phi_s:Pos:",Rtot)
      println("j0:Phi_s:Pos:",j0)
      println("κ_eff:Phi_s:Pos:",κ_eff)
      println("σ_eff:Phi_s:Pos:",σ_eff)
   end
else
   if Debug == 1
      println("D:Phi_s:Neg:",D)
      println("D_check:Phi_s:Neg:",D_check)
      println("ν_∞:Phi_s:Neg:",ν_∞)
      println("θ:Phi_s:Neg:",θ)
      println("κ:Phi_s:Neg:",κ)
      println("Rtot:Phi_s:Neg:",Rtot)
      println("j0:Phi_s:Neg:",j0)
      println("κ_eff:Phi_s:Neg:",κ_eff)
      println("σ_eff:Phi_s:Neg:",σ_eff)
      println("zero_tf:Phi_s:Neg:",zero_tf)
      #println("z:Phi_s:Neg:",z)
      #println("ν:Phi_s:Neg:",ν)
   end
end
return ϕ_tf, D, res0, D_term

end


# Executable

using LinearAlgebra, Random, Plots
using AutomatonEdification.NeuralNetworks, AutomatonEdification.ActivationFunctions, AutomatonEdification.LossFunctions

VECTORIZED = false

# Parameters
batch_size = 32
learning_rate = 0.2
epochs = 96000

nn = NeuralNetworks.create_nn([2,1], ActivationFunctions.sigmoid, LossFunctions.mse, 2, 1)

# Learn XOR
# [a, b] -> [a XOR b]
data = shuffle(repeat([[0,0],[0,1],[1,0],[1,1]], epochs))
# Each label is a vector (even if a singleton)
labels = map(point -> [float(point[1] ⊻ point[2])], data)

@time begin
    stats = VECTORIZED ?
        NeuralNetworks.train_vectorized!(nn, data, labels, learning_rate, batch_size) :
        NeuralNetworks.train!(nn, data, labels, learning_rate, batch_size)
end

for input in [[0,0],[0,1],[1,0],[1,1]]
    print("Input: $input / " *
          "Prediction: $(NeuralNetworks.predict(nn, input)) / " *
          "Actual: $(input[1] ⊻ input[2])\n")
end

plt = plot(1:100:length(stats.losses),
           stats.losses[1:100:end],
           seriestype = :scatter)
display(plt)


using Metatheory

taylor = @theory x a b begin
  exp(x) --> Σ(x^:n / factorial(big(:n)))
  cos(x) --> Σ((-1)^:n * x^2(:n) / factorial(big(2 * :n)))
  Σ(a) + Σ(b) --> Σ(a + b)
end

function expand(iters)
  RewriteRule(
    PatTerm(:call, :Σ, [PatVar(:a)], @__MODULE__),
    PatTerm(:call, :sum, [PatTerm(:(->), :(->), [:n, PatVar(:a)], @__MODULE__), 0:iters], @__MODULE__),
  )
end

a = rewrite(:(exp(x) + cos(x)), taylor)

r = expand(5000)
bexpr = rewrite(a, [r])

# you may want to do algebraic simplification
# with egraphs here

x = big(42)

b = eval(bexpr)
# 1.739274941520501044994695988622883932193276720547806372656638132701531037200611e+18

exp(x) + cos(x)
# 1.739274941520501046994695988622883932193276720547806372656638132701531037200651e+18

@testset "Infinite Series Approximation" begin
  @test b ≈ (exp(x) + cos(x))
end


minutes = 105

hours = minutes/60 # 1.75
println(hours)

# Floor Division
hours = minutes ÷ 60 # 1
println(hours)

# Modulus
remainder = minutes % 60 # 45
println(remainder)

typeof(true)
typeof(Bool)

x = 5
if 0 < x < 10
    println("This is cool")
end

# Keyboard Input
line = readline()
typeof(line)


using LinearAlgebraicRepresentation, ViewerGL, SparseArrays
Lar = LinearAlgebraicRepresentation; GL = ViewerGL

# using Revise
using OhMyREPL

# 3D Boolean example generation
#-------------------------------------------------------------------------------
n,m = 1,1
V,(VV,EV,FV) = Lar.cuboidGrid([n,m],true)
square = V,FV,EV
q = sqrt(2)
V, (VV,EV,FV) = Lar.cuboid([-q,-q], true, [q,q])

twosquares = Lar.Struct([ #(V,FV,EV),
    Lar.Struct([ Lar.t(-sqrt(2)/4, -sqrt(2)/2 ), Lar.r(pi/4), square ]),
    Lar.Struct([ Lar.t( sqrt(2)/4, -sqrt(2)/2 ), Lar.r(pi/4), square ])
    ])

V,FV,EV = Lar.struct2lar(twosquares)
GL.VIEW([ GL.GLGrid(V,FV), GL.GLFrame2 ]);
W,EW = Lar.fragmentlines((V,EV))
V,FVs,EVs = Lar.arrange2D(W,EW)
GL.VIEW(GL.GLExplode(V,FVs[1:3],1.,1.,1.,99,1));

circle = Lar.circle(1.06066/2)()

n,m = 1,1
V,(VV,EV,FV) = Lar.cuboidGrid([n,m],true)
square = V,EV
q = sqrt(2)
assembly = Lar.Struct([ #(V,FV,EV),
    # Lar.Struct([ Lar.t(-q/4, 0 ), Lar.r(pi/4), circle ]),
    Lar.Struct([ Lar.t( -q/4, -q/2 ), Lar.r(pi/4), square ])
    Lar.Struct([ Lar.t( q/4, -q/2 ), Lar.r(pi/4), square ])
])
V,EV = Lar.struct2lar(assembly)
GL.VIEW([ GL.GLGrid(V,EV, GL.COLORS[1],1), GL.GLFrame2 ]);

# boolmatrix = Lar.booleanops2d((V,EV))
W, copEV, copFE, boolmatrix = Lar.bool2d(assembly)
@show Matrix(boolmatrix)
@show Matrix(copFE)
A = boolmatrix[:,1]
B = boolmatrix[:,2]
AorB = A .| B
AandB = A .& B
AxorB = AorB .& (.! AandB)
expr = .&(AorB,(.!AandB)) # xor in prefix polac form

# TODO new method for "bool2d()" in src/Bool2d.jl (see template)
# W, copEV, copFE, boolmat, result = Lar.bool2d( expr, assembly )

union = Matrix(copFE)' * Int.(AorB)
intersection = Matrix(copFE)' * Int.(AandB)
xor = Matrix(copFE)' * AxorB

V = convert(Lar.Points,W')
EV = Lar.cop2lar(copEV)
EVor = [ev for (k,ev) in enumerate(EV) if abs(union[k])==1 ]
EVand = [ev for (k,ev) in enumerate(EV) if abs(intersection[k])==1 ]
EVxor = [ev for (k,ev) in enumerate(EV) if abs(xor[k])==1 ]
GL.VIEW([ GL.GLGrid(V,EVor, GL.COLORS[1],1), GL.GLFrame2 ]);
GL.VIEW([ GL.GLGrid(V,EVand, GL.COLORS[1],1), GL.GLFrame2 ]);
GL.VIEW([ GL.GLGrid(V,EVxor, GL.COLORS[1],1), GL.GLFrame2 ]);

VV = [[k] for k in 1:size(V,2)];
model = (V,[VV, EV])
GL.VIEW( GL.numbering(.5)( model,GL.COLORS[1],0.1 ) );


"""
A "type" to indicate a state of an automaton
"""
State = String

"""
An automaton having:
 - A set of start states q₀
 - A set of final/accepting states F
 - A set of input symbols (alphabet) Σ
 - A set of states Q
 - A transition function δ

Notations are from Wikipedia:
https://en.wikipedia.org/wiki/Deterministic_finite_automaton
"""
struct Automaton
    q₀::State
    F::Set{State}
    Σ::Set{Char}
    Q::Set{State}
    δ::Dict{Tuple{State,Char},Vector{State}}
end

function Automaton(δ::AbstractDict{},
                   q₀::State,
                   F::AbstractSet{State})
    # infer alphabet from transitions
    Σ = Set{Char}()

    # Q will be set of all states
    Q = Set{State}()
    # reached will be set of destination states
    reached = Set{State}()
    for (k, v) in δ
        # destination states
        for d_state in v
            push!(Q, State(d_state))
        end

        # non-final source state
        push!(Q, State(k[1]))

        push!(Σ, k[2])  # add to alphabet
    end

    Automaton(q₀, F, Σ, Q, δ)
end

abstract type AbstractFiniteAutomaton end

"""
    evaluate!(M::AbstractFiniteAutomaton, w::String)

Evaluates the input string using the given automaton.
The automaton is reset before evaluating.

Returns `true` if string is accepted and
`false` otherwise.
"""
function evaluate!(M::AbstractFiniteAutomaton, w)
    reset!(M)

    for a in w
        transition!(M, a) || return false
    end

    accepting(M)
end


### A Pluto.jl notebook ###
# v0.14.1

using Markdown
using InteractiveUtils

# ╔═╡ 5eebac68-891b-11eb-37ba-c1d9481c6134
begin
    using Pkg; Pkg.activate("MLJ_env", shared=true)
	using Test
	using Random
	using Distributions
	using PlutoUI
	using Printf
	using JSON

	push!(LOAD_PATH, "./src")
	using YaCounter
end

# ╔═╡ 87e5e2ec-8915-11eb-362b-6bf13a36b8e4
md"""
## NLP

ref. from book **"Data Science from Scratch"**, Chap 21

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

# ╔═╡ cb25501a-8915-11eb-3626-a1ae6d233f92
PlutoUI.TableOfContents(indent=true, depth=4, aside=true)

# ╔═╡ cb0838ae-8915-11eb-082f-3991192a101f
html"""

"""

# ╔═╡ caf03ada-8915-11eb-2935-01e110accba6
html"""

"""

# ╔═╡ 09066aa6-9049-11eb-27d6-b10b3120c900
begin
	const Int = Integer
	const T_Int = Tuple{Int, Int}
	const D_TV = Dict{Tuple{Int, Int}, Vector{Int}}
	const F = Float64
	const V_F = Vector{F}
end

# ╔═╡ cad66356-8915-11eb-0173-ada200678f63
md"""
### Gibbs Sampling

Generating samples from some distributions is easy. We can get uniform random variables with: `Uniform()`

and normal random variables with: `Normal(...)`

But some distributions are harder to sample from.

Gibbs sampling is a technique for generating samples from multidimensional distributions when we only know some of the conditional distributions.

Imagine rolling two dice. Let x be the value of the first die and y be the sum of the dice, and imagine we wanted to generate lots of (x, y) pairs. In this case it is easy to generate the samples directly:
"""

# ╔═╡ c11e47b0-9044-11eb-1f22-9de42ad879f1
Random.seed!(17)

# ╔═╡ dc3b7280-8915-11eb-258a-1789476897b9
function roll_die()::Int
	rand(1:6)
end

# ╔═╡ dc209b9a-8915-11eb-0f63-ab3b7c33793c
function direct_sample()::T_Int
	d1, d2 = roll_die(), roll_die()
	(d1, d1 + d2)
end

# ╔═╡ d7aade44-891b-11eb-00d3-abe7406c4b09
md"""
Imagine that we only knew the conditional distributions.

The distribution of y conditional on x is easy — if we know the value of x, y is
equally likely to be x + 1, x + 2, x + 3, x + 4, x + 5, or x + 6:
"""

# ╔═╡ 9ae99efe-891c-11eb-3d1f-89182e3f9ff8
function random_y_given_x(x::Integer)::Int
	"""
	Equally likely to be x + 1, x + 2, ... , x + 6
	"""
	x + roll_die()
end

# ╔═╡ ea1f4a2c-891d-11eb-3027-bd447dd9d1e1
md"""
However the other direction is a bit more complicated. For example, if we know that y is
2, then necessarily x is 1 (since the only way two dice can sum to 2 is if
both of them are 1). If we know y is 3, then x is equally likely to be 1 or 2.
Similarly, if y is 11, then x has to be either 5 or 6:
"""

# ╔═╡ 13e3ccb6-8aae-11eb-3a9e-85b5afc74097
function random_x_given_y(y::Integer)::Int
	if y ≤ 7
		## if total is less (or equal) than 7, the first die is equally likely to be
		## 1, 2, ..., (total - 1)
		rand(1:y)
	else
		## if the total is 7+, the first die is equally likely to be
		## (total - 6), (total - 5), ..., 6
		rand(y - 6:7)
	end
end

# ╔═╡ 13c729da-8aae-11eb-29aa-b171020ad8dd
md"""
The way Gibbs sampling works is that we start with any (valid) values for x and y and then repeatedly alternate:
  - replacing x with a random value picked conditional on y and
  - replacing y with a random value picked conditional on x.

After a number of iterations, the resulting values of x and y will represent a sample from the unconditional joint distribution:
"""

# ╔═╡ 2e9609b2-9046-11eb-0ac6-7ff568ff8668
function gibbs_sample(;num_iters=100)::T_Int
	x, y = 1, 2 # doesn't really matter
	for _ ∈ 1:num_iters
		x = random_x_given_y(y)
		y = random_y_given_x(x)
	end
	(x, y)
end

# ╔═╡ 13b2940c-8aae-11eb-2dd2-8dec30dd4881
function cmp_distributions(;num_samples=1000)::D_TV
	counts = D_TV()
	for _ ∈ 1:num_samples
		gs = gibbs_sample()
		v_s = get(counts, gs, [0, 0])
		v_s[1] += 1
		counts[gs] = v_s
		ds = direct_sample()
		v_s = get(counts, ds, [0, 0])
		v_s[2] += 1
		counts[ds] = v_s
	end
	counts
end

# ╔═╡ 8aa8bb6a-9047-11eb-1c48-7dfd972d7580
cmp_distributions()

# ╔═╡ dc0c5a5e-8915-11eb-2c62-5328b42add69
html"""

"""

# ╔═╡ dbefe77a-8915-11eb-39f1-5d0c84729739
md"""
### Topic Modeling

An approach to understanding our users’ interests might try to identify the topics
that underlie those interests. A technique called latent Dirichlet allocation (LDA) is commonly used
to identify common topics in a set of documents.
We’ll apply it to documents that consist of each user’s interests.

LDA has some similarities to the Naive Bayes classifier we built previously in that it assumes a probabilistic
model for documents. For our purposes the model assumes that:

  - There is some fixed number K of topics.

  - There is a random variable that assigns each topic an associated probability distribution over words.
    Think of this distribution as the probability of seeing word w given topic k.

  - There is another random variable that assigns each document a probability distribution over topics.
    Think of this distribution as the mixture of topics in document d.

  - Each word in a document was generated by first randomly picking a topic (from the document’s distribution
    of topics) and then randomly picking a word (from the topic’s distribution of words).

In particular, we have a collection of documents, each of which is a list of words. And we have a corresponding
collection of document_topics that assigns a topic (here a number between 0 and K – 1) to each word in each
document.

So, the fifth word in the fourth document is: documents[4][5]
and the topic from which that word was chosen is: document_topics[4][5]

This very explicitly defines each document’s distribution over topics, and it implicitly defines each topic’s distribution over words.

We can estimate the likelihood that topic 1 produces a certain word by comparing how many times topic 1 produces that word with how many
times topic 1 produces any word. (Similarly, when we built a spam filter, we compared how many times each word appeared in spams with the
total number of words appearing in spams.)

Although these topics are just numbers, we can give them descriptive names by looking at the words on which they put the heaviest weight.
We just have to somehow generate the document_topics. This is where Gibbs sampling comes into play.

  We start by assigning every word in every document a topic completely at random.

  Now we go through each document one word at a time. For that word and document, we construct weights for each topic that depend on the
  (current) distribution of topics in that document and the (current) distribution of words for that topic.

  We then use those weights to sample a new topic for that word.

  If we iterate this process many times, we will end up with a joint sample from the topic-word distribution and the document-topic distribution.

Let us start with a function to randomly choose an index based on an arbitrary set of weights:
"""

# ╔═╡ dbd86924-8915-11eb-351f-8362f09ba984
function sample_from(weights::V_F)::Int
	"""
	returns ix (index) with probability weights[ix] / sum(weights)
	"""
	total = sum(weights)
	rnd = total * rand()       ## uniform between 0 and total
	for (ix, w) ∈ enumerate(weights)
		rnd -= w
		rnd ≤ 0. && (return ix) ## return the smallest i such that
		#                       ## weights[0] + ... + weights[i] ≥ rnd
	end
end

# ╔═╡ d32b86e0-8928-11eb-193c-e3d7c85d23dd
md"""
if we give it weights [1, 1, 3] , then one-fifth of the time it will return 0, one-fifth of the time it will return 1, and three-fifths of the time it will return 2.
"""

# ╔═╡ 4a92cc8e-904a-11eb-11de-e77d09365187
begin
	draws = Counter([sample_from([.1, .1, .8]) for _ ∈ 1:1000])

	@test 10 < draws[1] < 190    ## should be ~10%, this is a really loose test
	@test 10 < draws[2] < 190    ## should be ~10%, this is a really loose test
	@test 650 < draws[3] < 950   ## should be ~80%, this is a really loose test
	@test sum(values(draws)) == 1000
end

# ╔═╡ f1bc0f20-904a-11eb-394e-23d1ee736caf
begin
	const Documents = [
    	["Hadoop", "Big Data", "HBase", "Java", "Spark", "Storm", "Cassandra"],
    	["NoSQL", "MongoDB", "Cassandra", "HBase", "Postgres"],
    	["Python", "scikit-learn", "scipy", "Julia",  "numpy", "statsmodels", "pandas"],
    	["R", "Python", "Julia", "statistics", "regression", "probability"],
    	["machine learning", "regression", "decision trees", "libsvm"],
    	["Python", "R", "Java", "C++", "Julia", "programming languages"],
    	["statistics", "probability", "mathematics", "theory"],
    	["machine learning", "scikit-learn", "Mahout", "neural networks"],
    	["neural networks", "deep learning", "Flux", "Big Data", "artificial intelligence"],
    	["Hadoop", "Java", "MapReduce", "Big Data"],
    	["statistics", "R", "statsmodels"],
    	["C++", "deep learning", "artificial intelligence", "probability"],
    	["pandas", "DataFrames", "Julia", "R", "Python"],
    	["databases", "HBase", "Postgres", "MySQL", "MongoDB"],
    	["libsvm", "regression", "support vector machines"],
		["Julia", "MLJ", "Flux", "DataFrames"]
	]

	const K = 4;
end

# ╔═╡ f1a241d0-904a-11eb-06cc-fda025d4f3b5
begin
	## a list of Counters, one for each document
	doc_topic_counts = [Counter() for _ ∈ Documents]

	## a list of Counters, one for each  topic
	topic_word_counts = [Counter() for _ ∈ 1:K]

	topic_counts = zeros(Int, K)     # a vector of numbers, one for each topic
	doc_lengths = length.(Documents) # a vector of numbers, one for each document

	unique_words = [w for doc ∈ Documents for w ∈ doc] |> unique
	const W, D = length(unique_words), length(Documents);
end

# ╔═╡ d1f14cce-904e-11eb-27c9-4353466db275
md"""
Now we are ready to define our conditional probability functions. Each Will have  smoothing term that ensures every topic has a nonzero chance of being chosen in any document and that every word has a nonzero chance of being chosen for any topic:
"""

# ╔═╡ d1b8b198-904e-11eb-0a40-27d960d3b303
function p_topic_given_doc(topic::Int, d::Int; ϵ=.1)::F
	"""
	A closure calculating the fraction of words in document 'd' that are assigned
	to 'topic' (plus some smoothing)
	"""
	(doc_topic_counts[d][topic] + ϵ) / (doc_lengths[d] + K * ϵ)
end

# ╔═╡ 60b8d5ca-905d-11eb-1238-43d2ed28bdfc
function p_word_given_topic(word::String, topic::Int; ϵ=.1)::F
	"""
	The fraction of words assigned to 'topic' that equal 'word' (plus some smoothing)
	"""
	(topic_word_counts[topic][word] + ϵ) / (topic_counts[topic] + W * ϵ)
end

# ╔═╡ 4ae5db5e-904f-11eb-2073-b17993b09f0f
function topic_weight(d::Int, word::String, k::Int)::F
	"""
	Given a document and a word in that document, return the weight for the kth topic
	"""
	p_word_given_topic(word, k) * p_topic_given_doc(k, d)
end

# ╔═╡ ea0dcb42-905e-11eb-25df-05704be5e114
function choose_new_topic(d::Int, word::String)::Int
	sample_from([topic_weight(d, word, k) for k ∈ 1:K])
end

# ╔═╡ 584b4fbc-905f-11eb-0631-ad00a20dc881
md"""
Given a word and its document, the likelihood of any topic choice depends on both how likely that topic is for the document and how likely that word is for the topic.

Now let us start assigning every word to a random topic and populating our counters appropriately:
"""

# ╔═╡ 4aca19be-904f-11eb-26af-bd9f21c61a0c
begin
	Random.seed!(42)

	doc_topics = [[rand(1:K) for w ∈ doc] for doc ∈ Documents]

	for d ∈ 1:D
		for (w, topic) ∈ zip(Documents[d], doc_topics[d])
			doc_topic_counts[d][topic] += 1
			topic_word_counts[topic][w] += 1
			topic_counts[topic] += 1
		end
	end
end

# ╔═╡ 4ed6af9a-905e-11eb-1b8a-152f021dabf4
md"""
The goal is to get a joint sample of the topics-word distribution and the documents-topic distribution. We do this using a form of Gibbs sampling that uses the conditional probabilities defined previously:
"""

# ╔═╡ 4a928fa8-904f-11eb-2f47-4d319a1a6e58
for iter ∈ 1:1000
	for d ∈ 1:D, (ix, (word, topic)) ∈ enumerate(zip(Documents[d], doc_topics[d]))
		## Remove this word-topic from the counts so that it does not
		## influence the weigths
		doc_topic_counts[d][topic] -= 1
		topic_word_counts[topic][word] -= 1
		topic_counts[topic] -= 1
		doc_lengths[d] -= 1

		## Choose new topic based on the weigths
		new_topic = choose_new_topic(d, word)
		doc_topics[d][ix] = new_topic

		## Update new topic
		doc_topic_counts[d][new_topic] += 1
		topic_word_counts[new_topic][word] += 1
		topic_counts[new_topic] += 1
		doc_lengths[d] += 1
	end
end

# ╔═╡ df809854-9071-11eb-0bed-1f9e8c66b023
md"""
The topics are just numbers (from 1 to 4), if we want names we have to find by ourselves.
"""

# ╔═╡ f3d142e2-906f-11eb-1a04-37c8d25ac050
with_terminal() do
	# 7 items per topic
	for (k, word_cnts) ∈ enumerate(topic_word_counts)
		for (ix, (w, cnt)) ∈ enumerate(most_common(word_cnts, 7))
			cnt > 0 && @printf("%3d/%2d - %-25s %4d\n", k, ix, w, cnt)
		end
		println()
	end
end

# ╔═╡ 4f708e46-9070-11eb-2e90-45e2e028bbdb
md"""
We may define the following topics:
  1. Big Data & Databases
  2. AI and Languages
  3. ML
  4. Statistics and Dynamic Languages
"""

# ╔═╡ 4b449308-9070-11eb-2aed-65775ffabadb
topic_names = [
	"Big Data & Databases",
	"AI and Languages",
	"ML",
	"Statistics and Dynamic Languages"
]

# ╔═╡ 89f83412-9073-11eb-11bf-e7a77ea377ab
with_terminal() do
	for (doc, topic_cnts) ∈ zip(Documents, doc_topic_counts)
		println(doc)
		for (topic, cnt) ∈ most_common(topic_cnts)
			cnt > 0. && @printf(" - %-20s => %2d\n", topic_names[topic], cnt)
		end
		println()
	end
end

# ╔═╡ Cell order:
# ╟─87e5e2ec-8915-11eb-362b-6bf13a36b8e4
# ╠═5eebac68-891b-11eb-37ba-c1d9481c6134
# ╟─cb25501a-8915-11eb-3626-a1ae6d233f92
# ╟─cb0838ae-8915-11eb-082f-3991192a101f
# ╟─caf03ada-8915-11eb-2935-01e110accba6
# ╠═09066aa6-9049-11eb-27d6-b10b3120c900
# ╟─cad66356-8915-11eb-0173-ada200678f63
# ╠═c11e47b0-9044-11eb-1f22-9de42ad879f1
# ╠═dc3b7280-8915-11eb-258a-1789476897b9
# ╠═dc209b9a-8915-11eb-0f63-ab3b7c33793c
# ╟─d7aade44-891b-11eb-00d3-abe7406c4b09
# ╠═9ae99efe-891c-11eb-3d1f-89182e3f9ff8
# ╟─ea1f4a2c-891d-11eb-3027-bd447dd9d1e1
# ╠═13e3ccb6-8aae-11eb-3a9e-85b5afc74097
# ╟─13c729da-8aae-11eb-29aa-b171020ad8dd
# ╠═2e9609b2-9046-11eb-0ac6-7ff568ff8668
# ╠═13b2940c-8aae-11eb-2dd2-8dec30dd4881
# ╠═8aa8bb6a-9047-11eb-1c48-7dfd972d7580
# ╟─dc0c5a5e-8915-11eb-2c62-5328b42add69
# ╟─dbefe77a-8915-11eb-39f1-5d0c84729739
# ╠═dbd86924-8915-11eb-351f-8362f09ba984
# ╟─d32b86e0-8928-11eb-193c-e3d7c85d23dd
# ╠═4a92cc8e-904a-11eb-11de-e77d09365187
# ╠═f1bc0f20-904a-11eb-394e-23d1ee736caf
# ╠═f1a241d0-904a-11eb-06cc-fda025d4f3b5
# ╟─d1f14cce-904e-11eb-27c9-4353466db275
# ╠═d1b8b198-904e-11eb-0a40-27d960d3b303
# ╠═60b8d5ca-905d-11eb-1238-43d2ed28bdfc
# ╠═4ae5db5e-904f-11eb-2073-b17993b09f0f
# ╠═ea0dcb42-905e-11eb-25df-05704be5e114
# ╟─584b4fbc-905f-11eb-0631-ad00a20dc881
# ╠═4aca19be-904f-11eb-26af-bd9f21c61a0c
# ╟─4ed6af9a-905e-11eb-1b8a-152f021dabf4
# ╠═4a928fa8-904f-11eb-2f47-4d319a1a6e58
# ╟─df809854-9071-11eb-0bed-1f9e8c66b023
# ╠═f3d142e2-906f-11eb-1a04-37c8d25ac050
# ╟─4f708e46-9070-11eb-2e90-45e2e028bbdb
# ╠═4b449308-9070-11eb-2aed-65775ffabadb
# ╠═89f83412-9073-11eb-11bf-e7a77ea377ab


module Stand

export Standardizer

using MLJModelInterface
using ..MLJBase.Tables
const MMI = MLJModelInterface
using Statistics
import ..MLJBase.MLJScientificTypes.coerce

const UNIVARIATE_STD_DESCR = "Standardize (whiten) univariate data."
const STANDARDIZER_DESCR = "Standardize (whiten) features (columns) "*
"of a table."

## UNIVARIATE STANDARDIZATION

"""
    UnivariateStandardizer()

Unsupervised model for standardizing (whitening) univariate data.
"""
mutable struct UnivariateStandardizer <: Unsupervised end

function MMI.fit(transformer::UnivariateStandardizer, verbosity::Int,
             v::AbstractVector{T}) where T<:Real
    std(v) > eps(Float64) ||
        @warn "Extremely small standard deviation encountered in standardization."
    fitresult = (mean(v), std(v))
    cache = nothing
    report = NamedTuple()
    return fitresult, cache, report
end

MMI.fitted_params(::UnivariateStandardizer, fitresult) =
    (mean_and_std = fitresult, )

# for transforming single value:
function MMI.transform(transformer::UnivariateStandardizer, fitresult, x::Real)
    mu, sigma = fitresult
    return (x - mu)/sigma
end

# for transforming vector:
MMI.transform(transformer::UnivariateStandardizer, fitresult, v) =
              [transform(transformer, fitresult, x) for x in v]

# for single values:
function MMI.inverse_transform(transformer::UnivariateStandardizer, fitresult, y::Real)
    mu, sigma = fitresult
    return mu + y*sigma
end

# for vectors:
MMI.inverse_transform(transformer::UnivariateStandardizer, fitresult, w) =
    [inverse_transform(transformer, fitresult, y) for y in w]

## STANDARDIZATION OF ORDINAL FEATURES OF TABULAR DATA

"""
    Standardizer(; features=Symbol[],
                   ignore=false,
                   ordered_factor=false,
                   count=false)

Unsupervised model for standardizing (whitening) the columns of
tabular data.  If `features` is unspecified then all columns
having `Continuous` element scitype are standardized. Otherwise, the
features standardized are the `Continuous` features named in
`features` (`ignore=false`) or `Continuous` features not named in
`features` (`ignore=true`). To allow standarization of `Count` or
`OrderedFactor` features as well, set the appropriate flag to true.

Instead of supplying a features vector, a Bool-valued callable with one
argument can be also be specified. For example, specifying
`Standardizer(features = name -> name in [:x1, :x3], ignore = true, count=true)`
has the same effect as `Standardizer(features = [:x1, :x3], ignore = true,
count=true)`, namely to standardise all `Continuous` and `Count` features,
with the exception of `:x1` and `:x3`.

The `inverse_tranform` method is supported provided `count=false` and
`ordered_factor=false` at time of fit.

# Example

```
X = (ordinal1 = [1, 2, 3],
     ordinal2 = coerce([:x, :y, :x], OrderedFactor),
     ordinal3 = [10.0, 20.0, 30.0],
     ordinal4 = [-20.0, -30.0, -40.0],
     nominal = coerce(["Your father", "he", "is"], Multiclass));
stand1 = Standardizer();
julia> transform(fit!(machine(stand1, X)), X)
[ Info: Training Machine{Standardizer} @ 7…97.
(ordinal1 = [1, 2, 3],
 ordinal2 = CategoricalValue{Symbol,UInt32}[:x, :y, :x],
 ordinal3 = [-1.0, 0.0, 1.0],
 ordinal4 = [1.0, 0.0, -1.0],
 nominal = CategoricalVale{String,UInt32}["Your father", "he", "is"],)

stand2 = Standardizer(features=[:ordinal3, ], ignore=true, count=true);
julia> transform(fit!(machine(stand2, X)), X)
[ Info: Training Machine{Standardizer} @ 1…87.
(ordinal1 = [-1.0, 0.0, 1.0],
 ordinal2 = CategoricalValue{Symbol,UInt32}[:x, :y, :x],
 ordinal3 = [10.0, 20.0, 30.0],
 ordinal4 = [1.0, 0.0, -1.0],
 nominal = CategoricalValue{String,UInt32}["Your father", "he", "is"],)
```

"""
mutable struct Standardizer <: Unsupervised
    # features to be standardized; empty means all
    features::Union{AbstractVector{Symbol}, Function}
    ignore::Bool # features to be ignored
    ordered_factor::Bool
    count::Bool
end

# keyword constructor
function Standardizer(
    ;
    features::Union{AbstractVector{Symbol}, Function}=Symbol[],
    ignore::Bool=false,
    ordered_factor::Bool=false,
    count::Bool=false
)
    transformer = Standardizer(features, ignore, ordered_factor, count)
    message = MMI.clean!(transformer)
    isempty(message) || throw(ArgumentError(message))
    return transformer
end

function MMI.clean!(transformer::Standardizer)
    err = ""
    if (
        typeof(transformer.features) <: AbstractVector{Symbol} &&
        isempty(transformer.features) &&
        transformer.ignore
    )
        err *= "Features to be ignored must be specified in features field."
    end
    return err
end

function MMI.fit(transformer::Standardizer, verbosity::Int, X)

    # if not a table, it must be an abstract vector, eltpye AbstractFloat:
    is_univariate = !Tables.istable(X)

    # are we attempting to standardize Count or OrderedFactor?
    is_invertible = !transformer.count && !transformer.ordered_factor

    # initialize fitresult:
    fitresult_given_feature = Dict{Symbol,Tuple{Float64,Float64}}()

    # special univariate case:
    if is_univariate
        fitresult_given_feature[:unnamed] =
            MMI.fit(UnivariateStandardizer(), verbosity - 1, X)[1]
        return (is_univariate=true,
                is_invertible=true,
                fitresult_given_feature=fitresult_given_feature),
        nothing, nothing
    end

    all_features = Tables.schema(X).names
    feature_scitypes =
        collect(elscitype(selectcols(X, c)) for c in all_features)
    scitypes = Vector{Type}([Continuous])
    transformer.ordered_factor && push!(scitypes, OrderedFactor)
    transformer.count && push!(scitypes, Count)
    AllowedScitype = Union{scitypes...}

    # determine indices of all_features to be transformed
    if transformer.features isa AbstractVector{Symbol}
        if isempty(transformer.features)
            cols_to_fit = filter!(eachindex(all_features) |> collect) do j
                feature_scitypes[j] <: AllowedScitype
            end
        else
            !issubset(transformer.features, all_features) && verbosity > -1 &&
                @warn "Some specified features not present in table to be fit. "
            cols_to_fit = filter!(eachindex(all_features) |> collect) do j
                ifelse(
                    transformer.ignore,
                    !(all_features[j] in transformer.features) &&
                        feature_scitypes[j] <: AllowedScitype,
                    (all_features[j] in transformer.features) &&
                        feature_scitypes[j] <: AllowedScitype
                )
            end
        end
    else
        cols_to_fit = filter!(eachindex(all_features) |> collect) do j
            ifelse(
                transformer.ignore,
                !(transformer.features(all_features[j])) &&
                    feature_scitypes[j] <: AllowedScitype,
                (transformer.features(all_features[j])) &&
                    feature_scitypes[j] <: AllowedScitype
            )
        end
    end
    fitresult_given_feature = Dict{Symbol,Tuple{Float64,Float64}}()

    isempty(cols_to_fit) && verbosity > -1 &&
        @warn "No features to standarize."

    # fit each feature and add result to above dict
    verbosity < 2 || @info "Features standarized: "
    for j in cols_to_fit
        col_data = if (feature_scitypes[j] <: OrderedFactor)
            coerce(selectcols(X, j), Continuous)
        else
            selectcols(X, j)
        end
        col_fitresult, cache, report =
            MMI.fit(UnivariateStandardizer(), verbosity - 1, col_data)
        fitresult_given_feature[all_features[j]] = col_fitresult
        verbosity < 2 ||
            @info "  :$(all_features[j])    mu=$(col_fitresult[1])  sigma=$(col_fitresult[2])"
    end

    fitresult = (is_univariate=false, is_invertible=is_invertible,
                 fitresult_given_feature=fitresult_given_feature)
    cache = nothing
    report = (features_fit=keys(fitresult_given_feature),)

    return fitresult, cache, report
end

function MMI.fitted_params(::Standardizer, fitresult)
    is_univariate, _, dic = fitresult
    is_univariate &&
        return fitted_params(UnivariateStandardizer(), dic[:unnamed])
    return (mean_and_std_given_feature=dic)
end

MMI.transform(::Standardizer, fitresult, X) =
    _standardize(transform, fitresult, X)

function MMI.inverse_transform(::Standardizer, fitresult, X)
    fitresult.is_invertible ||
        error("Inverse standardization is not supported when `count=true` "*
              "or `ordered_factor=true` during fit. ")
    return _standardize(inverse_transform, fitresult, X)
end

function _standardize(operation, fitresult, X)

    # `fitresult` is dict of column fitresults, keyed on feature names
    is_univariate, _, fitresult_given_feature = fitresult

    if is_univariate
        univariate_fitresult = fitresult_given_feature[:unnamed]
        return operation(UnivariateStandardizer(), univariate_fitresult, X)
    end

    features_to_be_transformed = keys(fitresult_given_feature)

    all_features = Tables.schema(X).names

    all(e -> e in all_features, features_to_be_transformed) ||
        error("Attempting to transform data with incompatible feature labels.")

    col_transformer = UnivariateStandardizer()

    cols = map(all_features) do ftr
        ftr_data = selectcols(X, ftr)
        if ftr in features_to_be_transformed
            col_to_transform = coerce(ftr_data, Continuous)
            operation(col_transformer,
                      fitresult_given_feature[ftr],
                      col_to_transform)
        else
            ftr_data
        end
    end

    named_cols = NamedTuple{all_features}(tuple(cols...))

    return MMI.table(named_cols, prototype=X)

    metadata_model(UnivariateStandardizer,
               input=AbstractVector{<:MLJBase.Infinite},
               output=AbstractVector{MLJBase.Continuous},
               weights=false,
               descr=UNIVARIATE_STD_DESCR,
               path="MLJBase.UnivariateStandardizer")

    metadata_model(Standardizer,
               input=MLJBase.Table,
               output=MLJBase.Table,
               weights=false,
               descr=STANDARDIZER_DESCR,
               path="MLJBase.Standardizer")

end

end


using FiniteDepthParametricMachines, Flux

t = -pi:0.1:pi

minibatch = 32
x = zeros(length(t), 1, minibatch)
y = zeros(length(t), 1, minibatch)
shift = 10
for i in 1:minibatch
    v = @. sin(t) + 0.1 * (rand() - 0.5)
    x[:, 1, i] .= v
    y[:, 1, i] .= circshift(v, 10)
end

dimensions = [1, 4, 4, 4]

## ConvMachine

machine = ConvMachine(dimensions, sigmoid; pad=(3, 0))

model = Flux.Chain(machine, Conv((1,), sum(dimensions) => 1)) |> f64

model(x)

function loss(X_train, Y_train)
    Flux.Losses.mse(model(X_train), Y_train)
end

loss(x, y)

opt = ADAM(0.1)
ps = Flux.params(model)

# check that learning happens correctly

for i in 1:1000
    gs = gradient(ps) do
        loss(x, y)
    end
    Flux.Optimise.update!(opt, ps, gs)
    if i % 10 == 0
        @show loss(x, y)
    end
end

## RecurMachine

machine = RecurMachine(dimensions, sigmoid; pad=3, timeblock=5)

model = Flux.Chain(machine, Conv((1,), sum(dimensions) => 1)) |> f64

model(x)

function loss(X_train, Y_train)
    Flux.Losses.mse(model(X_train), Y_train)
end

loss(x, y)

opt = ADAM(0.1)
ps = Flux.params(model)

# check that learning happens correctly

for i in 1:1000
    gs = gradient(ps) do
        loss(x, y)
    end
    Flux.Optimise.update!(opt, ps, gs)
    if i % 10 == 0
        @show loss(x, y)
    end
end


export AbstractThresholdLink
export cdf, invcdf
export NormalThresholdLink, StudentThresholdLink

"Abstract type for link functions between probabilities and values on the whole real line"
abstract type AbstractThresholdLink{TUnit, TReal} end

"""
    cdf(atl::AbstractThresholdLink)

Get the link function from real line to probability for the link specification `atl`
"""
function cdf(atl::AbstractThresholdLink) end

"""
    invcdf(atl::AbstractThresholdLink)

Get the link function from probability to real line for the link specification `atl`
"""
function invcdf(atl::AbstractThresholdLink) end

"Normal distribution link specification using StatsFuns implementation of normal cdf and invcdf"
struct NormalThresholdLink{T <: AbstractFloat} <: AbstractThresholdLink{T, T}
end

"""
    cdf(ntl::NormalThresholdLink)

Get the link function from real line to probability for `ntl` which is always the normal cdf
"""
function cdf(ntl::NormalThresholdLink)
    return normcdf
end

"""
    invcdf(ntl::NormalThresholdLink)

Get the link function from probability to real line for `ntl` which is always the normal inverse cdf
"""
function invcdf(ntl::NormalThresholdLink)
    return norminvcdf
end

"Student's distribution link specification using StatsFuns implementation of Student's cdf and invcdf"
struct StudentThresholdLink <: AbstractThresholdLink{Float64, Float64}
    df::Union{Float64, Int64}

    """
        StudentThresholdLink(df::Union{Float64, Int64})

    Construct a StudentThresholdLink with `df` degrees of freedom
    """
    function StudentThresholdLink(df::Union{Float64, Int64})
        if df <= 0
            throw(DomainError(df, "Degrees of freedom was not positive"))
        end
        return new(df)
    end
end

"""
    cdf(stl::StudentThresholdLink)

Get the link function from real line to probability for `stl` which is the Student's cdf with degrees
    of freedom specified by `stl`
"""
function cdf(stl::StudentThresholdLink)
    return q -> tdistcdf(stl.df, q)
end

"""
    invcdf(stl::StudentThresholdLink)

Get the link function from probability to real line for `stl` which is the Student's inverse cdf with degrees
    of freedom specified by `stl`
"""
function invcdf(stl::StudentThresholdLink)
    return p -> tdistinvcdf(stl.df, p)
end


#= DSP is removed from test dependencies in this version.
import MechanicalSketch
import MechanicalSketch: empty_figure, PALETTE, O, HE, WI, EM, finish, ∙, Point
import MechanicalSketch: @import_expand, set_scale_sketch, settext, scale_pt_to_unit
import MechanicalSketch: noise, normalize_datarange, place_image, BinLegend, @layer
import MechanicalSketch: poly, dimension_aligned, sethue, arrow, circle, prettypoly, Greys_9
using  MechanicalSketch: MechanicalUnits, upreferred, ComplexQuantity, Quantity
import MechanicalUnits:  dimension, NoDims
import DSP.Periodograms: spectrogram, Spectrogram
import DSP.Util:         nextfastfft
import DSP.Windows:      tukey
import Statistics:       mean
#let
if !@isdefined m²
    @import_expand(~m, s)
end
empty_figure(filename = joinpath(@__DIR__, "test_29.png"));

curpoint = O + (-WI / 2 + EM, -HE / 2 + 3EM)
velocities = range(0.05m/s, stop= 0.5m/s, length = 3)
settext("
The flow field visualization should have a linear relationship between velocity and luminosity.
We need to control the noise image wavelength spectrum to get that.
", curpoint)

curpoint += (0, 4EM)
settext("
For reference, let's first illustrate two equal amplitude wavelengths, λ, as greyscale:
    cos(2π x / 0.5m) + 0.5cos(2π x / 0.05m)
", curpoint, markup = true)

curpoint += (0, 3EM)
n_pixels = round(Int, 0.9WI)
physwidth = 2.0m
set_scale_sketch(physwidth, n_pixels)
length_one_pixel = physwidth / n_pixels
twowave(x) = cos(2π∙x / 0.5m) + 0.5cos(2π∙x / 0.05m)
no = [twowave(x) for x in (1:n_pixels)*length_one_pixel]
nno = normalize_datarange(no)
greylegend = BinLegend(;maxlegend = 1.0, noofbins = 128, colorscheme = reverse(Greys_9))
@layer begin
    place_image(curpoint, greylegend.(nno); centered = false)
    place_image(curpoint + (0,1), greylegend.(nno); centered = false)
    place_image(curpoint + (0,2), greylegend.(nno); centered = false)
    sethue(PALETTE[3])
    dimension_aligned(curpoint, curpoint + (physwidth, 0.0m), offset = -2.5EM)
    dimension_aligned(curpoint, curpoint + (0.5m, 0.0m), offset = -EM, toextension = (0EM, 7EM))
end

curpoint += (0, 3EM)
settext("
The same greyscale values shown as a curve:", curpoint)
curpoint += (0, EM)
points = map(1:n_pixels, nno) do xul, yul
    (xul, -yul * EM)
end
poly(curpoint .+ points, :stroke)

curpoint += (0, 2EM)
settext("
Amplitude-wavelength spectrum from sampling the same values:", curpoint, markup=true)

curpoint += (0, 3EM)
include("test_functions_29.jl")
spectrum = sampledspectrum_29(no .- 0.5, length_one_pixel, maximumwavelength = 0.5m)
draw_spectrum_29(curpoint, spectrum)

curpoint += (0, 2EM)

(λ_1, a_1), (λ_2, a_2) = two_largest_maxima_amplitude_wavelength(spectrum)
settext("
    Local maxima are (λ, amplitude) = [$((λ_1, a_1)), $((λ_2, a_2))]",
    curpoint, markup=true)

curpoint += (0, 2EM)
settext("
We can sample from a longer example and get a finer spectrum resolution:", curpoint)

curpoint += (0, 3EM)
longnoise = [twowave(x) for x in (1:100n_pixels)*length_one_pixel]
spectrum = sampledspectrum_29(longnoise, length_one_pixel, maximumwavelength = 0.5m)
draw_spectrum_29(curpoint, spectrum)

curpoint += (0, 2EM)
(λ_1, a_1), (λ_2, a_2) = two_largest_maxima_amplitude_wavelength(spectrum)
settext("
    Local maxima are (λ, amplitude) = [$((λ_1, a_1)), $((λ_2, a_2))]",
    curpoint, markup=true)

finish()
set_scale_sketch()
#end
=#


using LinearAlgebraicRepresentation
Lar = LinearAlgebraicRepresentation
using Plasm

n = 3000 #1000 #20000
t = 0.15 #0.4 #0.15

function randomlines(n=1000,t=0.2)
	V = zeros(Float64,2,2*n)
	EV = [zeros(Int64,2) for k=1:n]
	for k=1:n
		v1 = rand(Float64,2)
		v2 = rand(Float64,2)
		vm = (v1+v2)/2
		transl = rand(Float64,2)
		V[:,k] = (v1-vm)*t + transl
		V[:,n+k] = (v2-vm)*t + transl
		EV[k] = [k,n+k]
	end
	V = Plasm.normalize(V)
	model = (V,EV)
	return model
end

model = randomlines(n,t)
Sigma = Lar.spaceindex(model)
Plasm.view(model)

W,EW = Lar.fragmentlines(model)
Plasm.viewexploded(W,EW)(1.2,1.2,1.2)

V,EVs = Lar.biconnectedComponent((W,EW::Lar.Cells)) # 2-connected components (H & T)
hpcs = [ Plasm.lar2hpc(V,EVs[i]) for i=1:length(EVs) ]
Plasm.view([ Plasm.color(Plasm.colorkey[(k%12)==0 ? 12 : k%12])(hpcs[k]) for k=1:(length(hpcs)) ])


using Mamba

## Model and User-Defined Sampler Specifications

model = Model(

  y = Stochastic(1,
    (mu, s2) ->  MvNormal(mu, sqrt(s2)),
    false
  ),

  mu = Logical(1,
    (xmat, beta) -> xmat * beta,
    false
  ),

  beta = Stochastic(1,
    () -> MvNormal(2, sqrt(1000))
  ),

  s2 = Stochastic(
    () -> InverseGamma(0.001, 0.001)
  )

)

Gibbs_beta = Sampler([:beta],
  (beta, s2, xmat, y) ->
    begin
      beta_mean = mean(beta.distr)
      beta_invcov = invcov(beta.distr)
      Sigma = inv(Symmetric(xmat' * xmat / s2 + beta_invcov))
      mu = Sigma * (xmat' * y / s2 + beta_invcov * beta_mean)
      rand(MvNormal(mu, Sigma))
    end
)

Gibbs_s2 = Sampler([:s2],
  (mu, s2, y) ->
    begin
      a = length(y) / 2.0 + shape(s2.distr)
      b = sumabs2(y - mu) / 2.0 + scale(s2.distr)
      rand(InverseGamma(a, b))
    end
)

## Hybrid No-U-Turn and Slice Sampling Scheme
scheme1 = [NUTS(:beta),
           Slice(:s2, 3.0)]

## No-U-Turn Sampling Scheme
scheme2 = [NUTS([:beta, :s2])]

## User-Defined Sampling Scheme
scheme3 = [Gibbs_beta, Gibbs_s2]

## Sampling Scheme Assignment
setsamplers!(model, scheme1)

## Graph Representation of Nodes
draw(model)
draw(model, filename="lineDAG.dot")

## Data
line = Dict{Symbol, Any}(
  :x => [1, 2, 3, 4, 5],
  :y => [1, 3, 3, 3, 5]
)
line[:xmat] = [ones(5) line[:x]]

## Set Random Number Generator Seed
srand(123)

## Initial Values
inits = [
  Dict{Symbol, Any}(
    :y => line[:y],
    :beta => rand(Normal(0, 1), 2),
    :s2 => rand(Gamma(1, 1))
  )
  for i in 1:3
]

## MCMC Simulations

setsamplers!(model, scheme1)
sim1 = mcmc(model, line, inits, 10000, burnin=250, thin=2, chains=3)

setsamplers!(model, scheme2)
sim2 = mcmc(model, line, inits, 10000, burnin=250, thin=2, chains=3)

setsamplers!(model, scheme3)
sim3 = mcmc(model, line, inits, 10000, burnin=250, thin=2, chains=3)

## Gelman, Rubin, and Brooks Convergence Diagnostic
gelmandiag(sim1, mpsrf=true, transform=true) |> showall

## Geweke Convergence Diagnostic
gewekediag(sim1) |> showall

## Heidelberger-Welch Diagnostic
heideldiag(sim1) |> showall

## Raftery-Lewis Convergence Diagnostic
rafterydiag(sim1) |> showall

## Summary Statistics
describe(sim1)

## Highest Posterior Density Intervals
hpd(sim1) |> show

## Cross-Correlations
cor(sim1) |> show

## Lag-Autocorrelations
autocor(sim1) |> show

## State Space Change Rate (per Iteration)
changerate(sim1) |> show

## Deviance Information Criterion
dic(sim1) |> show

## Subset Sampler Output
sim = sim1[1000:5000, ["beta[1]", "beta[2]"], :]
describe(sim)

## Write to and Read from an External File
write("sim1.jls", sim1)
sim1 = read("sim1.jls", ModelChains)

## Restart the Sampler
sim = mcmc(sim1, 5000)
describe(sim)

## Plotting

## Default summary plot (trace and density plots)
p = plot(sim1)

## Write plot to file
draw(p, filename="summaryplot.svg")
#draw(p, filename="summaryplot.pdf", fmt=:pdf)

## Autocorrelation and running mean plots
p = plot(sim1, [:autocor, :mean], legend=true)
draw(p, nrow=3, ncol=2, filename="autocormeanplot.svg")
#draw(p, nrow=3, ncol=2, filename="autocormeanplot.pdf", fmt=:pdf)

## Pairwise contour plots
p = plot(sim1, :contour)
draw(p, nrow=2, ncol=2, filename="contourplot.svg")
#draw(p, nrow=2, ncol=2, filename="contourplot.pdf", fmt=:pdf)

## Development and Testing

setinputs!(model, line)             # Set input node values
setinits!(model, inits[1])          # Set initial values
setsamplers!(model, scheme1)        # Set sampling scheme

showall(model)                      # Show detailed node information

logpdf(model, 1)                    # Log-density sum for block 1
logpdf(model, 2)                    # Block 2
logpdf(model)                       # All blocks

sample!(model, 1)                  # Sample values for block 1
sample!(model, 2)                  # Block 2
sample!(model)                     # All blocks