"""
    add_self_loops(g::GNNGraph)

Return a graph with the same features as `g`
but also adding edges connecting the nodes to themselves.

Nodes with already existing self-loops will obtain a second self-loop.

If the graphs has edge weights, the new edges will have weight 1.
"""
function add_self_loops(g::GNNGraph{<:COO_T})
    s, t = edge_index(g)
    @assert g.edata === (;)
    ew = get_edge_weight(g)
    n = g.num_nodes
    nodes = convert(typeof(s), [1:n;])
    s = [s; nodes]
    t = [t; nodes]
    if ew !== nothing
        ew = [ew; fill!(similar(ew, n), 1)]
    end

    GNNGraph((s, t, ew),
        g.num_nodes, length(s), g.num_graphs,
        g.graph_indicator,
        g.ndata, g.edata, g.gdata)
end

function add_self_loops(g::GNNGraph{<:ADJMAT_T})
    A = g.graph
    @assert g.edata === (;)
    num_edges = g.num_edges + g.num_nodes
    A = A + I
    GNNGraph(A,
            g.num_nodes, num_edges, g.num_graphs,
            g.graph_indicator,
            g.ndata, g.edata, g.gdata)
end

function remove_self_loops(g::GNNGraph{<:COO_T})
    s, t = edge_index(g)
    # TODO remove these constraints
    @assert g.edata === (;)
    @assert get_edge_weight(g) === nothing

    mask_old_loops = s .!= t
    s = s[mask_old_loops]
    t = t[mask_old_loops]

    GNNGraph((s, t, nothing),
            g.num_nodes, length(s), g.num_graphs,
            g.graph_indicator,
            g.ndata, g.edata, g.gdata)
end

function remove_self_loops(g::GNNGraph{<:ADJMAT_T})
    @assert g.edata === (;)
    A = g.graph
    A[diagind(A)] .= 0
    if A isa AbstractSparseMatrix
        dropzeros!(A)
    end
    num_edges = numnonzeros(A)
    GNNGraph(A,
            g.num_nodes, num_edges, g.num_graphs,
            g.graph_indicator,
            g.ndata, g.edata, g.gdata)
end

"""
    remove_multi_edges(g::GNNGraph)

Remove multiple edges (also called parallel edges or repeated edges) from graph `g`.
"""
function remove_multi_edges(g::GNNGraph{<:COO_T})
    s, t = edge_index(g)
    # TODO remove these constraints
    @assert g.num_graphs == 1
    @assert g.edata === (;)
    @assert get_edge_weight(g) === nothing

    idxs, idxmax = edge_encoding(s, t, g.num_nodes)
    union!(idxs)
    s, t = edge_decoding(idxs, g.num_nodes)

    GNNGraph((s, t, nothing),
            g.num_nodes, length(s), g.num_graphs,
            g.graph_indicator,
            g.ndata, g.edata, g.gdata)
end

"""
    add_edges(g::GNNGraph, s::AbstractVector, t::AbstractVector; [edata])

Add to graph `g` the edges with source nodes `s` and target nodes `t`.
"""
function add_edges(g::GNNGraph{<:COO_T},
        snew::AbstractVector{<:Integer},
        tnew::AbstractVector{<:Integer};
        edata=nothing)

    @assert length(snew) == length(tnew)
    # TODO remove this constraint
    @assert get_edge_weight(g) === nothing

    edata = normalize_graphdata(edata, default_name=:e, n=length(snew))
    edata = cat_features(g.edata, edata)

    s, t = edge_index(g)
    s = [s; snew]
    t = [t; tnew]

    GNNGraph((s, t, nothing),
            g.num_nodes, length(s), g.num_graphs,
            g.graph_indicator,
            g.ndata, edata, g.gdata)
end

### TODO Cannot implement this since GNNGraph is immutable (cannot change num_edges)
# function Graphs.add_edge!(g::GNNGraph{<:COO_T}, snew::T, tnew::T; edata=nothing) where T<:Union{Integer, AbstractVector}
#     s, t = edge_index(g)
#     @assert length(snew) == length(tnew)
#     # TODO remove this constraint
#     @assert get_edge_weight(g) === nothing

#     edata = normalize_graphdata(edata, default_name=:e, n=length(snew))
#     edata = cat_features(g.edata, edata)

#     s, t = edge_index(g)
#     append!(s, snew)
#     append!(t, tnew)
#     g.num_edges += length(snew)
#     return true
# end

"""
    add_nodes(g::GNNGraph, n; [ndata])

Add `n` new nodes to graph `g`. In the
new graph, these nodes will have indexes from `g.num_nodes + 1`
to `g.num_nodes + n`.
"""
function add_nodes(g::GNNGraph{<:COO_T}, n::Integer; ndata=(;))
    ndata = normalize_graphdata(ndata, default_name=:x, n=n)
    ndata = cat_features(g.ndata, ndata)

    GNNGraph(g.graph,
            g.num_nodes + n, g.num_edges, g.num_graphs,
            g.graph_indicator,
            ndata, g.edata, g.gdata)
end

function SparseArrays.blockdiag(g1::GNNGraph, g2::GNNGraph)
    nv1, nv2 = g1.num_nodes, g2.num_nodes
    if g1.graph isa COO_T
        s1, t1 = edge_index(g1)
        s2, t2 = edge_index(g2)
        s = vcat(s1, nv1 .+ s2)
        t = vcat(t1, nv1 .+ t2)
        w = cat_features(get_edge_weight(g1), get_edge_weight(g2))
        graph = (s, t, w)
        ind1 = isnothing(g1.graph_indicator) ? ones_like(s1, Int, nv1) : g1.graph_indicator
        ind2 = isnothing(g2.graph_indicator) ? ones_like(s2, Int, nv2) : g2.graph_indicator
    elseif g1.graph isa ADJMAT_T
        graph = blockdiag(g1.graph, g2.graph)
        ind1 = isnothing(g1.graph_indicator) ? ones_like(graph, Int, nv1) : g1.graph_indicator
        ind2 = isnothing(g2.graph_indicator) ? ones_like(graph, Int, nv2) : g2.graph_indicator
    end
    graph_indicator = vcat(ind1, g1.num_graphs .+ ind2)

    GNNGraph(graph,
            nv1 + nv2, g1.num_edges + g2.num_edges, g1.num_graphs + g2.num_graphs,
            graph_indicator,
            cat_features(g1.ndata, g2.ndata),
            cat_features(g1.edata, g2.edata),
            cat_features(g1.gdata, g2.gdata))
end

# PIRACY
function SparseArrays.blockdiag(A1::AbstractMatrix, A2::AbstractMatrix)
    m1, n1 = size(A1)
    @assert m1 == n1
    m2, n2 = size(A2)
    @assert m2 == n2
    O1 = fill!(similar(A1, eltype(A1), (m1, n2)), 0)
    O2 = fill!(similar(A1, eltype(A1), (m2, n1)), 0)
    return [A1 O1
            O2 A2]
end

"""
    blockdiag(xs::GNNGraph...)

Equivalent to [`Flux.batch`](@ref).
"""
function SparseArrays.blockdiag(g1::GNNGraph, gothers::GNNGraph...)
    g = g1
    for go in gothers
        g = blockdiag(g, go)
    end
    return g
end

"""
    batch(gs::Vector{<:GNNGraph})

Batch together multiple `GNNGraph`s into a single one
containing the total number of original nodes and edges.

Equivalent to [`SparseArrays.blockdiag`](@ref).
See also [`Flux.unbatch`](@ref).

# Examples

```juliarepl
julia> g1 = rand_graph(4, 6, ndata=ones(8, 4))
GNNGraph:
    num_nodes = 4
    num_edges = 6
    ndata:
        x => (8, 4)

julia> g2 = rand_graph(7, 4, ndata=zeros(8, 7))
GNNGraph:
    num_nodes = 7
    num_edges = 4
    ndata:
        x => (8, 7)

julia> g12 = Flux.batch([g1, g2])
GNNGraph:
    num_nodes = 11
    num_edges = 10
    num_graphs = 2
    ndata:
        x => (8, 11)

julia> g12.ndata.x
8×11 Matrix{Float64}:
 1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
 1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
 1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
 1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
 1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
 1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
 1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
 1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
```
"""
Flux.batch(gs::Vector{<:GNNGraph}) = blockdiag(gs...)

"""
    unbatch(g::GNNGraph)

Opposite of the [`Flux.batch`](@ref) operation, returns
an array of the individual graphs batched together in `g`.

See also [`Flux.batch`](@ref) and [`getgraph`](@ref).

# Examples

```juliarepl
julia> gbatched = Flux.batch([rand_graph(5, 6), rand_graph(10, 8), rand_graph(4,2)])
GNNGraph:
    num_nodes = 19
    num_edges = 16
    num_graphs = 3

julia> Flux.unbatch(gbatched)
3-element Vector{GNNGraph{Tuple{Vector{Int64}, Vector{Int64}, Nothing}}}:
 GNNGraph:
    num_nodes = 5
    num_edges = 6

 GNNGraph:
    num_nodes = 10
    num_edges = 8

 GNNGraph:
    num_nodes = 4
    num_edges = 2
```
"""
function Flux.unbatch(g::GNNGraph)
    [getgraph(g, i) for i in 1:g.num_graphs]
end

"""
    getgraph(g::GNNGraph, i; nmap=false)

Return the subgraph of `g` induced by those nodes `j`
for which `g.graph_indicator[j] == i` or,
if `i` is a collection, `g.graph_indicator[j] ∈ i`.
In other words, it extract the component graphs from a batched graph.

If `nmap=true`, return also a vector `v` mapping the new nodes to the old ones.
The node `i` in the subgraph will correspond to the node `v[i]` in `g`.
"""
getgraph(g::GNNGraph, i::Int; kws...) = getgraph(g, [i]; kws...)

function getgraph(g::GNNGraph, i::AbstractVector{Int}; nmap=false)
    if g.graph_indicator === nothing
        @assert i == [1]
        if nmap
            return g, 1:g.num_nodes
        else
            return g
        end
    end

    node_mask = g.graph_indicator .∈ Ref(i)

    nodes = (1:g.num_nodes)[node_mask]
    nodemap = Dict(v => vnew for (vnew, v) in enumerate(nodes))

    graphmap = Dict(i => inew for (inew, i) in enumerate(i))
    graph_indicator = [graphmap[i] for i in g.graph_indicator[node_mask]]

    s, t = edge_index(g)
    w = get_edge_weight(g)
    edge_mask = s .∈ Ref(nodes)

    if g.graph isa COO_T
        s = [nodemap[i] for i in s[edge_mask]]
        t = [nodemap[i] for i in t[edge_mask]]
        w = isnothing(w) ? nothing : w[edge_mask]
        graph = (s, t, w)
    elseif g.graph isa ADJMAT_T
        graph = g.graph[nodes, nodes]
    end

    ndata = getobs(g.ndata, node_mask)
    edata = getobs(g.edata, edge_mask)
    gdata = getobs(g.gdata, i)

    num_edges = sum(edge_mask)
    num_nodes = length(graph_indicator)
    num_graphs = length(i)

    gnew = GNNGraph(graph,
                num_nodes, num_edges, num_graphs,
                graph_indicator,
                ndata, edata, gdata)

    if nmap
        return gnew, nodes
    else
        return gnew
    end
end

"""
    negative_sample(g::GNNGraph;
                    num_neg_edges = g.num_edges,
                    bidirected = is_bidirected(g))

Return a graph containing random negative edges (i.e. non-edges) from graph `g` as edges.

Is `bidirected=true`, the output graph will be bidirected and there will be no
leakage from the origin graph.

See also [`is_bidirected`](@ref).
"""
function negative_sample(g::GNNGraph;
        max_trials=3,
        num_neg_edges=g.num_edges,
        bidirected = is_bidirected(g))

    @assert g.num_graphs == 1
    # Consider self-loops as positive edges
    # Construct new graph dropping features
    g = add_self_loops(GNNGraph(edge_index(g), num_nodes=g.num_nodes))

    s, t = edge_index(g)
    n = g.num_nodes
    if s isa CuArray
        # Convert to gpu since set operations and sampling are not supported by CUDA.jl
        device = Flux.gpu
        s, t = Flux.cpu(s), Flux.cpu(t)
    else
        device = Flux.cpu
    end
    idx_pos, maxid = edge_encoding(s, t, n)
    if bidirected
        num_neg_edges = num_neg_edges ÷ 2
        pneg = 1 - g.num_edges / 2maxid # prob of selecting negative edge
    else
        pneg = 1 - g.num_edges / 2maxid # prob of selecting negative edge
    end
    # pneg * sample_prob * maxid == num_neg_edges
    sample_prob = min(1, num_neg_edges / (pneg * maxid) * 1.1)
    idx_neg = Int[]
    for _ in 1:max_trials
        rnd = randsubseq(1:maxid, sample_prob)
        setdiff!(rnd, idx_pos)
        union!(idx_neg, rnd)
        if length(idx_neg) >= num_neg_edges
            idx_neg = idx_neg[1:num_neg_edges]
            break
        end
    end
    s_neg, t_neg = edge_decoding(idx_neg, n)
    if bidirected
        s_neg, t_neg = [s_neg; t_neg], [t_neg; s_neg]
    end
    return GNNGraph(s_neg, t_neg, num_nodes=n) |> device
end

"""
    rand_edge_split(g::GNNGraph, frac; bidirected=is_bidirected(g)) -> g1, g2

Randomly partition the edges in `g` to from two graphs, `g1`
and `g2`. Both will have the same number of nodes as `g`.
`g1` will contain a fraction `frac` of the original edges,
while `g2` wil contain the rest.

If `bidirected = true` makes sure that an edge and its reverse go into the same split.
This option is supported only for bidirected graphs with no self-loops
and multi-edges.

`rand_edge_split` is tipically used to create train/test splits in link prediction tasks.
"""
function rand_edge_split(g::GNNGraph, frac; bidirected=is_bidirected(g))
    s, t = edge_index(g)
    ne = bidirected ? g.num_edges ÷ 2 : g.num_edges
    eids = randperm(ne)
    size1 = round(Int, ne * frac)

    if !bidirected
        s1, t1 = s[eids[1:size1]], t[eids[1:size1]]
        s2, t2 = s[eids[size1+1:end]], t[eids[size1+1:end]]
    else
        @assert is_bidirected(g)
        @assert !has_self_loops(g)
        @assert !has_multi_edges(g)
        mask = s .< t
        s, t = s[mask], t[mask]
        s1, t1 = s[eids[1:size1]], t[eids[1:size1]]
        s1, t1 = [s1; t1], [t1; s1]
        s2, t2 = s[eids[size1+1:end]], t[eids[size1+1:end]]
        s2, t2 = [s2; t2], [t2; s2]
    end
    g1 = GNNGraph(s1, t1, num_nodes=g.num_nodes)
    g2 = GNNGraph(s2, t2, num_nodes=g.num_nodes)
    return g1, g2
end

# """
# Transform vector of cartesian indexes into a tuple of vectors containing integers.
# """
ci2t(ci::AbstractVector{<:CartesianIndex}, dims) = ntuple(i -> map(x -> x[i], ci), dims)

@non_differentiable negative_sample(x...)
@non_differentiable add_self_loops(x...)     # TODO this is wrong, since g carries feature arrays, needs rrule
@non_differentiable remove_self_loops(x...)  # TODO this is wrong, since g carries feature arrays, needs rrule


# import Flux
# import GPUArrays
# import NNlib
import ProgressMeter
import ValueHistories
# import Zygote


module RunSettings
    #
    # This module contains things needed to control one run e.g. the output destination, number of households to use andd so on.
    #
    using Parameters

    export
        Settings,
        DEFAULT_SETTINGS,
        MT_Routing,
        uc_full,
        lmt_full,
        modelled_phase_in

    @enum MT_Routing uc_full lmt_full modelled_phase_in
    @with_kw mutable struct Settings
        uid :: Int = 1 # placeholder for maybe a user somewhere
        run_name :: String = "default_run"
        start_year :: Int = 2015
        end_year :: Int = 2018
        scotland_full :: Bool = true
        weighted :: Bool = false
        household_name = "model_households_scotland"
        people_name    = "model_people_scotland"

        num_households :: Int = 0
        num_people :: Int = 0
        prices_file = "indexes_sep_1_2021.tab"
        to_y :: Int = 2021
        to_q :: Int = 2
        output_dir :: String = joinpath(tempdir(),"output")
        # ... and so on
        means_tested_routing :: MT_Routing = uc_full
    end

    const DEFAULT_SETTINGS = Settings()

end


abstract ClassicalSimulator{T} <: Simulator{T}
abstract Classical2dSimulator{T} <: ClassicalSimulator{T}
abstract Classical3dSimulator{T} <: ClassicalSimulator{T}

function initializeCount!(simulator::ClassicalSimulator)
	simulator.countiteration = 0
end

function getSpaceCount(simulator::ClassicalSimulator)
	return getCount(simulator)
end

function getTrotterCount(simulator::ClassicalSimulator)
	return 0
end

function getFirstTerm(simulator::ClassicalSimulator)
	return getTemperature(simulator)
end


import ..GPU

import ..YOLO: loadWeights!

export loadWeights!

"""
    flipkernel(x)

Flips given kernel
"""
flipkernel(x) = x[end:-1:1, end:-1:1, :, :]

"""
    updateconv!(c, gamma, mean, variance)

Applies batch-normalization to given convolutional layer
"""
function updateconv!(c, gamma, mean, variance)
    gamma4 = reshape(gamma, 1, 1, 1, :)
    variance4 = reshape(variance, 1, 1, 1, :)
    gamma3 = reshape(gamma, 1, 1, :, 1)
    mean3 = reshape(mean, 1, 1, :, 1)
    variance3 = reshape(variance, 1, 1, :, 1)
    #update c.w
    a = convert(Float32, 0.001)
    c.w = (c.w .* gamma4) ./ sqrt.(variance4 .+ a)
    #update c.b
    c.b = c.b .- (gamma3 .* mean3 ./ sqrt.(variance3 .+ a))
    return c.w, c.b
end

"""
    loadWeights!(model::YOLO.v2_tiny.v2Chain, weights_filepath::String)

Loads layers' weights from given weights file path.
"""
function loadWeights!(model::Chain_layers, settings::Settings, nr_constants = 4)
    outlength = settings.cell_bboxes * (5 + settings.num_classes)

    io = open(settings.weights_filepath, "r") #do io
        readconstants!(io, nr_constants)
        #First Conv layer
        loadconv!(model.layers[1].layers[1], io, 3, 3, 3, 32)#25
        #Second Conv layer
        loadconv!(model.layers[1].layers[3], io, 3, 3, 32, 64)#37
        #Third Conv layer
        loadconv!(model.layers[1].layers[5], io, 3, 3, 64, 128)#49
        #4th Conv layer
        loadconv!(model.layers[1].layers[6], io, 1, 1, 128, 64)#57
        #5th Conv layer
        loadconv!(model.layers[1].layers[7], io, 3, 3, 64, 128)#65
        #6th Conv layer
        loadconv!(model.layers[1].layers[9], io, 3, 3, 128, 256)#77
	#7th Conv layer
        loadconv!(model.layers[1].layers[10], io, 1, 1, 256, 128)#85
	#8th Conv layer
        loadconv!(model.layers[1].layers[11], io, 3, 3, 128, 256)#93
	#9th Conv layer
        loadconv!(model.layers[1].layers[13], io, 3, 3, 256, 512)#105
	#10th Conv layer
        loadconv!(model.layers[1].layers[14], io, 1, 1, 512, 256)#113
	#11th Conv layer
        loadconv!(model.layers[1].layers[15], io, 3, 3, 256, 512)#121
	#12th Conv layer
        loadconv!(model.layers[1].layers[16], io, 1, 1, 512, 256)#129
	#13th Conv layer
        loadconv!(model.layers[1].layers[17], io, 3, 3, 256, 512)#137
	#14th Conv layer
        loadconv!(model.layers[2].layers[2], io, 3, 3, 512, 1024)#149
	#15th Conv layer
        loadconv!(model.layers[2].layers[3], io, 1, 1, 1024, 512)#157
	#16th Conv layer
        loadconv!(model.layers[2].layers[4], io, 3, 3, 512, 1024)#165
	#17th Conv layer
        loadconv!(model.layers[2].layers[5], io, 1, 1, 1024, 512)#173
	#18th Conv layer
        loadconv!(model.layers[2].layers[6], io, 3, 3, 512, 1024)#181
	#19th Conv layer
        loadconv!(model.layers[2].layers[7], io, 3, 3, 1024, 1024)#192
	#20th Conv layer
        loadconv!(model.layers[2].layers[8], io, 3, 3, 1024, 1024)#200
	#21st Conv layer
        loadconv!(model.layers[3].layers[1], io, 1, 1, 512, 64)#211
	#22nd Conv layer
        loadconv!(model.layers[4].layers[1], io, 3, 3, 1280, 1024)#225
	#23rd Conv layer
        loadconv_last!(model.layers[4].layers[2], io, 1, 1, 1024, 125)#233 no batch norm here
        if GPU >= 0
          end
    # end
end

"""
    loadconv!(c, io, d1, d2, d3, d4)

Loads the io to given convolutional layer and updates it by batch-normalization
"""
function loadconv!(c, io::IOStream, d1, d2, d3, d4)
    read!(io, c.b)
    gamma = Array{Float32}(undef, d4)
    mean = Array{Float32}(undef, d4)
    variance = Array{Float32}(undef, d4)
    read!(io, gamma)
    read!(io, mean)
    read!(io, variance)
    toRead = Array{Float32}(undef, d4 * d3 * d2 * d1)
    read!(io, toRead)
    toRead = reshape(toRead, d1, d2, d3, d4)
    c.w = permutedims(toRead, [2, 1, 3, 4])
    c.w, c.b = updateconv!(c, gamma, mean, variance)
    c.w = flipkernel(c.w)
    if GPU >= 0
        c.w = KnetArray(c.w)
        c.b = KnetArray(c.b)
    end
end

# to be simplified. the weights file does not have batch norm data for the last layer
function loadconv_last!(c, io::IOStream, d1, d2, d3, d4)
    read!(io, c.b)
    toRead = Array{Float32}(undef, d4 * d3 * d2 * d1)
    read!(io, toRead)
    toRead = reshape(toRead, d1, d2, d3, d4)
    c.w = permutedims(toRead, [2, 1, 3, 4])
    c.w = flipkernel(c.w)
    if GPU >= 0
        c.w = KnetArray(c.w)
        c.b = KnetArray(c.b)
    end
end
"""
    readconstants!(io)

Read constant and unnecessary numbers from the io stream
"""
function readconstants!(io::IOStream, nr_constants = 4)
    # major = read(io, Int32)
    # minor = read(io, Int32)
    # revision = read(io, Int32)
    # iseen = read(io, Int32)
	# dummy = read(io, Float32)
	for i = 1:nr_constants
		read(io, Float32)
	end
end

## initial
# io = open(settings.weights_filepath, "r") #do io
# 	readconstants!(io)
# 	#First Conv layer
# 	loadconv!(model.layers[1], io, 3, 3, 3, 32)#25
# 	#Second Conv layer
# 	loadconv!(model.layers[3], io, 3, 3, 32, 64)#37
# 	#Third Conv layer
# 	loadconv!(model.layers[5], io, 3, 3, 64, 128)#49
# 	#4th Conv layer
# 	loadconv!(model.layers[6], io, 1, 1, 128, 64)#57
# 	#5th Conv layer
# 	loadconv!(model.layers[7], io, 3, 3, 64, 128)#65
# 	#6th Conv layer
# 	loadconv!(model.layers[9], io, 3, 3, 128, 256)#77
# 	#7th Conv layer
# 	loadconv!(model.layers[10], io, 1, 1, 256, 128)#85
# 	#8th Conv layer
# 	loadconv!(model.layers[11], io, 3, 3, 128, 256)#93
# 	#9th Conv layer
# 	loadconv!(model.layers[13], io, 3, 3, 256, 512)#105
# 	#10th Conv layer
# 	loadconv!(model.layers[14], io, 1, 1, 512, 256)#113
# 	#11th Conv layer
# 	loadconv!(model.layers[15], io, 3, 3, 256, 512)#121
# 	#12th Conv layer
# 	loadconv!(model.layers[16], io, 1, 1, 512, 256)#129
# 	#13th Conv layer
# 	loadconv!(model.layers[17], io, 3, 3, 256, 512)#137
# 	#14th Conv layer
# 	loadconv!(model.layers[19], io, 3, 3, 512, 1024)#149
# 	#15th Conv layer
# 	loadconv!(model.layers[20], io, 1, 1, 1024, 512)#157
# 	#16th Conv layer
# 	loadconv!(model.layers[21], io, 3, 3, 512, 1024)#165
# 	#17th Conv layer
# 	loadconv!(model.layers[22], io, 1, 1, 1024, 512)#173
# 	#18th Conv layer
# 	loadconv!(model.layers[23], io, 3, 3, 512, 1024)#181
# 	#19th Conv layer
# 	loadconv!(model.layers[24], io, 3, 3, 1024, 1024)#192
# 	#20th Conv layer
# 	loadconv!(model.layers[25], io, 3, 3, 1024, 1024)#200
# 	#21st Conv layer
# 	loadconv!(model.layers[26], io, 1, 1, 512, 64)#211
# 	#22nd Conv layer
# 	loadconv!(model.layers[27], io, 3, 3, 1280, 1024)#225
# 	#23rd Conv layer
# 	loadconv_last!(model.layers[28], io, 1, 1, 1024, 125)#233
# 	if GPU >= 0
# 	  end


export add_scalar_inverse

function add_scalar_inverse(mgr::Ptr{Manager}, f::Ptr{Node}, epsilon::Ptr{Node})
    result = Cudd_addScalarInverse(mgr, f, epsilon)
    if result == C_NULL
        throw(OutOfMemoryError())
    end
    return result
end


export exitwithteststatus, asfloat16, asfloat32, asfloat64, asint, histdict, asdict
export tryasfloat32, tryasfloat64, tryasint, fromimage
export serve

function exitwithteststatus()
    s = FactCheck.getstats()
    if s["nNonSuccessful"] == 0
        print("   PASSED!")
    else
        print("   FAILED: $(s["nFailures"]) failures and $(s["nErrors"]) errors")
    end
    println("   ( $(s["nNonSuccessful"]+s["nSuccesses"]) tests for runtests.jl $(join(ARGS, " ")))")
    exit(s["nNonSuccessful"])
end

asint(a) = round(Int, a)
asint(a::AbstractString) = parse(Int, a)
asfloat16(a) = map(Float16, a)
asfloat32(a) = map(Float32, a)
asfloat32(a::AbstractString) = parse(Float32, a)
if isinstalled("Images")
    function asfloat32{T<:AbstractImage}(a::T)
        a = raw(a)
        if ndims(a) == 3
            a = permutedims(a,[3,2,1])
            a = a[:,:,1:min(sizeo(a),3)]
        else
            a = a'
        end
        @p asfloat32 a | divby 255 | clamp 0 1
    end
end

asfloat64(a) = map(Float64, a)
asfloat64(a::AbstractString) = parse(Float64, a)

tryasint(a, d = a) = try asint(a) catch return d end
tryasfloat32(a, d = a) = try asfloat32(a) catch return d end
tryasfloat64(a, d = a) = try asfloat64(a) catch return d end

histdict(a, field) = @p extract a field | histdict
function histdict(a)
    d = Dict()
    @p map a x->d[x] = get(d,x,0) + 1
    d
end

asdict(a) = @p fieldnames a | map (x->Pair(x,getfield(a,x))) | Dict
asdict{T<:Union{Dict,AbstractString,Array}}(a::T) = a

function serve(basepath::AbstractString, args...;kargs...)
    basepath = abspath(basepath)
    serve(args...; kargs...) do req, res
        filename = @p concat basepath req.resource | normpath
        @show filename
        if startswith(filename, basepath) && existsfile(filename) && !isdir(filename)
            HttpServer.FileResponse(filename)
        else
            Response(403)
        end
    end
end

function serve(f::Function, portrange = 8080:8199; ngrok = false)
    eval(:(using HttpServer))
    server = Server(HttpHandler(f))
    port, tcp = listenany(fst(portrange))
    close(tcp)
    @async run(server, port)
    if ngrok
        (stdout,stdin,process) = readandwrite(`ngrok http $port -log stdout --log-level "debug"`);
        a = eachline(stdout)
        b = []
        @async for x in a
            if contains(x,"Hostname:")
                push!(b,x)
                break
            end
        end
        for x in 1:50
            isempty(b) || break
            sleep(0.1)
        end
        return @p fst b | split "Hostname:" | last | split ".ngrok.io" | fst | concat ".ngrok.io"
    end
    Int(port)
end


fbroadcast(f::Function, a::NamedTuple{K}) where K = NamedTuple{K}(f.(values(a)))
fbroadcast(f::Function, a::OrderedDict) = OrderedDict(k => f(v) for (k, v) in a)
fbroadcast(f::Function, a) = f.(a)

treemap(f::Function, t; kwargs...) = treemap((t, chs) -> f(only(t), chs), (t,); kwargs...)
treemap(f::Function, ts...; kwargs...) = treemap(f, ts; kwargs...)
function treemap(f::Function, ts::Tuple; complete::Bool=false, order::AbstractOrder=PostOrder())
    _treemap(f, ts, complete, order)
end

# TODO finish preorder, think about how both maps should behave in case of complete traversals
# maybe annotate about named tuples? With traits?
#
# select_keys(x, y) = error("Inconsistent types of children of mapped vertex and original vertices")
# select_keys(x::Tuple, y::Tuple) = x[eachindex(y)]
# function select_keys(x::NamedTuple, y::NamedTuple)
#     NamedTuple{keys(y)}(tuple((x[k] for k in keys(y))...))
# end

function _treemap(f::Function, ts::Tuple, complete::Bool, order::PreOrder)
    error("Treemaps using PreOrder() are not supported yet")
end

# function _treemap(f::Function, ts::Tuple, complete::Bool, order::PreOrder)
#     n = f(ts, _children_sorted.(ts))
#     isleaf(n) && return n
#     nchs = children(n)
#     chs = _children_pairs_keys(ts, complete)
#     @show keys(nchs), keys(chs)
#     @assert keys(nchs) ⊆ keys(chs)
#     mchs = fbroadcast(ts -> _treemap(f, ts, complete, order), select_keys(chs, nchs))
#     set_children(n, mchs)
# end

function _treemap(f::Function, ts::Tuple, complete::Bool, order::PostOrder)
    chs = fbroadcast(ts -> _treemap(f, ts, complete, order), _children_pairs(ts, complete))
    return f(ts, chs)
end

function _treemap(f::Function, ts::Tuple, complete::Bool, order::LevelOrder)
    error("Treemaps using LevelOrder() are not supported yet")
end

function treemap!(f::Function, ts...; complete::Bool=false, order::AbstractOrder=PostOrder())
    foreach(f, NodeIterator(ts...; order=order, complete=complete))
end
function leafmap!(f::Function, ts...; complete::Bool=false, order::AbstractOrder=PostOrder())
    foreach(f, LeafIterator(ts...; order=order, complete=complete))
end
function typemap!(f::Function, t::Union{Type, Tuple{Vararg{Type}}}, ts...; complete::Bool=false,
                  order::AbstractOrder=PostOrder())
    foreach(f, TypeIterator(t, ts...; order=order, complete=complete))
end
function predicatemap!(f::Function, pred::Function, ts...; complete::Bool=false,
                  order::AbstractOrder=PostOrder())
    foreach(f, PredicateIterator(pred, ts...; order=order, complete=complete))
end


using Cbc
using QuadraticToBinary

CBC_OPTIMIZER = Cbc.Optimizer()
# MOI.set(CBC_OPTIMIZER, MOI.Silent(), false)
MOI.set(CBC_OPTIMIZER, MOI.TimeLimitSec(), 5)

CBC = CBC_OPTIMIZER

push!(solvers, (opt = CBC, mode = BilevelJuMP.SOS1Mode()))
push!(solvers_sos, (opt = CBC, mode = BilevelJuMP.SOS1Mode()))
push!(solvers_indicator, (opt = CBC, mode = BilevelJuMP.IndicatorMode()))

push!(solvers_fa, (opt = CBC, mode = BilevelJuMP.FortunyAmatMcCarlMode(with_slack = false)))
push!(solvers_fa, (opt = CBC, mode = BilevelJuMP.FortunyAmatMcCarlMode(with_slack = true)))
push!(solvers_fa2, (opt = CBC, mode = BilevelJuMP.FortunyAmatMcCarlMode(primal_big_M = 100, dual_big_M = 100)))

CBC_CACHED = MOIU.CachingOptimizer(MOIU.UniversalFallback(MOIU.Model{Float64}()), CBC_OPTIMIZER)
push!(solvers_cached, (opt = CBC_CACHED, mode = BilevelJuMP.SOS1Mode()))

push!(solvers_bin_exp, (
    opt = QuadraticToBinary.Optimizer{Float64}(CBC_CACHED),
    mode = BilevelJuMP.StrongDualityMode(1e-9, inequality = true)))
MOI.set(solvers_bin_exp[end].opt, QuadraticToBinary.GlobalVariablePrecision(), 1e-5)

push!(solvers_bin_exp, (
    opt = QuadraticToBinary.Optimizer{Float64}(CBC_CACHED),
    mode = BilevelJuMP.StrongDualityMode(inequality = false)))
MOI.set(solvers_bin_exp[end].opt, QuadraticToBinary.GlobalVariablePrecision(), 1e-5)

QTB_CBC_BRIDGED = MOI.Bridges.full_bridge_optimizer(
    QuadraticToBinary.Optimizer{Float64}(CBC_CACHED, lb=-100,ub=100),
    Float64)
MOI.Bridges.add_bridge(QTB_CBC_BRIDGED, MOI.Bridges.Constraint.SOCtoNonConvexQuadBridge{Float64})
MOI.set(QTB_CBC_BRIDGED, QuadraticToBinary.GlobalVariablePrecision(), 1e-5)

push!(solvers_sos_quad_bin, (
    opt = QTB_CBC_BRIDGED,
    mode = BilevelJuMP.SOS1Mode()))

push!(solvers_fa_quad_bin, (
    opt = QTB_CBC_BRIDGED,
    mode = BilevelJuMP.FortunyAmatMcCarlMode(primal_big_M = 100, dual_big_M = 100)))

QTB_CBC = QuadraticToBinary.Optimizer{Float64}(CBC_CACHED, lb=-5, ub=5)
QTB_CBC_B = MOI.Bridges.Constraint.SOCtoNonConvexQuad{Float64}(QTB_CBC)
push!(solvers_fa_quad_bin_mixed, (
    opt = QTB_CBC_B,
    mode = BilevelJuMP.MixedMode(default=
        BilevelJuMP.FortunyAmatMcCarlMode(primal_big_M = 10, dual_big_M = 10))))
MOI.set(QTB_CBC_B, QuadraticToBinary.GlobalVariablePrecision(), 1e-3)


import VideoIO
import Plots
import AbstractPlotting

import Makie
using Images, CoordinateTransformations, OffsetArrays

function main_makie()
    f = VideoIO.opencamera()
    img = read(f)
    tfm = recenter(RotMatrix(pi/2), Images.center(img))
    img=parent(warp(img,tfm))
    scene = Makie.Scene(resolution = size(img))
    makieimg = Makie.image!(
        scene,
        img,
        show_axis = false,
        scale_plot = true
    )[end]
    display(scene)

    while !eof(f)
        # OffsetArrays -> Array
        img=parent(warp(read(f),tfm))
        @info size(img)
        makieimg[1] = img
        sleep(1/f.framerate)
    end
end

function main_plots()
    f = VideoIO.opencamera()
    img = read(f)
    p=Plots.plot(img) |> display
    for i in 1:100
        @info i
        read!(f,img)
        Plots.plot(img, title="$i") |> display
    end
end


### abstract type hierarchy

abstract type Basis end
abstract type Basislike <: Basis end

# the basis in which derivatives are sparse for a given field
struct DerivBasis <: Basislike end
const Ð! = DerivBasis
const Ð  = DerivBasis

# the basis in which lensing is a pixel remapping for a given field
struct LenseBasis <: Basislike end
const Ł! = LenseBasis
const Ł  = LenseBasis

# the nearest harmonic basis, e.g. for anything QU its QUFourier and
# for anything EB its EBFourier
abstract type HarmonicBasis <: Basislike end

## fields
abstract type Field{B<:Basis,T} <: AbstractVector{T} end
abstract type ImplicitField{B<:Basis,T} <: Field{B,T} end

## linear operators
abstract type ImplicitOp{T} <: AbstractMatrix{T} end
const DiagOp{F<:Field,T} = Diagonal{T,F}
# includes Diagonal and Adjoint wrappers, which can't be made <:ImplicitOp directly
const FieldOp{T} = Union{ImplicitOp{T},Adjoint{T,<:ImplicitOp{T}},Diagonal{T,<:Field{<:Any,T}}}

# scalars which are allowed in our expressions must be real because we
# implicitly assume our maps are real, and addition/multiplication by a complex
# number, even of the fourier transform, would break this.
const Scalar = Real

# other useful unions
const FieldOrOp   = Union{Field,FieldOp}
const FieldOpScal = Union{Field,FieldOp,Scalar}

## basis types
abstract type BasisProd{Bs} <: Basis end

struct Basis2Prod{B₁,B₂}    <: BasisProd{Tuple{B₁,B₂}} end
struct Basis3Prod{B₁,B₂,B₃} <: BasisProd{Tuple{B₁,B₂,B₃}} end

abstract type S0Basis <: Basis end
abstract type PolBasis <: Basis end

struct Map     <: S0Basis end
struct Fourier <: S0Basis end
struct 𝐈       <: PolBasis end
struct 𝐐𝐔      <: PolBasis end
struct 𝐄𝐁      <: PolBasis end

const QUMap      = Basis2Prod{    𝐐𝐔, Map     }
const EBMap      = Basis2Prod{    𝐄𝐁, Map     }
const QUFourier  = Basis2Prod{    𝐐𝐔, Fourier }
const EBFourier  = Basis2Prod{    𝐄𝐁, Fourier }
const IQUMap     = Basis3Prod{ 𝐈, 𝐐𝐔, Map     }
const IEBMap     = Basis3Prod{ 𝐈, 𝐄𝐁, Map     }
const IQUFourier = Basis3Prod{ 𝐈, 𝐐𝐔, Fourier }
const IEBFourier = Basis3Prod{ 𝐈, 𝐄𝐁, Fourier }

# handy for picking out anything Map/Fourier
const SpatialBasis{B,I,P} = Union{B, Basis2Prod{P,B}, Basis3Prod{I,P,B}}

## applying and converting bases

LenseBasis(                            ::S0Basis)  = Map
LenseBasis(::Basis2Prod{   <:PolBasis, <:S0Basis}) = QUMap
LenseBasis(::Basis3Prod{𝐈, <:PolBasis, <:S0Basis}) = IQUMap
DerivBasis(                            ::S0Basis)  = Fourier
DerivBasis(::Basis2Prod{   <:PolBasis, <:S0Basis}) = QUFourier
DerivBasis(::Basis3Prod{𝐈, <:PolBasis, <:S0Basis}) = IQUFourier
HarmonicBasis(                         ::S0Basis)  = Fourier
HarmonicBasis(::Basis2Prod{𝐐𝐔,         <:S0Basis}) = QUFourier
HarmonicBasis(::Basis2Prod{𝐄𝐁,         <:S0Basis}) = EBFourier

# B(::Basis) converts the basis type, e.g. Map(QUFourier()) = QUMap
(::Type{B})(::B′) where {B<:S0Basis, B′<:S0Basis} = B
(::Type{B})(::Basis2Prod{  Pol,B′}) where {Pol, B<:S0Basis, B′<:S0Basis} = Basis2Prod{  Pol,B}
(::Type{B})(::Basis3Prod{𝐈,Pol,B′}) where {Pol, B<:S0Basis, B′<:S0Basis} = Basis3Prod{𝐈,Pol,B}

# B(f::Field) where B is a basis converts f to that basis. This is the
# fallback if the field is already in the right basis.
(::Type{B})(f::Field{B}) where {B<:Basis} = f
# This is the fallback if no conversion is explicilty defined, which
# tries to first convert the basis, e.g. Map(f::BaseQUFourier) becomes
# Map(QUFourier())(f) which calls QUMap(f). This is also used for
# Basislike objects like LenseBasis(...)
(::Type{B})(f::Field{B′}) where {B′<:Basis,B<:Basis} = B(B′())(f)

# In-place version of the above
(::Type{B})(f′::Field, f::Field{B′}) where {B′<:Basis,B<:Basis} = B(B′())(f′,f)
# And the case where its already in the right basis (but note, we
# never actually set f′ in this case, which is more efficient, but
# necessitates some care when using this construct)
(::Type{B})(f′::Field{B}, f::Field{B}) where {B<:Basis} = f

# Basis conversion automatically maps over arrays
(::Type{B})(a::AbstractArray{<:Field}...) where {B<:Basis} = B.(a...)

# The abstract `Basis` type means "any basis", hence this conversion rule:
Basis(f::Field) = f

# used in make_field_aliases below
basis_aliases = OrderedDict(
    "Map"        => Map,
    "Fourier"    => Fourier,
    "QUMap"      => QUMap,
    "QUFourier"  => QUFourier,
    "EBMap"      => EBMap,
    "EBFourier"  => EBFourier,
    "IQUMap"     => IQUMap,
    "IQUFourier" => IQUFourier,
    "IEBMap"     => IEBMap,
    "IEBFourier" => IEBFourier,
    "S0"         => S0Basis,
    "QU"         => Basis2Prod{   𝐐𝐔        , <:S0Basis},
    "EB"         => Basis2Prod{   𝐄𝐁        , <:S0Basis},
    "S2Map"      => Basis2Prod{   <:PolBasis, Map},
    "S2Fourier"  => Basis2Prod{   <:PolBasis, Fourier},
    "S2"         => Basis2Prod{   <:PolBasis, <:S0Basis},
    "IQU"        => Basis3Prod{𝐈, 𝐐𝐔        , <:S0Basis},
    "IEB"        => Basis3Prod{𝐈, 𝐄𝐁        , <:S0Basis},
    "S02Map"     => Basis3Prod{𝐈, <:PolBasis, Map},
    "S02Fourier" => Basis3Prod{𝐈, <:PolBasis, Fourier},
    "S02"        => Basis3Prod{𝐈, <:PolBasis, <:S0Basis},
    "Field"      => Any
)

# Enumerates all the fields types like FlatMap, FlatFourier, etc...
# for a field_name like "Flat" and a bound on the M type parameter in
# BaseField{B,M,T,A}. Note: the seemingly-redundant <:AbstractArray{T}
# in the argument (which is enforced in BaseField anyway) is there to
# help prevent method ambiguities
function make_field_aliases(field_root, M_bound; export_names=true, extra_aliases=Dict())
    for (basis_alias, B) in merge(basis_aliases, extra_aliases)
        F = Symbol(field_root, basis_alias)
        if isconcretetype(B)
            @eval const $F{       M<:$M_bound, T, A<:AbstractArray{T}} = BaseField{$B, M, T, A}
        else
            @eval const $F{B<:$B, M<:$M_bound, T, A<:AbstractArray{T}} = BaseField{B,  M, T, A}
        end
        if export_names
            @eval export $F
        end
    end
end

# for printing
for (alias,B) in basis_aliases
    if isconcretetype(B)
        @eval typealias(::Type{$B}) = $alias
    end
end

## generic promotion rules
basis_promotion_rules = Dict(
    # spin-0
    (Map,        Fourier)     => Map,
    # spin-2
    (QUMap,      QUFourier)   => QUMap,
    (EBMap,      EBFourier)   => EBFourier,
    (QUMap,      EBMap)       => QUMap,
    (QUFourier,  EBFourier)   => QUFourier,
    (QUMap,      EBFourier)   => QUMap,
    (QUFourier,  EBMap)       => QUFourier,
    # spin-(0,2)
    (IQUMap,     IQUFourier)  => IQUMap,
    (IEBMap,     IEBFourier)  => IEBFourier,
    (IQUMap,     IEBMap)      => IQUMap,
    (IQUFourier, IEBFourier)  => IQUFourier,
    (IQUMap,     IEBFourier)  => IQUMap,
    (IQUFourier, IEBMap)      => IQUFourier
)
for ((B₁,B₂),B) in basis_promotion_rules
    @eval promote_basis_generic_rule(::$B₁, ::$B₂) = $B()
end
promote_basis_generic_rule(::B, ::B) where {B<:Basis} = B()
promote_basis_generic_rule(::Any, ::Any) = Unknown()
promote_basis_generic(x, y) = select_known_rule(promote_basis_generic_rule, x, y)
unknown_rule_error(::typeof(promote_basis_generic_rule), ::B₁, ::B₂) where {B₁, B₂} =
    error("Can't promote fields in $(typealias(B₁)) and $(typealias(B₂)) bases.")

## stricter rules used only in broadcasting
promote_basis_strict_rule(::B,   ::B )        where {B <:Basis}                                                    = B()
promote_basis_strict_rule(::B,   ::Basislike) where {B <:Basis}                                                    = B()
promote_basis_strict_rule(::B₂,  ::B₀)        where {B₀<:Union{Map,Fourier}, B₂ <:Basis2Prod{  <:Union{𝐐𝐔,𝐄𝐁},B₀}} = B₂()
promote_basis_strict_rule(::B₀₂, ::B₀)        where {B₀<:Union{Map,Fourier}, B₀₂<:Basis3Prod{𝐈,<:Union{𝐐𝐔,𝐄𝐁},B₀}} = B₀₂()
promote_basis_strict_rule(::Any, ::Any)                                                                            = Unknown()
promote_basis_strict(x, y) = select_known_rule(promote_basis_strict_rule, x, y)
unknown_rule_error(::typeof(promote_basis_strict_rule), ::B₁, ::B₂) where {B₁, B₂} =
    error("Can't broadcast fields in $(typealias(B₁)) and $(typealias(B₂)) bases.")

# convenience "getter" functions for the B basis type parameter
basis(f::F) where {F<:Field} = basis(F)
basis(::Type{<:Field{B}}) where {B<:Basis} = B
basis(::Type{<:Field}) = Basis
basis(::AbstractVector) = Basis

### printing
typealias(::Type{B}) where {B<:Basis} = string(B)
Base.show_datatype(io::IO, t::Type{<:Union{Field,FieldOp}}) = print(io, typealias(t))
Base.isempty(::ImplicitOp) = true
Base.isempty(::ImplicitField) = true
Base.isempty(::Diagonal{<:Any,<:ImplicitField}) = true
function Base.summary(io::IO, x::FieldOp)
    try
        print(io, join(size(x), "×"), " ")
    catch err
        @assert err isa MethodError
        print(io, "⍰×⍰ ")
    end
    Base.showarg(io, x, true)
end
function Base.summary(io::IO, x::ImplicitField)
    try
        print(io, size(x,1), "-element ")
    catch err
        @assert err isa MethodError
        print(io, "⍰-element ")
    end
    Base.showarg(io, x, true)
end

### logdet

"""
    logdet(L::FieldOp, θ)

If `L` depends on `θ`, evaluates `logdet(L(θ))` offset by its fiducial value at
`L()`. Otherwise, returns 0.
"""
logdet(L::FieldOp, θ) = depends_on(L,θ) ? logdet(L()\L(θ)) : 0
logdet(L::Int, θ) = 0 # the default returns Float64 which unwantedly poisons the backprop to Float64
logdet(L, θ) = logdet(L)

### Simulation

@doc doc"""
    simulate(Σ; rng=global_rng_for(Σ), seed=nothing)

Draw a simulation from the covariance matrix `Σ`, i.e. draw a random vector
$\xi$ such that the covariance $\langle \xi \xi^\dagger \rangle = \Sigma$.

The random number generator `rng` will be used and advanced in the proccess, and
is by default the appropriate one depending on if `Σ` is backed by `Array` or
`CuArray`.

The `seed` argument can also be used to seed the `rng`.
"""
function simulate(Σ; rng=global_rng_for(Σ), seed=nothing, kwargs...)
    isnothing(seed) || seed!(rng, seed)
    simulate(rng, Σ; kwargs...)
end
function white_noise(ξ; rng=global_rng_for(ξ), seed=nothing, kwargs...)
    isnothing(seed) || seed!(rng, seed)
    white_noise(ξ, rng; kwargs...)
end
function fixed_white_noise(ξ; rng=global_rng_for(Σ), seed=nothing, kwargs...)
    isnothing(seed) || seed!(rng, seed)
    fixed_white_noise(ξ, rng; kwargs...)
end

global_rng_for(x::T) where {T<:AbstractArray} = global_rng_for(T)
global_rng_for(::Type{<:Array}) = Random.default_rng()

### spin adjoints
# represents a field which is adjoint over just the "spin" indices.
# multiplying such a field by a non-adjointed one should be the inner
# product over just the spin indices, hence return a spin-0 field,
# rather than a scalar. note: these are really only lightly used in
# one place in LenseFlow, so they have almost no real functionality
# for now.
struct SpinAdjoint{F<:Field}
    f :: F
end
spin_adjoint(f::Field) = SpinAdjoint(f)

### misc

function ≈(x::Field{<:Any,T}, y::Field{<:Any,S}; atol=0, rtol=Base.rtoldefault(T,S,atol)) where {T,S}
    _norm(x) = norm(unbatch(norm(x)))
    _norm(x - y) <= max(atol, rtol*max(_norm(x), _norm(y)))
end

# allows L(θ) or L(ϕ) to work and be a no-op for things which don't
# depend on parameters or a field
(L::Union{FieldOp,UniformScaling})(::Union{Field,NamedTuple}) = L

# allow using `I` as a lensing operator to represent no lensing
alloc_cache(x, ::Any) = x
cache(x, ::Any) = x
cache!(x, ::Any) = x

# caching for adjoints
cache(L::Adjoint, f) = cache(L',f)'
cache!(L::Adjoint, f) = cache!(L',f)'

# todo: fix this
*(::UniformScaling{Bool}, L::FieldOp) = L

# we use Field cat'ing mainly for plotting, e.g. plot([f f; f f]) plots a 2×2
# matrix of maps. the following definitions make it so that Fields aren't
# splatted into a giant matrix when doing [f f; f f] (which they would othewise
# be since they're Arrays)
hvcat(rows::Tuple{Vararg{Int}}, values::Field...) = hvcat(rows, ([x] for x in values)...)
hcat(values::Field...) = hcat(([x] for x in values)...)

### printing
print_array(io::IO, f::Field) = !isempty(f) && print_array(io, f[:])
show_vector(io::IO, f::Field) = !isempty(f) && show_vector(io, f[:])
Base.has_offset_axes(::Field) = false # needed for Diagonal(::Field) if the Field is implicitly-sized

# addition/subtraction works between any fields and scalars, promotion is done
# automatically if fields are in different bases
for op in (:+,:-), (T1,T2,promote) in ((:Field,:Scalar,false),(:Scalar,:Field,false),(:Field,:Field,true))
    @eval ($op)(a::$T1, b::$T2) = broadcast($op, ($promote ? promote(a,b) : (a,b))...)
end

# multiplication/division is not strictly defined for abstract vectors, but
# make it work anyway if the two fields are exactly the same type, in which case
# its clear we wanted broadcasted multiplication/division.
for op in (:*, :/)
    @eval ($op)(a::Field{B}, b::Field{B}) where {B} = broadcast($op, a, b)
end

# package code should use batch_index(f, 1), but for interactive work its
# convenient to be able to do f[!,1]
getindex(x::Union{Real,Field,FieldOp}, ::typeof(!), I) = batch_index(x, I)

# adapt_structure(to, x::Union{ImplicitOp,ImplicitField}) = x
# adapt_structure(to, arr::Array{<:Field}) = map(f -> adapt(to,f), arr)

one(f::Field) = fill!(similar(f), one(eltype(f)))
norm(f::Field) = sqrt(dot(f,f))
# sum_kbn(f::Field) = sum_kbn(f[:])

@init @require PyCall="438e738f-606a-5dbb-bf0a-cddfbfd45ab0" begin
    # never try to auto-convert Fields or FieldOps to Python arrays
    PyCall.PyObject(x::Union{FieldOp,Field}) = PyCall.pyjlwrap_new(x)
end


function networkTiming(; iters = 100)
    for model in [vKITTI_Conv_D,vKITTI_Conv_RGB]
        x = KnetArray(zeros(Float32,224,224,3,1));
        w = Array[
            #Conv64
            xavier(Float32,3,3,3,64),        zeros(Float32,1,1,64,1),
            xavier(Float32,3,3,64,64),       zeros(Float32,1,1,64,1),
            #Conv128
            xavier(Float32,3,3,64,128),      zeros(Float32,1,1,128,1),
            xavier(Float32,3,3,128,128),     zeros(Float32,1,1,128,1),
            #Conv256
            xavier(Float32,3,3,128,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            #Conv512
            xavier(Float32,3,3,256,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            #Scores
            xavier(Float32,1,1,512,13),      zeros(Float32,1,1,13,1),
            #Deconv
            bilinear(Float32,32,32,13,13),   zeros(Float32,1,1,13,1),
        ]; w = map(KnetArray,w);

        println(model)
        times = []
        for i=1:iters
            push!(times,(@timed y = model(w,x))[2])
        end
        println("Minimum: $(minimum(times))s ($(1/minimum(times)) FPS) Maximum: $(maximum(times))s ($(1/maximum(times)) FPS) Average: $(mean(times))s ($(1/mean(times)) FPS)")
    end
    for model in [vKITTI_Conv_RGBD,]
        x = KnetArray(zeros(Float32,224,224,6,1));
        w = Array[
            #RGB
            #Conv64
            xavier(Float32,3,3,3,64),        zeros(Float32,1,1,64,1),
            xavier(Float32,3,3,64,64),       zeros(Float32,1,1,64,1),
            #Conv128
            xavier(Float32,3,3,64,128),      zeros(Float32,1,1,128,1),
            xavier(Float32,3,3,128,128),     zeros(Float32,1,1,128,1),
            #Conv256
            xavier(Float32,3,3,128,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            #Conv512
            xavier(Float32,3,3,256,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            #Colorized Depth
            #Conv64
            xavier(Float32,3,3,3,64),        zeros(Float32,1,1,64,1),
            xavier(Float32,3,3,64,64),       zeros(Float32,1,1,64,1),
            #Conv128
            xavier(Float32,3,3,64,128),      zeros(Float32,1,1,128,1),
            xavier(Float32,3,3,128,128),     zeros(Float32,1,1,128,1),
            #Conv256
            xavier(Float32,3,3,128,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            #Conv512
            xavier(Float32,3,3,256,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            #Scores
            xavier(Float32,1,1,512,13),      zeros(Float32,1,1,13,1),
            #Deconv
            bilinear(Float32,32,32,13,13),   zeros(Float32,1,1,13,1),
        ]; w = map(KnetArray,w);

        println(model)
        times = []
        for i=1:iters
            push!(times,(@timed y = model(w,x))[2])
        end
        println("Minimum: $(minimum(times))s ($(1/minimum(times)) FPS) Maximum: $(maximum(times))s ($(1/maximum(times)) FPS) Average: $(mean(times))s ($(1/mean(times)) FPS)")
    end
    println("***********************")
    h = KnetArray(zeros(Float32,28,28,512,1))
    c = KnetArray(zeros(Float32,28,28,512,1))
    for model in [vKITTI_LSTM_D,vKITTI_LSTM_RGB]
        x = KnetArray(zeros(Float32,224,224,3,1));
        w = Array[
            #Conv64
            xavier(Float32,3,3,3,64),        zeros(Float32,1,1,64,1),
            xavier(Float32,3,3,64,64),       zeros(Float32,1,1,64,1),
            #Conv128
            xavier(Float32,3,3,64,128),      zeros(Float32,1,1,128,1),
            xavier(Float32,3,3,128,128),     zeros(Float32,1,1,128,1),
            #Conv256
            xavier(Float32,3,3,128,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            #Conv512
            xavier(Float32,3,3,256,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            #LSTM
            xavier(Float32,3,3,512,512),     xavier(Float32,3,3,512,512),   zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     xavier(Float32,3,3,512,512),   zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     xavier(Float32,3,3,512,512),   zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     xavier(Float32,3,3,512,512),   zeros(Float32,1,1,512,1),
            #Scores
            xavier(Float32,1,1,512,13),      zeros(Float32,1,1,13,1),
            #Deconv
            bilinear(Float32,32,32,13,13),   zeros(Float32,1,1,13,1),
        ]; w = map(KnetArray,w);

        println(model)
        times = []
        for i=1:iters
            push!(times,(@timed y = model(w,h,c,x))[2])
        end
        println("Minimum: $(minimum(times))s ($(1/minimum(times)) FPS) Maximum: $(maximum(times))s ($(1/maximum(times)) FPS) Average: $(mean(times))s ($(1/mean(times)) FPS)")
    end
    for model in [vKITTI_LSTM_RGBD,]
        x = KnetArray(zeros(Float32,224,224,6,1));
        w = Array[
            #RGB
            #Conv64
            xavier(Float32,3,3,3,64),        zeros(Float32,1,1,64,1),
            xavier(Float32,3,3,64,64),       zeros(Float32,1,1,64,1),
            #Conv128
            xavier(Float32,3,3,64,128),      zeros(Float32,1,1,128,1),
            xavier(Float32,3,3,128,128),     zeros(Float32,1,1,128,1),
            #Conv256
            xavier(Float32,3,3,128,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            #Conv512
            xavier(Float32,3,3,256,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            #Colorized Depth
            #Conv64
            xavier(Float32,3,3,3,64),        zeros(Float32,1,1,64,1),
            xavier(Float32,3,3,64,64),       zeros(Float32,1,1,64,1),
            #Conv128
            xavier(Float32,3,3,64,128),      zeros(Float32,1,1,128,1),
            xavier(Float32,3,3,128,128),     zeros(Float32,1,1,128,1),
            #Conv256
            xavier(Float32,3,3,128,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            xavier(Float32,3,3,256,256),     zeros(Float32,1,1,256,1),
            #Conv512
            xavier(Float32,3,3,256,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     zeros(Float32,1,1,512,1),
            #LSTM
            xavier(Float32,3,3,512,512),     xavier(Float32,3,3,512,512),   zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     xavier(Float32,3,3,512,512),   zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     xavier(Float32,3,3,512,512),   zeros(Float32,1,1,512,1),
            xavier(Float32,3,3,512,512),     xavier(Float32,3,3,512,512),   zeros(Float32,1,1,512,1),
            #Scores
            xavier(Float32,1,1,512,13),      zeros(Float32,1,1,13,1),
            #Deconv
            bilinear(Float32,32,32,13,13),   zeros(Float32,1,1,13,1),
        ]; w = map(KnetArray,w);

        println(model)
        times = []
        for i=1:iters
            push!(times,(@timed y = model(w,h,c,x))[2])
        end
        println("Minimum: $(minimum(times))s ($(1/minimum(times)) FPS) Maximum: $(maximum(times))s ($(1/maximum(times)) FPS) Average: $(mean(times))s ($(1/mean(times)) FPS)")
    end
end


module BLAS

using Dates
using DataFrames
using Distributions

# default problem is set up for tx and rx being DataFrames
# with only a time::DateTime column needed in each

Base.@kwdef struct Problem
  tx::DataFrame
  rx::DataFrame
  σₜ::Float64 = 10.0
  time::Function = x -> datetime2unix(DateTime(x.time))
  delay::Function = (tx, rx) -> 0.0
  passoc::Function = (tx, rx) -> 1.0
  pfalse::Function = rx -> 0.1
end

struct State
  score::Float64
  backlink::Union{State,Missing}
  assoc::Union{Pair{Int},Missing}
  i::Int
  j::Int
  μₜ::Float64
  σₜ::Float64
end

Base.show(io::IO, s::State) = print(io, "<i=$(s.i), j=$(s.j), μₜ=$(s.μₜ), $(s.assoc), $(s.score)>")

function Base.show(io::IO, ss::AbstractArray{State})
  print(io, "[\n")
  for s ∈ ss
    print(io, "  <i=$(s.i), j=$(s.j), μₜ=$(s.μₜ), $(s.assoc), $(s.score)>\n")
  end
  print(io, "]\n")
end

score(state) = state.score

function isduplicate(state, 𝒮)
  for s ∈ 𝒮
    s === state && continue
    (s.i != state.i || s.j != state.j || s.assoc !== state.assoc) && continue
    state.score > s.score && continue
    state.score < s.score && return true
    hash(state) < hash(s) && return true
  end
  false
end

function solve(P::Problem; nhypothesis=30)
  𝒮 = [State(0.0, missing, missing, 0, 0, 0.0, P.σₜ)]
  for (j, rx) ∈ enumerate(eachrow(P.rx))
    𝒮⁺ = State[]
    for state ∈ 𝒮
      # TODO: track σₜ better, adjusting it based on the time difference for each entry
      𝒟ₜ = Normal(state.μₜ, state.σₜ)
      pfalse = P.pfalse(rx)
      p = pfalse * pdf(𝒟ₜ, state.μₜ)
      push!(𝒮⁺, State(state.score + log10(p), state, missing, state.i, j, state.μₜ, state.σₜ))
      for i ∈ state.i+1:size(P.tx,1)
        tx = P.tx[i,:]
        Δt = P.time(rx) - P.time(tx) - P.delay(tx, rx)
        Δt < -3 * state.σₜ && break
        p = (1 - pfalse) * pdf(𝒟ₜ, Δt) * P.passoc(tx, rx)
        push!(𝒮⁺, State(state.score + log10(p), state, i => j, i, j, Δt, state.σₜ))
      end
    end
    sort!(𝒮⁺; by=score, rev=true)
    if isinf(𝒮⁺[1].score)
      @warn "Ran out of possibilities for RX[$j]!"
      @show 𝒮⁺
      break
    end
    filter!(s -> s.score ≥ 𝒮⁺[1].score - 1, 𝒮⁺)
    filter!(s -> !isduplicate(s, 𝒮⁺), 𝒮⁺)
    length(𝒮⁺) > nhypothesis && (𝒮⁺ = 𝒮⁺[1:nhypothesis])
    𝒮 = 𝒮⁺
  end
  assoc = DataFrame(txid=Int[], rxid=[])
  state = 𝒮[1]
  while state !== missing
    state.assoc === missing || push!(assoc, (state.assoc...,))
    state = state.backlink
  end
  sort!(assoc, :txid)
  assoc.txtime = P.tx[assoc.txid,:time]
  assoc.rxtime = P.rx[assoc.rxid,:time]
  assoc.Δt = [P.time(P.rx[assoc.rxid[i],:]) - P.time(P.tx[assoc.txid[i],:]) - P.delay(P.tx[assoc.txid[i],:], P.rx[assoc.rxid[i],:]) for i ∈ 1:size(assoc, 1)]
  assoc
end

end # module


##############################################################################
#
# CVortex.jl
#
# Provides GPU accelerated vortex particle and vortex filaments methods. A
# wrapper for the C CVortex library.
#
# Copyright 2019-2020 HJA Bird
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
##############################################################################

"""
	CVortex

A GPU accelerated vortex particle and vortex filament library.
"""
module CVortex

	export 	particle_induced_velocity,
			filament_induced_velocity,
			particle_induced_dvort,
			filament_induced_dvort,
			particle_visc_induced_dvort,
			particle_field_vorticity,
			filament_induced_velocity_influence_matrix,
			RegularisationFunction,
			singular_regularisation,
			planetary_regularisation,
			gaussian_regularisation,
			winckelmans_regularisation,
			redistribute_particles_on_grid,
			particle_pedrizzetti_relaxation,
			RedistributionFunction,
			lambda0_redistribution,
			lambda1_redistribution,
			lambda2_redistribution,
			lambda3_redistribution,
			m4p_redistribution,
			number_of_accelerators,
			number_of_enabled_accelerators,
			accelerator_name,
			accelerator_enabled,
			accelerator_enable,
			accelerator_disable
	#-------------------------------------------------------------------------
	# Loading shared library binary (cvortex)

	import Libdl: dlopen

	const libcvortex = joinpath(
		dirname(dirname(@__FILE__)), "deps/libcvortex")
	function __init__()
		try
			dlopen(libcvortex)
		catch
			error("$(libcvortex) cannot be opened. Consider "*
				"rebuilding the package.")
		end
		# Inialisation is automatic:
		ccall(
			("cvtx_initialise", libcvortex),
			Cvoid, ())
	end

	#-------------------------------------------------------------------------
	include("ConvertionChecks.jl")
	include("Vec3f.jl")
	include("Vec2f.jl")
	include("Accelerators.jl")
	include("RegularisationFunction.jl")
	include("RedistributionFunction.jl")
	include("VortexParticle3D.jl")
	include("VortexParticle2D.jl")
	include("VortexFilament.jl")
	include("CVortexInfo.jl")

end #module


module OBC

using Distributions
using Iterators
using SAMC

export
    plot_all,
    plot_trace,
    logsum,
    get_bbox,
    gen_grid,
    gelman_rubin,
    get_bbox,
    gen_grid,
    gen_unit_grid

type BinaryClassifier{T<:Sampler,S<:MCMC}
    cls1 :: T
    cls2 :: T
    mcmc1 :: S
    mcmc2 :: S
end

include("cls_utils.jl")
include("utils.jl")

end # module


module MortalityTables

include("Mortality.jl")

export MortalityTable,
    q, p, qx,
    TableMetaData
end # module


module Groebner

debug() = false

PRIMES = 0
F4TIME = 0.0
RECTIME = 0.0
CORRTIME = 0.0

ADD_ROW_HIT  = 0
ADD_ROW_MISS = 0
ADD_ROW_COLLISION = 0

function printhash()
    global ADD_ROW_HIT
    global ADD_ROW_MISS
    global ADD_ROW_COLLISION

    println("ADD_ROW_HIT = $(ADD_ROW_HIT)")
    println("ADD_ROW_MISS = $(ADD_ROW_MISS)")
    println("ADD_ROW_COLLISION = $(ADD_ROW_COLLISION)")

    ADD_ROW_HIT  = 0
    ADD_ROW_MISS = 0
    ADD_ROW_COLLISION = 0
end
ADD_ROW_HIT  = 0
ADD_ROW_MISS = 0

function printall()
    global PRIMES
    global F4TIME
    global RECTIME
    global CORRTIME
    s = sum((F4TIME, RECTIME, CORRTIME))
    println("run success,")
    println("PRIMES = $PRIMES")
    println("F4TIME = $(F4TIME/s)")
    println("RECTIME = $(RECTIME/s)")
    println("CORRTIME = $(CORRTIME/s)")
    PRIMES = 0
    F4TIME = 0.0
    RECTIME = 0.0
    CORRTIME = 0.0
end

# most of these are not needed
# TODO
import AbstractAlgebra
import AbstractAlgebra.Generic: MPoly, GFElem, MPolyRing, Poly
import AbstractAlgebra: leading_term, QQ, PolynomialRing, terms,
                        coeff, divides, base_ring, elem_type,
                        rref, isconstant, leading_coefficient,
                        map_coefficients, monomials, degree,
                        degrees, isconstant, leading_monomial,
                        GF, gens, MatrixSpace, coefficients,
                        crt, ordering, exponent_vectors, lift,
                        MPolyBuildCtx, finish, push_term!, ZZ,
                        content, change_base_ring, exponent_vector,
                        lcm, monomial, RingElem, set_exponent_vector!,
                        nvars, data, characteristic, isdivisible_by,
                        divexact, symbols

# for example systems
import Combinatorics

import Primes
import Primes: nextprime

import Random

import Logging
import Logging: ConsoleLogger, LogLevel

import MultivariatePolynomials
import MultivariatePolynomials: AbstractPolynomial, AbstractPolynomialLike

# type aliases for internal objects
include("internaltypes.jl")

# so, what is that exactly?
include("utils.jl")

# some simple reference implementations
include("common.jl")

# computation parameters control
include("metainfo.jl")

# input-output conversions for polynomials
include("io.jl")

#= operations with numbers =#
# unsigned arithmetic for finite field computations
include("arithmetic/unsigned.jl")
# modular arithmetic for CRT and rational reconstruction
include("arithmetic/modular.jl")

#= generic f4 implementation =#
# the heart of this library
include("f4/structs.jl")
include("f4/symbolic.jl")
include("f4/hash.jl")
include("f4/linear.jl")
include("f4/sorting.jl")
include("f4/lucky.jl")
include("f4/coeffs.jl")
include("f4/f4.jl")

# f4 with tracing
include("f4/f4trace.jl")

include("f4/groebner.jl")
include("f4/isgroebner.jl")
include("f4/normalform.jl")
include("f4/correctness.jl")
# include("f4/statistics.jl")

#= generic fglm implementation =#
include("fglm/linear.jl")
include("fglm/fglm.jl")

# api
include("interface.jl")

# example systems definitions
include("testgens.jl")

export groebner
export isgroebner
export normalform

export fglm
export kbase

end


module LayeredArrays

using LinearAlgebra

using Base: @_propagate_inbounds_meta, @_inline_meta, @propagate_inbounds

export
# AbstractLayeredArray
    AbstractLayeredArray,
    AbstractLayeredMatrix,
    AbstractLayeredVector,
    layerof,
# LayeredArray
    LayeredArray,
    LayeredMatrix,
    LayeredVector,
# LazyLayeredArray
    LazyLayeredArray,
    LazyLayeredMatrix,
    LazyLayeredVector,
# macro
    @layered

include("helpers.jl")
include("AbstractLayeredArray.jl")
include("LayeredArray.jl")
include("AdjointLayeredArray.jl")
include("broadcast.jl")
include("LazyLayeredArray.jl")

end # module


using PyCall, PyPlot
@pyimport pandas as pd
@pyimport seaborn as sns

# This script relies on data that is generated by running smc-abc-script.jl

emu_plot_data = pd.DataFrame(data= Dict( :param_1=>emu_out.population[3][:,1],:param_2=>emu_out.population[3][:,2],:param_3=>emu_out.population[3][:,3]))
sim_plot_data = pd.DataFrame(data= Dict( :param_1=>sim_out.population[3][:,1],:param_2=>sim_out.population[3][:,2],:param_3=>sim_out.population[3][:,3]))
PyPlot.ion()
fig, ax = PyPlot.subplots(figsize=(15,15),ncols=3, nrows=3)
     left   =  0.125  # the left side of the subplots of the figure
     right  =  0.9    # the right side of the subplots of the figure
     bottom =  0.1    # the bottom of the subplots of the figure
     top    =  0.9    # the top of the subplots of the figure
     wspace =  .5     # the amount of width reserved for blank space between subplots
     hspace =  .5    # the amount of height reserved for white space between subplots
     PyPlot.subplots_adjust(
         left    =  left,
         bottom  =  bottom,
         right   =  right,
         top     =  top,
         wspace  =  wspace,
         hspace  =  hspace
     )
     PyPlot.ioff()
     cmap = sns.cubehelix_palette(as_cmap=true, dark=0, light=1, reverse=true)
     # marginals
     sns.distplot(sim_plot_data[:param_1],bins=15, kde= true, hist=true,rug= false ,color = "cornflowerblue",ax=ax[1,1])
     sns.distplot(sim_plot_data[:param_2],bins=15, kde= true, hist=true,rug= false ,   color = "cornflowerblue", ax=ax[2,2])
     sns.distplot(sim_plot_data[:param_3],bins=15, kde= true, hist=true,rug= false ,  color = "cornflowerblue", ax=ax[3,3])
     sns.distplot(emu_plot_data[:param_1],bins=15, kde= true, hist=true,rug= false ,   color = "salmon", label = "lisi",axlabel = "Parameter 1",ax=ax[1,1])
    sns.distplot(emu_plot_data[:param_2],bins=15, kde= true, hist=true,rug= false ,  color = "salmon", label = "lisi",axlabel = "Parameter 2",ax=ax[2,2])
    sns.distplot(emu_plot_data[:param_3],bins=15, kde= true, hist=true,rug= false ,   color = "salmon", label = "lisi",axlabel = "Parameter 3",ax=ax[3,3])
    #joint
     sns.jointplot(x="param_1", y="param_2", data=emu_plot_data, kind="kde",color="lightpink", ax=ax[2,1])

     sns.jointplot(x="param_1", y="param_3", data=emu_plot_data, kind="kde", color="lightpink",ax=ax[3,1])
     sns.jointplot(x="param_2", y="param_1", data=emu_plot_data, kind="kde", color="lightpink",ax=ax[3,2])
     #HERE scatter
     sns.jointplot(y="param_1", x="param_2", data=sim_plot_data, kind="kde", color="cornflowerblue", ax=ax[1,2])
     sns.jointplot(y="param_1", x="param_3", data=sim_plot_data, kind="kde", color="cornflowerblue",ax=ax[1,3])
     sns.jointplot(y="param_2", x="param_1", data=sim_plot_data, kind="kde", color="cornflowerblue",ax=ax[2,3])
     ax[1,2][:set_xlabel]("Parameter 2")
     ax[1,2][:set_ylabel]("Parameter 1")
     #
     ax[1,3][:set_xlabel]("Parameter 3")
     ax[1,3][:set_ylabel]("Parameter 1")
     #
     ax[2,3][:set_xlabel]("Parameter 3")
     ax[2,3][:set_ylabel]("Parameter 2")
     #
     ax[2,1][:set_xlabel]("Parameter 1")
     ax[2,1][:set_ylabel]("Parameter 2")
     #
     ax[3,1][:set_xlabel]("Parameter 1")
     ax[3,1][:set_ylabel]("Parameter 3")
     #
     ax[3,2][:set_xlabel]("Parameter 2")
     ax[3,2][:set_ylabel]("Parameter 3")
PyPlot.show(fig)


module HCBE
using SimpleHypergraphs, Random,  ProgressBars, LightGraphs, ProgressMeter, GraphPlot, StatsBase, JSON, StatsPlots

include("./algorithms/HypergraphClustering.jl")
include("./algorithms/Weighted.jl")
include("./algorithms/ClusteringUtil.jl")
include("./IO.jl")
end # module


"""
Create a 1Q GST sequence for a csv file of single qubit Clifford numbers

create_1Q_GST_seqs(file, q; Id_length=50e-9, num_cals=500)

file	 : path to csv file
q		 : name of qubit in scope
Id_length : length of the Id pulse in seconds
num_cals	 : number of calibration sequences for 0 and 1

"""
function create_1Q_GST_seqs(file, q; Id_length=50e-9, num_cals=500)

	# create pulses once in a library
	m = MEAS(q)
	pulse_lib = [DiAC(q,x) for x in 1:24]
	insert!(pulse_lib, 1, QGL.PulseBlock(Id(q,Id_length)))

	seqs = Vector{Vector{QGL.SequenceEntry}}()
	f = open(file, "r")
	for ln in eachline(f)
		# convert from string to Int and 0-indexed to 1-indexed
		pulse_nums = map(x -> parse(Int, x) + 1, split(ln, ','))
		seq = Vector{QGL.SequenceEntry}(length(pulse_nums) + 1)
		map!(x -> pulse_lib[x], seq, pulse_nums)
		seq[end] = m
		push!(seqs, seq)
	end
	cals = cal_seqs((q,),num_repeats=nuCals)
	seqs = vcat(seqs, cals)
	return seqs
end


import Pkg; Pkg.add(Pkg.PackageSpec(url="https://github.com/JuliaComputing/JuliaAcademyData.jl"))
using JuliaAcademyData; activate("World of machine learning")

# # IMDB Movie Review Sentiment Analysis Demo

#-

# ## Setup
# * Adds required packages to Julia.
# * Loads the data and a pretrained model.
# * See [07.imdb.ipynb](https://github.com/denizyuret/Knet.jl/blob/v1.1.1/tutorial/07.imdb.ipynb) to see how the model was trained.

cd(datapath("imdb"))
include("imdb.jl")

# ## Sample Data
# The model was trained using 25000 movie reviews such as the following (shift-ENTER to see random example)
# Each review was tokenized, lowercased, truncated to max 150 words and a 30,000 word vocabulary.

r = rand(1:length(xtrn))
println(reviewstring(xtrn[r],ytrn[r]))
flush(stdout)

# ## Test
# We test the model on 25000 never before seen reviews on the test set (shift-ENTER to see random example)
# The test accuracy is around 86%

r = rand(1:length(xtst))
println(reviewstring(xtst[r],ytst[r]))
println(predictstring(xtst[r]))
flush(stdout)

# ## User Input
# In this cell you can enter your own review and let the model guess the sentiment

userinput = readline(stdin)
words = split(lowercase(userinput))
ex = [get!(imdbdict,wr,UNK) for wr in words]
ex[ex.>VOCABSIZE].=UNK
println(predictstring(ex))

#-

Courses/World of machine learning/imdbdemo.jl


module RobRecSolver

using Base.Distributed
using JuMP
using CPLEX

using MicroLogging
using ConfParser

export
    #Modules
    Experiments,

    # Types
    ProblemDescriptor,
    KnapsackProblemDescriptor,
    AssignmentProblemDescriptor,

    # Functions
    minimumAssignmentProblem,
    getAssignmentConstraints,

    minimumKnapsackProblem,
    getKnapsackConstraints,

    initialScenario,

    adversarialProblem,

    evaluationProblem,

    incrementalProblem,
    recoverableProblem,
    selectionLowerBound,

    lagrangianLowerBound,
    relaxedIncrementalProblem,

    # Utils
    loadProperties,
    getProperty,
    getProblemSize,
    getSaneComputationLimit,
    hasEqualCardinalityProperty,
    getCardinality

files = [
        "minimum_knapsack_problem",
        "minimum_assignment_problem",
        "initial_scenario",
        "evaluation_problem",
        "recoverable_problem",
        "adversarial_problem",
        "incremental_problem",
        "selection_lower_bound",
        "lagrangian_lower_bound",
        "problem_descriptors",
        "properties" # Add more files here
    ]

    for file in files
        include("$(file).jl")
    end

    # Import experiments
    include(joinpath("experiments", "Experiments.jl"))
    using .Experiments

    configure_logging(min_level=:debug)

end # module


module MadNLPSchur

import ..MadNLPGraph:
    @kwdef, Logger, @debug, @warn, @error,
    AbstractOptions, AbstractLinearSolver, EmptyLinearSolver, set_options!, SparseMatrixCSC, SubVector, StrideOneVector,
    SymbolicException,FactorizationException,SolveException,InertiaException,
    introduce, factorize!, solve!, improve!, is_inertia, inertia,
    default_linear_solver, default_dense_solver, get_csc_view, get_cscsy_view, nnz, mul!,
    TwoStagePartition, set_blas_num_threads, blas_num_threads, @blas_safe_threads

const INPUT_MATRIX_TYPE = :csc

@kwdef mutable struct Options <: AbstractOptions
    schur_custom_partition::Bool = false
    schur_num_parts::Int = 2
    schur_part::Vector{Int} = Int[]
    schur_subproblem_solver::Module
    schur_dense_solver::Module
end

mutable struct SolverWorker
    V::Vector{Int}
    V_0_nz::Vector{Int}
    csc::SparseMatrixCSC{Float64,Int32}
    csc_view::SubVector{Float64}
    compl::SparseMatrixCSC{Float64,Int32}
    compl_view::SubVector{Float64}
    M::AbstractLinearSolver
    w::Vector{Float64}
end

mutable struct Solver <: AbstractLinearSolver
    csc::SparseMatrixCSC{Float64,Int32}
    inds::Vector{Int}
    tsp::TwoStagePartition

    schur::Matrix{Float64}
    colors
    fact

    V_0::Vector{Int}
    csc_0::SparseMatrixCSC{Float64,Int32}
    csc_0_view::SubVector{Float64}
    w_0::Vector{Float64}

    sws::Vector{SolverWorker}

    opt::Options
    logger::Logger
end

function Solver(csc::SparseMatrixCSC{Float64};
                option_dict::Dict{Symbol,Any}=Dict{Symbol,Any}(),
                opt=Options(schur_subproblem_solver=default_linear_solver(),
                            schur_dense_solver=default_dense_solver()),
                logger=Logger(),
                kwargs...)

    set_options!(opt,option_dict,kwargs...)
    if string(opt.schur_subproblem_solver) == "MadNLP.Mumps"
        @warn(logger,"When Mumps is used as a subproblem solver, Schur is run in serial.")
        @warn(logger,"To use parallelized Schur, use Ma27 or Ma57.")
    end

    inds = collect(1:nnz(csc))
    tsp = TwoStagePartition(csc,opt.schur_part,opt.schur_num_parts)

    V_0   = findall(tsp.part.==0)
    colors = get_colors(length(V_0),opt.schur_num_parts)

    csc_0,csc_0_view = get_cscsy_view(csc,V_0,inds=inds)
    schur = Matrix{Float64}(undef,length(V_0),length(V_0))

    w_0 = Vector{Float64}(undef,length(V_0))

    sws = Vector{Any}(undef,opt.schur_num_parts)

    copied_option_dict = copy(option_dict)
    @blas_safe_threads for k=1:opt.schur_num_parts
        sws[k] = SolverWorker(
            tsp,V_0,csc,inds,k,opt.schur_subproblem_solver,logger,k==1 ? option_dict : copy(copied_option_dict))
    end
    fact = opt.schur_dense_solver.Solver(schur)
    return Solver(csc,inds,tsp,schur,colors,fact,V_0,csc_0,csc_0_view,w_0,sws,opt,logger)
end

get_colors(n0,K) = [findall((x)->mod(x-1,K)+1==k,1:n0) for k=1:K]

function SolverWorker(tsp,V_0,csc::SparseMatrixCSC{Float64},inds::Vector{Int},k,
                      SubproblemSolverModule::Module,logger::Logger,option_dict::Dict{Symbol,Any})

    V    = findall(tsp.part.==k)

    csc_k,csc_k_view = get_cscsy_view(csc,V,inds=inds)
    compl,compl_view = get_csc_view(csc,V,V_0,inds=inds)
    V_0_nz = findnz(compl.colptr)

    M    = length(V) == 0 ?
        EmptyLinearSolver() : SubproblemSolverModule.Solver(csc_k;option_dict=option_dict,logger=logger)
    w    = Vector{Float64}(undef,csc_k.n)

    return SolverWorker(V,V_0_nz,csc_k,csc_k_view,compl,compl_view,M,w)
end

function findnz(colptr)
    nz = Int[]
    for j=1:length(colptr)-1
        colptr[j]==colptr[j+1] || push!(nz,j)
    end
    return nz
end

function factorize!(M::Solver)
    M.schur.=0.
    M.csc_0.nzval.=M.csc_0_view
    M.schur.=M.csc_0
    @blas_safe_threads for sw in M.sws
        sw.csc.nzval.=sw.csc_view
        sw.compl.nzval.=sw.compl_view
        factorize!(sw.M)
    end

    # asymchronous multithreading doesn't work here
    for q = 1:length(M.colors)
        @blas_safe_threads for k = 1:length(M.sws)
            for j = M.colors[mod(q+k-1,length(M.sws))+1] # each subprob works on a different color
                factorize_worker!(j,M.sws[k],M.schur)
            end
        end
    end
    factorize!(M.fact)

    return M
end

function factorize_worker!(j,sw,schur)
    j in sw.V_0_nz || return
    sw.w.= view(sw.compl,:,j)
    solve!(sw.M,sw.w)
    mul!(view(schur,:,j),sw.compl',sw.w,-1.,1.)
end

function solve!(M::Solver,x::AbstractVector{Float64})
    M.w_0 .= view(x,M.V_0)
    @blas_safe_threads for sw in M.sws
        sw.w.=view(x,sw.V)
        solve!(sw.M,sw.w)
    end
    for sw in M.sws
        mul!(M.w_0,sw.compl',sw.w,-1.,1.)
    end
    solve!(M.fact,M.w_0)
    view(x,M.V_0).=M.w_0
    @blas_safe_threads for sw in M.sws
        x_view = view(x,sw.V)
        sw.w.= x_view
        mul!(sw.w,sw.compl,M.w_0,1.,1.)
        solve!(sw.M,sw.w)
        x_view.=sw.w
    end
    return x
end

is_inertia(M::Solver) = is_inertia(M.fact) && is_inertia(M.sws[1].M)
function inertia(M::Solver)
    numpos,numzero,numneg = inertia(M.fact)
    for k=1:M.opt.schur_num_parts
        _numpos,_numzero,_numneg =  inertia(M.sws[k].M)
        numpos += _numpos
        numzero += _numzero
        numneg += _numneg
    end
    return (numpos,numzero,numneg)
end

function improve!(M::Solver)
    for sw in M.sws
        improve!(sw.M) || return false
    end
    return true
end

function introduce(M::Solver)
    for sw in M.sws
        sw.M isa EmptyLinearSolver || return "schur equipped with "*introduce(sw.M)
    end
end

end # module


using CSV
using DataFrames

export personality2scores

ans_mapping = Dict("HO" => 1, "O" => 2, "N" => 3, "E" => 4, "HE" => 5)
reverse(ans::Number)::Number = 6 - ans
reversed_questions = [
    # Page 113
    1, 61, 121, 181,
    36, 96, 156,
    11, 71,
    46, 106, 166,
    21, 81, 141, 231,
    56, 116, 176, 206, 236,
    # Page 114
    32, 92,
    7, 67, 187,
    42, 102, 162, 222,
    17, 77, 137,
    52, 112,
    # Page 115
    33, 93, 153, 183,
    8, 68, 128,
    43, 103, 163,
    18, 78, 138, 198, 228,
    53, 113, 173,
    # Page 116
    4, 64, 124,
    39, 99, 159, 189, 219,
    14, 74, 134,
    49, 109, 169, 199, 229,
    24, 84, 144, 234,
    59, 119,
    # Page 117
    95, 155,
    10, 70, 130, 190, 220,
    45, 105,
    20, 80, 140,
    55, 115, 205,
    30, 90, 150,
]
is_reversed(question::Number)::Bool = question in reversed_questions

function ans2num(ans::AbstractString, question::Number)::Number
    ans = string(ans)
    ans = ans_mapping[ans]
    return is_reversed(question) ? reverse(ans) : ans
end

function ans2num(ans::AbstractString, question::AbstractString)
    return ans2num(string(ans), parse(Int64, question[2:end]))
end

ans2num(ans::Missing, question::AbstractString) = ans2num("N", question)
ans2num(ans::AbstractString, question::Symbol) = ans2num(string(ans), string(question))

function personality2digits(lima::DataFrame)::DataFrame
    lima_digits = select(lima, [:id, :completed_at])

    for question in names(lima)[3:end]
        digits = map(ans -> ans2num(ans, question), lima[!, question])
        lima_digits = hcat(lima_digits, DataFrame(question => digits))
    end
    return lima_digits
end

facets = Dict(
    "N1" => [1, 31, 61, 91, 121, 151, 181, 211],
    "N2" => [6, 36, 66, 96, 126, 156, 186, 216],
    "N3" => [11, 41, 71, 101, 131, 161, 191, 221],
    "N4" => [16, 46, 76, 106, 136, 166, 196, 226],
    "N5" => [21, 51, 81, 111, 141, 171, 201, 231],
    "N6" => [26, 56, 86, 116, 146, 176, 206, 236],
    "E1" => [2, 32, 62, 92, 122, 152, 182, 212],
    "E2" => [7,37, 67, 97, 127, 157, 187, 217],
    "E3" => [12, 42, 72, 102, 132, 162, 192, 222],
    "E4" => [17, 47, 77, 107, 137, 167, 197, 227],
    "E5" => [22, 52, 82, 112, 142, 172, 202, 232],
    "E6" => [27, 57, 87, 117, 147, 177, 207, 237],
    "O1" => [3, 33, 63, 93, 123, 153, 183, 213],
    "O2" => [8, 38, 68, 98, 128, 158, 188, 218],
    "O3" => [13, 43, 73, 103, 133, 163, 193, 223],
    "O4" => [18, 48, 78, 108, 138, 168, 198, 228],
    "O5" => [23, 53, 83, 113, 143, 173, 203, 233],
    "O6" => [28, 58, 88, 118, 148, 178, 208, 238],
    "A1" => [4, 34, 64, 94, 124, 154, 184, 214],
    "A2" => [9, 39, 69, 99, 129, 159, 189, 219],
    "A3" => [14, 44, 74, 104, 134, 164, 194, 224],
    "A4" => [19, 49, 79, 109, 139, 169, 199, 229],
    "A5" => [24, 54, 84, 114, 144, 174, 204, 234],
    "A6" => [29, 59, 89, 119, 149, 179, 209, 239],
    "C1" => [5, 35, 65, 95, 125, 155, 185, 215],
    "C2" => [10, 40, 70, 100, 130, 160, 190, 220],
    "C3" => [15, 45, 75, 105, 135, 165, 195, 225],
    "C4" => [20, 50, 80, 110, 140, 170, 200, 230],
    "C5" => [25, 55, 85, 115, 145, 175, 205, 235],
    "C6" => [30, 60, 90, 120, 150, 180, 210, 240]
)

function facet(question::Number)::String
    for key in keys(facets)
        if question in facets[key]
            return string(key)
        end
    end
    return "question not listed in facets"
end

domain(question::Number)::Char = facet(question)[1]
answer(lima::DataFrame, row::Number, question::Symbol) = lima[row, question]
answer(lima::DataFrame, row::Number, question::Number) = answer(lima, row, Symbol("v" * string(question)))

function score_everyone(lima_digits::DataFrame, facet::String)
    answers(row::Number) = map(question -> answer(lima_digits, row, question), facets[facet])
    map(row -> sum(answers(row)), 1:nrow(lima_digits))
end

function personality2facets(lima_digits::DataFrame)::DataFrame
    lima_facets = select(lima_digits, [:id, :completed_at])

    for facet in sort(collect(keys(facets)))
        df = DataFrame(Symbol(facet) => score_everyone(lima_digits, facet))
        lima_facets = hcat(lima_facets, df)
    end
    return lima_facets
end

function personality2scores(lima::DataFrame)::DataFrame
    lima_digits = personality2digits(lima)
    lima_facets = personality2facets(lima_digits)
    lima_scores = copy(lima_facets)

    answers(row::Number, facet::String) = map(question -> answer(lima_digits, row, question), facets[facet])
    score_everyone(facet::String) = map(row -> sum(answers(row, facet)), 1:nrow(lima_digits))

    domains = Set(map(key -> string(key)[1], collect(keys(facets))))
    get_facets(domain::Char)::Array{String,1} = filter(key -> string(key)[1] == domain, collect(keys(facets)))

    facet_scores(domain::Char) = map(score_everyone, get_facets(domain))
    score_everyone(domain::Char) = sum(facet_scores(domain))

    for domain in sort(collect(domains))
        df = DataFrame(Symbol(domain) => score_everyone(domain))
        lima_scores = hcat(lima_scores, df)
    end

    return lima_scores
end


using Ramnet.Encoders
using Ramnet.Models.AltDiscriminators:RegressionDiscriminator

using LinearAlgebra:Diagonal

using ..Approximators:RegularizedContinousPolicy, VDiscriminator
# using ..Buffers: MultiStepDynamicBuffer, add!
using ..Buffers: FixedSizeBuffer, add!, sample!, take_last, get

mutable struct ContinousRegularizedAC{OS,AS,T <: Real,O <: StaticArray{Tuple{OS},T,1},A <: StaticArray{Tuple{AS},T,1},E <: AbstractEncoder{T}} <: AbstractAgent{O,A}
    steps::Int
    γ::Float64
    batchsize::Int
    actor::RegularizedContinousPolicy{OS,AS,T,O,A,E}
    critic::VDiscriminator
    # buffer::MultiStepDynamicBuffer{O,A}
    buffer::FixedSizeBuffer{O,A}
    observation::Union{Nothing,O}
    done::Bool
    action::Union{Nothing,A}
    min_obs::Union{Nothing,O}
    max_obs::Union{Nothing,O}
    unevaluated_experiences::Int
    # rng::MersenneTwister
end

function ContinousRegularizedAC(::Type{O}, ::Type{A}, steps, n, α, η, batchsize, epochs, discount, forgetting_factor, encoder::E, start_cov::Float64, end_cov::Float64, cov_decay::Int, buffersize::Int; seed::Union{Nothing,Int}=nothing) where {OS,AS,T <: Real,O <: StaticArray{Tuple{OS},T,1},A <: StaticArray{Tuple{AS},T,1},E <: AbstractEncoder{T},C <: AbstractMatrix{T}}
    # !isnothing(seed) && seed < 0 && throw(DomainError(seed, "Seed must be non-negative"))
    rng = isnothing(seed) ? MersenneTwister() : MersenneTwister(seed)

    n < 1 && throw(DomainError(n, "Tuple size must be at least 1"))

    actor = RegularizedContinousPolicy(O, A, n, encoder;α, start_cov, end_cov, cov_decay, η, epochs, partitioner=:uniform_random, seed)
    # critic = RegressionDiscriminator{1}(OS, 32, encoder; seed, γ=forgetting_factor)
    critic = VDiscriminator(O, 32, forgetting_factor, encoder, seed)

    # buffer = MultiStepDynamicBuffer{O,A}()
    buffer = FixedSizeBuffer{O,A}(buffersize, rng)

    ContinousRegularizedAC{OS,AS,T,O,A,E}(steps, discount, batchsize, actor, critic, buffer, nothing, false, nothing, nothing, nothing, 0)
end

function ContinousRegularizedAC(env, encoder::E; steps, tuple_size, alpha, learning_rate, batchsize, epochs, discount, forgetting_factor=1.0, start_cov, end_cov, cov_decay, buffersize, seed=nothing) where {T <: Real,E <: AbstractEncoder{T}}
    ContinousRegularizedAC(observation_type(env), action_type(env), steps, tuple_size, alpha, learning_rate, batchsize, epochs, discount, forgetting_factor, encoder, start_cov, end_cov, cov_decay, buffersize; seed)
end

function observe!(agent::ContinousRegularizedAC{OS,AS,T,O,A,E}, observation::O) where {OS,AS,T,O,A,E}
    agent.observation = observation
    agent.done = false

    agent.action = _select_action(agent, observation)

    # Finding the edges of the domain
    if isnothing(agent.min_obs)
        agent.min_obs = observation
    else
        agent.min_obs = min.(agent.min_obs, observation)
    end

    if isnothing(agent.max_obs)
        agent.max_obs = observation
    else
        agent.max_obs = max.(agent.max_obs, observation)
    end

    nothing
end

# TODO: All observe! methods are the same for all agents. Generalize.
# TODO: This method does not need to take in the action
function observe!(agent::ContinousRegularizedAC{OS,AS,T,O,A,E}, action::A, reward::Float64, observation::O, done::Bool) where {OS,AS,T,O,A,E}
    # if reward < 0
    #     println(reward)
    # end

    add!(agent.buffer, agent.observation, agent.action, reward)
    agent.unevaluated_experiences += 1

    if !done
        agent.action = _select_action(agent, observation)
    end

    agent.observation = observation
    agent.done = done

  # Finding the edges of the domain
    agent.min_obs = min.(agent.min_obs)
    agent.max_obs = max.(agent.max_obs)

    nothing
end

function _select_action(agent::ContinousRegularizedAC{OS,AS,T,O,A,E}, observation::O) where {OS,AS,T,O,A,E}
    select_action(agent.actor, observation)
end

function select_action!(agent::ContinousRegularizedAC{OS,AS,T,O,A,E}, observation::O) where {OS,AS,T,O,A,E}
    if !isnothing(agent.action)
        return agent.action
    end

    return _select_action(agent, observation)
end

function update!(agent::ContinousRegularizedAC)
    if agent.done
        G = 0.0
        # for transition in Iterators.reverse(agent.buffer)
        #     G = transition.reward + agent.γ * G

        #     δ = G - (predict(agent.critic, transition.observation) |> first)
        #     update!(agent.actor, transition.observation, transition.action, δ)
        #     train!(agent.critic, transition.observation, G)
        # end

        # reset!(agent.buffer)

        for ex in take_last(agent.buffer, agent.unevaluated_experiences)
            G = ex.reward + agent.γ * G
            ex.value = G

            # Não estou fazendo uso de vantagem aqui! (diferença entre valor previso e observado)
            update!(agent.actor, sample!(agent.buffer, agent.batchsize), agent.critic)
            # train!(agent.critic, ex.observation, G)
            update!(agent.critic, ex.observation, G)
        end

        agent.unevaluated_experiences = 0

    # Preste atenção nesse teste! Como você não remove mais experiências do buffer (popfirst), essa
    # condição só será satisfeita uma única vez! Não é o comportamento desejado!
    # talvez length(agent.buffer) > agent.steps? Não exatamente. Preciso que já tenham ocorrido
    # agent.steps no episódio corrente. Posso usar agent.unevaluated_experiences!
    elseif agent.unevaluated_experiences == agent.steps
        # G = predict(agent.critic, agent.observation) |> first
        G = agent.critic(agent.observation)

        # for transition in Iterators.reverse(agent.buffer)
        #     G = transition.reward + agent.γ * G
        # end

        # t = popfirst!(agent.buffer)

        # δ = G - (predict(agent.critic, t.observation) |> first)
        # update!(agent.actor, t.observation, t.action, δ)
        # train!(agent.critic, t.observation, G)

        for ex in take_last(agent.buffer, agent.steps)
            G = ex.reward + agent.γ * G
        end

        ex = get(agent.buffer, agent.buffer.tail - agent.steps)
        ex.value = G

        # Não estou fazendo uso de vantagem aqui! (diferença entre valor previso e observado)
        update!(agent.actor, sample!(agent.buffer, agent.batchsize), agent.critic)
        # train!(agent.critic, ex.observation, G)
        update!(agent.critic, ex.observation, G)

        agent.unevaluated_experiences -= 1
    end

    nothing
end

function reset!(agent::ContinousRegularizedAC; seed::Union{Nothing,Int}=nothing)
    if !isnothing(seed)
        if seed ≥ 0
        seed!(agent.rng, seed)
        else
            throw(DomainError(seed, "Seed must be non-negative"))
        end
    end

    reset!(agent.actor)
    # Ramnet.reset!(agent.critic)
    reset!(agent.critic)
    reset!(agent.buffer)

    nothing
end


module CMDPs

export generate_sim_q, run_study
export RewardvsN, RewardvsNResult
export TuneDPW, TuneDPWResult, report
export TuneCBTS, TuneCBTSResult

using POMDPs, POMDPToolbox, MCTS
using Parameters, DataFrames
using Walk1DMDP, VDPTag2
using POMDPModels #MountainCar, InvertedPendulum
using LightDarkPOMDPs
using RecipesBase

include("walk1d.jl")
include("vdptag.jl")
include("inverted.jl")
include("mountaincar.jl")
include("lightdark2d.jl")
include("sim.jl")
include("studies.jl")
include("tuning.jl")
include("observers.jl")

end # module


struct MultiFieldCellField{DS<:DomainStyle} <: CellField
  single_fields::Vector{<:CellField}
  domain_style::DS

  function MultiFieldCellField(single_fields::Vector{<:CellField})
    @assert length(single_fields) > 0
    f1 = first(single_fields)

    # Find a suitable domain style
    if any( map(i->DomainStyle(i)==ReferenceDomain(),single_fields) )
      domain_style = ReferenceDomain()
    else
      domain_style = PhysicalDomain()
    end

    new{typeof(domain_style)}(single_fields,domain_style)
  end
end

function CellData.get_data(f::MultiFieldCellField)
  s = """
  Function get_data is not implemented for MultiFieldCellField at this moment.
  You need to extract the individual fields and then evaluate them separatelly.

  If ever implement this, evaluating a `MultiFieldCellField` directly would provide,
  at each evaluation point, a tuple with the value of the different fields.
  """
  @notimplemented s
end

function CellData.get_triangulation(f::MultiFieldCellField)
  s1 = first(f.single_fields)
  trian = get_triangulation(s1)
  @check all(map(i->trian===get_triangulation(i),f.single_fields))
  trian
end
CellData.DomainStyle(::Type{MultiFieldCellField{DS}}) where DS = DS()
num_fields(a::MultiFieldCellField) = length(a.single_fields)
Base.getindex(a::MultiFieldCellField,i::Integer) = a.single_fields[i]
Base.iterate(a::MultiFieldCellField)  = iterate(a.single_fields)
Base.iterate(a::MultiFieldCellField,state)  = iterate(a.single_fields,state)


function field_getter(
    obuf,
    prefix :: Symbol,
    struct_type,
    map :: Dict{Symbol, Any})

    for key in keys(map)
        jl_name = symbol(prefix, key)
        field_type, field_offset = map[key]
        field_offset = convert(UInt64, field_offset)

        getter_code = quote
            function $jl_name(ptr::Ptr{$struct_type})
                field_ptr = convert(Ptr{$field_type}, ptr + $field_offset)
                return unsafe_load(field_ptr)
               end
        end

        # Debug output of generated code
        if false && endswith(string(jl_name), "size")
            println(getter_code)
        end

        push!(obuf, getter_code)
    end
end

function gen_fieldgetters(obuf)

    field_getter(
        obuf,
        :queue_info_,
        :hsa_queue_t,
        Dict(
            :type => (:hsa_queue_type_t, 0),
            :features => (:hsa_queue_feature_t, 4),
            :base_address => (UInt64, 8), # TODO handle ifdefs for machine_model/endianness
            :doorbell_signal => (UInt64, 16),
            :size => (UInt32, 24),
            # UInt32 reserved
            :id => (UInt64, 32),
        )
    )
end