module JuMPExtension
# Simple example of JuMP extension used in the tests to check that JuMP works well with extensions
# The main difference between `JuMP.Model` and `JuMPExtension.MyModel` is the fact that in `add_variable` (resp. `add_constraint`),
# `JuMP.Model` applies the modification to its `moi_backend` field while
# `JuMPExtension.MyModel` stores the `AbstractVariable` (resp. `AbstractConstraint`) in a list.

using MathOptInterface
const MOI = MathOptInterface
import JuMP

struct ConstraintIndex
    value::Int # Index in `model.constraints`
end

mutable struct MyModel <: JuMP.AbstractModel
    nextvaridx::Int                                 # Next variable index is nextvaridx+1
    variables::Dict{Int, JuMP.ScalarVariable}       # Map varidx -> variable
    var_to_name::Dict{Int, String}                  # Map varidx -> name
    name_to_var::Union{Dict{String, Int}, Nothing}  # Map varidx -> name
    nextconidx::Int                                 # Next constraint index is nextconidx+1
    constraints::Dict{ConstraintIndex,
                      JuMP.AbstractConstraint}      # Map conidx -> variable
    con_to_name::Dict{ConstraintIndex, String}      # Map conidx -> name
    name_to_con::Union{Dict{String, ConstraintIndex},
                       Nothing}                     # Map name -> conidx
    objectivesense::MOI.OptimizationSense
    objective_function::JuMP.AbstractJuMPScalar
    obj_dict::Dict{Symbol, Any}                     # Same that JuMP.Model's field `obj_dict`
    function MyModel()
        new(0, Dict{Int, JuMP.AbstractVariable}(),
            Dict{Int, String}(), nothing,                        # Variables
            0, Dict{ConstraintIndex, JuMP.AbstractConstraint}(),
            Dict{ConstraintIndex, String}(), nothing,            # Constraints
            MOI.FEASIBILITY_SENSE,
            zero(JuMP.GenericAffExpr{Float64, MyVariableRef}),
            Dict{Symbol, Any}())
    end
end
Base.broadcastable(model::MyModel) = Ref(model)

JuMP.object_dictionary(model::MyModel) = model.obj_dict

# Variables
struct MyVariableRef <: JuMP.AbstractVariableRef
    model::MyModel # `model` owning the variable
    idx::Int       # Index in `model.variables`
end
Base.copy(v::MyVariableRef) = v
Base.copy(v::MyVariableRef, new_model::MyModel) = MyVariableRef(new_model, v.idx)

Base.:(==)(v::MyVariableRef, w::MyVariableRef) = v.model === w.model && v.idx == w.idx
Base.broadcastable(v::MyVariableRef) = Ref(v)
JuMP.isequal_canonical(v::MyVariableRef, w::MyVariableRef) = v == w
JuMP.variable_type(::MyModel) = MyVariableRef
function JuMP.add_variable(m::MyModel, v::JuMP.AbstractVariable, name::String="")
    m.nextvaridx += 1
    vref = MyVariableRef(m, m.nextvaridx)
    m.variables[vref.idx] = v
    JuMP.set_name(vref, name)
    vref
end
function JuMP.delete(model::MyModel, vref::MyVariableRef)
    @assert JuMP.is_valid(model, vref)
    delete!(model.variables, vref.idx)
    delete!(model.var_to_name, vref.idx)
end
function JuMP.is_valid(model::MyModel, vref::MyVariableRef)
    return (model === vref.model &&
            vref.idx in keys(model.variables))
end
JuMP.num_variables(m::MyModel) = length(m.variables)

# Internal functions
variable_info(vref::MyVariableRef) = vref.model.variables[vref.idx].info
function update_variable_info(vref::MyVariableRef, info::JuMP.VariableInfo)
    vref.model.variables[vref.idx] = JuMP.ScalarVariable(info)
end

JuMP.has_lower_bound(vref::MyVariableRef) = variable_info(vref).has_lb
function JuMP.lower_bound(vref::MyVariableRef)::Float64
    @assert !JuMP.is_fixed(vref)
    return variable_info(vref).lower_bound
end
function JuMP.set_lower_bound(vref::MyVariableRef, lower)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(true, lower,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.delete_lower_bound(vref::MyVariableRef)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(false, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
JuMP.has_upper_bound(vref::MyVariableRef) = variable_info(vref).has_ub
function JuMP.upper_bound(vref::MyVariableRef)::Float64
    @assert !JuMP.is_fixed(vref)
    return variable_info(vref).upper_bound
end
function JuMP.set_upper_bound(vref::MyVariableRef, upper)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           true, upper,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.delete_upper_bound(vref::MyVariableRef)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           false, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
JuMP.is_fixed(vref::MyVariableRef) = variable_info(vref).has_fix
function JuMP.fix_value(vref::MyVariableRef)::Float64
    return variable_info(vref).fixed_value
end
function JuMP.fix(vref::MyVariableRef, value; force::Bool = false)
    info = variable_info(vref)
    if !force && (info.has_lb || info.has_ub)
        error("Unable to fix $(vref) to $(value) because it has existing bounds.")
    end
    update_variable_info(vref, JuMP.VariableInfo(
        false, info.lower_bound, false, info.upper_bound, true, value,
        info.has_start, info.start, info.binary, info.integer)
    )
    return
end
function JuMP.unfix(vref::MyVariableRef)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           false, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.start_value(vref::MyVariableRef)::Union{Nothing, Float64}
    return variable_info(vref).start
end
function JuMP.set_start_value(vref::MyVariableRef, start)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           true, start,
                                           info.binary, info.integer))
end
JuMP.is_binary(vref::MyVariableRef) = variable_info(vref).binary
function JuMP.set_binary(vref::MyVariableRef)
    @assert !JuMP.is_integer(vref)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           true, info.integer))
end
function JuMP.unset_binary(vref::MyVariableRef)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           false, info.integer))
end
JuMP.is_integer(vref::MyVariableRef) = variable_info(vref).integer
function JuMP.set_integer(vref::MyVariableRef)
    @assert !JuMP.is_binary(vref)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, true))
end
function JuMP.unset_integer(vref::MyVariableRef)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, false))
end

# Constraints
const MyConstraintRef = JuMP.ConstraintRef{MyModel, ConstraintIndex}
JuMP.constraint_type(::MyModel) = MyConstraintRef
function JuMP.add_constraint(model::MyModel, c::JuMP.AbstractConstraint,
                             name::String="")
    model.nextconidx += 1
    index = ConstraintIndex(model.nextconidx)
    cref = JuMP.ConstraintRef(model, index, JuMP.shape(c))
    model.constraints[index] = c
    JuMP.set_name(cref, name)
    return cref
end
function JuMP.delete(model::MyModel, constraint_ref::MyConstraintRef)
    @assert JuMP.is_valid(model, constraint_ref)
    delete!(model.constraints, constraint_ref.index)
    delete!(model.con_to_name, constraint_ref.index)
end
function JuMP.is_valid(model::MyModel, constraint_ref::MyConstraintRef)
    return (model === constraint_ref.model &&
            constraint_ref.index in keys(model.constraints))
end
function JuMP.constraint_object(cref::MyConstraintRef)
    return cref.model.constraints[cref.index]
end

# Objective
function JuMP.set_objective(m::MyModel, sense::MOI.OptimizationSense,
                            f::JuMP.AbstractJuMPScalar)
    m.objectivesense = sense
    m.objective_function = f
end
function JuMP.set_objective(m::MyModel, sense::MOI.OptimizationSense, f::Real)
    m.objectivesense = sense
    m.objective_function = JuMP.GenericAffExpr{Float64, MyVariableRef}(f)
end
JuMP.objective_sense(model::MyModel) = model.objectivesense
function JuMP.set_objective_sense(model::MyModel, sense)
    model.objectivesense = sense
end
JuMP.objective_function_type(model::MyModel) = typeof(model.objective_function)
JuMP.objective_function(model::MyModel) = model.objective_function
function JuMP.objective_function(model::MyModel, FT::Type)
    # InexactError should be thrown, this is needed in `objective.jl`
    if !(model.objective_function isa FT)
        throw(InexactError(:objective_function, FT,
                           typeof(model.objective_function)))
    end
    return model.objective_function::FT
end

# Names
JuMP.name(vref::MyVariableRef) = vref.model.var_to_name[vref.idx]
function JuMP.set_name(vref::MyVariableRef, name::String)
    vref.model.var_to_name[vref.idx] = name
    vref.model.name_to_var = nothing
end
function JuMP.variable_by_name(model::MyModel, name::String)
    if model.name_to_var === nothing
        # Inspired from MOI/src/Utilities/model.jl
        model.name_to_var = Dict{String, Int}()
        for (var, var_name) in model.var_to_name
            if haskey(model.name_to_var, var_name)
                # -1 is a special value that means this string does not map to
                # a unique variable name.
                model.name_to_var[var_name] = -1
            else
                model.name_to_var[var_name] = var
            end
        end
    end
    index = get(model.name_to_var, name, nothing)
    if index isa Nothing
        return nothing
    elseif index == -1
        error("Multiple variables have the name $name.")
    else
        return MyVariableRef(model, index)
    end
end
JuMP.name(cref::MyConstraintRef) = cref.model.con_to_name[cref.index]
function JuMP.set_name(cref::MyConstraintRef, name::String)
    cref.model.con_to_name[cref.index] = name
    cref.model.name_to_con = nothing
end
function JuMP.constraint_by_name(model::MyModel, name::String)
    if model.name_to_con === nothing
        # Inspired from MOI/src/Utilities/model.jl
        model.name_to_con = Dict{String, ConstraintIndex}()
        for (con, con_name) in model.con_to_name
            if haskey(model.name_to_con, con_name)
                # -1 is a special value that means this string does not map to
                # a unique constraint name.
                model.name_to_con[con_name] = ConstraintIndex(-1)
            else
                model.name_to_con[con_name] = con
            end
        end
    end
    index = get(model.name_to_con, name, nothing)
    if index isa Nothing
        return nothing
    elseif index.value == -1
        error("Multiple constraints have the name $name.")
    else
        # We have no information on whether this is a vector constraint
        # or a scalar constraint
        return JuMP.ConstraintRef(model, index, JuMP.ScalarShape())
    end
end

# Show
function JuMP.show_backend_summary(io::IO, model::MyModel) end
function JuMP.show_objective_function_summary(io::IO, model::MyModel)
    println(io, "Objective function type: ",
            JuMP.objective_function_type(model))
end
function JuMP.objective_function_string(print_mode, model::MyModel)
    return JuMP.function_string(print_mode, JuMP.objective_function(model))
end
_plural(n) = (isone(n) ? "" : "s")
function JuMP.show_constraints_summary(io::IO, model::MyModel)
    n = length(model.constraints)
    print(io, "Constraint", _plural(n), ": ", n)
end
function JuMP.constraints_string(print_mode, model::MyModel)
    strings = String[]
    # Sort by creation order, i.e. ConstraintIndex value
    constraints = sort(collect(model.constraints), by = c -> c.first.value)
    for (index, constraint) in constraints
        push!(strings, JuMP.constraint_string(print_mode, constraint))
    end
    return strings
end

end


using GLVisualize, GeometryTypes, GLAbstraction, ModernGL, FileIO, Reactive

if !isdefined(:runtests)
    window = glscreen()
    timesignal = loop(linspace(0f0, 1f0, 360))
end

description = """
Showing bilboard particles, which always face the camera. Also showing how
to use Images as a particle primitive.
"""

rotation_angle = const_lift(*, timesignal, 2f0*pi)
rotation = map(rotationmatrix_z, rotation_angle)

const b = Point3f0[(rand(Point3f0)*2)-1 for i=1:64]

sprites = visualize(
    (Circle(Point2f0(0), 0.25f0), b),
    billboard=true, image=loadasset("foxy.png"),
    model=rotation
)

_view(sprites, window, camera=:perspective)

if !isdefined(:runtests)
    renderloop(window)
end


using DrWatson, GPUAcceleratedTracking, CUDA, Tracking, GNSSSignals, StructArrays, ProgressMeter;
import Tracking: Hz, ms;
@quickactivate "GPUAcceleratedTracking"

N = 2048:32:262_144
err_rel  = zeros(length(N))
@showprogress 0.5 for (idx, num_samples) in enumerate(N)
    # num_samples = 2_048
    num_ants = 1
    num_correlators = 3
    enable_gpu = Val(true)
    system = GPSL1(use_gpu = Val(true));
    # system_h = GPSL1(use_gpu = Val(false));
    codes = system.codes
    # codes_text_mem_simple = CuTexture(
        # CuTextureArray(codes)
    # )
    codes_text_mem = CuTexture(
        CuTextureArray(codes),
        address_mode = CUDA.ADDRESS_MODE_WRAP,
        interpolation = CUDA.NearestNeighbour(),
        normalized_coordinates = true
    )
    code_frequency = get_code_frequency(system)
    code_length = get_code_length(system)
    start_code_phase = 0.0f0
    carrier_phase = 0.0f0
    carrier_frequency = 1500Hz
    prn = 1

    # Generate the signal;
    signal, sampling_frequency = gen_signal(system, prn, carrier_frequency, num_samples, num_ants = NumAnts(num_ants), start_code_phase = start_code_phase, start_carrier_phase = carrier_phase)

    # Generate correlator;
    correlator = EarlyPromptLateCorrelator(NumAnts(num_ants), NumAccumulators(num_correlators))
    correlator_sample_shifts = get_correlator_sample_shifts(system, correlator, sampling_frequency, 0.5)
    num_of_shifts = correlator_sample_shifts[end] - correlator_sample_shifts[1]

    # Generate blank code and carrier replica, and downconverted signal;
    # code_replica_cpu = zeros(Float32, num_samples + num_of_shifts)
    code_replica = CUDA.zeros(Float32, num_samples + num_of_shifts)
    code_replica_text_mem = CUDA.zeros(Float32, num_samples + num_of_shifts)

    # Generate CUDA kernel tuning parameters;
    threads_per_block = 768
    blocks_per_grid = cld.(num_samples, threads_per_block)

    @cuda threads=threads_per_block blocks=blocks_per_grid gen_code_replica_texture_mem_kernel!(
        code_replica_text_mem,
        codes_text_mem, # texture memory codes
        code_frequency,
        sampling_frequency,
        start_code_phase,
        prn,
        num_samples + num_of_shifts,
        correlator_sample_shifts[1],
        code_length
    )
    @cuda threads=threads_per_block blocks=blocks_per_grid gen_code_replica_kernel!(
        code_replica,
        codes, # texture memory codes
        code_frequency,
        sampling_frequency,
        start_code_phase,
        prn,
        num_samples + num_of_shifts,
        correlator_sample_shifts[1],
        code_length
    )
    # Tracking.gen_code_replica!(code_replica_cpu, system, code_frequency, sampling_frequency, start_code_phase, 1, num_samples, correlator_sample_shifts, prn)

    # code_replica_h = Array(code_replica)
    # code_replica_text_mem_h = Array(code_replica_text_mem)
    # signal = StructArray{ComplexF32}((ones(Float32, num_samples), zeros(Float32, num_samples) ))
    # code_phases = get_code_frequency(system) / sampling_frequency .* (0:num_samples-1) .+ start_code_phase
    # spread_signal = StructArray(signal .* system_h.codes[1 .+ mod.(floor.(Int, code_phases), get_code_length(system)), prn])
    # accums_true = Tracking.correlate(correlator, spread_signal, code_replica_cpu, correlator_sample_shifts, 1, num_samples)
    # accums = Tracking.correlate(correlator, spread_signal, code_replica_h, correlator_sample_shifts, 1, num_samples)
    # accums_text_mem = Tracking.correlate(correlator, spread_signal, code_replica_text_mem_h, correlator_sample_shifts, 1, num_samples)

    # err_rel = sum(abs.(code_replica - code_replica_text_mem)) / num_samples
    err_rel[idx] = sum(abs.(code_replica - code_replica_text_mem)) / num_samples
end

x = vec(collect(N / 0.001)) # convert to Hz
data = vec(100 .* err_rel)
data_bar = mean(data)
data_med = median(data)
data_max = maximum(data)

using CairoMakie
fig = Figure(font = "Times New Roman")
ax = Axis(
    fig,
    xlabel = "Sampling Frequency [Hz]",
    ylabel = "Relative Code Phase Error [%]",
    xscale = log10,
    title = "Relative code phase error of the texture memory code replica generation for 1 ms GPS L1 C/A signal",
    # xlim = [0 400_000],
    xminorgridvisible = true,
    xminorticksvisible = true,
    xminorticks = IntervalsBetween(9),
    # yticks = (10.0 .^(-5:1:-3)),
        # yticks = (10.0 .^(-5:1:-3)),

    xticklabelsize = 18,
    yticklabelsize = 18
)
xlims!(ax, 10^6, 5*10^8)
# string_data_bar = "$(round(data_bar, sigdigits=3))%"
# string_data_max = "$(round(data_max, sigdigits=3))%"
# string_data_med = "$(round(data_med, sigdigits=3))%"

# # textmu = "μ = " * string_data_bar
# # textmax = "max = " * string_data_max
# textmed = "median = " * string_data_med
lines!(ax, x,data)
# hlines!(ax, data_bar, color = :dimgrey, linestyle = :dash)
# hlines!(ax, data_med, color = :dimgrey, linestyle = :dash)
# hlines!(ax, data_max, color = :dimgrey, linestyle = :dash)
# text!(textmu, position = (9*10^8, 0.1+data_bar), align = (:right, :baseline))
# text!(textmax, position = (9*10^8, data_max - 0.2), align = (:right, :baseline))
# text!(textmed, position = (5*10^8, 0.5+data_med), align = (:center, :baseline))

fig[1,1] = ax
fig

@quickactivate "GPUAcceleratedTracking"

raw_data_df = collect_results(datadir("benchmarks/codereplica"))

sort!(raw_data_df, :num_samples)
samples = unique(Vector{Int64}(raw_data_df[!, :num_samples]))
algorithm_names = unique(Vector{String}(raw_data_df[!, :algorithm]))
samples = unique(Vector{Int64}(raw_data_df[!, :num_samples]))
x = samples ./ 0.001 # convert to Hz
algorithm_names = unique(Vector{String}(raw_data_df[!, :algorithm]))

# fig = Figure(
#     # resolution = (1000, 700),
#     font = "Times New Roman"
# )
ax2 = Axis(
    fig,
    xlabel = "Sampling Frequency [Hz]",
    ylabel = "Generation Time [s]",
    xscale = log10,
    yscale = log10,
    title = "Comparison between global memory and texture memory code replica generation for 1 ms GPS L1 C/A signal",
    xminorgridvisible = true,
    xminorticksvisible = true,
    xminorticks = IntervalsBetween(9),
    # yticks = (10.0 .^(-5:1:-3)),
    xticklabelsize = 18,
    yticklabelsize = 18

)
xlims!(ax2, 10^6, 5*10^8)
ylims!(ax2, 1.0e-5, 3.0e-3)

lin = Array{Lines}(undef, length(algorithm_names));
sca = Array{Scatter}(undef, length(algorithm_names));
markers = [:circle, :rect]
for (idx, name) = enumerate(algorithm_names)
    time = 10 ^ (-9) * vec((
        raw_data_df |>
        @filter(
            _.algorithm == name
        ) |> DataFrame
    ).Minimum)

    lin[idx] = lines!(
        ax2,
        x,
        time
    )
    sca[idx] = scatter!(
        ax2,
        x,
        time,
        marker = markers[idx],
        markersize = 15
    )
end
realtime = lines!(ax2, [10^6, 5 * 10^8], [10 ^ (-3), 10 ^ (-3)], color=:grey, linestyle=:dashdot)

fig[2,1] = ax2
fig
elements = [[lin[1] sca[1]], [lin[2] sca[2]]]
labels = ["Global Memory", "Texture Memory"]
axislegend(ax2, elements, labels, "Code Replication Algorithms",  position = :lt)
fig
# save( plotsdir("benchmark_textmem.pdf"), fig)

save(plotsdir("code_phase.pdf"), fig)


mutable struct HE{T}
    e::E{T}
    t::HE{T}
    next::HE{T}
    prev::HE{T}
    fid::Int
    function HE(e::E{T}, fid::Int) where T
        he = new{T}()
        he.e = e
        he.fid = fid

        return he
    end
    HE{T}() where T = new{T}()
end
twin(he::HE) = he.t
next(he::HE) = he.next
prev(he::HE) = he.prev
function show(io::IO, he::HE)
    print(io, he.e)
end

struct Face{T}
    id::Int
    components::Vector{HE{T}}
end

Face{T}(id) where T = Face{T}(id, HE{T}[])

struct DCEL{T}
    faces::Vector{Face{T}}
    origins::Vector{Pair{V{T}, HE{T}}}
end
function show(io::IO, dcel::DCEL)
    for face in dcel.faces
        println(io, "Face: ", face.id)
        for (i, v0) in enumerate(face.components)
            println(io, "  part: ", i)
            v = v0
            while true
                println(io, v.e)
                v = next(v)
                v.e === v0.e && break
            end
        end
    end
end

DCEL{T}() where T = DCEL{T}(Face{T}[], Pair{V{T}, HE{T}}[])

function DCEL(shape::Shapefile.Polygon, id)
    dcel = DCEL{Float64}()
    face = Face{Float64}(id)
    push!(dcel.faces, face)
    origins = dcel.origins
    points = shape.points
    # I suppose that doing in one loop is more efficient, but doing in two loops is way easier
    offset = 0
    for i in 2:length(points)
        # Weirdly, dcel keep data in counterclockwise direction
        e = HE(E(V(points[i]), V(points[i - 1])), id)
        i == 2 && push!(face.components, e)
        etwin = HE(E(V(points[i - 1]), V(points[i])), -1)
        e.t = etwin
        push!(origins, V(points[i]) => e)
    end
    xlast = length(points) + offset - 1
    xfrst = offset + 1
    for i in xfrst+1:xlast-1
        origins[i][2].next = origins[i - 1][2]
        origins[i][2].prev = origins[i + 1][2]
        origins[i][2].t.next = origins[i + 1][2].t
        origins[i][2].t.prev = origins[i - 1][2].t
    end
    origins[xfrst][2].prev = origins[xfrst+1][2]
    origins[xfrst][2].next = origins[xlast][2]
    origins[xlast][2].prev = origins[xfrst][2]
    origins[xlast][2].next = origins[xlast-1][2]

    origins[xfrst][2].t.next = origins[xfrst+1][2].t
    origins[xfrst][2].t.prev = origins[xlast][2].t
    origins[xlast][2].t.next = origins[xfrst][2].t
    origins[xlast][2].t.prev = origins[xlast-1][2].t

    return dcel
end


module Types

    using RandomExtensions
    import Dates

    export Entity, EntityData, GAmodel, targetParams, GAParams

    abstract type Entity end
    # -------

    mutable struct EntityData
        entity
        generation::Int
        scores :: Array

        EntityData(entity, generation::Int) = new(entity, generation)
        EntityData(entity, model) = new(entity, model.params.currentGeneration)
    end

    mutable struct GAParams
        populationSize ::  Int
        generations ::  Int
        genomeSize ::  Int
        maxGenomeSize :: Int
        crossoverRate :: Float64
        mutationRate :: Float64
        elitism  :: Bool # Keep previous generation's fittest individual in place of worst in current
        historyPath :: IOStream  # Path to save log of fitness history at each generation. Can be used to plot on Excel chart etc.

        totalFitness :: Float64
        targetFitness :: Float64
        targetFitnessCount ::  Int
        currentGeneration ::  Int

        #thisGeneration :: Array
        #nextGeneration :: Array
        #fitnessTable :: Array{Float64,1}
    end

    # -------

    mutable struct GAmodel
        initial_pop_size::Int
        gen_num::Int

        population::Array
        refSet ::Array
        subSets :: Array
        #pop_data::Array{EntityData}
        #freezer::Array{EntityData}

        rng::AbstractRNG

        ga
        specific_fitness

        scores :: Array
        params :: GAParams

        instructionsSet #:: AbstractDict

        GAmodel() = new(0, 1, Any[], Any[], Any[], MersenneTwister(time_ns()), nothing, nothing, [],
                GAParams(136, 1000 , 150, 150, 0.7, 0.01, true, open("output.txt", "w") ,  0.0 , 0, 0.0 , 0 ),
                Dict() )
        GAmodel(params :: GAParams) = new(0, 1, Any[], Any[], Any[], MersenneTwister(time_ns()),
                nothing, nothing, [], params, Dict())
    end

end  # module Types


using Parameters

abstract type AbstractThermoUncertainty end

struct EmptyThermoUncertainty <: AbstractThermoUncertainty end

@with_kw struct ConstantThermoUncertainty <: AbstractThermoUncertainty
    u::AbstractFloat = nothing
end
(c::ConstantThermoUncertainty)(T::AbstractFloat=nothing) = c.u


##file to include in ComplexModeler.jl which contains all files associated to Matpower

include("MatpowerGenerator.jl")
include("MatpowerLoad.jl")
include("MatpowerROPFShunt.jl")
include("MatpowerLine_π_Imax.jl")
include("MatpowerVolt.jl")
include("read_matpower_simple_Imax.jl")


"""
    Sinograms module
"""
module Sinograms

const RealU = Number # Real or Unitful

include("fbp2.jl")
include("fbp2_sino_filter.jl")
include("fbp2_back.jl")
include("fbp2_back_fan.jl")
include("fbp_ramp.jl")
include("fbp2_window.jl")
include("fbp2_sino_weight.jl")

include("fbp-par.jl")

end # module


module QuantumControl

using QuantumPropagators
export propagate, propstep!, propstep

using QuantumControlBase
export ControlProblem, Objective, WeightedObjective, liouvillian
export discretize, discretize_on_midpoints, getcontrols, get_control_parameters
export get_tlist_midpoints
export propagate_objective
export optimize
export chain_infohooks

module Shapes
    using QuantumControlBase.Shapes
    export flattop, box, blackman
end

module Functionals
    using QuantumControlBase.Functionals
    export F_ss, J_T_ss, chi_ss!, F_sm, J_T_sm, chi_sm!, F_re, J_T_re, chi_re!
    export grad_J_T_sm!
end

using Krotov
using GRAPE

using Pkg
using UUIDs

"""Print the versions of the packages constituting QuantumControl."""
function print_versions()
    project_toml = Pkg.TOML.parsefile(joinpath(@__DIR__, "..", "Project.toml"))
    version = project_toml["version"]
    direct_deps = project_toml["deps"]
    deps = Pkg.dependencies()
    pkg_names = [name for name in keys(direct_deps) if name ∉ ["Pkg", "UUIDs"]]
    col_width = maximum([length(name) for name in pkg_names])
    for name in reverse(pkg_names)
        pkginfo = deps[UUIDs.UUID(direct_deps[name])]
        if pkginfo.is_tracking_path
            println("$(rpad(name, col_width)): $(pkginfo.version) ($(pkginfo.source))")
        else
            println("$(rpad(name, col_width)): $(pkginfo.version)")
        end
    end
    println("$(rpad("QuantumControl", col_width)): $version")
end

end


using Test
using ASE
using JuLIP.Testing

h0("Testing ASE.jl")

@testset "ASE" begin
   @testset "Atoms"  begin include("testase.jl"); end
   @testset "Calculators" begin include("test_calculators.jl"); end
   @testset "JuLIP vs ASE" begin include("test_asevsjulip.jl"); end
end


export beamsearch, max_violation!

"""
    beamsearch(initstate, beamsize, expand)

Requirements of state:
* member: score
* member: step
"""
function beamsearch{T}(initstate::T, beamsize::Int, expand::Function)
    chart = Vector{T}[]
    push!(chart, [initstate])
    lessthan{T}(x::T, y::T) = x.score > y.score

    k = 1
    while k <= length(chart)
        states = chart[k]
        length(states) > beamsize && sort!(states, lt=lessthan)
        for i = 1:min(beamsize, length(states))
            for s in expand(states[i])
                while s.step > length(chart)
                    push!(chart, T[])
                end
                push!(chart[s.step], s)
            end
        end
        k += 1
    end
    sort!(chart[end], lt=lessthan)
    chart
end

function to_seq{T}(finalstate::T)
    seq = T[]
    s = finalstate
    # while s != nothing
    while s.step > 1
        unshift!(seq, s)
        s = s.prev
    end
    unshift!(seq, s)
    seq
end

"""
    max_violation!

Ref: L. Huang et al, "Structured Perceptron with Inexact Seatch", ACL 2012.
"""
function max_violation!{T}(gold::T, pred::T, train_gold, train_pred)
    goldseq, predseq = to_seq(gold), to_seq(pred)
    maxk, maxv = 1, 0.0
    for k = 1:length(goldseq)
        v = predseq[k].score - goldseq[k].score
        if k == 1 || v >= maxv
            maxk = k
            maxv = v
        end
    end
    for k = 2:maxk
        train_gold(goldseq[k])
        train_pred(predseq[k])
    end
end

#=
st_lessthan(x::State, y::State) = y.score < x.score

function beamsearch(beamsize::Int, initstate::State, expand::Function)
    chart = Vector{State}[[initstate]]
    i = 1
    while i <= length(chart)
        states = chart[i]
        length(states) > beamsize && sort!(states, lt=st_lessthan)
        for j = 1:min(beamsize, length(states))
            for s in expand(states[j])
                while s.step > length(chart) push!(chart, []) end
                push!(chart[s.step], s)
            end
        end
        i += 1
    end
    sort!(chart[end], lt=st_lessthan)
    chart[end][1]
end

function state2array(s::State)
    res = Vector{State}(s.step)
    st = s
    while st.step > 1
        res[st.step] = st
        st = st.prev
    end
    res[st.step] = st
    res
end

function maxviolate!(gold::State, pred::State)
    golds = state2array(gold)
    preds = state2array(pred)
    maxv  = typemin(Float); maxk = 1
    for k = 2:min(length(golds), length(preds))
        v = preds[k].score - golds[k].score
        if v >= maxv
            maxv, maxk = v, k
        end
    end
    for i = 2:maxk
        traingold!(model, golds[i])
        trainpred!(model, preds[i])
    end
end
=#


"""
    stopping_time(criterion, losses; verbosity=0)
    stopping_time(criterion, losses, is_training; verbosity=0)

Determine the stopping time for the iterator `losses`, given
`stopping::StoppingCriterion`. Include the `Bool` vector `is_training`
of matching length, when there is a distinction between
"out-of-sample" losses and "training" losses.

If losses completes before a stop, then `0` is returned.

```
julia> stopping_time(NotANumber(), [10.0, 3.0, NaN, 4.0])
3

julia> stopping_time(NotANumber(), [10.0, 3.0, 5.0, 4.0])
0
```
"""
function stopping_time(criterion::EarlyStopper, losses, training; verbosity=0)

    t = 0 # counts regular `update` calls but ignores `update_training` calls
    for (loss, training) in zip(losses, training)
        if !training
            t += 1  # Increment count of out-of-sample updates
        end

        # Update criterion state
        is_done = done!(criterion, loss; training = training)

        if verbosity > 0
            @info "loss updates: $t"
            @info "state: $(criterion.state)"
        end
        is_done && return t
    end

    # No stopping
    return 0
end

# If training is not provided -> Assume always out-of-sample
stopping_time(criterion, losses; kwargs...) =
    stopping_time(criterion, losses, Iterators.repeated(false); kwargs...)

stopping_time(c::StoppingCriterion, args...; kwargs...) =
    stopping_time(EarlyStopper(c), args...; kwargs...)


using JuliaDB, DataFrames, XLSX

#I have an xlsx with a lot of data from DIPPR 801 (2197 compounds and their properties)
#using JuliaDB, i put the data in a julia-friendly format

function main1() #load the table from the xlsx
df = DataFrame(XLSX.readtable("DIPPR 801.xlsx", "DIPPR 801")...)
t = table(df)
println(0)

for i = 1:11
    st = Array{Union{String,Missing}}(undef,2197)
    for j = 1:2197
        x11 = df[i][j]
        st[j]= "$x11"
    end
    t = setcol(t,i,st)
    println(i)
end

for i = 12:length(colnames(t))
    fl  = Array{Union{Float64,Missing}}(undef,2197)
    for j = 1:2197
        fl[j]=df[i][j]
    end
    t = setcol(t,i,fl)
    println(i)
end

return t

end

#function searchchem(t::IndexedTable,query::String)
#    regex_q = Regex(query)
#    for i =

#0 - from 1 to 100
#1 - 101:200 ->


"""
    get_parameters_vals(items::Vector{<:AbstractItem})

# Description

Returns a matrix with item parameters displayed by row.
"""
function get_parameters_vals(items::Vector{<:AbstractItem})
ret = Vector{Vector{Float64}}(undef, size(items, 1))
max_length = 1
i_2 = 0
for i in items
    i_2 += 1
    local pars = get_parameters_vals(i)
    ret[i_2] = copy(pars)
    max_length = max_length < size(pars, 1) ? size(pars, 1) : max_length
end

for i_3 = 1:i_2
    local length_i = size(ret[i_3], 1)
    if length_i < max_length
        ret[i_3] = vcat(ret[i_3], zeros(Float64, max_length - length_i))
    end
end
return permutedims(reduce(hcat, ret))
end

"""
    get_parameters(items::Vector{<:AbstractItem})

# Description

Returns a vector of `<:AbstractParameters` objects.
"""
function get_parameters(items::Vector{<:AbstractItem})
return map( i -> i.parameters, items)
end


module LRP_CONFIG
using ExplainableAI
using ExplainableAI: LRPSupportedLayer, LRPSupportedActivation
"""
    LRP_CONFIG.supports_layer(layer)

Check whether LRP can be used on a layer or a Chain.
To extend LRP to your own layers, define:
```julia
LRP_CONFIG.supports_layer(::MyLayer) = true          # for structs
LRP_CONFIG.supports_layer(::typeof(mylayer)) = true  # for functions
```
"""
supports_layer(l) = false
supports_layer(::LRPSupportedLayer) = true
"""
    LRP_CONFIG.supports_activation(σ)

Check whether LRP can be used on a given activation function.
To extend LRP to your own activation functions, define:
```julia
LRP_CONFIG.supports_activation(::typeof(myactivation)) = true  # for functions
LRP_CONFIG.supports_activation(::MyActivation) = true          # for structs
```
"""
supports_activation(σ) = false
supports_activation(::LRPSupportedActivation) = true
end # LRP_CONFIG module

_check_layer(::Val{:LRP}, layer) = LRP_CONFIG.supports_layer(layer)
_check_layer(::Val{:LRP}, c::Chain) = all(_check_layer(Val(:LRP), l) for l in c)

function _check_activation(::Val{:LRP}, layer)
    hasproperty(layer, :σ) && return LRP_CONFIG.supports_activation(layer.σ)
    return true
end
_check_activation(::Val{:LRP}, c::Chain) = all(_check_activation(Val(:LRP), l) for l in c)

"""
    check_model(method::Symbol, model; verbose=true)

Check whether the given method can be used on the model.
Currently, model checks are only implemented for LRP, using the symbol `:LRP`.

# Example
julia> check_model(:LRP, model)
"""
check_model(method::Symbol, model; kwargs...) = check_model(Val(method), model; kwargs...)
function check_model(::Val{:LRP}, c::Chain; verbose=true)
    layer_checks = collect(_check_layer.(Val(:LRP), c.layers))
    activation_checks = collect(_check_activation.(Val(:LRP), c.layers))
    passed_layer_checks = all(layer_checks)
    passed_activation_checks = all(activation_checks)

    passed_layer_checks && passed_activation_checks && return true

    layer_names = [_print_name(l) for l in c]
    activation_names = [_print_activation(l) for l in c]

    verbose && _show_check_summary(
        c, layer_names, layer_checks, activation_names, activation_checks
    )
    if !passed_layer_checks
        verbose && display(
            Markdown.parse(
                """# Layers failed model check
                Found unknown layers `$(join(unique(layer_names[.!layer_checks]), ", "))`
                that are not supported by ExplainableAI's LRP implementation yet.

                If you think the missing layer should be supported by default, please [submit an issue](https://github.com/adrhill/ExplainableAI.jl/issues).

                These model checks can be skipped at your own risk by setting the LRP-analyzer keyword argument `skip_checks=true`.

                ## Using custom layers
                If you implemented custom layers, register them via
                ```julia
                LRP_CONFIG.supports_layer(::MyLayer) = true               # for structs
                LRP_CONFIG.supports_layer(::typeof(mylayer)) = true  # for functions
                ```
                The default fallback for this layer will use Automatic Differentiation according to "Layer-Wise Relevance Propagation: An Overview".
                """,
            ),
        )
        throw(ArgumentError("Unknown layers found in model"))
    end
    if !passed_activation_checks
        verbose && display(
            Markdown.parse(
                """ # Activations failed model check
                Found layers with unknown  or unsupported activation functions
                `$(join(unique(activation_names[.!activation_checks]), ", "))`.
                LRP assumes that the model is a "deep rectifier network" that only contains ReLU-like activation functions.

                If you think the missing activation function should be supported by default, please [submit an issue](https://github.com/adrhill/ExplainableAI.jl/issues).

                These model checks can be skipped at your own risk by setting the LRP-analyzer keyword argument `skip_checks=true`.

                ## Using custom activation functions
                If you use custom ReLU-like activation functions, register them via
                ```julia
                LRP_CONFIG.supports_activation(::typeof(myfunction)) = true  # for functions
                LRP_CONFIG.supports_activation(::MyActivation) = true        # for structs
                ```
                """,
            ),
        )
        throw(ArgumentError("Unknown or unsupported activation functions found in model"))
    end
end


using MathOptInterface
const MOI = MathOptInterface
const MOIT = MathOptInterface.Test
const MOIU = MathOptInterface.Utilities
const MOIB = MathOptInterface.Bridges

using Test

# It needs to be called first to trigger the crash.
include("issue980.jl")

# Tests for solvers are located in MOI.Test.

include("dummy.jl")

# MOI tests not relying on any submodule
@testset "MOI" begin
    include("isbits.jl")
    include("isapprox.jl")
    include("interval.jl")
    include("errors.jl")
    include("functions.jl")
    include("sets.jl")
    include("attributes.jl")
    include("instantiate.jl")
end

# Utilities submodule tests
@testset "MOI.$(submodule)" for submodule in [
    "Bridges",
    "FileFormats",
    "Test",
    "Utilities",
    "Benchmarks",
]
    include("$(submodule)/$(submodule).jl")
end

# Test hygiene of @model macro
include("hygiene.jl")


@testset "autodiff" begin

    @testset "fill" begin
        tp = Gen.new_tape()
        x = Gen.track(2.3, tp)
        y = fill(x, 3)
        grad = rand(3)
        Gen.deriv!(y, grad)
        Gen.reverse_pass!(tp)
        @test isapprox(Gen.deriv(x), sum(grad))

        tp = Gen.new_tape()
        x = Gen.track(2.3, tp)
        y = fill(x, 3, 2)
        grad = rand(3, 2)
        Gen.deriv!(y, grad)
        Gen.reverse_pass!(tp)
        @test isapprox(Gen.deriv(x), sum(grad))
    end

end


using Parameters

"""
Types and Structs
"""
abstract type Instruction end

@with_kw struct SetPlayers <: Instruction
    name::String = "players"
    writeResults::Bool = true
    playerone::Function
    playertwo::Function
end

@with_kw struct Repeater <: Instruction
    itinerary::Vector{Instruction}
    iterates::Int
end

@with_kw struct PlayGame <: Instruction
    name::String = "playedGame"
    writeResults::Bool = true
end

@with_kw struct RandomBoard <: Instruction
    name::String = "board"
    choices::Int = 2
    writeResults::Bool = true
    symmetric::Bool = false
end

@with_kw struct CustomBoard <: Instruction
    board::Array
    name::String = "board"
    writeResults::Bool = true
end

@with_kw struct Trials <: Instruction
    itineraries::Vector{Vector{Instruction}}
end

"""
Instruction Handlers
"""
function manager(instruction::Instruction, inputDict::Dict, config::Dict)
    """
    'hub' function, which directs the call to the function corresponding
    to the given function
    """
    if haskey(inputDict,"dir") == false
        println("here")
        currentdir = string(@__DIR__)
        if isdir(currentdir*"/tests") == false
            mkdir(currentdir*"/tests")
        end
        inputDict["dir"] = currentdir*"/tests"
    end

    functionDict = Dict(
        Repeater => repeater,
        PlayGame => playgame,
        RandomBoard => randomboard,
        CustomBoard => customboard,
        Trials => trials,
        SetPlayers => setplayers
        )

    instructionType = typeof(instruction)
    outputDict = functionDict[instructionType](instruction, inputDict, config)
    if hasfield(instructionType,:writeResults)
        if instruction.writeResults
            save(outputDict,outputDict["dir"],instruction.name)
        end
    end
    return outputDict
end

function repeater(instruction::Instruction,inputDict::Dict, config::Dict)
    outputDict = inputDict
    basedir = inputDict["dir"]
    for i in 1:instruction.iterates
        for step in instruction.itinerary
            outputDict["dir"] = basedir*"/iterate_"*string(i)
            if isdir(outputDict["dir"]) == false
                mkdir(outputDict["dir"])
            end
            outputDict = manager(step,outputDict,config)
        end
    end
    return outputDict
end

function playgame(instruction::Instruction,inputDict::Dict,config::Dict)
    """
    requires:
        board: a matrix of choices
        player_one, player_two: two functions which accept the dictionary of
            available information, and return a choice
    """
    # if a KeyError occurs, the previous instruction may be incompatible
    # with the currect instruction.
    board = inputDict["board"]

    player_one = inputDict["player_one"]
    player_two = inputDict["player_two"]

    prev_choice_one = 0
    prev_choice_two = 0

    try
        prev_choice_one = inputDict["prev_choice_one"]
        prev_choice_two = inputDict["prev_choice_two"]
    catch KeyError
    end

    playerOneDict = Dict(
    "board" => board,
    "my_prev_choice" => prev_choice_one,
    "their_prev_choice" => prev_choice_two
    )
    playerTwoDict = Dict(
    "board" => [board[1],board[2]],
    "my_prev_choice" => prev_choice_two,
    "their_prev_choice" => prev_choice_one
    )

    i = player_one(playerOneDict)
    j = player_two(playerTwoDict)

    outputDict = inputDict
    outputDict["prev_choice_one"] = i
    outputDict["prev_choice_two"] = j

    println("Player One Score: ",board[1][i,j],". Player Two Score: ",board[2][j,i],".")

    return outputDict
end

function randomboard(instruction::Instruction,inputDict::Dict,config::Dict)
    outputDict = inputDict
    n = instruction.choices
    board = [zeros(n,n),zeros(n,n)]
    for i in 1:length(board[1])
        board[1][i] = rand(1:10)
    end
    if instruction.symmetric
        board[2] = deepcopy(board[1])
    else
        for i in 1:length(board[1])
            board[2][i] = rand(1:10)
        end
    end
    outputDict["board"] = board
    outputDict["choices"] = length(board[1][1,:])
    return outputDict
end

function customboard(instruction::Instruction,inputDict::Dict,config::Dict)
    outputDict = inputDict
    outputDict["board"] = instruction.board
    outputDict["choices"] = length(outputDict["board"][1][1,:])
    return outputDict
end

function setplayers(instruction::Instruction,inputDict::Dict,config::Dict)
    outputDict = inputDict
    outputDict["player_one"] = instruction.playerone
    outputDict["player_two"] = instruction.playertwo
    outputDict["prev_choice_one"] = 0
    outputDict["prev_choice_two"] = 0
    return outputDict
end

function trials(instruction::Instruction,inputDict::Dict,config::Dict)
end

"""
Helper Functions
"""
function save(dict::Dict, dir::String, filename::String)
    f = open(dir*"/"*filename*".dat","w")
    for (i,key) in enumerate(sort(collect(keys(dict))))
        if key != "dir"
            if i != 1
                write(f, '\n')
            end
            write(f,string(key)*' '*string(dict[key]))
        end
    end
    close(f)
end

"""
Strategy Functions
"""
function A100(infoDict)
    return 1
end

function B100(infoDict)
    return 2
end

function greedy(infoDict)
    idx = argmax(infoDict["board"][1])
    return idx[1]
end

config = Dict()
inputDict = Dict()

players = SetPlayers(playerone = greedy, playertwo = B100)
genboard = RandomBoard(symmetric = true)
rungame = PlayGame()

trial = Repeater([players,genboard,rungame],100)

outputDict = manager(trial,inputDict,config)


using Dates
include("Network.jl")

"""
    flow(;apikey, bbox, s=1.0, m=1/60)

Returns real-time vehicle speed in every s seconds for m minutes in a bounding box using HERE Traffic API.

# Arguments
apikey::String   : API key to access HERE
bbox::String     : Bounding box defined as `"lat₁,long₁;lat₂,long₂"`
s::Float64=1.0   : gap between every API call
m::Float64=1/60  : time-period of analysis

For more information visit: https://developer.here.com/documentation/traffic/dev_guide/topics/guide.html

Example request: https://developer.here.com/documentation/traffic/dev_guide/topics_v6.1/example-flow-bounding-box.html
"""
function flow(;apikey, bbox, s=1.0, m=1/60)
    # Create request url
    baseurl = "https://traffic.ls.hereapi.com"
    path = "traffic/6.2"
    resource = "flow.xml"
    url = "$baseurl/$path/$resource?apiKey=$apikey&bbox=$bbox&responseattributes=shape"

    # Fetch network
    df = network(apikey=apikey, bbox=bbox)

    # Fetch real-time speeds
    t = 1
    while t ≤ m * 60
        Tₒ = Dates.format(now(), "HH:MM:SS:sss")
        df[!, "rts @$Tₒ"] = zeros(nrow(df))
        r = HTTP.request(:GET, url)
        f = parsexml(String(r.body))
        rootnode = root(f)
        k = 1
        for node in iterate(rootnode)
            if nodename(node) == "FI"
                childnode = lastelement(node)
                v = parse(Float64, childnode["SU"])
                df[k, "rts @$Tₒ"] = v
                k += 1
            end
        end
        t += s
        sleep(s)
    end

    return df
end


@testset "aspect ratio" begin
    @test spy(rand(2, 2)).graphics.grid  |> size == (5, 2)  # 2 + (2x1) = 5 (columns padding)
    @test spy(rand(4, 4)).graphics.grid  |> size == (5, 2)
    @test spy(rand(8, 4)).graphics.grid  |> size == (5, 2)
    @test spy(rand(12, 4)).graphics.grid |> size == (5, 3)
    @test spy(rand(8, 8)).graphics.grid  |> size == (5, 2)
    @test spy(rand(8, 12)).graphics.grid |> size == (6, 2)
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 80, 80, .15), fix_ar=true)
    test_ref("references/spy/fix_aspect_ratio_80x80_.txt", @show_col(p, :displaysize=>T_SZ))
end

@testset "sizing" begin
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 0, 0, .1))
    test_ref("references/spy/default_0x0.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 10, 10, .15))
    @test p isa Plot
    test_ref("references/spy/default_10x10.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 10, 15, .15))
    test_ref("references/spy/default_10x15.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 15, 10, .15))
    test_ref("references/spy/default_15x10.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 2000, 200, .0001))
    test_ref("references/spy/default_2000x200.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 200, 2000, .0001))
    test_ref("references/spy/default_200x2000.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 2000, 2000, .1))
    test_ref("references/spy/default_overdrawn.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 200, 200, .001))
    test_ref("references/spy/default_200x200_normal.txt", @show_col(p, :displaysize=>T_SZ))
    test_ref("references/spy/default_200x200_normal_nocolor.txt", @show_nocol(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(Matrix(_stable_sprand(RNG, 200, 200, .001)))
    test_ref("references/spy/default_200x200_normal.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 200, 200, .001), width=10)
    test_ref("references/spy/default_200x200_normal_small.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 200, 200, .001), height=5)
    test_ref("references/spy/default_200x200_normal_small.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 200, 200, .001), height=5, width=20)
    test_ref("references/spy/default_200x200_normal_misshaped.txt", @show_col(p, :displaysize=>T_SZ))
end

@testset "parameters" begin
    seed!(RNG, 1337)
    p = @inferred spy(_stable_sprand(RNG, 200, 200, .001), color=:green)
    test_ref("references/spy/parameters_200x200_green.txt", @show_col(p, :displaysize=>T_SZ))
    test_ref("references/spy/parameters_200x200_green_nocolor.txt", @show_nocol(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = spy(_stable_sprand(RNG, 200, 200, .001), title="Custom Title", canvas=DotCanvas, border=:ascii)
    test_ref("references/spy/parameters_200x200_dotcanvas.txt", @show_col(p, :displaysize=>T_SZ))
    seed!(RNG, 1337)
    p = spy(_stable_sprand(RNG, 200, 200, .99), show_zeros=true)
    test_ref("references/spy/parameters_200x200_zeros.txt", @show_col(p, :displaysize=>T_SZ))
end


module CTPlots

Base.Experimental.@optlevel 1

export plot_image, plot_sinogram, plot_tomogram
export plot_gray_scale

using RecipesBase: plot, plot!
using RecipesBase
using ..Monads
using ..CTImages: CTImage, CTSinogram, CTTomogram, ctfn, CTImageOrTomog
using ..CTImages: ctimage, ctsinogram, cttomogram
using ..TestImages: AbstractTestImage, ImageParams, gray_scale_indices
using ..CTScan: AbstractCTScanner
using ..Utils: linspace, ORI
using IntervalSets

function __init__()
    ENV["GKS_ENCODING"] = "utf-8"
end

"""
    plot_image(image::AbstractMatrix{T}; <keyword arguments>) where {T}

Plot CT image `image` with predefined options.

Additional keyword arguments are passed to the Plots library functions.

See also: [`plot_sinogram`](@ref), [`plot_tomogram`](@ref)
"""
function plot_image(image::AbstractMatrix{T}; kwargs...) where {T}
    plot(CTImage(image))
end

"""
    plot_tomogram(tomog::AbstractMatrix{T}; <keyword arguments>) where {T}

Plot reconstructed CT image `tomog` with predefined options.

Alias for `plot_image`. See [`plot_image`](@ref) for full reference.
Additional keyword arguments are passed to the Plots library.

See also: [`plot_sinogram`](@ref), [`plot_image`](@ref)
"""
function plot_tomogram(tomog::AbstractMatrix{T}; kwargs...) where {T}
    plot(CTTomogram(tomog); kwargs...)
end

"""
    plot_sinogram(sinog::AbstractMatrix{T}; <keyword arguments>) where {T}

Plot sinogram `sinog` with predefined options.

Additional keyword arguments are passed to the underlying plotting driver.

See also: [`plot_image`](@ref), [`plot_tomogram`](@ref)
"""
function plot_sinogram(sinog::AbstractMatrix{T}; kwargs...) where {T}
    plot(CTSinogram(sinog); kwargs...)
end

# TODO: Check lims for plot again!

@recipe function plot(
    xs::AbstractVector, ys::AbstractVector, image::CTImageOrTomog
)
    rows, cols = size(image)
    proj = get(plotattributes, :projection, nothing)
    aspect_ratio --> :equal
    #seriestype --> :heatmap
    _seriestype = get(plotattributes, :seriestype, nothing)
    if proj === :polar
        yaxis --> false
    elseif _seriestype === :heatmap
        tick_direction --> :out
        # xlims --> ((xs[1], xs[end]) .+ (-0.5, 0.5))
        xlims --> (xs[1], xs[end])
        # ylims --> ((ys[1], ys[end]) .+ (-0.5, 0.5))
        ylims --> (ys[1], ys[end])
    end
    xs, ys, mjoin(image)
end

@recipe function plot(
    xsi::Interval, ysi::Interval, image::CTImageOrTomog
)
    rows, cols = size(image)
    xs = linspace(xsi, cols)
    ys = linspace(ysi, rows)
    xs, ys, image
end

@recipe function plot(
    image::CTImageOrTomog,
    α::Optional{Real} = nothing,
    β::Optional{Real} = nothing,
)
    rows, cols = size(image)
    proj = get(plotattributes, :projection, nothing)
    if proj === :polar
        α = maybe(rows, α)
        β = maybe(2π, β)
        rs = linspace(0..α, rows+1)
        ϕs = linspace(0..β, cols+1)
        ϕs, rs, image
    else
        β = maybe(maybe((rows - 1) / 2, α), β)
        α = maybe((cols - 1) / 2, α)
        xs = linspace(-α..α, cols)
        ys = linspace(-β..β, rows)
        xs, ys, image
    end
end

const _sinog_xticks = [45i for i in 0:8]

@recipe function plot(xs::AbstractVector, ys::AbstractVector, sinog::CTSinogram)
    #seriestype --> :heatmap
    _seriestype = get(plotattributes, :seriestype, nothing)
    if _seriestype === :heatmap
        tick_direction --> :out
        # xlims --> ((xs[begin], xs[end]) .+ (-0.5, 0.5) .* Δϕ)
        xlims --> (xs[begin], xs[end])
        # ylims --> ((ys[begin], ys[end]) .+ (-0.5, 0.5))
        ylims --> (ys[begin], ys[end])
    end
    xs, ys, mjoin(sinog)
end

@recipe function plot(θsi::Interval, tsi::Interval, sinog::CTSinogram)
    nd, nϕ = size(sinog)
    ts = linspace(tsi, nd)
    θs = linspace(θsi, nϕ)
    θs, ts, sinog
end

@recipe function plot(sinog::CTSinogram, α::Optional{Real} = nothing)
    nd, nϕ = size(sinog)
    xs = linspace(ORI(0..360), nϕ) # Now we use midpoints and not edges to support more backends
    α = maybe((nd - 1) / 2, α)
    ys = linspace(-α..α, nd)
    xticks --> _sinog_xticks
    xrotation --> -45
    xs, ys, sinog
end

@recipe function plot(
    xs::Union{AbstractVector,Interval},
    ys::Union{AbstractVector,Interval},
    gs::AbstractTestImage
)
    xs, ys, gs.image
end

@recipe function plot(
    gs::AbstractTestImage,
    α::Optional{Real} = nothing,
    β::Optional{Real} = nothing
)
    gs.image, α, β
end

@recipe function plot(imp::ImageParams, gray_scale_data::AbstractVector...)
    _, xs = gray_scale_indices(imp)
    seriestype --> :scatter
    linewidth --> 1.3
    markersize --> 1.8
    markerstrokewidth --> 0
    legend --> :outertopright
    x --> (xs .- imp.cols ÷ 2)
    gray_scale_data
end

@recipe function plot(gst::AbstractCTScanner, s::Symbol)
    gst, Val(s)
end

for (nm, ctf) ∈ pairs(ctfn)
    v = Val{nm}
    @eval begin
        @recipe function plot(gst::AbstractCTScanner, ::$v)
            mjoin($ctf(gst))
        end
    end
end

"""
    plot_image(gst::AbstractCTScanner)

Plot input image created for the test `gst`.
"""
plot_image(gst::AbstractCTScanner; kwargs...) = plot(gst, :image; kwargs...)

"""
    plot_sinogram(gst::AbstractCTScanner)

Plot the sinogram computed for the test `gst`.
"""
plot_sinogram(gst::AbstractCTScanner; kwargs...) = plot(gst, :sinog; kwargs...)

"""
    plot_tomogram(gst::AbstractCTScanner)

Plot the reconstructed image for the test `gst`.
"""
plot_tomogram(gst::AbstractCTScanner; kwargs...) = plot(gst, :tomog; kwargs...)

function _get_options(options, i)
    if isnothing(options)
        return (;)
    end
    if length(options) == 0
        return (;)
    end
    if i > length(options)
        return (;)
    end
    options[i]
end

"""
    plot_gray_scale(
        imp::ImageParams,
        gray_scale_data...;
        options = nothing,
        <keyword arguments>
    )

Plot gray scale data.

Additional keyword arguments are passed to the Plots library.
"""
function plot_gray_scale(
    imp::ImageParams,
    data::AbstractVector...;
    options = nothing,
    kwargs...)
    p = plot(imp, first(data); st = :line)
    for i ∈ 2:length(data)
        opts = _get_options(options, i)
        plot!(p, imp, data[i]; opts..., kwargs...)
    end
    p
end

end # module


# This file was generated, do not modify it. # hide
roc_curve = roc(ŷ, y[validation])
plt = scatter(roc_curve, legend=false)
plot!(plt, xlab="false positive rate", ylab="true positive rate")
plot!([0, 1], [0, 1], linewidth=2, linestyle=:dash, color=:black)

savefig(joinpath(@OUTPUT, "EX-telco-roc.svg")) # hide


module VlasovBenchmarks

using BenchmarkTools

const SUITE = BenchmarkGroup()

include(joinpath(dirname(@__FILE__),"..","src","VlasovSolver","LaxWendroff.jl"))
import .LaxWendroff
SUITE["LaxWendroff"] = BenchmarkGroup()
SUITE["LaxWendroff c"] = BenchmarkGroup()

include(joinpath(dirname(@__FILE__),"..","src","VlasovSolver","Upwind.jl"))
import .Upwind
SUITE["Upwind"] = BenchmarkGroup()
SUITE["Upwind с"] = BenchmarkGroup()

include(joinpath(dirname(@__FILE__),"..","src","VlasovSolver","Godunov.jl"))
import .Godunov
SUITE["Godunov constant"] = BenchmarkGroup()
SUITE["Godunov linear"] = BenchmarkGroup()
SUITE["Godunov linear VanLeer"] = BenchmarkGroup()

include(joinpath(dirname(@__FILE__),"..","src","VlasovSolver","SemiLagrangian.jl"))
import .SemiLagrangian
SUITE["SemiLagrangian linear"] = BenchmarkGroup()
SUITE["SemiLagrangian quadratic"] = BenchmarkGroup()
SUITE["SemiLagrangian cubic"] = BenchmarkGroup()

include(joinpath(dirname(@__FILE__),"..","src","VlasovSolver","PFC.jl"))
import .PFC
SUITE["PFC"] = BenchmarkGroup()

Δx = Dict()
Δt = Dict()
v = Dict()
f₀ = Dict()
f = Dict()
advect! = Dict()

for N in [100, 1000, 10000]
    Δx[N] = 0.01
    Δt[N] = 0.8*Δx[N]
    v[N] = 1
    f₀[N] = [1.0 + 0.01*sin(2π*i*Δx[N]) for i = 0:N]
    f[N] = similar(f₀[N])
    advect![N] = Dict()

    advect![N][:LaxWendroff] = LaxWendroff.generate_solver(f₀[N], f[N], v[N]*Δt[N]/Δx[N])
    SUITE["LaxWendroff"]["advect $N"] = @benchmarkable advect![$N][:LaxWendroff]()

    advect![N][:LaxWendroff_c] = LaxWendroff.generate_solver(f₀[N], f[N])
    SUITE["LaxWendroff c"]["advect $N"] = @benchmarkable advect![$N][:LaxWendroff_c]($(v[N]*Δt[N]/Δx[N]))

    advect![N][:Upwind] = Upwind.generate_solver(f₀[N], f[N], v[N]*Δt[N]/Δx[N])
    SUITE["Upwind"]["advect $N"] = @benchmarkable advect![$N][:Upwind]()

    advect![N][:Upwind_с] = Upwind.generate_solver(f₀[N], f[N])
    SUITE["Upwind с"]["advect $N"] = @benchmarkable advect![$N][:Upwind_с]($(v[N]*Δt[N]/Δx[N]))

    advect![N][:Godunov_constant] = Godunov.generate_solver(f₀[N], f[N], :Riemann_constant)
    SUITE["Godunov constant"]["advect $N"] = @benchmarkable advect![$N][:Godunov_constant]($(v[N]*Δt[N]/Δx[N]))

    advect![N][:Godunov_linear] = Godunov.generate_solver(f₀[N], f[N], :Riemann_linear)
    SUITE["Godunov linear"]["advect $N"] = @benchmarkable advect![$N][:Godunov_linear]($(v[N]*Δt[N]/Δx[N]))

    advect![N][:Godunov_linear_VanLeer] = Godunov.generate_solver(f₀[N], f[N], :Riemann_linear; flux_limiter = :VanLeer)
    SUITE["Godunov linear VanLeer"]["advect $N"] = @benchmarkable advect![$N][:Godunov_linear_VanLeer]($(v[N]*Δt[N]/Δx[N]))

    advect![N][:SemiLagrangian_linear] = SemiLagrangian.generate_solver(f₀[N], f[N]; interpolation_order = :Linear)
    SUITE["SemiLagrangian linear"]["advect $N"] = @benchmarkable advect![$N][:SemiLagrangian_linear]($(v[N]*Δt[N]/Δx[N]))

    advect![N][:SemiLagrangian_quadratic] = SemiLagrangian.generate_solver(f₀[N], f[N]; interpolation_order = :Quadratic)
    SUITE["SemiLagrangian quadratic"]["advect $N"] = @benchmarkable advect![$N][:SemiLagrangian_quadratic]($(v[N]*Δt[N]/Δx[N]))

    advect![N][:SemiLagrangian_cubic] = SemiLagrangian.generate_solver(f₀[N], f[N]; interpolation_order = :Cubic)
    SUITE["SemiLagrangian cubic"]["advect $N"] = @benchmarkable advect![$N][:SemiLagrangian_cubic]($(v[N]*Δt[N]/Δx[N]))

    advect![N][:PFC] = PFC.generate_solver(f₀[N], f[N])
    SUITE["PFC"]["advect $N"] = @benchmarkable advect![$N][:PFC]($(v[N]*Δt[N]/Δx[N]))
end

end # module


""" Backend-agnostic layout of wiring diagrams via morphism expressions.

This module lays out wiring diagrams for visualization, independent of any
specific graphics system. It uses the structure of a morphism expression to
determine the layout. Thus, the first step of the algorithm is to convert the
wiring diagram to a symbolic expression, using the submodule
`WiringDiagrams.Expressions`. Morphism expressions may also be given directly.
"""
module WiringDiagramLayouts
export LayoutOrientation, LeftToRight, RightToLeft, TopToBottom, BottomToTop,
  layout_diagram, layout_box

using Compat
import Base: sign
using LinearAlgebra: dot, normalize
using Logging
using Parameters: @with_kw_noshow
using StaticArrays: StaticVector, SVector
using Statistics: mean

using ...Syntax
using ...Theories: ObExpr, HomExpr, dom, codom, compose, id, otimes, braid
using ...WiringDiagrams

# Data types
############

const AbstractVector2D = StaticVector{2,<:Real}
const Vector2D = SVector{2,Float64}

""" Orientation of wiring diagram.
"""
@enum LayoutOrientation LeftToRight RightToLeft TopToBottom BottomToTop

is_horizontal(orient::LayoutOrientation) = orient in (LeftToRight, RightToLeft)
is_vertical(orient::LayoutOrientation) = orient in (TopToBottom, BottomToTop)
is_positive(orient::LayoutOrientation) = orient in (LeftToRight, TopToBottom)
is_negative(orient::LayoutOrientation) = orient in (RightToLeft, BottomToTop)

sign(orient::LayoutOrientation) = is_positive(orient) ? +1 : -1
port_sign(kind::PortKind, orient::LayoutOrientation) =
  (kind == InputPort ? -1 : +1) * sign(orient)
port_sign(d::WiringDiagram, port::Port, orient::LayoutOrientation) =
  (port.box in outer_ids(d) ? -1 : +1) * port_sign(port.kind, orient)

svector(orient::LayoutOrientation, first, second) =
  is_horizontal(orient) ? SVector(first, second) : SVector(second, first)

""" Internal data type for configurable options of wiring diagram layout.
"""
@with_kw_noshow struct LayoutOptions
  orientation::LayoutOrientation = LeftToRight
  default_box_shape::Symbol = :rectangle
  box_shapes::AbstractDict = Dict()
  box_styles::AbstractDict = Dict()
  outer_ports_layout::Symbol = :isotonic
  anchor_wires::Union{Bool,AbstractSet,AbstractVector} = [:id,:braid]
  base_box_size::Real = 2
  box_stretch::Real = 1
  sequence_pad::Real = 2
  parallel_pad::Real = 1
  junction_size::Real = 0.25
end

svector(opts::LayoutOptions, args...) = svector(opts.orientation, args...)

""" Layout for box in a wiring diagram.

Suggests a shape, size, and position for the box. Also includes the box's
value, typically used for labels, and a style name, which does not affect the
layout but is interpreted by graphics backends to adjust the visual style.

Specific features of the shape are determined by the graphics backend. For
example, a rectangle could be rendered with or without rounded corners or even
as another, similar shape, such as a parallelogram.
"""
@with_kw_noshow mutable struct BoxLayout
  value::Any = nothing
  shape::Symbol = :rectangle
  position::Vector2D = zeros(Vector2D)
  size::Vector2D = zeros(Vector2D)
  style::Union{Symbol,Nothing} = nothing
end

position(layout::BoxLayout) = layout.position
size(layout::BoxLayout) = layout.size
lower_corner(layout::BoxLayout) = position(layout) - size(layout)/2
upper_corner(layout::BoxLayout) = position(layout) + size(layout)/2

position(box::AbstractBox) = position(box.value)
size(box::AbstractBox) = size(box.value)
lower_corner(box::AbstractBox) = lower_corner(box.value)
upper_corner(box::AbstractBox) = upper_corner(box.value)

contents_lower_corner(diagram::WiringDiagram) =
  mapreduce(lower_corner, (c,d) -> min.(c,d), boxes(diagram))
contents_upper_corner(diagram::WiringDiagram) =
  mapreduce(upper_corner, (c,d) -> max.(c,d), boxes(diagram))

""" Layout for port in a wiring diagram.
"""
@with_kw_noshow struct PortLayout
  value::Any = nothing
  position::Vector2D = zeros(Vector2D) # Position relative to box center.
  normal::Vector2D = zeros(Vector2D)   # Outward unit normal vector.
  label_wires::Bool = true             # Show labels for wires into/out of port?
  reverse_wires::Bool = false          # Reverse wires into/out of port?
end

position(layout::PortLayout) = layout.position
normal(layout::PortLayout) = layout.normal

""" Interior point of wire in a wiring diagram.
"""
mutable struct WirePoint
  position::Vector2D     # Position of wire at point.
  tangent::Vector2D      # Unit tangent vector along wire at point.
end

position(point::WirePoint) = point.position
tangent(point::WirePoint) = point.tangent

const WireLayout = Union{Vector{WirePoint},Nothing}

wire_points(points::Vector{WirePoint}) = points
wire_points(::Nothing) = WirePoint[]
wire_points(wire::Wire) = wire_points(wire.value)

# Main entry point
##################

""" Lay out a wiring diagram or morphism expression for visualization.

If a wiring diagram is given, it is first to converted to a morphism expression.

The layout is calculated with respect to a cartesian coordinate system with
origin at the center of the diagram and the positive y-axis pointing
*downwards*. Box positions are relative to their centers. All positions and
sizes are dimensionless (unitless).
"""
function layout_diagram(Syntax::Module, diagram::WiringDiagram; kw...)
  layout_diagram(to_hom_expr(Syntax, diagram); kw...)
end
function layout_diagram(Ob::Type, Hom::Type, diagram::WiringDiagram; kw...)
  layout_diagram(to_hom_expr(Ob, Hom, diagram); kw...)
end

function layout_diagram(expr::HomExpr; kw...)::WiringDiagram
  opts = LayoutOptions(; kw...)
  diagram = layout_hom_expr(expr, opts)
  layout_outer_ports!(diagram, opts, method=opts.outer_ports_layout)
end

# Morphism expression layout
############################

""" Lay out a morphism expression as a wiring diagram.
"""
layout_hom_expr(f::HomExpr, opts) = layout_box(f, opts)

layout_hom_expr(f::HomExpr{:compose}, opts) =
  compose_with_layout!(map(arg -> layout_hom_expr(arg, opts), args(f)), opts)
layout_hom_expr(f::Union{HomExpr{:otimes},HomExpr{:oplus}}, opts) =
  otimes_with_layout!(map(arg -> layout_hom_expr(arg, opts), args(f)), opts)

layout_hom_expr(f::HomExpr{:id}, opts) = layout_pure_wiring(f, opts)
layout_hom_expr(f::Union{HomExpr{:braid},HomExpr{:swap}}, opts) =
  layout_pure_wiring(f, opts)

layout_hom_expr(f::HomExpr{:mcopy}, opts) = layout_supply(f, opts, shape=:junction)
layout_hom_expr(f::HomExpr{:delete}, opts) = layout_supply(f, opts, shape=:junction)
layout_hom_expr(f::HomExpr{:mmerge}, opts) = layout_supply(f, opts, shape=:junction)
layout_hom_expr(f::HomExpr{:create}, opts) = layout_supply(f, opts, shape=:junction)

layout_hom_expr(f::HomExpr{:plus}, opts) =
  layout_supply(f, opts, shape=:junction, style=:variant_junction)
layout_hom_expr(f::HomExpr{:zero}, opts) =
  layout_supply(f, opts, shape=:junction, style=:variant_junction)
layout_hom_expr(f::HomExpr{:coplus}, opts) =
  layout_supply(f, opts, shape=:junction, style=:variant_junction)
layout_hom_expr(f::HomExpr{:cozero}, opts) =
  layout_supply(f, opts, shape=:junction, style=:variant_junction)

layout_hom_expr(f::HomExpr{:dunit}, opts) =
  layout_supply(f, opts, shape=:junction, visible=false, pad=false)
layout_hom_expr(f::HomExpr{:dcounit}, opts) =
  layout_supply(f, opts, shape=:junction, visible=false, pad=false)

layout_port(A::ObExpr{:dual}; kw...) =
  PortLayout(; value=A, reverse_wires=true, kw...)
wire_label(mime::MIME, A::ObExpr{:dual}) = wire_label(mime, first(A))

function layout_box(f::HomExpr, opts::LayoutOptions; kw...)
  layout_box(collect(dom(f)), collect(codom(f)), opts; value=f, kw...)
end

function layout_pure_wiring(f::HomExpr, opts::LayoutOptions; kw...)
  layout_pure_wiring(to_wiring_diagram(f, identity, identity), opts;
    anchor_wires=head(f) in opts.anchor_wires, kw...)
end

function layout_supply(f::HomExpr, opts::LayoutOptions;
                       ob::Union{ObExpr,Nothing}=nothing, kw...)
  obs = collect(isnothing(ob) ? first(f)::ObExpr : ob)
  n = length(obs)
  ndom, ncodom = ndims(dom(f)) ÷ n, ndims(codom(f)) ÷ n
  diagram = otimes_with_layout!(map(obs) do ob
    layout_box(repeat([ob], ndom), repeat([ob], ncodom), opts; kw...)
  end, opts)
  anchor_wires = head(f) in opts.anchor_wires
  if n > 1 && ndom > 1
    σ = reshape(permutedims(reshape(1:ndom*n, n, :)), :)
    σ_diagram = permute(dom(diagram), σ, inverse=true)
    σ_diagram = layout_pure_wiring(σ_diagram, opts, anchor_wires=anchor_wires)
    diagram = compose_with_layout!(σ_diagram, diagram, opts)
  end
  if n > 1 && ncodom > 1
    σ = reshape(permutedims(reshape(1:ncodom*n, n, :)), :)
    σ_diagram = permute(codom(diagram), σ)
    σ_diagram = layout_pure_wiring(σ_diagram, opts, anchor_wires=anchor_wires)
    diagram = compose_with_layout!(diagram, σ_diagram, opts)
  end
  diagram
end

# Diagram layout
################

""" Compose wiring diagrams and their layouts.

Compare with: `WiringDiagram.compose`.
"""
function compose_with_layout!(d1::WiringDiagram, d2::WiringDiagram, opts::LayoutOptions)
  diagram = compose(d1, d2; unsubstituted=true)
  dir = svector(opts, sign(opts.orientation), 0)
  place_adjacent!(d1, d2; dir=dir, pad=-opts.sequence_pad)
  substitute_with_layout!(size_to_fit!(diagram, opts, pad=false))
end
function compose_with_layout!(diagrams::Vector{<:WiringDiagram}, opts::LayoutOptions)
  foldl((d1,d2) -> compose_with_layout!(d1, d2, opts), diagrams)
end

""" Tensor wiring diagrams and their layouts.

Compare with: `WiringDiagram.otimes`.
"""
function otimes_with_layout!(d1::WiringDiagram, d2::WiringDiagram, opts::LayoutOptions)
  # Compare with `WiringDiagrams.otimes`.
  diagram = otimes(d1, d2; unsubstituted=true)
  dir = svector(opts, 0, 1)
  place_adjacent!(d1, d2; dir=dir, pad=-opts.parallel_pad)
  substitute_with_layout!(size_to_fit!(diagram, opts, pad=false))
end
function otimes_with_layout!(diagrams::Vector{<:WiringDiagram}, opts::LayoutOptions)
  foldl((d1,d2) -> otimes_with_layout!(d1, d2, opts), diagrams)
end

""" Size a wiring diagram to fit its contents.

The inner boxes are also shifted to be centered within the new bounds.
"""
function size_to_fit!(diagram::WiringDiagram, opts::LayoutOptions; pad::Bool=true)
  nin, nout = length(input_ports(diagram)), length(output_ports(diagram))
  minimum_size = minimum_diagram_size(nin, nout, opts)

  lower, upper = contents_lower_corner(diagram), contents_upper_corner(diagram)
  content_size = upper - lower
  if pad; content_size += 2 * diagram_padding(opts) end
  size = max.(minimum_size, content_size)

  content_center = (lower + upper)/2
  shift_contents!(diagram, -content_center)
  diagram.value = BoxLayout(size=size)
  diagram
end

""" Substitute sub-wiring diagrams, preserving their layouts.
"""
function substitute_with_layout!(d::WiringDiagram)
  substitute_with_layout!(d, filter(v -> box(d,v) isa WiringDiagram, box_ids(d)))
end
function substitute_with_layout!(d::WiringDiagram, vs::Vector{Int})
  for v in vs
    sub = box(d, v)::WiringDiagram
    shift_contents!(sub, position(sub))
  end
  substitute(d, vs; merge_wire_values=merge_wire_layouts)
end

""" Place one box adjacent to another.

The absolute positions are undefined; only relative positions are guaranteed.
"""
function place_adjacent!(box1::AbstractBox, box2::AbstractBox;
                         dir::AbstractVector2D=SVector(1,0), pad::Real=0)
  layout1, layout2 = box1.value::BoxLayout, box2.value::BoxLayout
  layout1.position = -(pad .+ size(layout1))/2 .* dir
  layout2.position = (pad .+ size(layout2))/2 .* dir
end

""" Shift all boxes and wires within wiring diagram by a fixed offset.
"""
function shift_contents!(diagram::WiringDiagram, offset::AbstractVector2D)
  for box in boxes(diagram)
    layout = box.value::BoxLayout
    layout.position += offset
  end
  for wire in wires(diagram)
    for point in wire_points(wire)
      point.position += offset
    end
  end
  diagram
end

""" Compute the minimum size of a wiring diagram from the number of its ports.
"""
function minimum_diagram_size(nin::Int, nout::Int, opts::LayoutOptions)
  default_box_size(nin, nout, opts, stretch=0) + 2*diagram_padding(opts)
end
function diagram_padding(opts::LayoutOptions)
  svector(opts, opts.sequence_pad, opts.parallel_pad)
end

# Box layout
############

""" Lay out a box and its ports.

By default the box is rectangular, but other shapes are also supported.
"""
function layout_box(inputs::Vector, outputs::Vector, opts::LayoutOptions;
                    shape::Union{Symbol,Nothing}=nothing,
                    style::Union{Symbol,Nothing}=nothing, value=nothing, kw...)
  shape = get(opts.box_shapes, value) do
    isnothing(shape) ? opts.default_box_shape : shape
  end
  style = get(opts.box_styles, value, style)
  layout_box(Val{shape}, inputs, outputs, opts; style=style, value=value, kw...)
end

function layout_box(::Type{Val{:rectangle}}, inputs::Vector, outputs::Vector,
                    opts::LayoutOptions; shape::Symbol=:rectangle, kw...)
  size = default_box_size(length(inputs), length(outputs), opts)
  box = Box(BoxLayout(; shape=shape, size=size, kw...),
            layout_linear_ports(InputPort, inputs, size, opts),
            layout_linear_ports(OutputPort, outputs, size, opts))
  size_to_fit!(singleton_diagram(box), opts)
end

function layout_box(::Type{Val{:circle}}, inputs::Vector, outputs::Vector,
                    opts::LayoutOptions; pad::Bool=true, kw...)
  size = default_box_size(1, 1, opts)
  radius = first(size) / 2
  box = Box(BoxLayout(; shape=:circle, size=size, kw...),
            layout_circular_ports(InputPort, inputs, radius, opts; pad=pad),
            layout_circular_ports(OutputPort, outputs, radius, opts; pad=pad))
  size_to_fit!(singleton_diagram(box), opts)
end

function layout_box(::Type{Val{:ellipse}}, inputs::Vector, outputs::Vector,
                    opts::LayoutOptions; pad::Bool=true, kw...)
  size = default_box_size(length(inputs), length(outputs), opts)
  radii = size / 2
  box = Box(BoxLayout(; shape=:ellipse, size=size, kw...),
            layout_elliptical_ports(InputPort, inputs, radii, opts; pad=pad),
            layout_elliptical_ports(OutputPort, outputs, radii, opts; pad=pad))
  size_to_fit!(singleton_diagram(box), opts)
end

function layout_box(::Type{Val{:triangle}}, inputs::Vector, outputs::Vector,
                    opts::LayoutOptions; kw...)
  @assert length(inputs) <= 1 "Cannot use triangle shape with multiple inputs"
  layout_box(Val{:rectangle}, inputs, outputs, opts; shape=:triangle, kw...)
end
function layout_box(::Type{Val{:invtriangle}}, inputs::Vector, outputs::Vector,
                    opts::LayoutOptions; kw...)
  @assert length(outputs) <= 1 "Cannot use invtriangle shape with multiple outputs"
  layout_box(Val{:rectangle}, inputs, outputs, opts; shape=:invtriangle, kw...)
end

# Although `trapezoid` is the standard term in North American English,
# we use the term `trapezium` because both Graphviz and TikZ do.
function layout_box(::Type{Val{:trapezium}}, inputs::Vector, outputs::Vector,
                    opts::LayoutOptions; kw...)
  layout_box(Val{:rectangle}, inputs, outputs, opts; shape=:trapezium, kw...)
end
function layout_box(::Type{Val{:invtrapezium}}, inputs::Vector, outputs::Vector,
                    opts::LayoutOptions; kw...)
  layout_box(Val{:rectangle}, inputs, outputs, opts; shape=:invtrapezium, kw...)
end

function layout_box(::Type{Val{:junction}}, inputs::Vector, outputs::Vector,
                    opts::LayoutOptions; visible::Bool=true, pad::Bool=true, kw...)
  nin, nout = length(inputs), length(outputs)
  shape, radius = visible ? (:junction, opts.junction_size) : (:invisible, 0)
  size = 2*SVector(radius, radius)
  box = Box(BoxLayout(; shape=shape, size=size, kw...),
            layout_circular_ports(InputPort, inputs, radius, opts;
                                  pad=pad, label_wires = nin == 1 || nout == 0),
            layout_circular_ports(OutputPort, outputs, radius, opts;
                                  pad=pad, label_wires = nin == 0 || nout == 1))
  size_to_fit!(singleton_diagram(box), opts)
end

""" Compute the default size of a rectangular box from the number of its ports.

We use the unique formula consistent with the padding for monoidal products,
ensuring that the size of a product of boxes depends only on the total number of
ports, not on the number of boxes.
"""
function default_box_size(nin::Int, nout::Int, opts::LayoutOptions;
                          stretch::Union{Real,Nothing}=nothing)
  base_size = opts.base_box_size
  stretch = isnothing(stretch) ? opts.box_stretch : stretch
  n = max(1, nin, nout)
  svector(opts, stretch*base_size, n*base_size + (n-1)*opts.parallel_pad)
end

# Port layout
#############

""" Lay out ports linearly, for use with a rectangular box.

Assumes the box is at least as large as `default_box_size()`.
"""
function layout_linear_ports(port_kind::PortKind, port_values::Vector,
                             box_size::AbstractVector2D, opts::LayoutOptions; kw...)
  n = length(port_values)
  coeffs = range(-(n-1), n-1, step=2) / 2 # Always length n
  coords = (opts.base_box_size + opts.parallel_pad) .* collect(coeffs)
  layout_linear_ports(port_kind, port_values, coords, box_size, opts, kw...)
end

function layout_linear_ports(port_kind::PortKind, port_values::Vector,
                             port_coords::Vector, box_size::AbstractVector2D,
                             opts::LayoutOptions; kw...)
  normal_dir = svector(opts, port_sign(port_kind, opts.orientation), 0.0)
  port_dir = svector(opts, 0.0, 1.0)
  start = box_size/2 .* normal_dir
  positions = [ start + coord .* port_dir for coord in port_coords ]
  PortLayout[ layout_port(value, position=pos, normal=normal_dir; kw...)
              for (value, pos) in zip(port_values, positions) ]
end

""" Lay out ports along a circular arc.
"""
function layout_circular_ports(port_kind::PortKind, port_values::Vector,
                               radius::Real, opts::LayoutOptions; kw...)
  radii = SVector(radius, radius)
  layout_elliptical_ports(port_kind, port_values, radii, opts; kw...)
end

""" Lay out ports along an elliptical arc.
"""
function layout_elliptical_ports(port_kind::PortKind, port_values::Vector,
                                 radii::AbstractVector2D, opts::LayoutOptions;
                                 pad::Bool=true, kw...)
  n = length(port_values)
  port_dir = svector(opts, port_sign(port_kind, opts.orientation), 0)
  θ1, θ2 = if port_dir == SVector(1,0); (π/2, -π/2)
    elseif port_dir == SVector(0,1); (-π, 0)
    elseif port_dir == SVector(-1,0); (π/2, 3π/2)
    elseif port_dir == SVector(0,-1); (π, 0) end
  θs = collect(pad ? range(θ1,θ2,length=n+2)[2:n+1] : range(θ1,θ2,length=n))
  dirs = [ SVector(cos(θ),-sin(θ)) for θ in θs ] # positive y-axis downwards
  PortLayout[ layout_port(value, position=radii .* dir, normal=dir; kw...)
              for (value, dir) in zip(port_values, dirs) ]
end

""" Lay out outer ports of wiring diagram.
"""
function layout_outer_ports!(diagram::WiringDiagram, opts::LayoutOptions; kw...)
  original_value = x -> x isa PortLayout ? x.value : x # XXX: This is ugly.
  diagram.input_ports = map(original_value, input_ports(diagram))
  diagram.output_ports = map(original_value, output_ports(diagram))
  diagram.input_ports, diagram.output_ports =
    layout_outer_ports(diagram, opts; kw...)
  diagram
end

""" Lay out outer ports of wiring diagram.
"""
function layout_outer_ports(diagram::WiringDiagram, opts::LayoutOptions;
                            method::Symbol=:fixed, kw...)
  if !has_port_layout_method(method)
    if method == :isotonic
      @warn """
        Both Convex.jl and SCS.jl must be loaded to use isotonic
        regression for port layout. Falling back to fixed layout.
        """ maxlog=1
    else
      error("Unknown port layout method: $method")
    end
    method = :fixed
  end
  layout_outer_ports(diagram, opts, Val{method}; kw...)
end

function layout_outer_ports(diagram::WiringDiagram, opts::LayoutOptions,
                            ::Type{Val{:fixed}})
  (layout_linear_ports(InputPort, input_ports(diagram), size(diagram), opts),
   layout_linear_ports(OutputPort, output_ports(diagram), size(diagram), opts))
end

function layout_outer_ports(diagram::WiringDiagram, opts::LayoutOptions,
                            ::Type{Val{:isotonic}})
  inputs, outputs = input_ports(diagram), output_ports(diagram)
  diagram_size = diagram.value.size
  port_dir = svector(opts, 0, 1)
  port_coord = v -> dot(v, port_dir)
  max_range = port_coord(diagram_size)
  lower, upper = -max_range/2, max_range/2
  pad = opts.base_box_size + opts.parallel_pad

  # Solve optimization problem for input ports.
  # Note that we are minimizing distance only along the port axis, not in the
  # full Euclidean plan.
  ys = [ Float64[] for i in eachindex(inputs) ]
  for wire in out_wires(diagram, input_id(diagram))
    i, layout = wire.source.port, wire.value::WireLayout
    if !(isnothing(layout) || isempty(layout))
      push!(ys[i], port_coord(position(first(layout))))
    elseif wire.target.box != output_id(diagram)
      push!(ys[i], port_coord(position(diagram, wire.target)))
    end
  end
  y = Float64[ isempty(y) ? 0.0 : mean(y) for y in ys ]
  in_coords = solve_isotonic(y; lower=lower, upper=upper, pad=pad)

  # Solve optimization problem for output ports.
  ys = [ Float64[] for i in eachindex(outputs) ]
  for wire in in_wires(diagram, output_id(diagram))
    i, layout = wire.target.port, wire.value::WireLayout
    if !(isnothing(layout) || isempty(layout))
      push!(ys[i], port_coord(position(last(layout))))
    elseif wire.source.box != input_id(diagram)
      push!(ys[i], port_coord(position(diagram, wire.source)))
    end
  end
  y = Float64[ isempty(y) ? 0.0 : mean(y) for y in ys ]
  out_coords = solve_isotonic(y; lower=lower, upper=upper, pad=pad)

  (layout_linear_ports(InputPort, inputs, in_coords, diagram_size, opts),
   layout_linear_ports(OutputPort, outputs, out_coords, diagram_size, opts))
end
solve_isotonic(args...; kw...) = error("Isotonic regression backend not available")

has_port_layout_method(method::Symbol) = has_port_layout_method(Val{method})
has_port_layout_method(::Type{<:Val}) = false
has_port_layout_method(::Type{Val{:fixed}}) = true

layout_port(value; kw...) = PortLayout(; value=value, kw...)

function position(diagram::WiringDiagram, port::Port)
  pos = position(port_value(diagram, port))
  if !(port.box in outer_ids(diagram))
    pos += position(box(diagram, port.box))
  end
  pos
end

function normal(diagram::WiringDiagram, port::Port)
  (port.box in outer_ids(diagram) ? -1 : +1) * normal(port_value(diagram, port))
end

# Wire layout
#############

""" Lay out the wires in a wiring diagram with no boxes.
"""
function layout_pure_wiring(diagram::WiringDiagram, opts::LayoutOptions;
                            anchor_wires::Bool=true)
  @assert nboxes(diagram) == 0
  inputs, outputs = input_ports(diagram), output_ports(diagram)
  size = minimum_diagram_size(length(inputs), length(outputs), opts)
  result = WiringDiagram(BoxLayout(size=size), inputs, outputs)
  if anchor_wires
    result = layout_outer_ports!(result, opts, method=:fixed)
    e1, e2 = svector(opts, 1, 0), svector(opts, 0, 1)
    for wire in wires(diagram)
      src, tgt = wire.source, wire.target
      src_pos, tgt_pos = (position(port_value(result, p)) for p in (src, tgt))
      mid_pos = (src_pos + tgt_pos) / 2
      v = sign.((tgt_pos - src_pos) .* e1) + (tgt.port - src.port) .* e2
      point = WirePoint(mid_pos .* e2, normalize(v))
      add_wire!(result, Wire([point], src, tgt))
    end
  else
    add_wires!(result, wires(diagram))
  end
  result
end

merge_wire_layouts(left::WireLayout, middle::WireLayout, right::WireLayout) =
  vcat(wire_points(left), wire_points(middle), wire_points(right))

# Labels
########

""" Label for box in wiring diagram.
"""
box_label(mime::MIME, value) = diagram_element_label(mime, value)

""" Label for wire in wiring diagram.

Note: This function takes a port value, not a wire value.
"""
wire_label(mime::MIME, value) = diagram_element_label(mime, value)

""" Label for element in wiring diagram.

By default, both `box_label` and `wire_label` fall back to this function.
"""
diagram_element_label(::MIME, value) = string(value)
diagram_element_label(::MIME, ::Nothing) = ""

function diagram_element_label(::MIME"text/latex", expr::GATExpr)
  string('$', sprint(show_latex, expr), '$')
end
function diagram_element_label(::MIME"text/latex", s::Union{String,Symbol})
  string('$', s, '$')
end

end


using CUDA: CuArray
using Oceananigans.Architectures: AbstractCPUArchitecture, AbstractGPUArchitecture

import Base: zeros

zeros(FT, ::AbstractCPUArchitecture, N...) = zeros(FT, N...)

function zeros(FT, ::AbstractGPUArchitecture, N...)
    a = CuArray{FT}(undef, N...)
    a .= 0
    return a
end

zeros(arch::AbstractArchitecture, grid, N...) = zeros(eltype(grid), arch, N...)


struct AnimatedGIF{T, A<:AbstractArray} <: AbstractArray{T, 3}
    data::A
    fps::Int

    # NOTE: the default fps is chosen purely by experience and may be changed in the future
    function AnimatedGIF{T, A}(data::A; fps::Real=min(10, size(data, 3)÷2)) where {T, A<:AbstractArray}
        if fps < 1
            fps = 1
            @warn "FPS should be larger than 1" FPS=fps
        end

        new{T, A}(data, ceil(Int, fps))
    end
end
AnimatedGIF(data::AbstractArray; kwargs...) = AnimatedGIF{eltype(data), typeof(data)}(data; kwargs...)

# Display gif requires ImageMagick v1.2.0, which requires Julia 1.3
Base.showable(::MIME"image/gif", agif::AnimatedGIF) = true
function Base.show(io::IO, ::MIME"image/gif", agif::AnimatedGIF)
    FileIO.save(_format_stream(format"GIF", io), agif.data; fps=agif.fps)
end

Base.size(A::AnimatedGIF) = size(A.data)
Base.axes(A::AnimatedGIF) = axes(A.data)
Base.@propagate_inbounds Base.getindex(A::AnimatedGIF, inds::Vararg{Int}) = getindex(A.data, inds...)

"""
    gif(img; kwargs...)
    gif(frames; kwargs...)

Convert 3D `img` or 2D frame list `frames` to animated gif array.

!!! compat "ImageMagick 1.2"
    ImageMagick at least 1.2 is required to generate gif image. For ImageMagick 0.y.z versions,
    ImageMagick at least 0.7.7 also works.

# Arguments

- `img::AbstractArray{T, 3}`: the last dimension is the time axis.
- `frames`: vector of 2d arrays.

# Parameters

- `fps::Int`: frame per second.

# Examples:

You can use this to view a 3D image:

```julia
using ImageShow, TestImages
ImageShow.gif(testimage("mri-stack"))
```

Or use it to dynamically check how different parameters change the function behavior:

```julia
using TestImages, ImageShow, ImageTransformations
img = testimage("cameraman")
ImageShow.gif([imrotate(img, θ, axes(img)) for θ in -π/4:π/16:π/4]; fps=3)
```

See also `mosaic`, provided by `MosaicViews`/`ImageCore`, for a 2d alternative of `gif`.
"""
gif(img::AbstractArray{<:Colorant, 3}; kwargs...) = AnimatedGIF(img; kwargs...)
gif(img::AbstractArray{<:Real, 3}; kwargs...) = AnimatedGIF(of_eltype(Gray, img); kwargs...)
function gif(frames::AbstractVector{<:AbstractMatrix}; kwargs...)
    FT = eltype(eltype(frames))
    fillvalue = zero(FT) # require ColorVectorSpace for RGB and Gray

    # FIXME: sym_paddedviews+StackView makes it unable to infer the types
    # TODO: sym_paddedviews(zero(FT), frames...) would just break whatever the fancy lazy
    #       feature `frames` has and thus slows the code
    sz = size(first(frames))
    frames = all(A->sz == size(A), frames) ? frames : sym_paddedviews(zero(FT), frames...)
    stacked_frames = StackView{FT}(frames, Val(3))
    gif(stacked_frames; kwargs...)
end

# Because any object can be callable (not just `Function`), we add this method to distinguish
# the `f` function version and throw a menaingful message.
# TODO: Maybe we could just support `gif(framestack...)`?
gif(::AbstractMatrix, ::AbstractMatrix...) = throw(ArgumentError("Do you mean `gif([A, Bs...])"))

"""
    gif(f, Xs; kwargs...)
    gif(f, Xs, Ys...; kwargs...)

A lazy version of `gif([f(X) for X in Xs]; kwargs...)` that allocates memory only when needed.

# Parameters

- `fps::Int`: frame per second.

# Examples

Rotate the image and see how things going:

```julia
using TestImages, ImageShow, ImageTransformations
img = testimage("cameraman")
ImageShow.gif(-π/4:π/16:π/4]; fps=3) do θ
    imrotate(img, θ, axes(img))
end
```

The following example is less meaningful, but it shows how multiple arguments are passed:

```julia
sizes = 16:4:64
values = range(0, stop=1, length=length(sizes))
gif = ImageShow.gif(values, sizes) do v, x
    fill(RGB(v, v, v), ntuple(_->x, 2)...)
end
```
"""
gif(f, arg1, args...; kwargs...) = gif(mappedarray(f, arg1, args...); kwargs...)
# MappedArrays are not efficient here https://github.com/JuliaArrays/MappedArrays.jl/issues/46
gif(frames::AbstractMappedArray; kwargs...) = gif(collect(frames); kwargs...)