"""
    NestedSamplers.Proposals

This module contains the different algorithms for proposing new points within a bounding volume in unit space.

The available implementations are
* [`Proposals.Uniform`](@ref) - samples uniformly within the bounding volume
* [`Proposals.RWalk`](@ref) - random walks to a new point given an existing one
* [`Proposals.RStagger`](@ref) - random staggering away to a new point given an existing one
* [`Proposals.Slice`](@ref) - slicing away to a new point given an existing one
* [`Proposals.RSlice`](@ref) - random slicing away to a new point given an existing one
"""
module Proposals

using ..Bounds

using Random
using LinearAlgebra
using Parameters

export AbstractProposal

"""
    NestedSamplers.AbstractProposal

The abstract type for live point proposal algorithms.

# Interface

Each `AbstractProposal` must have this function,
```julia
(::AbstractProposal)(::AbstractRNG, point, loglstar, bounds, loglikelihood, prior_transform)
```
which, given the input `point` with loglikelihood `loglstar` inside a `bounds`, returns a new point in unit space, prior space, the loglikelihood, and the number of function calls.
"""
abstract type AbstractProposal end

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

# Helper for checking unit-space bounds
unitcheck(us) = all(u -> 0 < u < 1, us)

"""
    Proposals.Uniform()

Propose a new live point by uniformly sampling within the bounding volume.
"""
struct Uniform <: AbstractProposal end

function (::Uniform)(rng::AbstractRNG,
    point::AbstractVector,
    logl_star,
    bounds::AbstractBoundingSpace,
    loglike,
    prior_transform)

    ncall = 0
    while true
        u = rand(rng, bounds)
        unitcheck(u) || continue
        v = prior_transform(u)
        logl = loglike(v)
        ncall += 1
        logl ≥ logl_star && return u, v, logl, ncall
    end
end

Base.show(io::IO, p::Uniform) = print(io, "NestedSamplers.Proposals.Uniform")

"""
    Proposals.RWalk(;ratio=0.5, walks=25, scale=1)

Propose a new live point by random walking away from an existing live point.

## Parameters
- `ratio` is the target acceptance ratio
- `walks` is the minimum number of steps to take
- `scale` is the proposal distribution scale, which will update _between_ proposals.
"""
@with_kw mutable struct RWalk <: AbstractProposal
    ratio = 0.5
    walks = 25
    scale = 1.0

    @assert 1 / walks ≤ ratio ≤ 1 "Target acceptance ratio must be between 1/`walks` and 1"
    @assert walks > 1 "Number of steps must be greater than 1"
    @assert scale ≥ 0 "Proposal scale must be non-negative"
end

function (prop::RWalk)(rng::AbstractRNG,
                       point::AbstractVector,
                       logl_star,
                       bounds::AbstractBoundingSpace,
                       loglike,
                       prior_transform;
                       kwargs...)
    # setup
    n = length(point)
    scale_init = prop.scale
    accept = reject = fail = nfail = nc = ncall = 0
    local du, u_prop, logl_prop, u, v, logl

    while nc < prop.walks || iszero(accept)
        # get proposed point
        while true
            # check scale factor to avoid over-shrinking
            prop.scale < 1e-5 * scale_init && error("Random walk sampling appears to be stuck.")
            # transform to proposal distribution
            du = randoffset(rng, bounds)
            u_prop = @. point + prop.scale * du
            # inside unit-cube
            unitcheck(u_prop) && break

            fail += 1
            nfail += 1
            # check if stuck generating bad numbers
            if fail > 100 * prop.walks
                @warn "Random number generation appears extremely inefficient. Adjusting the scale-factor accordingly"
                fail = 0
                prop.scale *= exp(-1/n)
            end
        end
        # check proposed point
        v_prop = prior_transform(u_prop)
        logl_prop = loglike(v_prop)
        if logl_prop ≥ logl_star
            u = u_prop
            v = v_prop
            logl = logl_prop
            accept += 1
        else
            reject += 1
        end
        nc += 1
        ncall += 1

        # check if stuck generating bad points
        if nc > 50 * prop.walks
            @warn "Random walk proposals appear to be extremely inefficient. Adjusting the scale-factor accordingly"
            prop.scale *= exp(-1/n)
            nc = accept = reject = 0
        end
    end

    # update proposal scale using acceptance ratio
    update_scale!(prop, accept, reject, n)

    return u, v, logl, ncall
end

# update proposal scale using target acceptance ratio
function update_scale!(prop, accept, reject, n)
    ratio = accept / (accept + reject)
    norm = max(prop.ratio, 1 - prop.ratio) * n
    scale = prop.scale * exp((ratio - prop.ratio) / norm)
    prop.scale = min(scale, sqrt(n))
    return prop
end

"""
    Proposals.RStagger(;ratio=0.5, walks=25, scale=1)

Propose a new live point by random staggering away from an existing live point.
This differs from the random walk proposal in that the step size here is exponentially adjusted
to reach a target acceptance rate _during_ each proposal, in addition to _between_
proposals.

## Parameters
- `ratio` is the target acceptance ratio
- `walks` is the minimum number of steps to take
- `scale` is the proposal distribution scale, which will update _between_ proposals.
"""
@with_kw mutable struct RStagger <: AbstractProposal
    ratio = 0.5
    walks = 25
    scale = 1.0

    @assert 1 / walks ≤ ratio ≤ 1 "Target acceptance ratio must be between 1/`walks` and 1"
    @assert walks > 1 "Number of steps must be greater than 1"
    @assert scale ≥ 0 "Proposal scale must be non-negative"
end

function (prop::RStagger)(rng::AbstractRNG,
                          point::AbstractVector,
                          logl_star,
                          bounds::AbstractBoundingSpace,
                          loglike,
                          prior_transform;
                          kwargs...)
    #setup
    n = length(point)
    scale_init = prop.scale
    accept = reject = fail = nfail = nc = ncall = 0
    stagger = 1
    local du, u_prop, logl_prop, u, v, logl

    while nc < prop.walks || iszero(accept)
        # get proposed point
        while true
            # check scale factor to avoid over-shrinking
            prop.scale < 1e-5 * scale_init && error("Random walk sampling appears to be stuck.")
            # transform to proposal distribution
            du = randoffset(rng, bounds)
            u_prop = @. point + prop.scale * stagger * du
            # inside unit-cube
            unitcheck(u_prop) && break

            fail += 1
            nfail += 1
            # check if stuck generating bad numbers
            if fail > 100 * prop.walks
                @warn "Random number generation appears extremely inefficient. Adjusting the scale-factor accordingly"
                fail = 0
                prop.scale *= exp(-1/n)
            end
        end
        # check proposed point
        v_prop = prior_transform(u_prop)
        logl_prop = loglike(v_prop)
        if logl_prop ≥ logl_star
            u = u_prop
            v = v_prop
            logl = logl_prop
            accept += 1
        else
            reject += 1
        end
        nc += 1
        ncall += 1

        # adjust _stagger_ to target an acceptance ratio of `prop.ratio`
        ratio = accept / (accept + reject)
        if ratio > prop.ratio
            stagger *= exp(1 / accept)
        elseif ratio < prop.ratio
            stagger /= exp(1 / reject)
        end

        # check if stuck generating bad points
        if nc > 50 * prop.walks
            @warn "Random walk proposals appear to be extremely inefficient. Adjusting the scale-factor accordingly"
            prop.scale *= exp(-1 / n)
            nc = accept = reject = 0
        end
    end

    # update proposal scale using acceptance ratio
    update_scale!(prop, accept, reject, n)

    return u, v, logl, ncall
end

"""
    Proposals.Slice(;slices=5, scale=1)

Propose a new live point by a series of random slices away from an existing live point.
This is a standard _Gibbs-like_ implementation where a single multivariate slice is a combination of `slices` univariate slices through each axis.

## Parameters
- `slices` is the minimum number of slices
- `scale` is the proposal distribution scale, which will update _between_ proposals.
"""
@with_kw mutable struct Slice <: AbstractProposal
    slices = 5
    scale = 1.0

    @assert slices ≥ 1 "Number of slices must be greater than or equal to 1"
    @assert scale ≥ 0 "Proposal scale must be non-negative"
end

function (prop::Slice)(rng::AbstractRNG,
                       point::AbstractVector,
                       logl_star,
                       bounds::AbstractBoundingSpace,
                       loglike,
                       prior_transform;
                       kwargs...)
    # setup
    n = length(point)
    nc = nexpand = ncontract = 0
    local u, v, logl

    # modifying axes and computing lengths
    axes = Bounds.axes(bounds)
    axes = prop.scale .* axes'
    # slice sampling loop
    for it in 1:prop.slices

        # shuffle axis update order
        idxs = shuffle!(rng, collect(Base.axes(axes, 1)))

        # slice sample along a random direction
        for idx in idxs

            # select axis
            axis = axes[idx, :]

            u, v, logl, nc, nexpand, ncontract = sample_slice(rng, axis, point, logl_star, loglike,
                                                              prior_transform, nc, nexpand, ncontract)
        end # end of slice sample along a random direction
    end # end of slice sampling loop

    # update slice proposal scale based on the relative size of the slices compared to the initial guess
    prop.scale = prop.scale * nexpand / (2.0 * ncontract)

    return u, v, logl, nc
end   # end of function Slice

"""
    Proposals.RSlice(;slices=5, scale=1)
Propose a new live point by a series of random slices away from an existing live point.
This is a standard _random_ implementation where each slice is along a random direction based on the provided axes.
## Parameters
- `slices` is the minimum number of slices
- `scale` is the proposal distribution scale, which will update _between_ proposals.
"""
@with_kw mutable struct RSlice <: AbstractProposal
    slices = 5
    scale = 1.0

    @assert slices ≥ 1 "Number of slices must be greater than or equal to 1"
    @assert scale ≥ 0 "Proposal scale must be non-negative"
end

function (prop::RSlice)(rng::AbstractRNG,
                       point::AbstractVector,
                       logl_star,
                       bounds::AbstractBoundingSpace,
                       loglike,
                       prior_transform;
                       kwargs...)
    # setup
    n = length(point)
    nc = nexpand = ncontract = 0
    local u, v, logl
    # random slice sampling loop
    for it in 1:prop.slices

        # propose a direction on the unit n-sphere
        drhat = randn(rng, n)
        drhat /= norm(drhat)

        # transform and scale into parameter space
        axis = prop.scale .* (Bounds.axes(bounds) * drhat)
        u, v, logl, nc, nexpand, ncontract = sample_slice(rng, axis, point, logl_star, loglike,
                                                          prior_transform, nc, nexpand, ncontract)
    end # end of random slice sampling loop

    # update random slice proposal scale based on the relative size of the slices compared to the initial guess
    prop.scale = prop.scale * nexpand / (2.0 * ncontract)

    return u, v, logl, nc
end # end of function RSlice

# Method for slice sampling
function sample_slice(rng, axis, u, logl_star, loglike, prior_transform, nc, nexpand, ncontract)
    # define starting window
    r = rand(rng)  # initial scale/offset
    u_l = @. u - r * axis  # left bound
    if unitcheck(u_l)
        v_l = prior_transform(u_l)
        logl_l = loglike(v_l)
    else
        logl_l = -Inf
    end
    nc += 1
    nexpand += 1

    u_r = u_l .+ axis # right bound
    if unitcheck(u_r)
        v_r = prior_transform(u_r)
        logl_r = loglike(v_r)
    else
        logl_r = -Inf
    end
    nc += 1
    nexpand += 1

    # stepping out left and right bounds
    while logl_l ≥ logl_star
        u_l .-= axis
        if unitcheck(u_l)
            v_l = prior_transform(u_l)
            logl_l = loglike(v_l)
        else
            logl_l = -Inf
        end
        nc += 1
        nexpand += 1
    end

    while logl_r ≥ logl_star
        u_r .+= axis
        if unitcheck(u_r)
            v_r = prior_transform(u_r)
            logl_r = loglike(v_r)
        else
            logl_r = -Inf
        end
        nc += 1
        nexpand += 1
    end

    # sample within limits. If the sample is not valid, shrink the limits until the `logl_star` bound is hit
    window_init = norm(u_r - u_l)  # initial window size
    while true
        # define slice and window
        u_hat = u_r - u_l
        window = norm(u_hat)

        # check if the slice has shrunk to be ridiculously small
        window < 1e-5 * window_init && error("Slice sampling appears to be stuck.")

        # propose a new position
        r = rand(rng)
        u_prop = @. u_l + r * u_hat
        if unitcheck(u_prop)
            v_prop = prior_transform(u_prop)
            logl_prop = loglike(v_prop)
        else
            logl_prop = -Inf
        end
        nc += 1
        ncontract += 1

        # if success, then move to the new position
        if logl_prop ≥ logl_star
            return u_prop, v_prop, logl_prop, nc, nexpand, ncontract
        # if fail, then check if the new point is to the left/right of the original point along the proposal axis and update the bounds accordingly
        else
            s = dot(u_prop - u, u_hat) # check sign (+/-)
            if s < 0   # left
                u_l = u_prop
            elseif s > 0  # right
                u_r = u_prop
            else # if `s = 0` something has gone wrong
                error("Slice sampler has failed to find a valid point.")
            end
        end
    end # end of sample within limits while
end

end # module Proposals


# For rubberband, we draw the selection region on the front canvas, and repair
# by copying from the back. Note that the front canvas has
#     user coordinates = device coordinates,
# so device_to_user doesn't do anything. The back canvas has
#     user coordinates = image pixel coordinates,
# so is the correct reference for anything dealing with image pixels.
type RubberBand
    pos1::Vec2
    pos2::Vec2
    moved::Bool
end

function rbdraw(r::GraphicsContext, rb::RubberBand)
    rectangle(r, rb.pos1.x, rb.pos1.y, rb.pos2.x-rb.pos1.x, rb.pos2.y-rb.pos1.y)
    set_line_width(r, 1)
    set_dash(r, [3.0,3.0], 3.0)
    set_source_rgb(r, 1, 1, 1)
    stroke_preserve(r)
    set_dash(r, [3.0,3.0], 0.0)
    set_source_rgb(r, 0, 0, 0)
    stroke_preserve(r)
end

# callback_done is executed when the user finishes drawing the rubberband.
# Its syntax is callback_done(canvas, boundingbox), where the boundingbox is
# in user coordinates.
function rubberband_start(c::Canvas, x, y, callback_done::Function)
    # Copy the surface to another buffer, so we can repaint the areas obscured by the rubberband
    r = getgc(c)
    Graphics.save(r)
    reset_transform(r)
    ctxcopy = copy(r)
    rb = RubberBand(Vec2(x,y), Vec2(x,y), false)
    callbacks_old = (c.mouse.button1motion, c.mouse.button1release)
    c.mouse.button1motion = (c, x, y) -> rubberband_move(c, rb, x, y, ctxcopy)
    c.mouse.button1release = (c, x, y) -> rubberband_stop(c, rb, x, y, ctxcopy, callbacks_old, callback_done)
end

function rubberband_move(c::Canvas, rb::RubberBand, x, y, ctxcopy)
    r = getgc(c)
    if rb.moved
        # Erase the previous rubberband by copying from back surface to front
        set_source(r, ctxcopy)
        # Since the path was already created and preserved, we just modify its properties
        set_line_width(r, 2)
        set_dash(r, Float64[])
        stroke(r)
    end
    rb.moved = true
    # Draw the new rubberband
    rb.pos2 = Vec2(x, y)
    rbdraw(r, rb)
    reveal(c)
    Tk.update()
end

function rubberband_stop(c::Canvas, rb::RubberBand, x, y, ctxcopy, callbacks_old, callback_done)
    c.mouse.button1motion = callbacks_old[1]
    c.mouse.button1release = callbacks_old[2]
    if !rb.moved
        return
    end
    r = getgc(c)
    set_source(r, ctxcopy)
    set_line_width(r, 2)
    stroke(r)
    reveal(c)
    restore(r)
    Tk.update()
    x1, y1 = rb.pos1.x, rb.pos1.y
    if abs(x1-x) > 2 || abs(y1-y) > 2
        # It moved sufficiently, let's execute the callback
        xu, yu = device_to_user(r, x, y)
        x1u, y1u = device_to_user(r, x1, y1)
        zoombb = BoundingBox(min(x1u,xu), max(x1u,xu), min(y1u,yu), max(y1u,yu))
        callback_done(c, zoombb)
    end
end


function ptype_disksize_nodefaults(h::IAEAHeader{Nf, Ni}) where {Nf, Ni}
    1 + # typ
    4 + # energy
    12 + # x,y,z
    8 + # u,v (w is not stored)
    4 + # weight
    4 * Nf +
    4 * Ni
end

function ptype_disksize(h::IAEAHeader{Nf, Ni}) where {Nf, Ni}
    size_reduction_due_to_defaults = sizeof(h.record_contents)
    ptype_disksize_nodefaults(h) - size_reduction_due_to_defaults
end

@generated function read_default(io::IO,
                             ::Val{field},
                             h::IAEAHeader{Nf,Ni,NT}) where {field,Nf,Ni,NT}
    if field in fieldnames(NT)
        :(h.record_contents.$field)
    else
        :(read_(io, Float32))
    end
end

@generated function write_default(io::IO,
                             ::Val{field},
                             p::IAEAParticle,
                             h::IAEAHeader{Nf,Ni,NT}) where {field,Nf,Ni,NT}
    if field in fieldnames(NT)
        quote
            @assert p.$field == h.record_contents.$field
            0
        end
    else
        :(write(io, p.$field))
    end
end
@noinline function read_particle(io::IO, h::IAEAHeader{Nf, Ni}) where {Nf, Ni}

    P = ptype(h)
    typ8 = read_(io, Int8)
    typ = ParticleType(abs(typ8))
    E = read_(io, Float32)
    new_history = E < 0
    E = abs(E)
    x = read_default(io, Val(:x), h)
    y = read_default(io, Val(:y), h)
    z = read_default(io, Val(:z), h)
    u = read_default(io, Val(:u), h)
    v = read_default(io, Val(:v), h)
    weight = read_default(io, Val(:weight), h)

    sign_w = Float32(-1)^(typ8 < 0)
    u,v,w = compute_u_v_w(u,v,sign_w)

    extra_floats = read_(io, NTuple{Nf, Float32})
    extra_ints = read_(io, NTuple{Ni, Int32})
    P(typ,
        E,weight,
        x,y,z,
        u,v,w,
        new_history,
        extra_floats, extra_ints,
    )
end

@noinline function write_particle(io::IO,
                                  p::IAEAParticle{Nf, Ni},
                                  h::IAEAHeader{Nf, Ni}) where {Nf, Ni}
    typ8 = Int8(p.typ)
    sign_typ8 = Int8(-1)^(p.w < 0)
    typ8 = sign_typ8 * typ8
    sign_E = Float32(-1)^(p.new_history)
    E = sign_E * p.E
    ret = 0
    ret += write(io, typ8, E)
    ret += write_default(io, Val(:x), p, h)
    ret += write_default(io, Val(:y), p, h)
    ret += write_default(io, Val(:z), p, h)
    ret += write_default(io, Val(:u), p, h)
    ret += write_default(io, Val(:v), p, h)
    ret += write_default(io, Val(:weight), p, h)
    for f in p.extra_floats
        ret += write(io, f)
    end
    for i in p.extra_ints
        ret += write(io, i)
    end
    ret
end

ptype(h::Type{IAEAHeader{Nf, Ni, NT}}) where {Nf, Ni, NT} = IAEAParticle{Nf, Ni}


export
    SyncBP,
    AsyncBP,
    CoherentBP

"""
    SyncBP(range_size = 10)

A window filter that uses a synchronous window with ThreePassBP with the given range size.
"""
SyncBP(range_size = 10) = WindowFilter(SyncWindow(), ThreePassBP(range_size))

"""
    AsyncBP(range_size = 10, T = Float64)

A window filter that uses an asynchronous window with ThreePassBP with the given range size.
`T` represents the time type and must be the same as used in creation of the runtime.
"""
AsyncBP(range_size = 10, T = Float64) = WindowFilter(AsyncWindow{T}(), ThreePassBP(range_size))

"""
    CoherentBP(range_size = 10, T = Float64)

A window filter that uses a coherent window with ThreePassBP with the given range size.
`T` represents the time type and must be the same as used in creation of the runtime.
"""
CoherentBP(range_size = 10, T = Float64) = WindowFilter(CoherentWindow{T}(), ThreePassBP(range_size))


module StructuredVariationalBayesTest

using Test
using ForneyLab
using ForneyLab: SoftFactor, generateId, addNode!, associate!, inferUpdateRule!, outboundType, isApplicable, setTargets!, messagePassingSchedule
using ForneyLab: VBGaussianMeanVarianceOut, SVBGaussianMeanPrecisionMGVD, SVBGaussianMeanPrecisionOutVGD, VBGammaOut, SVBGaussianMeanPrecisionW

# Integration helper
mutable struct MockNode <: SoftFactor
    id::Symbol
    interfaces::Vector{Interface}
    i::Dict{Int,Interface}

    function MockNode(vars::Vector{Variable}; id=generateId(MockNode))
        n_interfaces = length(vars)
        self = new(id, Vector{Interface}(undef, n_interfaces), Dict{Int,Interface}())
        addNode!(currentGraph(), self)

        for idx = 1:n_interfaces
            self.i[idx] = self.interfaces[idx] = associate!(Interface(self), vars[idx])
        end

        return self
    end
end

@structuredVariationalRule(:node_type     => MockNode,
                           :outbound_type => Message{PointMass},
                           :inbound_types => (Nothing, Message{PointMass}, ProbabilityDistribution),
                           :name          => SVBMock1VGD)

@structuredVariationalRule(:node_type     => MockNode,
                           :outbound_type => Message{PointMass},
                           :inbound_types => (Message{PointMass}, Nothing, ProbabilityDistribution),
                           :name          => SVBMock2GVD)

@structuredVariationalRule(:node_type     => MockNode,
                           :outbound_type => Message{PointMass},
                           :inbound_types => (ProbabilityDistribution, Nothing),
                           :name          => SVBMock3DV)

@testset "@structuredVariationalRule" begin
    @test SVBMock1VGD <: StructuredVariationalRule{MockNode}
    @test SVBMock2GVD <: StructuredVariationalRule{MockNode}
    @test SVBMock3DV <: StructuredVariationalRule{MockNode}
    @test isApplicable(SVBMock1VGD, [Nothing, Message{PointMass}, ProbabilityDistribution])
    @test !isApplicable(SVBMock1VGD, [Nothing, Message{PointMass}, ProbabilityDistribution, ProbabilityDistribution])
end

@testset "inferUpdateRule!" begin
    g = FactorGraph()
    v1 = Variable()
    n1 = MockNode([v1])
    v2 = Variable()
    n2 = MockNode([v2])
    v3 = Variable()
    n3 = MockNode([v3])
    nd = MockNode([v1, v2, v3])

    pfz = PosteriorFactorization()
    pf12 = PosteriorFactor([v1, v2])
    pf3 = PosteriorFactor(v3)
    setTargets!(pf12, pfz)
    setTargets!(pf3, pfz)

    entry1 = ScheduleEntry(nd.i[1], StructuredVariationalRule{MockNode})
    inferUpdateRule!(entry1, entry1.message_update_rule, Dict{Interface, Type}(nd.i[2].partner => Message{PointMass}))
    @test entry1.message_update_rule == SVBMock1VGD

    entry2 = ScheduleEntry(nd.i[2], StructuredVariationalRule{MockNode})
    inferUpdateRule!(entry2, entry2.message_update_rule, Dict{Interface, Type}(nd.i[1].partner => Message{PointMass}))
    @test entry2.message_update_rule == SVBMock2GVD

    entry3 = ScheduleEntry(nd.i[3], StructuredVariationalRule{MockNode})
    inferUpdateRule!(entry3, entry3.message_update_rule, Dict{Interface, Type}())
    @test entry3.message_update_rule == SVBMock3DV
end

@testset "messagePassingSchedule" begin
    g = FactorGraph()
    s_0 = Variable()
    nd_s_0 = GaussianMeanVariance(s_0, constant(0.0), constant(1.0))
    w = Variable()
    nd_w = Gamma(w, constant(1.0), constant(1.0))

    s = Variable[]
    nd_s = FactorNode[]

    s_min = s_0
    for i = 1:3
        s_i = Variable()
        nd_s_i = GaussianMeanPrecision(s_i, s_min, w)
        push!(s, s_i)
        push!(nd_s, nd_s_i)

        s_min = s_i
    end
    nd_s_i = GaussianMeanVariance(s_min, constant(0.0), constant(huge))
    push!(nd_s, nd_s_i)

    pfz = PosteriorFactorization()
    q_w = PosteriorFactor(w)
    q_s = PosteriorFactor([s_0; s])

    setTargets!(q_s, pfz, external_targets=true)
    schedule_q_s = messagePassingSchedule(q_s)
    @test length(schedule_q_s) == 8
    @test ScheduleEntry(nd_s_0.i[:out], VBGaussianMeanVarianceOut) in schedule_q_s
    @test ScheduleEntry(nd_s[4].i[:out], VBGaussianMeanVarianceOut) in schedule_q_s
    @test ScheduleEntry(nd_s[3].i[:m], SVBGaussianMeanPrecisionMGVD) in schedule_q_s
    @test ScheduleEntry(nd_s[2].i[:m], SVBGaussianMeanPrecisionMGVD) in schedule_q_s
    @test ScheduleEntry(nd_s[1].i[:m], SVBGaussianMeanPrecisionMGVD) in schedule_q_s
    @test ScheduleEntry(nd_s[1].i[:out], SVBGaussianMeanPrecisionOutVGD) in schedule_q_s
    @test ScheduleEntry(nd_s[2].i[:out], SVBGaussianMeanPrecisionOutVGD) in schedule_q_s
    @test ScheduleEntry(nd_s[3].i[:out], SVBGaussianMeanPrecisionOutVGD) in schedule_q_s

    setTargets!(q_w, pfz, external_targets=true)
    schedule_q_w = messagePassingSchedule(q_w)
    @test length(schedule_q_w) == 6
    @test ScheduleEntry(nd_w.i[:out], VBGammaOut) in schedule_q_w
    @test ScheduleEntry(nd_s[1].i[:w], SVBGaussianMeanPrecisionW) in schedule_q_w
    @test ScheduleEntry(nd_s[2].i[:w], SVBGaussianMeanPrecisionW) in schedule_q_w
    @test ScheduleEntry(nd_s[3].i[:w], SVBGaussianMeanPrecisionW) in schedule_q_w
end

@testset "messagePassingAlgorithm" begin
    g = FactorGraph()
    s_0 = Variable()
    nd_s_0 = GaussianMeanVariance(s_0, constant(0.0), constant(1.0))
    w = Variable()
    nd_w = Gamma(w, constant(1.0), constant(1.0))

    s = Variable[]
    nd_s = FactorNode[]

    s_min = s_0
    for i = 1:3
        s_i = Variable()
        nd_s_i = GaussianMeanPrecision(s_i, s_min, w)
        push!(s, s_i)
        push!(nd_s, nd_s_i)

        s_min = s_i
    end
    nd_s_i = GaussianMeanVariance(s_min, constant(0.0), constant(huge))
    push!(nd_s, nd_s_i)

    pfz = PosteriorFactorization([s_0; s], w)
    algo = messagePassingAlgorithm(s)

    @test isa(algo, InferenceAlgorithm)
end

end # module


# ------------------------------------------------------------------
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------

# merge interval tables
function mergetables(intervals, pars)
    # get column names
    bh, from, to = pars.holeid, pars.from, pars.to

    # read all interval tables
    interv = intervals isa Interval ? [intervals] : intervals
    tabs = [f.file isa String ? CSV.read(f.file, DataFrame) : f.file for f in interv]

    # rename main columns if necessary
    for i in 1:length(tabs)
        t = interv[i]
        rename!(tabs[i], t.holeid => bh, t.from => from, t.to => to)
    end

    # merge all tables and get unique from
    hcols = [bh,from,to]
    out   = vcat(map(x->select(x,hcols),tabs)..., cols=:union)
    out   = unique(vcat(select(out,[bh,from]),rename(select(out,[bh,to]), to => from), cols=:union))
    sort!(out, [bh, from])

    # add unique to and calculate length
    shift = collect(2:size(out,1))
    push!(shift,1) # check if works for every case
    out[!,to]   = out[shift,from]
    out[!,:CHK] = out[shift,bh]
    out = out[(out[!,to] .> out[!,from]) .& (out[!,bh] .== out[!,:CHK]),[bh,from,to]]
    out[!,:LENGTH] = out[!,to] - out[!,from]

    # add all tables values to the merged intervals
    cols = []
    for i in 1:length(tabs)
        push!(cols,setdiff(names(tabs[i]),string.(hcols)))
        tabs[i][!,"_LEN$i"] = tabs[i][!,to]-tabs[i][!,from]
        out = leftjoin(out,select(tabs[i], Not(to)),on=[bh,from],makeunique=true)
        select!(tabs[i], Not("_LEN$i"))
    end

    # leftjoin might affect order of the output after DataFrames 1.0. sort again
    sort!(out, [bh, from])

    # loop all merged intervals and columns
    for i in 1:size(out,1)
        for j in 1:length(cols)
            # check how many merged intervals compose one table interval
            ismissing(out[i,"_LEN$j"]) && continue
            adj = Dict(1 => 0, 2 => out[i,"_LEN$j"]-out[i,"LENGTH"])
            adj[2] ≈ 0 && continue
            while adj[2] > 0 && adj[2] ≉  0
                adj[1] += 1
                adj[2] -= out[i+adj[1],"LENGTH"]
            end

            # repeat table interval values to next i2 merged intervals
            i1, i2 = (i+1), (i+adj[1])
            for col in cols[j]
                crow = out[i,col]
                for k in i1:i2
                    out[k,col] = crow
                end
            end
        end
    end

    # delete auxiliary length variables
    tcols = [Symbol("_LEN$j") for j in 1:length(tabs)]
    select!(out, Not(tcols))
end


import Celeste.SDSSIO: RunCamcolField, PlainFITSStrategy

function parse_rcfs_nsrcs(rcf_nsrcs_file)
    all_rcfs = Vector{RunCamcolField}()
    all_rcf_nsrcs = Vector{Int16}()
    open(rcf_nsrcs_file) do f
        for ln in eachline(f)
            lp = split(ln, '\t')
            run = parse(Int16, lp[1])
            camcol = parse(UInt8, lp[2])
            field = parse(Int16, lp[3])
            nsrc = parse(Int16, lp[4])
            push!(all_rcfs, RunCamcolField(run, camcol, field))
            push!(all_rcf_nsrcs, nsrc)
        end
    end
    all_rcfs, all_rcf_nsrcs
end

function parse_boxfile(boxfile)
    boxes = Vector{BoundingBox}()
    boxes_rcf_idxs = Vector{Vector{Int32}}()
    open(boxfile) do f
        for ln in eachline(f)
            lp = split(ln, '\t')
            if length(lp) != 5
                Log.one_message("ERROR: malformed line in box file:\n> $ln ")
                return boxes, boxes_rcf_idxs
            end

            ss = split(lp[4], ' ')
            ramin = parse(Float64, ss[1])
            ramax = parse(Float64, ss[2])
            decmin = parse(Float64, ss[3])
            decmax = parse(Float64, ss[4])
            bb = BoundingBox(ramin, ramax, decmin, decmax)
            push!(boxes, bb)

            ris = split(lp[5], ',')
            rcf_idxs = [parse(Int32, ri) for ri in ris]
            push!(boxes_rcf_idxs, rcf_idxs)
        end
    end
    if length(boxes) < 1
        Log.one_message("$boxfile is empty?")
    end
    return boxes, boxes_rcf_idxs
end


#
# Copyright (c) 2021 Tobias Thummerer, Lars Mikelsons, Josef Kircher
# Licensed under the MIT license. See LICENSE file in the project root for details.
#

using Plots
using OrdinaryDiffEq: ODESolution

"""
Plots ODE-Solution from FMU (ODE states are interpreted as the FMU states).

Optional `t_in_solution` controls if the first state in the solution is interpreted as t(ime).
Optional keyword argument `maxLabelLength` controls the maximum length for legend labels (too long labels are cut from front).
"""
function fmiPlot(fmu::FMU2, solution::ODESolution, t_in_solution = false; maxLabelLength=64)

    t = []

    offset = 0
    if t_in_solution
        t = collect(data[1] for data in solution.u)
        offset = 1
    else
        t = solution.t
        offset = 0
    end

    numStates = length(solution.u[1]) - offset

    fig = Plots.plot(xlabel="t [s]")
    for s in 1:numStates
        vr = fmu.modelDescription.stateValueReferences[s]
        vrName = fmi2ValueReference2String(fmu, vr)[1]

        values = collect(data[s+offset] for data in solution.u)

        # prevent legend labels from getting too long
        label = "$vrName ($vr)"
        labelLength = length(label)
        if labelLength > maxLabelLength
            label = "..." * label[labelLength-maxLabelLength:end]
        end

        Plots.plot!(fig, t, values, label=label)
    end
    fig
end

# extend the original plot-command by plotting FMUs
function Plots.plot(fmu::FMU2, solution::ODESolution, t_in_solution = false)
    fmiPlot(fmu, solution, t_in_solution)
end

"""
Plots fmi2SimulationResult.
"""
function fmiPlot(sd::fmi2SimulationResult)

    @assert sd != nothing ["fmiPlot(...): Simulation result is nothing, maybe you started a simulation without keyword `recordValues` or with `recordValues=[]` ?"]

    ts = fmi2SimulationResultGetTime(sd)

    numVars = length(sd.dataPoints[1])-1

    fig = Plots.plot(xlabel="t [s]")
    for i in 1:numVars
        vr = sd.valueReferences[i]
        vrName = fmi2ValueReference2String(sd.fmu, vr)[1]
        values = fmi2SimulationResultGetValuesAtIndex(sd, i+1)
        Plots.plot!(fig, ts, values, label="$vrName ($vr)")
    end
    fig
end

# extend the original plot-command by plotting FMUs.
function Plots.plot(sd::fmi2SimulationResult)
    fmiPlot(sd)
end


import .AST: ASTMDP, ASTMetrics

"""
    print_metrics(planner)
    print_metrics(mdp::ASTMDP)
    print_metrics(metrics::ASTMetrics)

Print failure metrics including:
- iteration of first failure
- number of failures
- failure rate
"""
print_metrics(planner) = print_metrics(planner.mdp.metrics)
print_metrics(mdp::ASTMDP) = print_metrics(mdp.metrics)
function print_metrics(metrics::ASTMetrics)
    E = metrics.event

    if findfirst(E) === nothing
        @info "No failures recorded."
        return 0
    else
        first_failure = findfirst(E)
        num_evals = length(E)
        println("First failure: ", first_failure, " of ", num_evals)
        num_failures = sum(E)
        println("Number of failures: ", num_failures)
        failure_rate = sum(E)/length(E) * 100
        println("Failure rate: ", round(failure_rate, digits=5), "%")
        return failure_rate
    end
end


using CLI

add(x::Int, y::Int) = x + y
mul(x::Int, y::Int) = x * y

leaf1 = LeafCommand(:add, add, Signature([Int, Int], []))
leaf2 = LeafCommand(:mul, mul, Signature([Int, Int], []))
node = NodeCommand("math", [leaf1, leaf2])

maincmd = MainCommand("cal", [], [node])

stream = ARGStream(ARGS)
resolve(stream, maincmd; debug=false)


immutable LocationIterator{w,T}
    range::UnitRange{Int}
    index::FMIndex{w,T}
end

length(iter::LocationIterator) = length(iter.range)
@inline start(iter::LocationIterator) = 1
@inline done(iter::LocationIterator, i) = i > length(iter)
@inline next(iter::LocationIterator, i) = sa_value(iter.range[i], iter.index) + 1, i + 1


using Snappy
##### set a path
path = "c:/scratch/test.parquet"

##### load metadata
metadata(path)

#### continues to try

io = open(path)

# the dicvtionary page starts here at 4
seek(io, 4); dict_page_header = read_thrift(io, PAR2.PageHeader)

compressed_length = dict_page_header.compressed_page_size

data_compressed = read(io, compressed_length)
data_uncompressed = Snappy.uncompress(b)

data_actual = reinterpret(Int64, data_uncompressed)

next_page = read_thrift(io, PAR2.PageHeader)

Parquet.coltype(page.colchunk)

io = IOBuffer(data_unc)
using Parquet
@which Parquet.read_plain_values(io, num_values, ctype)

typ = ctype
@which Parquet.read_plain(io, typ)

Parquet.PLAIN_JTYPES[typ+1]

@which Parquet.read_fixed(io, Int64)

ret = convert(UInt64, 0)
N = 8
T = UInt64
for n in 0:(N-1)
    global ret
    byte = convert(T, read(io, UInt8))
    ret |= (byte << *(8,n))
end
reinterpret(Int64, ret)

res = [read(io, Int64) for i in 1:389490]

Int(length(data_unc)//8)

pos= position(io)
read_thrift(io, PAR2.PageHeader)

data = read(io, 34966)
data_unc = Snappy.uncompress(data)

reinterpret(Int32, data_unc)

seek(io, 4)
read_thrift(io, PAR2.PageHeader)

num_values
read_plain_values(io, num_values, 6)

using StatsBase
countmap([pg.hdr._type for pg in pgs])

Debugger.@enter read_column(path, 1)


mutable struct UnfoldProposeState{T}
    choices::DynamicChoiceMap # NOTE: could be read-only vector assignment
    weight::Float64
    retvals::Vector{T}
    state::T
end

function process_new!(gen_fn::Unfold{T,U}, params::Tuple, key::Int,
                      state::UnfoldProposeState{T}) where {T,U}
    local new_state::T
    kernel_args = (key, state.state, params...)
    (submap, weight, new_state) = propose(gen_fn.kernel, kernel_args)
    set_submap!(state.choices, key, submap)
    state.weight += weight
    state.retvals[key] = new_state
    state.state = new_state
end

function propose(gen_fn::Unfold{T,U}, args::Tuple) where {T,U}
    len = args[1]
    init_state = args[2]
    params = args[3:end]
    choices = choicemap()
    state = UnfoldProposeState{T}(choices, 0., Vector{T}(undef,len), init_state)
    for key=1:len
        process_new!(gen_fn, params, key, state)
    end
    (state.choices, state.weight, PersistentVector{T}(state.retvals))
end


using DataStructures

"""
Hybrid Actor-critic PF
----------------------

Hybrid model : Actor-critic & Value iteration with Particle filter

This algorithm is a weighted sum of Actor-critic and a model-based algorithm
that implements value iteration and estimates the
transition matrix via a particle filter.
In other words, the model-based structure is the same as in
Forward Learner, but now the world-model is estimated optimally and not via a
delta rule.

See learner/forwardlearnerBF.jl, learner/actorcriticlearner.jl
and learner/hybridlearner.jl for the core implementation.

Note 1
------
With particle filtering we need to run multiple times with different seeds.
As final LL we report the MEAN across seeds.
For signals/results we keep track of the ones of all seeds
and in the end report the one with the MAXIMUM LL.

Note 2
------
get_all_puzzles(), get_all_episodes_in_puzzle(), get_data_by_puzzle_episode()
are in rl_data_toolsforalgos.jl

Note 3
------
Temperature of softmax is removed, because this learner in NOT convex and weights
can be in (0, +Inf). The temperature is absorbed in the weights. Otherwise we
it is overparametrized.
"""
function getLL_Hybrid_AC_FWD_BF(data::Array{Int,2},
                            alpha::Float64, eta_critic::Float64,
                            gamma::Float64, lambda_actor::Float64,
                            lambda_critic::Float64,
                            surpriseprobability::Float64,
                            stochasticity::Float64,
                            nparticles::Union{Int, Float64},
                            w_MB::Float64,
                            w_MF::Float64;
                            particlefilterseeds = [1,2,3],
                            init_R = 0.,
                            init_policy=nothing,
                            init_Vs = nothing,
                            use_replacing_trace=true,
                            do_debug_print = false)

    nparticles = converttoInt(nparticles)
    data_stats, nr_states, nr_actions, nr_samples = get_data_stats_and_dimensions(data)
    if data_stats.nr_subjects != 1
        error("Invalid argument: getLL_Hybrid_AC_FWD supports only one subject")
    end
    all_puzzles = get_all_puzzles(data)

    action_selection_probs_seeds = Array{Array{Float64, 1}, 1}(undef, length(particlefilterseeds))
    [action_selection_probs_seeds[i_seed] = zeros(Float64, nr_samples) for i_seed in 1:length(particlefilterseeds)]
    LL_seeds = zeros(Float64, length(particlefilterseeds))

    Tsas_seeds = Array{Any, 1}(undef, length(particlefilterseeds))

    # signals:
    TD_seeds =Array{Array{Float64,1}, 1}(undef, length(particlefilterseeds))
    [TD_seeds[i_seed] = zeros(Float64, nr_samples) for i_seed in 1:length(particlefilterseeds)]
    Surprise_seeds =Array{Array{Float64,1}, 1}(undef, length(particlefilterseeds))
    [Surprise_seeds[i_seed] = zeros(Float64, nr_samples) for i_seed in 1:length(particlefilterseeds)]
    V_state_seeds =Array{Array{Float64,1}, 1}(undef, length(particlefilterseeds))
    [V_state_seeds[i_seed] = zeros(Float64, nr_samples + data_stats.nr_rewards) for i_seed in 1:length(particlefilterseeds)]
    γ_surprise_seeds =Array{Array{Float64,1}, 1}(undef, length(particlefilterseeds))
    [γ_surprise_seeds[i_seed] = zeros(Float64, nr_samples) for i_seed in 1:length(particlefilterseeds)]
    SPE_seeds =Array{Array{Float64,1}, 1}(undef, length(particlefilterseeds))
    [SPE_seeds[i_seed] = zeros(Float64, nr_samples) for i_seed in 1:length(particlefilterseeds)]

    final_policyparameters_record_seed = Array{Dict{Int,Any}, 1}(undef, length(particlefilterseeds))
    [final_policyparameters_record_seed[i_seed] = Dict{Int,Any}() for i_seed in 1:length(particlefilterseeds)]
    finalQSA_Hybrid_record_seed = Array{Dict{Int,Any}, 1}(undef, length(particlefilterseeds))
    [finalQSA_Hybrid_record_seed[i_seed] = Dict{Int,Any}() for i_seed in 1:length(particlefilterseeds)]
    finalQSA_MB_record_seed = Array{Dict{Int,Any}, 1}(undef, length(particlefilterseeds))
    [finalQSA_MB_record_seed[i_seed] = Dict{Int,Any}() for i_seed in 1:length(particlefilterseeds)]

    for i_seed in 1:length(particlefilterseeds)
        learnerMB = []
        learnerMF = []
        learnerHybrid = []
        modulatingfactor = 0.
        action_counter = 0
        for i_puzzle in all_puzzles
            # Puzzle = independent block of the experiment.
            # The code was made for general purposes, in this experiment we
            # have only ONE puzzle.
            learnerMB = ForwardLearnerBF(nr_states = nr_states,
                            nr_actions = nr_actions,
                            γ = gamma,
                            init_R = 0.,
                            nparticles = nparticles,
                            surpriseprobability = surpriseprobability,
                            stochasticity = stochasticity,
                            seedlearner = particlefilterseeds[i_seed],
                            )
            if init_policy == nothing # providing init_policy w/o init_Vs is considered a bug
                learnerMF = ActorCritic(nr_states = nr_states,
                                nr_actions = nr_actions,
                                α_critic = eta_critic,
                                α_actor = alpha,
                                γ = gamma,
                                λ_critic = lambda_critic,
                                λ_actor = lambda_actor,
                                use_replacing_trace = use_replacing_trace,
                                do_debug_print = do_debug_print)
            else
                learnerMF = ActorCritic(nr_states = nr_states,
                                nr_actions = nr_actions,
                                α_critic = eta_critic,
                                α_actor = alpha,
                                γ = gamma,
                                λ_critic = lambda_critic,
                                λ_actor = lambda_actor,
                                policyparameters = copy(init_policy[current_puzzle_id]),
                                V = copy(init_Vs[current_puzzle_id]),
                                use_replacing_trace = use_replacing_trace,
                                do_debug_print = do_debug_print)
            end
            learnerHybrid = Hybrid(nr_states = nr_states,
                            nr_actions = nr_actions,
                            w_MB = w_MB,
                            w_MF = w_MF
                            )
            all_epi_in_puzzle = get_all_episodes_in_puzzle(data, i_puzzle)
            for i_episode in all_epi_in_puzzle
                episode_data = get_data_by_puzzle_episode(data, i_puzzle, i_episode)
                path_length = size(episode_data)[1]  # = T
                if do_debug_print
                    println("i_puzzle:$i_puzzle | i_episode:$i_episode | path_length:$path_length")
                end
                isendofexperiment = false
                for t in 1:path_length
                    action_counter += 1
                    state = episode_data[t, Int(c_state)]
                    action = episode_data[t, Int(c_action)]
                    reward = episode_data[t, Int(c_reward)]
                    next_state = episode_data[t, Int(c_nextState)]
                    done = reward>0 ? true : false
                    if do_debug_print
                        # println("t:$t | s:$state | a:$action | R:$reward | s':$next_state | p(s,a):$(round(p_action,2)) | p(s,1;+-bias): $(round(p1_pos,2))/$(round(p1_neg,2))")
                        println("t:$t | s:$state | a:$action | R:$reward | s':$next_state | done: $done")
                    end

                    # Hybrid learner
                    update!(learnerHybrid, state, action, reward, learnerMB.QSA, learnerMF.policyparameters) #Q_hybrid =  w_MB*learnerMB.QSA + w_MF*learnerMF.QSA

                    p_actions = getSoftmaxProbs(learnerHybrid.QSA[state, :], 1., [0., 0.])

                    action_selection_probs_seeds[i_seed][action_counter] = copy(p_actions[action])
                    LL_seeds[i_seed] += log(p_actions[action])

                    # ForwardLearner-like update (ie Value iteration),
                    # but with model estimated via particle filter
                    update!(learnerMB, state, action, next_state, reward)
                    γ_surprise_seeds[i_seed][action_counter] = copy(learnerMB.Testimate.γ_surprise[1])
                    Surprise_seeds[i_seed][action_counter] = copy(learnerMB.Testimate.Sgm[1])
                    SPE_seeds[i_seed][action_counter] = copy(learnerMB.Testimate.SPE[1])

                    # Actor-Critic
                    update!(learnerMF, state, action, next_state, reward, p_actions[action], modulatingfactor)
                    TD_seeds[i_seed][action_counter] = copy(learnerMF.td_error[1])

                end # end t
            end # end i_episode
            final_policyparameters_record_seed[i_seed][i_puzzle] = copy(learnerMF.policyparameters)
            finalQSA_Hybrid_record_seed[i_seed][i_puzzle] = copy(learnerHybrid.QSA)
            finalQSA_MB_record_seed[i_seed][i_puzzle] = copy(learnerMB.QSA)
        end # end i_puzzle
        Tsas_seeds[i_seed]= deepcopy(learnerMB.Testimate.Ps1a0s0)
    end # seeds

    LL = get_meanLL_acrossseeds(LL_seeds)

    action_selection_probs_cat = hcat(action_selection_probs_seeds...)
    action_selection_probs = mean(action_selection_probs_cat, dims=2)[:]
    trial_by_trial_p_sa = (action_selection_probs)

    # For signals: return the ones corresponding to the Max LL across seeds
    LL_max, seed_idx = findmax(LL_seeds)
    TD_record = copy(TD_seeds[seed_idx])
    SPE_record = copy(SPE_seeds[seed_idx])
    trial_by_trial_p_sa_max = (action_selection_probs_seeds[seed_idx])

    algo = "hybrid_actorcritic_FWD_BF"

    param_v = [alpha, eta_critic, gamma, lambda_actor, lambda_critic, surpriseprobability, stochasticity, nparticles, w_MB, w_MF]
    param_str = "alpha=$(round(alpha,digits=4)),
                eta_critic=$(round(eta_critic,digits=4)),
                gamma=$(round(gamma,digits=4)),
                lambda_actor=$(round(lambda_actor,digits=4)),
                lambda_critic=$(round(lambda_critic,digits=4)),
                surpriseprobability=$(round(surpriseprobability,digits=4)),
                stochasticity=$(round(stochasticity,digits=4)),
                nparticles=$nparticles,
                w_MB=$(round(w_MB,digits=4)),
                w_MF=$(round(w_MF,digits=4))"

    results_dict = Dict{String,Any}("finalQSA_Hybrid_record" => finalQSA_Hybrid_record_seed[seed_idx],
                                    "final_PolicyParameters_MF" => final_policyparameters_record_seed[seed_idx],
                                    "finalQSA_MB_record" => finalQSA_MB_record_seed[seed_idx],
                                    "final_Tsas" => Tsas_seeds[seed_idx],
                                    "Surprise" => Surprise_seeds[seed_idx],
                                    "gamma_surprise" => γ_surprise_seeds[seed_idx],
                                    "nparticles" => nparticles,
                                    "particlefilterseeds" => particlefilterseeds,
                                    "trial_by_trial_p_sa_max" => trial_by_trial_p_sa_max)

    signals = Step_by_step_signals(data_stats.all_subject_ids[1],
    TD_record, #RPE
    SPE_record, #SPE
    trial_by_trial_p_sa,
    LL,
    data_stats,
    algo,
    param_v,
    param_str,
    99, #biased_action_idx, dummy value
    results_dict)
    return LL, signals
end


printstyled("  resample!\n", color=:light_green)

function check_resamp()
  S = randSeisData(fs_min=20.0)
  sz = sizeof(S)
  (xx, ta, aa, xx, xx) = @timed resample(S, fs=10.0)
  return [aa/sz ta]
end

N = 20
check_resamp()
C = randSeisChannel()
nx = 8640000
C.t = [1 0; nx 0]
C.fs = 100.0
C.x = rand(Float32, nx)
resample!(C, 50.0)

C = randSeisChannel()
C.t = [       1  1559599308051202;
          29229          46380573;
         194240                 0]
C.x = randn(194240)
C.fs = 40.0
resample!(C, 10.0)
@test length(C.x) == 48560

# A controlled test with two groups of channels, 40.0 Hz and 20.0 Hz
printstyled("    downsample\n", color=:light_green)
S = randSeisData(4)
S.id = ["NN.STA1.00.EHZ", "NN.STA2.00.EHZ", "NN.STA3.00.EHZ", "NN.STA4.00.EHZ"]
n = 400
fs = 20.0
t20 = [1 1559599308051202; n 0]
t40 = [1 1559599308051202; 2n 0]
S.fs = [fs, 2fs, fs, 2fs]
S.t = [deepcopy(t20), deepcopy(t40), deepcopy(t20), deepcopy(t40)]
S.x = [randn(n), randn(2n), randn(n), randn(2n)]
U = deepcopy(S)
resample!(S, fs=fs)
for i = 1:S.n
  @test length(S.x[i]) == n
  @test S.fs[i] == fs
  @test S.t[i] == t20
end

norm_sz = Array{Float64,2}(undef,N,2)
printstyled("      trial ", color=:light_green)
for i = 1:N
  if i > 1
    print("\b\b\b\b\b")
  end
  printstyled(string(lpad(i, 2), "/", N), color=:light_green)
  norm_sz[i,:] = check_resamp()
end
println("")
println(stdout, "  mean overhead (", N, " random trials): ",
  @sprintf("%0.1f%%", 100.0*(mean(norm_sz[:,1])-1.0)), "; ",
  "mean t = " , @sprintf("%0.2f", mean(norm_sz[:,2])))

# Now with upsampling
printstyled("    upsample (Issue #50)\n", color=:light_green)
S = deepcopy(U)
resample!(S, fs=2fs)
for i = 1:S.n
  L = length(S.x[i])
  @test L in (2n, 2n-1)
  @test S.fs[i] == 2fs
  @test S.t[i] == (L == 2n ? t40 : [1 1559599308051202; 2n-1 0])
end

printstyled("    test on long, gapless Float32 SeisChannel\n", color=:light_green)

S = randSeisChannel()
nx = 8640000
S.t = [1 0; nx 0]
S.fs = 100.0
S.x = rand(Float32, nx)
U = SeisData(deepcopy(S))
C = resample(S, 50.0)
sz = sizeof(S)

(xx, ta, aa, xx, xx) = @timed resample!(S, 50.0)
println(stdout, "  resample!: overhead = ",
  @sprintf("%0.1f%%", 100.0*(aa/sz - 1.0)), ", t = ",
  @sprintf("%0.2f", ta), " s")


"""
Package: Forecast

    seaborne(query = "imports")

Return estimates of world seaborne trade from AIS data collected by Marine Traffic.

The data contains deadweight imports and exprts for France, Germany and the United Kingdom from 2015-04-01 to 2021-05-02. The fields are:

num_pc:     number of port calls
mtc:        metric tons of cargo
dwt:        deadweight tonnage
suffix_ma:  30-day moving averages

Data available at UN COMTRADE Monitor.Cerdeiro, Komaromi, Liu and Saeed (2020).

# Arguments
- `query`: Defaults to "paper" and accepts the following values:
    - "full": Full original dataset from from 2015-04-01 to 2021-05-02
    - "imports": Subset with imports.

# Returns
Dataframe containing information based on the parameter `query`

# Examples
```julia-repl
julia> seaborne()
[ Info: Seaborne deadweight trade imports from AIS
2199×4 DataFrame
  Row │ Date        France   Germany  UK
      │ Date        Int64    Int64    Int64
──────┼──────────────────────────────────────
    1 │ 2015-04-01   507946   878377  599573
    2 │ 2015-04-02   332043  1501614  772714
    3 │ 2015-04-03   810077   941663  262994
   [...]
```
"""
function seaborne(query::String = "imports")

    @assert query in ["full","imports"]

    path = art_path("seaborne")

    sb_df = open(joinpath(path, "seaborne.csv")) do file
        CSV.read(file,DataFrame,
                 dateformat = "mm/dd/yyyy HH:MM:SS pp",
                 types = Dict(:date => Date))
    end

    if query == "full"
        @info "Full dataset with estimates of world seaborne trade from AIS"
        return sb_df
    end

    if query == "imports"
        @info "Seaborne deadweight trade imports from AIS"

        # Group by Country and Flow to select Imports
        gbt = groupby(sb_df, [:country_name, :flow], sort=true)
        return DataFrame(Dict(:Date => gbt[1].date ,
                              :UK => gbt[1].dwt, :Germany => gbt[3].dwt, :France => gbt[5].dwt))

    end
end


import MappedArrays

"""
    OneStateAutomaton

Automaton with one state and the `nt` events 1, ..., `nt`.
"""
struct OneStateAutomaton <: AbstractAutomaton
    nt::Int
end

"""
    OneStateTransition

Transition of `OneStateAutomaton` with label `σ`.
"""
struct OneStateTransition <: AbstractTransition
    σ::Int
end

states(A::OneStateAutomaton) = Base.OneTo(1)
nstates(A::OneStateAutomaton) = 1
transitiontype(A::OneStateAutomaton) = OneStateTransition
function transitions(A::OneStateAutomaton)
    # mappedarray avoid allocation
    MappedArrays.mappedarray(OneStateTransition, Base.OneTo(A.nt))
end
ntransitions(A::OneStateAutomaton) = A.nt
source(::OneStateAutomaton, t::OneStateTransition) = 1
event(::OneStateAutomaton, t::OneStateTransition) = t.σ
target(::OneStateAutomaton, t::OneStateTransition) = 1
in_transitions(A::OneStateAutomaton, s) = transitions(A)
out_transitions(A::OneStateAutomaton, s) = transitions(A)
function has_transition(A::OneStateAutomaton, q, r)
    @assert q == 1
    @assert r == 1
    return !iszero(A.nt)
end
function has_transition(A::OneStateAutomaton, t::OneStateTransition)
    return 1 <= t.σ <= A.nt
end

mutable struct OneStateStateProperty{T} <: StateProperty{T}
    automaton::OneStateAutomaton
    value::Union{Nothing, T}
end
state_property_type(::Type{OneStateAutomaton}, T::Type) = OneStateStateProperty{T}
function state_property(automaton::OneStateAutomaton, T::Type)
    return OneStateStateProperty{T}(automaton, nothing)
end
function Base.getindex(p::OneStateStateProperty,
                       s::Int)
    @assert isone(s)
    p.value
end
function Base.setindex!(p::OneStateStateProperty, value,
                        s::Int)
    @assert isone(s)
    p.value = value
end

struct OneStateTransitionProperty{T} <: TransitionProperty{T}
    automaton::OneStateAutomaton
    value::Vector{T}
end
transition_property_type(::Type{OneStateAutomaton}, T::Type) = OneStateTransitionProperty{T}
function transition_property(automaton::OneStateAutomaton, T::Type)
    return OneStateTransitionProperty(automaton, Vector{T}(undef, automaton.nt))
end
function Base.getindex(p::OneStateTransitionProperty,
                       t::OneStateTransition)
    p.value[t.σ]
end
function Base.setindex!(p::OneStateTransitionProperty, value,
                        t::OneStateTransition)
    p.value[t.σ] = value
end


# Author: Youngjun Kim, youngjun@stanford.edu
# Date: 06/09/2014

module PilotResponse

export
    AbstractPilotResponse,

    initialize,
    update,

    updatePilotResponse,

    SimplePilotResponse,
    SimplePRResolutionAdvisory,
    SimplePRCommand,

    StochasticLinearPR,
    StochasticLinearPRCommand,
    StochasticLinearPRRA,

    LLDetPR,
    LLDetPRAircraft,
    LLDetPRCommand,
    LLDetPRRA,
    LLDetPROutput

import CommonInterfaces: initialize, update

using AbstractPilotResponseImpl

using SimplePilotResponseImpl

using StochasticLinearPRImpl

using LLDetPRImpl

end


# Plot from MPB-data (::AbstractFourierLattice) -------------------------------------------
using .PyPlot

function plot_lattice_from_mpbparams(filepath::String; kwargs...)
    Rs, flat, isoval, _ = lattice_from_mpbparams(filepath)
    plot(flat, Rs; isoval=isoval, kwargs...) # via Crystalline.jl overload of PyPlot.jl's `plot`
end


export Variable, @RV

"""
A `Variable` encompasses one or more edges in a `FactorGraph`.
"""
mutable struct Variable <: AbstractVariable
    id::Symbol
    edges::Vector{Edge}

    function Variable(;id=generateId(Variable))
        self = new(id, Vector{Edge}())
        addVariable!(currentGraph(), self)
        return self
    end
end

"""
`associate!(interface, variable)` associates `interface` with
`variable` by connecting `interface` to an `Edge` belonging to `variable`.
"""
function associate!(iface::Interface, var::Variable)
    if isempty(var.edges)
        # Make a new Edge
        Edge(var, iface)
    elseif var.edges[end].b == nothing
        # Connect to the loose end of an existing Edge
        connect!(var.edges[end], iface)
    else
        # Insert an equality constraint node
        equ_idx = Int((length(var.edges) - 1) / 2) + 1
        equ = Equality(id=Symbol("equ_$(var.id)_$(equ_idx)"))
        disconnected_iface = var.edges[end].b
        disconnect!(var.edges[end], disconnected_iface)
        connect!(var.edges[end], equ.interfaces[1])
        Edge(var, disconnected_iface, equ.interfaces[2])
        Edge(var, iface, equ.interfaces[3])
    end

    return iface
end

"""
Collect all edges corresponding with variable(s)
"""
edges(variable::Variable) = Set{Edge}(variable.edges)
edges(variables::Set{Variable}) = union(Set((edges(v) for v=variables))...)

Base.isless(v1::Variable, v2::Variable) = isless("$(v1.id)", "$(v2.id)")

"""
`@RV` provides a convenient way to add `Variable`s and `FactorNode`s to the graph.

Examples:

```
# Automatically create new Variable x, try to assign x.id = :x if this id is available
@RV x ~ GaussianMeanVariance(constant(0.0), constant(1.0))

# Explicitly specify the id of the Variable
@RV [id=:my_y] y ~ GaussianMeanVariance(constant(0.0), constant(1.0))

# Automatically assign z.id = :z if this id is not yet taken
@RV z = x + y

# Manual assignment
@RV [id=:my_u] u = x + y

# Just create a variable
@RV x
@RV [id=:my_x] x
```
"""
macro RV(options_expr::Expr, definition)
    # Parse options
    options_expr.head == :vect || return :(error("Incorrect use of @RV macro: options argument must be a vector expression"))
    definition isa Expr || definition isa Symbol || return :(error("Incorrect use of @RV macro: definition expression must be a valid expression or symbol"))
    options = Dict{Symbol, Any}()
    for arg in options_expr.args
        arg isa Expr && arg.head == :(=) || return :(error("Incorrect use of @RV macro: options item must be an assignment expression"))
        options[arg.args[1]] = arg.args[2]
    end

    # Parse RV definition expression
    # It can take three forms:
    # FORM 1: @RV x ~ Probdist(...)
    # FORM 2: @RV x = a + b
    # FORM 3: @RV x
    expr = if rv_isa_form1(definition)
        rv_form1(definition, definition.args[2], definition.args[3], options)
    elseif rv_isa_form2(definition)
        rv_form2(definition, definition.args[1], definition.args[2], options)
    elseif rv_isa_form3(definition)
        rv_form3(definition, definition, nothing, options)
    else
        :(error("Unsupported usage of @RV."))
    end

    return esc(expr)
end

macro RV(expr)
    # Pass empty options object
    return esc(:(@RV [] $(expr)))
end

# Parse RV definition expression

# FORM 1: @RV x ~ Probdist(...)
rv_isa_form1(expr::Expr) = expr.head === :call && expr.args[1] === :(~)
rv_isa_form1(expr)       = false

function rv_form1(def, target, node, options)
    var_id = extract_variable_id(target, options)

    if isa(node.args[2], Expr) && (node.args[2].head == :parameters)
        node.args = vcat(node.args[1:2], [target], node.args[3:end])
    else
        node.args = vcat([node.args[1]; target], node.args[2:end])
    end

    # Build total expression
    return quote
        begin
            # Use existing Variable if it exists, otherwise create a new one
            $(target) = try
                $(target)
            catch _
                Variable(id = $(var_id))
            end

            # Create new variable if:
            #   - the existing object is not a Variable
            #   - the existing object is a Variable from another FactorGraph
            if (!isa($(target), Variable)
                || !haskey(currentGraph().variables, $(target).id)
                || currentGraph().variables[$(target).id] !== $(target))

                $(target) = Variable(id = $(var_id))
            end

            $(node)
            $(target)
        end
    end
end

# FORM 2: @RV x = a + b
rv_isa_form2(expr::Expr) = expr.head === :(=)
rv_isa_form2(expr)       = false

function rv_form2(def, target, node, options)
    var_id = extract_variable_id(target, options)

    # Form 2 always creates a new Variable
    # Build complete expression
    var_id_sym = gensym()
    return quote
        begin
            $(def)
            $(var_id_sym) = $(var_id)
            if $(var_id_sym) != :auto
                # update id of newly created Variable
                currentGraph().variables[$(var_id_sym)] = $(target)
                delete!(currentGraph().variables, $(target).id)
                $(target).id = $(var_id_sym)
            end
            $(target)
        end
    end
end

# FORM 3: @RV x
rv_isa_form3(expr::Symbol) = true
rv_isa_form3(expr::Expr)   = expr.head === :ref
rv_isa_form3(expr)         = false

function rv_form3(def, target, node, options)
    var_id = extract_variable_id(target, options)
    return quote
        $(target) = Variable(id = $(var_id))
    end
end

# If variable expression is a symbol
# RV x ...
function extract_variable_id(expr::Symbol, options)
    if haskey(options, :id)
        return check_id_available(options[:id])
    else
        return guard_variable_id(:($(string(expr))))
    end
end

# If variable expression is an indexing expression
# RV x[i] ...
function extract_variable_id(expr::Expr, options)
    if haskey(options, :id)
        return check_id_available(options[:id])
    else
        argstr = map(arg -> :(string($arg)), @view expr.args[2:end])
        return guard_variable_id(:($(string(expr.args[1]) * "_") * $(reduce((current, item) -> :($current * "_" * $item), argstr))))
    end
end

# Fallback
function extract_variable_id(expr, options)
    return :(ForneyLab.generateId(Variable))
end

function check_id_available(expr)
    return :(!haskey(currentGraph().variables, $(expr)) ? $(expr) : error("Specified id is already assigned to another Variable"))
end

# Ensure that variable has a unique id in a current factor graph, generate a new one otherwise
function guard_variable_id(expr)
    idsymbol = :(Symbol($(expr)))
    return :(!haskey(currentGraph().variables, $idsymbol) ? $idsymbol : ForneyLab.generateId(Variable))
end


using MonoMath
using Test
using Unicode

@testset "MonoMath" begin
  # Check that all codepoints used in the samples are valid Unicode
  for (i, s) in enumerate(MonoMath.text_samples)
    @testset "Sample $i" begin
      for (j, c) in enumerate(s)
        @testset "Codepoint $j ($c = $(repr(codepoint(c))))" begin
          @test Unicode.isassigned(c)
        end
      end
    end
  end
end


module Day05

using AdventOfCode2019

function day05(input::String = readInput(joinpath(@__DIR__, "..", "data", "day05.txt")))
    data = parse.(Int, split(input, ","))

    c1 = Channel{Int}(2)
    put!(c1, 1)
    out1 = AdventOfCode2019.IntCode.run_program!(copy(data), c1, nothing, nothing)
    close(c1)

    c2 = Channel{Int}(2)
    put!(c2, 5)
    out2 = AdventOfCode2019.IntCode.run_program!(copy(data), c2, nothing, nothing)
    close(c2)

    return [out1[end], out2[end]]
end

end # module


module TabularTDLearning

using POMDPs
using POMDPLinter
using POMDPPolicies
using POMDPSimulators
using Parameters
using Random

import POMDPs: Solver, solve, Policy

export
    QLearningSolver,
    SARSASolver,
    SARSALambdaSolver,
    solve

include("q_learn.jl")
include("sarsa.jl")
include("sarsa_lambda.jl")

end # module


using LinearAlgebra
using Espresso
import Espresso: @get, @get_or_create,
    genname, to_context, subs_bcast_with_dot,
    assign_chain

include("utils.jl")
include("rules.jl")
include("deriv.jl")
include("xdiff.jl")
include("xgrad0.jl")
include("helpers.jl")
include("debug.jl")


# functions to try to debug cui3.out
# converted to test function

PhyloNetworks.CHECKNET || error("need CHECKNET==true in PhyloNetworks to test snaq in test_correctLik.jl")

@testset "test: level-1 partitions" begin

global net, flag, flag2, flag3, nocycle
text = "(Xmayae,((Xhellerii,(((Xclemenciae_F2,Xmonticolus):1.458,(((((Xmontezumae,(Xnezahuacoyotl)#H26:0.247::0.804):0.375,((Xbirchmanni_GARC,Xmalinche_CHIC2):0.997,Xcortezi):0.455):0.63,(#H26:0.0::0.196,((Xcontinens,Xpygmaeus):1.932,(Xnigrensis,Xmultilineatus):1.401):0.042):2.439):2.0)#H7:0.787::0.835,(Xmaculatus,(Xandersi,(Xmilleri,((Xxiphidium,#H7:9.563::0.165):1.409,(Xevelynae,(Xvariatus,(Xcouchianus,(Xgordoni,Xmeyeri):0.263):3.532):0.642):0.411):0.295):0.468):0.654):1.022):0.788):1.917)#H27:0.149::0.572):0.668,Xalvarezi):0.257,(Xsignum,#H27:1.381::0.428):4.669);"

#net = readTopology("cui3.out")
net = readTopology(text)
#printPartitions(net)
#printEdges(net)
cleanBL!(net)
cleanAfterRead!(net,false)
length(net.hybrid)
@test [n.number for n in net.hybrid] == [7,15,25] # or: wrong hybrid nodes"

# ----------------- update everything but partition ------------------
@testset "udpate hybrid 1" begin
n=net.hybrid[1];
flag, nocycle, edgesInCycle, nodesInCycle = updateInCycle!(net,n);
@test flag
@test [n.number for n in nodesInCycle] == [7,-16,-10,-11,-12]
@test [n.number for n in edgesInCycle] == [16,24,15,9,8]
flag2, edgesGammaz = updateGammaz!(net,n,false);
@test flag2
@test isempty([n.number for n in edgesGammaz])
@test !n.isBadDiamondI
@test !n.isBadDiamondII
flag3, edgesRoot = updateContainRoot!(net,n);
@test flag3
@test [n.number for n in edgesRoot] == [7]
end

@testset "udpate hybrid 2" begin
n=net.hybrid[2];
flag, nocycle, edgesInCycle, nodesInCycle = updateInCycle!(net,n);
@test flag
@test [n.number for n in nodesInCycle] == [15,-24,-23,-22,-21,-20,-8]
@test [n.number for n in edgesInCycle] == [31,32,42,43,44,45,26]
flag2, edgesGammaz = updateGammaz!(net,n,false);
@test flag2
@test isempty([n.number for n in edgesGammaz])
@test !n.isBadDiamondI
@test !n.isBadDiamondII
flag3, edgesRoot = updateContainRoot!(net,n);
@test flag3
@test [n.number for n in edgesRoot] == [25,15,9,6,14,12,10,11,13,24,23,19,17,18,22,20,21]
end

@testset "udpate hybrid 3" begin
n=net.hybrid[3];
flag, nocycle, edgesInCycle, nodesInCycle = updateInCycle!(net,n);
@test flag
@test [n.number for n in nodesInCycle] == [25,-29,-2,-3,-4]
@test [n.number for n in edgesInCycle] == [53,54,51,49,48]
flag2, edgesGammaz = updateGammaz!(net,n,false);
@test flag2
@test isempty([n.number for n in edgesGammaz])
@test !n.isBadDiamondI
@test !n.isBadDiamondII
flag3, edgesRoot = updateContainRoot!(net,n);
@test flag3
@test [n.number for n in edgesRoot] == [47,5,3,4,46,45,27,44,28,43,29,42,32,30,41,33,40,34,39,35,38,36,37]

@test isempty(net.partition)
end

# -------------- update partition ---------------------
@testset "partition hybrid 1" begin
n=net.hybrid[1];
nocycle, edgesInCycle, nodesInCycle = identifyInCycle(net,n);
@test [n.number for n in edgesInCycle] == [16,24,15,9,8]
@test [n.number for n in nodesInCycle] == [7,-16,-10,-11,-12]
updatePartition!(net,nodesInCycle)
@test length(net.partition) == 5
@test [n.number for n in net.partition[1].edges] == [7]
@test [n.number for n in net.partition[2].edges] == [23,19,17,18,22,20,21]
@test [n.number for n in net.partition[3].edges] == [25]
@test [n.number for n in net.partition[4].edges] == [14,12,10,11,13]
@test [n.number for n in net.partition[5].edges] == [6]
end

@testset "partition hybrid 2" begin
n=net.hybrid[2];
nocycle, edgesInCycle, nodesInCycle = identifyInCycle(net,n);
@test [n.number for n in nodesInCycle] == [15,-24,-23,-22,-21,-20,-8]
@test [n.number for n in edgesInCycle] == [31,32,42,43,44,45,26]
updatePartition!(net,nodesInCycle)
@test length(net.partition) == 11
@test [n.number for n in net.partition[6].edges] == [30]
@test [n.number for n in net.partition[7].edges] == [41,33,40,34,39,35,38,36,37]
@test [n.number for n in net.partition[8].edges] == [29]
@test [n.number for n in net.partition[9].edges] == [28]
@test [n.number for n in net.partition[10].edges] == [27]
@test [n.number for n in net.partition[11].edges] == [46,5,3,4,47]
end

@testset "partition hybrid 3" begin
n=net.hybrid[3];
nocycle, edgesInCycle, nodesInCycle = identifyInCycle(net,n);
@test [n.number for n in nodesInCycle] == [25,-29,-2,-3,-4]
@test [n.number for n in edgesInCycle] == [53,54,51,49,48]
updatePartition!(net,nodesInCycle)
@test length(net.partition) == 15
@test [n.number for n in net.partition[12].edges] == [52]
@test [n.number for n in net.partition[13].edges] == [1]
@test [n.number for n in net.partition[14].edges] == [50]
@test [n.number for n in net.partition[15].edges] == [2]
end
#printPartitions(net)
end


#  notebook(dir=pwd(),detached=true)

import Pkg
Pkg.activate(joinpath(@__DIR__, ".."))
using Revise
# include("jac-main.jl")
using JAC


struct STRtree{T}
    ptr::Ptr{GEOSSTRtree}  # void pointer to the tree
    items::T  # any geometry for which an envelope can be derived
end

"""
    STRtree(items; nodecapacity=10, context::GEOSContext=_context)

Create a Sort Tile Recursive tree for fast intersection queries of objects for which an
envelope is defined. The tree cannot be changed after its creation.

## Arguments
- `items`: The items to store in the tree.
- `nodecapacity`: The maximum number of items that can be stored per tree node (default 10).
- `context`: The GEOS context in which the tree should be created in. Defaults to the global
  LibGEOS context.
"""
function STRtree(items; nodecapacity = 10, context::GEOSContext = _context)
    tree = LibGEOS.GEOSSTRtree_create_r(context.ptr, nodecapacity)
    for item in items
        envptr = envelope(item).ptr
        GEOSSTRtree_insert_r(context.ptr, tree, envptr, pointer_from_objref(item))
    end
    return STRtree(tree, items)
end

"""
    query(tree::STRtree, geometry; context::GEOSContext=_context)

Returns the objects within `tree`, whose envelope intersects the envelope of `geometry`.

## Arguments
- `tree`: The STRtree to query
- `geometry`: The LibGEOS geometry (e.g. Polygon) to run the query for
- `context`: The GEOS context. Defaults to the global LibGEOS context.
"""
function query(
    tree::STRtree{T},
    geometry::Geometry;
    context::GEOSContext = _context,
) where {T}
    matches = eltype(T)[]

    # called for each overlapping item in the tree, used to push the item to matches
    function callback(item, userdata)::Ptr{Cvoid}
        item_ = unsafe_pointer_to_objref(item)
        userdata_ = unsafe_pointer_to_objref(userdata)
        push!(userdata_, item_)
        return C_NULL
    end

    cfun_callback = @cfunction($callback, Ptr{Cvoid}, (Ptr{Cvoid}, Ptr{Cvoid}))
    GEOSSTRtree_query_r(
        context.ptr,
        tree.ptr,
        geometry.ptr,
        cfun_callback,
        pointer_from_objref(matches),
    )
    return matches
end


using Gadfly
using DelimitedFiles
using DataFrames
using ColorTypes

cores = readdlm("../data/cores.dat", Float64, comment_char='#')
frequency = readdlm("../data/frequency.dat", Float64, comment_char='#')
specint = readdlm("../data/specint.dat", Float64, comment_char='#')
transistors = readdlm("../data/transistors.dat", Float64, comment_char='#')
watts = readdlm("../data/watts.dat", Float64, comment_char='#')

df = vcat(
    DataFrame(Year=transistors[:,1], y=transistors[:,2], label="#Transistors\n (thousands)"),
    DataFrame(Year=specint[:,1], y=specint[:,2], label="Single Thread\n Performance\n (SpecINT x 10³)"),
    DataFrame(Year=frequency[:,1], y=frequency[:,2], label="Frequency \n(MHz)"),
    DataFrame(Year=watts[:,1], y=watts[:,2], label="Power (Watts)"),
    DataFrame(Year=cores[:,1], y=cores[:,2], label="#Cores"),
)

p = Gadfly.plot(df,
    x = :Year,
    y = :y,
    size = [0.5mm],
    color = :label,
    #shape = :label,
    Scale.y_log10,
    Guide.title("Microprocessors metrics"),
    Guide.ylabel(nothing),
    Guide.colorkey(title=""),
    Coord.cartesian(ymin = -1, ymax=8),
    Geom.point,
    Geom.smooth,
    Theme(
        minor_label_color = colorant"white",
        major_label_color = colorant"white",
        key_label_color = colorant"white",
        highlight_width = 0.5pt
    )
)

p |> SVG("../images/microprocessors.svg", 15cm, 9cm)
p |> PNG("../images/microprocessors.png", 15cm, 9cm)


module CustomSaltTest

using Hashids
using Test

const TESTDATA = [
    [0],
    [1, 2, 3],
    [10000000000, 0, 0, 0, 999999999999999],
    [827364827638428762424, 0x410498019283098083],
    [340282366920938463463374607431768211456]
]

function test_with_custom_salt(salt::String)
    config = Hashids.configure(salt)
    for numbers in TESTDATA
        @test decode(config, encode(config, numbers)) == numbers
    end
end

@testset "Custom Salt" begin

@testset "\"\"" begin
    test_with_custom_salt("")
end  # of "\"\""

@testset "\"   \"" begin
    test_with_custom_salt("   ")
end  # of "\"   \""

@testset "long salt" begin
    test_with_custom_salt(raw"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890`~!@#$%^&*()-_=+\\|'\";:/?.>,<{[}]")
end  # of "long salt"

@testset "weird salt" begin
    test_with_custom_salt(raw"`~!@#$%^&*()-_=+\\|'\";:/?.>,<{[}]")
end  # of "weird salt"

@testset "ultra weird salt" begin
    test_with_custom_salt("🤺👩🏿🦳🛁👩🏻🦷🤦♂🐁☝🏼✍🏾👉🏽🇸🇰❤🍭")
end  # of "ultra weird salt"

end  # of "Custom Salt"

end  # module


using Gcode
using Base.Test

def setUp(self):
    self.outfile = TemporaryFile()
    self.g = G(outfile=self.outfile, print_lines=False,
               aerotech_include=False)

def tearDown(self):
    self.g.teardown()
    del self.outfile
    del self.g

def test_init(self):
    self.assertEqual(self.g.movement_mode, 'relative')

def test_set_home(self):
    g = self.g
    g.set_home()
    expected = 'G92'
    self.assert_output(expected)
    g.set_home(x=10, y=20, A=5)
    expected += """
G92 X10.000000 Y20.000000 A5.000000"""
    self.assert_output(expected)
    self.assert_position({'A': 5.0, 'x': 10.0, 'y': 20.0, 'z': 0})
    g.set_home(y=0)
    self.assert_position({'A': 5.0, 'x': 10.0, 'y': 0.0, 'z': 0})

def test_reset_home(self):
    self.g.reset_home()
    expected = 'G92.1'
    self.assert_output(expected)

def test_relative(self):
    self.g.relative()
    self.assertEqual(self.g.movement_mode, 'relative')
    self.assert_output('G91')

def test_absolute(self):
    self.g.absolute()
    self.assertEqual(self.g.movement_mode, 'absolute')
    self.assert_output('G90')

def test_feed(self):
    self.g.feed(10)
    self.assert_output('F10')

def test_dwell(self):
    self.g.dwell(10)
    self.assert_output('G4 P10')

def test_setup(self):
    self.outfile = TemporaryFile()
    self.g = G(outfile=self.outfile, print_lines=False)
    expected = open(os.path.join(HERE, '../header.txt')).read()
    self.assert_output(expected)

def test_home(self):
    self.g.home()
    expected = """
G90
G1 X0.000000 Y0.000000
G91
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 0})

def test_move(self):
    g = self.g
    g.move(10, 10)
    self.assert_position({'x': 10.0, 'y': 10.0, 'z': 0})
    g.move(10, 10, A=50)
    self.assert_position({'x': 20.0, 'y': 20.0, 'A': 50, 'z': 0})
    expected = """
G1 X10.000000 Y10.000000
G1 X10.000000 Y10.000000 A50.000000
"""
    self.assert_output(expected)

def test_abs_move(self):
    self.g.abs_move(10, 10)
    expected = """
G90
G1 X10.000000 Y10.000000
G91
"""
    self.assert_output(expected)
    self.assert_position({'x': 10, 'y': 10, 'z': 0})

    self.g.abs_move(5, 5)
    expected += """
G90
G1 X5.000000 Y5.000000
G91
"""
    self.assert_output(expected)
    self.assert_position({'x': 5, 'y': 5, 'z': 0})

    self.g.abs_move(15, 0, D=5)
    expected += """
G90
G1 X15.000000 Y0.000000 D5.000000
G91
"""
    self.assert_output(expected)
    self.assert_position({'x': 15, 'y': 0, 'D': 5, 'z': 0})

def test_arc(self):
    with self.assertRaises(RuntimeError):
        self.g.arc()

    self.g.arc(x=10, y=0)
    expected = """
G17
G2 Y0 X10 R5.0
"""
    self.assert_output(expected)
    self.assert_position({'x': 10, 'y': 0, 'z': 0})

    self.g.arc(x=5, A=0, direction='CCW', radius=5)
    expected += """
G16 X Y A
G18
G3 A0 X5 R5
"""
    self.assert_output(expected)
    self.assert_position({'x': 15, 'y': 0, 'A': 0, 'z': 0})

    self.g.arc(x=0, y=10, helix_dim='D', helix_len=10)
    expected += """
G16 X Y D
G17
G2 Y10 X0 R5.0 G1 D10
"""
    self.assert_output(expected)
    self.assert_position({'x': 15, 'y': 10, 'A': 0, 'D': 10, 'z': 0})

    with self.assertRaises(RuntimeError):
        self.g.arc(x=10, y=10, radius=1)

def test_abs_arc(self):
    self.g.abs_arc(x=0, y=10)
    expected = """
G90
G17
G2 Y10 X0 R5.0
G91
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 10, 'z': 0})

    self.g.abs_arc(x=0, y=10)
    expected += """
G90
G17
G2 Y10 X0 R0.0
G91
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 10, 'z': 0})

def test_rect(self):
    self.g.rect(10, 5)
    expected = """
G1 Y5.000000
G1 X10.000000
G1 Y-5.000000
G1 X-10.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 0})

    self.g.rect(10, 5, start='UL')
    expected += """
G1 X10.000000
G1 Y-5.000000
G1 X-10.000000
G1 Y5.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 0})

    self.g.rect(10, 5, start='UR')
    expected += """
G1 Y-5.000000
G1 X-10.000000
G1 Y5.000000
G1 X10.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 0})

    self.g.rect(10, 5, start='LR')
    expected += """
G1 X-10.000000
G1 Y5.000000
G1 X10.000000
G1 Y-5.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 0})

    self.g.rect(10, 5, start='LL', direction='CCW')
    expected += """
G1 X10.000000
G1 Y5.000000
G1 X-10.000000
G1 Y-5.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 0})

    self.g.rect(10, 5, start='UL', direction='CCW')
    expected += """
G1 Y-5.000000
G1 X10.000000
G1 Y5.000000
G1 X-10.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 0})

    self.g.rect(10, 5, start='UR', direction='CCW')
    expected += """
G1 X-10.000000
G1 Y-5.000000
G1 X10.000000
G1 Y5.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 0})

    self.g.rect(10, 5, start='LR', direction='CCW')
    expected += """
G1 Y5.000000
G1 X-10.000000
G1 Y-5.000000
G1 X10.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 0})

def test_meander(self):
    self.g.meander(2, 2, 1)
    expected = """
G91
G1 X2.000000
G1 Y1.000000
G1 X-2.000000
G1 Y1.000000
G1 X2.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 2, 'y': 2, 'z': 0})

    self.g.meander(2, 2, 1.1)
    expected += """
;WARNING! meander spacing updated from 1.1 to 1.0
G91
G1 X2.000000
G1 Y1.000000
G1 X-2.000000
G1 Y1.000000
G1 X2.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 4, 'y': 4, 'z': 0})

    self.g.meander(2, 2, 1, start='UL')
    expected += """
G91
G1 X2.000000
G1 Y-1.000000
G1 X-2.000000
G1 Y-1.000000
G1 X2.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 6, 'y': 2, 'z': 0})

    self.g.meander(2, 2, 1, start='UR')
    expected += """
G91
G1 X-2.000000
G1 Y-1.000000
G1 X2.000000
G1 Y-1.000000
G1 X-2.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 4, 'y': 0, 'z': 0})

    self.g.meander(2, 2, 1, start='LR')
    expected += """
G91
G1 X-2.000000
G1 Y1.000000
G1 X2.000000
G1 Y1.000000
G1 X-2.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 2, 'y': 2, 'z': 0})

    self.g.meander(2, 2, 1, start='LR', orientation='y')
    expected += """
G91
G1 Y2.000000
G1 X-1.000000
G1 Y-2.000000
G1 X-1.000000
G1 Y2.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 4, 'z': 0})

    self.g.meander(3, 2, 1, start='LR', orientation='y')
    expected += """
G91
G1 Y2.000000
G1 X-1.000000
G1 Y-2.000000
G1 X-1.000000
G1 Y2.000000
G1 X-1.000000
G1 Y-2.000000
"""
    self.assert_output(expected)
    self.assert_position({'x': -3, 'y': 4, 'z': 0})

def test_clip(self):
    self.g.clip()
    expected = """
G16 X Y Z
G18
G3 X0 Z4 R2.0
"""
    self.assert_output(expected)
    self.assert_position({'y': 0, 'x': 0, 'z': 4})

    self.g.clip(axis='A', direction='-y', height=10)
    expected += """
G16 X Y A
G19
G2 Y0 A10 R5.0
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 4, 'A': 10})

    self.g.clip(axis='A', direction='-y', height=-10)
    expected += """
G16 X Y A
G19
G3 Y0 A-10 R5.0
"""
    self.assert_output(expected)
    self.assert_position({'x': 0, 'y': 0, 'z': 4, 'A': 0})

### helper functions #####################################################

def assert_output(self, expected):
    if isinstance(expected, basestring):
        expected = expected.split('\n')
    expected = [x.strip() for x in expected if x.strip()]
    self.outfile.seek(0)
    lines = self.outfile.readlines()
    lines = [x.strip() for x in lines if x.strip()]
    self.assertListEqual(lines, expected)

def assert_position(self, expected):
    self.assertEqual(self.g.current_position, expected)


using Base.Test
using Musical
using Musical.Notations # A
using Musical.SPN # note octave freq A4
using MIDI # MIDI.Note

@test A isa Musical.Notations.Notation
@test A4 isa Musical.SPN.NoteOctave
@test note(A4) == A
@test octave(A4) == 4
@test freq(A4) == 440.0
@test MIDI.Note(A4) == MIDI.Note(69, 0, 0, 0)


"""
	$(SIGNATURES)

For all colleges (to keep track of shared parameters).
"""
abstract type GradRuleSet <: ModelObject end

abstract type GradRuleSwitches end
abstract type GradRule end

@common_fields grad_rule_set_common begin
    objId :: ObjectId
    pvec :: ParamVector
end

include("grad_rule_all.jl");
include("grad_rule_simple.jl");
include("grad_rule_linear.jl");
include("grad_rule_logistic.jl");

## --------  For testing

make_test_grad_rule(switches :: GradRuleSwitches, iCollege :: Integer) =
	make_grad_rule(
		make_grad_rule_set(ObjectId(:test), switches),
		iCollege);

function grad_rule_test_switches()
	return (GradRuleSwitchesSimple(),
		GradRuleSwitchesLogistic(),
		GradRuleSwitchesLinear(useHcLevel = true),
		GradRuleSwitchesLinear(useHcLevel = true, byCollege = false),
		GradRuleSwitchesLinear(useHcLevel = false, hMin = 0.2))
end

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


include("./NAMD_example1.jl")
include("./NAMD_example2.jl")
include("./NAMD_example3.jl")
include("./NAMD_example4.jl")
include("./NAMD_example5.jl")
include("./NAMD_example6.jl")
include("./NAMD_example7.jl")

include("./ivan.jl")


# #############################################################################
# #############################################################################
#Interactive PSS plot
function plotpss_interactive(resultsdir::AbstractString;kwargs...)

    # #########################################################################
    #Initiate plot

    #Prepare plot
    fig = plt[:figure](figsize=(9,9))
    axes = fig[:gca](aspect="equal")

    #Plot the non-interactive version
    #and save PSS plot state
    pssarray::Array{Float64,2},residarray::Array{Int64,2} = plotpss(resultsdir;kwargs...)

    # #########################################################################
    #Interactions tasks

    #Highlight bouncepoints on PSS
    #This makes the PSS orbit from a single set of initial conditions stand out
    #from other points, and any periodicity in the orbit to be more obvious
    function pss_highlight(xdata::Float64,ydata::Float64)
        #Get appropriate entry in PSS data arrays based on position on PSS
        range::Array{Float64,1} = [plt[:axis]()...]
        thetaindex::Int64 = getbinindex(xdata,range[1],range[2],size(pssarray,2))
        chiindex::Int64 = getbinindex(ydata,range[4],range[3],size(pssarray,1))

        #Check that entry index is within bounds
        if thetaindex != 0 && chiindex != 0
            #Read info from data arrays and recorded results
            score::Float64 = pssarray[chiindex,thetaindex]
            resultid::Int64 = residarray[chiindex,thetaindex]
            if resultid > 0
                bouncepoints::Array{Float64,2} = readresults(resultsdir,resultid,"bouncepoints")

                #Plot bouncepoints on PSS as red squares with thick black borders for visibility
                plt[:plot](bouncepoints[:,1],bouncepoints[:,2],
                           ".",mec="k",mfc="w",mew=2,ms=10,axes=axes)
                plt[:axis](range)
                plt[:draw]()
            end
        end
    end

    #Dehighlight selected bouncepoints
    function pss_dehighlight()
        #Get lines in axes and remove them all
        lines = axes[:get_lines]()
        for line in lines
            line[:remove]()
        end
        plt[:draw]()
    end

    #Clear PSS plot and replot
    function pss_replot(newrange::Array{Float64,1})
        axes[:cla]()
        pssarray,residarray = plotpss(resultsdir::AbstractString;axes=axes,range=newrange)
        plt[:axis](newrange)
        plt[:draw]()
    end

    #Pan
    function pss_pan(dxdata::Float64,dydata::Float64)
        #Get transformed range
        range::Array{Float64,1} = [plt[:axis]()...]
        newrange::Array{Float64,1} = [range[1]-dxdata, range[2]-dxdata,
                                      range[3]-dydata, range[4]-dydata]
        pss_replot(newrange)
    end

    #Zoom
    function pss_zoom(zoomsteps::Int64)
        #Calculate scaling (8 steps to scale by factor of 2)
        scale::Float64 = 2.0^(zoomsteps/8)

        #Get transformed range
        range::Array{Float64,1} = [plt[:axis]()...]
        centerx::Float64,centery::Float64 = (range[2]+range[1])/2, (range[4]+range[3])/2
        newdx::Float64,newdy::Float64 = scale*(range[2]-range[1])/2, scale*(range[4]-range[3])/2
        newrange::Array{Float64,1} = [centerx-newdx, centerx+newdx,
                                      centery-newdy, centery+newdy]
        pss_replot(newrange)
    end

    # #########################################################################
    #Event handlers

    #Left clicking selects the mode to be emphasized
    #Right clicking deselects any emphasized mode
    #Middle clicking and dragging moves PSS
    #Scrolling zooms in and out

    #Mouse press-drag-release state
    pss_dragstart::Array{Float64,1} = [0.0,0.0]
    pss_button::Int64 = 1
    pss_pressed::Bool = false

    #Press event
    function onpress(mev::PyObject)
        #Ignore all mouse presses outside axes, or when a button is already pressed
        if !pss_pressed && mev[:inaxes] != nothing
            #Store new press-drag-release state
            pss_pressed = true
            pss_button = mev[:button]
            pss_dragstart = Float64[mev[:xdata],mev[:ydata]]
        end
    end

    #Release event
    function onrelease(mev::PyObject)
        #All button releases other than from the one first pressed are ignored
        if pss_pressed && mev[:button] == pss_button
            #Toggle pressed state
            pss_pressed = false

            #Ensure release is in axes
            axes = mev[:inaxes]
            if axes != nothing
                #Choose task depending on button pressed
                if pss_button == 1
                    #Left release
                    #Only clicks without drags are handled
                    if mev[:xdata] == pss_dragstart[1] && mev[:ydata] == pss_dragstart[2]
                        #Select mode with point at click location
                        pss_dehighlight()
                        pss_highlight(mev[:xdata],mev[:ydata])
                    end
                elseif pss_button == 3
                    #Right release
                    #Only clicks without drags are handled
                    if mev[:xdata] == pss_dragstart[1] && mev[:ydata] == pss_dragstart[2]
                        #Deselect mode
                        pss_dehighlight()
                    end
                elseif pss_button == 2
                    #Middle release
                    pss_pan(mev[:xdata]-pss_dragstart[1],mev[:ydata]-pss_dragstart[2])
                end
            end
        end
    end

    #Mouse scroll state
    pss_zoomsteps::Int64 = 0

    #Scroll event
    function onscroll(mev::PyObject)
        axes = mev[:inaxes]
        if axes != nothing
            #Zoom on scroll
            pss_zoomsteps += mev[:step]
        end
    end

    #PSS closed state
    pss_closed::Bool = false

    #Close event
    function onclose(cev::PyObject)
        pss_closed = true
    end

    # #########################################################################
    #Connect handlers and show plot

    #Connect event handlers
    fig[:canvas][:mpl_connect]("button_press_event",onpress)
    fig[:canvas][:mpl_connect]("button_release_event",onrelease)
    fig[:canvas][:mpl_connect]("scroll_event",onscroll)
    fig[:canvas][:mpl_connect]("close_event",onclose)

    #Show plot in the background
    plt[:ion]()
    plt[:show]()
    plt[:ioff]()

    #Update zoom level at regular intervals
    while !pss_closed
        if pss_zoomsteps != 0
            zoomsteps::Int64 = pss_zoomsteps
            pss_zoomsteps = 0
            pss_zoom(zoomsteps)
        end
        sleep(1)
    end

end


const _club_base = 0x1F0D1
const _diamond_base = 0x1F0C1
const _heart_base = 0x1F0B1
const _spade_base = 0x1F0A1

function _make_chars(base::Integer)
    vals = collect(base:base+10)
    append!(vals, base + 12)
    append!(vals, base + 13)
    return Char.(vals)

end

_cards = _make_chars(_club_base)
append!(_cards, _make_chars(_diamond_base))
append!(_cards, _make_chars(_heart_base))
append!(_cards, _make_chars(_spade_base))

import Base.Char

"""
    Char(c::Card)

Return the unicode character for that playing card.

```
julia> Char(4♣)
'🃔': Unicode U+1F0D4 (category So: Symbol, other)
```
"""
function Char(c::Card)
    return @inbounds _cards[index(c)]
end


abstract type AbstractTranslocation end
abstract type AbstractDissipativeTranslocation <: AbstractTranslocation end
abstract type AbstractLosslessTranslocation <: AbstractTranslocation end

@mix @columns @flattenable struct Trans{F}
    κtra::F        | 0.6     | _   | Beta(2.0, 2.0)  | [0.0,1.0]      | _ | "Reserve flux allocated to translocation"
end

@mix @columns @flattenable struct MultiTrans{D,P}
    destnames::D   | false | (:leaf,) | _ | _              | _         | _ | "The organ/s translocated to"
    proportions::P | true  | (1.0,)   | _ | Beta(2.0, 2.0) | [0.0,1.0] | _ | "The proportion of translocation sent in the first translocation. Only for inetermediaries. nothing = 100%"
end

@mix @columns @flattenable struct DissTrans{MoMo}
    y_E_ET::MoMo   | true | 0.8       | mol*mol^-1      | Beta(2.0, 2.0)  | [0.0,1.0]    | _ | "Translocated reserve:"
end

# Dissipation types
@Trans @MultiTrans @DissTrans struct DissipativeMultipleTranslocation{} <: AbstractDissipativeTranslocation end
@Trans @DissTrans struct DissipativeTranslocation{} <: AbstractDissipativeTranslocation  end
@Trans @MultiTrans struct LosslessMultipleTranslocation{} <: AbstractLosslessTranslocation end
@Trans struct LosslessTranslocation{} <: AbstractLosslessTranslocation end

abstract type AbstractRejection end

@columns struct DissipativeRejection{MoMo} <: AbstractRejection
    y_EC_ECT::MoMo       | 1.0             | mol*mol^-1      | Beta(2.0, 2.0)  | [0.0,1.0] | _ | "Translocated C-reserve"
    y_EN_ENT::MoMo       | 1.0             | mol*mol^-1      | Beta(2.0, 2.0)  | [0.0,1.0] | _ | "Translocated N-reserve"
end
struct LosslessRejection <: AbstractRejection end

# Recurse through all organs. A loop would not be type-stable.
# translocation!(organs::Tuple, destorgans::Tuple) = begin
#     props = buildprops(organs[1])
#     translocation!(organs[1], destorgans, organs[1].params.translocation.destnames, props)
#     translocation!(tail(organs), destorgans)
# end
# translocation!(organs::Tuple{}, destorgans::Tuple) = nothing
# translocation!(organ::Organ, destorgans::Tuple, destnames::Symbol, props) =
#     translocation!(organ, destorgans, (destnames,), props)
# # Translocate to organs with names in the destnames list
# translocation!(organ::Organ, destorgans::Tuple, destnames, props) = begin
#     for i = 1:length(destnames)
#         if destorgans[1].params.name == destnames[i]
#             reuse_rejected!(organ, destorgans[1], props[i])
#             translocate!(organ, destorgans[1], props[i])
#             break
#         end
#     end
#     translocation!(organ, tail(destorgans), destnames, props)
# end
# translocation!(organ::Organ, destorgans::Tuple{}, destnames, props) = nothing

# Add the last remainder proportion (so that its not a model parameter)
# buildprops(o::Organ) = buildprops(o.params.translocation.proportions)
# buildprops(x::Nothing) = (1.0)
# buildprops(x::Number) = (x, 1 - x)
# buildprops(xs::Tuple) = (xs..., 1 - sum(xs))

"""
Versions for E, CN and CNE reserves.

Translocation is occurs between adjacent organs.
This function is identical both directiono, and ox represents
whichever is not the current organs. Will not run with less than 2 organs.
"""
translocate!(o1, o2, prop) = translocate!(trans_pars(o1), o1, o2, prop)
translocate!(p::Nothing, o1, o2, prop) = nothing
translocate!(p::AbstractDissipativeTranslocation, o1, o2, prop) = begin
    # outgoing translocation
    trans = κtra(o1) * o1.J1[:E,:ctb]
    reserve_drain!(o1, Val(:tra), trans)

    # incoming translocation
    transx = κtra(o2) * o2.J1[:E,:ctb]
    o1.J[:E,:tra] += transx * y_E_ET(o2)

    # loss = transx * (1 - y_E_ET(o2))
    # reserve_loss!(o2, loss)
    # conversion_loss!(o2, transx * y_E_ET(o2), n_N_E(o2))
    nothing
end

translocate!(p::AbstractLosslessTranslocation, o1, o2, prop) = begin
    # outgoing translocation
    trans = κtra(o1) * o1.J1[:E,:ctb]
    reserve_drain!(o1, Val(:tra), trans)

    # incoming translocation
    transx = κtra(o2) * o2.J1[:E,:ctb]
    o1.J[:E,:tra] += transx

    # conversion_loss!(o2, transx, n_N_E(o2))
    nothing
end

"""
Translocation occurs between adjacent organs.
This function is identical both directiono, so on represents
whichever is not the current organs.

Will not run with less than 2 organs.
"""
translocation!(organs::Tuple{T1,T2}) where {T1, T2} = begin
    translocate_rejected!(organs[1], organs[2], 1.0)
    translocate_rejected!(organs[2], organs[1], 1.0)
    translocate!(organs[1], organs[2], 1.0)
    translocate!(organs[2], organs[1], 1.0)
end
# translocation!(organs::Tuple) = translocation!(organs...)
translocation!(organs::Tuple{T}) where T = nothing
translocation!(organs::Tuple{}) = nothing

"""
Reallocate state rejected from synthesizing units.
TODO: add a 1-organs method. How does this interact with assimilation?
"""
translocate_rejected!(source, dest, prop) = translocate_rejected!(rejection_pars(source), source, dest, prop)
translocate_rejected!(f::Nothing, source, dest, prop) = nothing
translocate_rejected!(f::DissipativeRejection, source, dest, prop) = begin
    Js, J1s, Jd = flux(source), flux1(source), flux(dest)
    transC = J1s[:C,:rej] # * (1 - κEC(o))
    transN = J1s[:N,:rej] # * (1 - κEN(o))
    Js[:C,:rej] = -transC
    Js[:N,:rej] = -transN
    Jd[:C,:tra] = f.y_EC_ECT * transC
    Jd[:N,:tra] = f.y_EN_ENT * transN
    # J1[:C,:los] += transC * (1 - y_EC_ECT(o)) + transN * (1 - y_EN_ENT(o))
    # J1[:N,:los] += (transC * (1 - y_EC_ECT(o)), transN * (1 - y_EN_ENT(o))) ⋅ (n_N_EC(o), n_N_EN(o))
    nothing
end
translocate_rejected!(rejected::LosslessRejection, source, dest, prop) = begin
    Js, J1s, Jd = flux(source), flux1(source), flux(dest)
    transC = J1s[:C,:rej] # * (1 - κEC(o))
    transN = J1s[:N,:rej] # * (1 - κEN(o))
    Js[:C,:rej] = -transC
    Js[:N,:rej] = -transN
    Jd[:C,:tra] = transC
    Jd[:N,:tra] = transN
    nothing
end

κtra(trans_pars::AbstractTranslocation) = trans_pars.κtra


using Test, AdvancedHMC, LinearAlgebra, UnicodePlots, Random
using Statistics: mean, var, cov
include("common.jl")

@testset "Matrix mode" begin
    n_chains_max = 20
    n_chains_list = collect(1:n_chains_max)
    θ_init_list = [rand(D, n_chains) for n_chains in n_chains_list]
    ϵ = 0.2
    lf = Leapfrog(ϵ)
    i_test = 5
    lfi = Leapfrog(fill(ϵ, i_test))
    lfi_jittered = JitteredLeapfrog(fill(ϵ, i_test), 1.0)
    n_steps = 10
    n_samples = 12_000
    n_adapts = 2_000

    for metricT in [
        UnitEuclideanMetric,
        DiagEuclideanMetric,
        # DenseEuclideanMetric  # not supported at the moment
    ], τ in [
        StaticTrajectory(lfi, n_steps),
        StaticTrajectory(lfi_jittered, n_steps),
        HMCDA(lf, ϵ * n_steps)
    ]
        n_chains = n_chains_list[i_test]
        metric = metricT((D, n_chains))
        h = Hamiltonian(metric, ℓπ, ∂ℓπ∂θ)
        # NoAdaptation
        samples, stats = sample(h, τ, θ_init_list[i_test], n_samples; verbose=false)
        @test mean(samples) ≈ zeros(D, n_chains) atol=RNDATOL * n_chains
        # Adaptation
        for adaptor in [
            Preconditioner(metric),
            NesterovDualAveraging(0.8, lfi.ϵ),
            NaiveHMCAdaptor(
                Preconditioner(metric),
                NesterovDualAveraging(0.8, lfi.ϵ),
            ),
            StanHMCAdaptor(
                Preconditioner(metric),
                NesterovDualAveraging(0.8, lfi.ϵ),
            ),
        ]
            τ isa HMCDA && continue
            samples, stats = sample(h, τ, θ_init_list[i_test], n_samples, adaptor, n_adapts; verbose=false, progress=false)
            @test mean(samples) ≈ zeros(D, n_chains) atol=RNDATOL * n_chains
        end
        # Passing a vector of same RNGs
        rng = [MersenneTwister(1) for _ in 1:n_chains]
        h = Hamiltonian(metricT((D, n_chains)), ℓπ, ∂ℓπ∂θ)
        θ_init = repeat(rand(D), 1, n_chains)
        samples, stats = sample(rng, h, τ, θ_init, n_samples; verbose=false)
        all_same = true
        for i_sample in 2:10
            for j in 2:n_chains
                all_same = all_same && samples[i_sample][:,j] == samples[i_sample][:,1]
            end
        end
        @test all_same
    end
    @info "Adaptation tests for HMCDA with NesterovDualAveraging are skipped"

    # Simple time benchmark
    let metricT=UnitEuclideanMetric
        τ = StaticTrajectory(lf, n_steps)

        time_mat = Vector{Float64}(undef, n_chains_max)
        for (i, n_chains) in enumerate(n_chains_list)
            h = Hamiltonian(metricT((D, n_chains)), ℓπ, ∂ℓπ∂θ)
            t = @elapsed samples, stats = sample(h, τ, θ_init_list[i], n_samples; verbose=false)
            time_mat[i] = t
        end

        # Time for multiple runs of single chain
        time_seperate = Vector{Float64}(undef, n_chains_max)

        for (i, n_chains) in enumerate(n_chains_list)
            t = @elapsed for j in 1:n_chains
                h = Hamiltonian(metricT(D), ℓπ, ∂ℓπ∂θ)
                samples, stats = sample(h, τ, θ_init_list[i][:,j], n_samples; verbose=false)
            end
            time_seperate[i] = t
        end

        # Make plot
        fig = lineplot(
            collect(1:n_chains_max),
            time_mat;
            title="Scalabiliry of multiple chains",
            name="matrix parallism",
            xlabel="Num of chains",
            ylabel="Time (s)"
        )
        lineplot!(fig, collect(n_chains_list), time_seperate; color=:blue, name="seperate")
        println(); show(fig); println(); println()
    end
end