text:
"""
    emptybatch(model::T) where {T<:Model}

Create an empty batched input array on the GPU if available.
"""
function emptybatch(model::T) where {T<:Model}
    modelInputSize = getModelInputSize(model)
    gpu(zeros(Float32, modelInputSize...))
end

"""
    flipdict(dict::Dict)

Flip the key=>value pair for each entry in a dict.
"""
flipdict(dict::Dict) = Dict(map(x->(dict[x],x),collect(keys(dict))))

"""
    createcountdict(dict::Dict)

Create a dict copy of namesdict, for counting the occurances of each named object.
"""
createcountdict(dict::Dict) = Dict(map(x->(x,0),collect(keys(dict))))

"""
    drawBoxes(img::Array, model::YOLO.yolo, padding::Array, results)
    drawBoxes!(img::Array, model::YOLO.yolo, padding::Array, results)

Draw boxes on image for each BBOX result.
"""
drawBoxes(img::Array, model::YOLO.yolo, padding::Array, results; transpose=true) = drawBoxes!(copy(img), model, padding, results, transpose=transpose)
function drawBoxes!(img::Array, model::YOLO.yolo, padding::Array, results; transpose=true)
    imgratio = size(img,2) / size(img,1)
    if transpose
        modelratio = model.cfg[:width] / model.cfg[:height]
        x1i, y1i, x2i, y2i = [1, 2, 3, 4]
    else
        modelratio = model.cfg[:height] / model.cfg[:width]
        x1i, y1i, x2i, y2i = [2, 1, 4, 3]
    end
    if modelratio > imgratio
        h, w = size(img,1) .* (1, modelratio)
    else
        h, w = size(img,2) ./ (modelratio, 1)
    end

    # p1 = Point(1, 1)
    # p2 = Point(round(Int, w-((padding[x1i]+padding[x2i])*w)), round(Int, h-((padding[y1i]+padding[y2i])*h)))
    # draw!(img, LineSegment(p1, p2), zero(eltype(img)))
    length(results) == 0 && return img
    for i in 1:size(results,2)
        bbox = results[1:4, i] .- padding
        class = results[end-1, i]
        conf = results[5,i]
        p = Point(round(Int, bbox[x1i]*w)+1, round(Int, bbox[y1i]*h)+1)
        q = Point(round(Int, bbox[x2i]*w), round(Int, bbox[y1i]*h)+1)
        r = Point(round(Int, bbox[x1i]*w)+1, round(Int, bbox[y2i]*h))
        s = Point(round(Int, bbox[x2i]*w), round(Int, bbox[y2i]*h))
        pol = Polygon([p,q,s,r])
        draw!(img, pol, zero(eltype(img)))
    end
    return img
end

"""
    benchmark(;select = [1,3,4,5,6], reverseAfter:Bool=false)

Convenient benchmarking
"""
function benchmark(;select = [1,3,4,5,6], reverseAfter::Bool = false)
    pretrained_list = [
                        YOLO.v2_tiny_416_COCO,
                        YOLO.v2_608_COCO,
                        YOLO.v3_tiny_416_COCO,
                        YOLO.v3_320_COCO,
                        YOLO.v3_416_COCO,
                        YOLO.v3_608_COCO,
                        YOLO.v3_spp_608_COCO
                        ][select]
    reverseAfter && (pretrained_list = vcat(pretrained_list, reverse(pretrained_list)))
    IMG = rand(RGB,416,416)

    header = ["Model" "loaded?" "load time (s)" "ran?" "run time (s)" "run time (fps)"]
    table = Array{Any}(undef, length(pretrained_list), 6)
    for (i, pretrained) in pairs(pretrained_list)
        modelname = string(pretrained)
        @info "Loading and running $modelname"
        table[i,:] = [modelname false "-" "-" "-" "-"]

        t_load = @elapsed begin
            mod = pretrained(silent=true)
        end
        table[i, 2] = true
        table[i, 3] = round(t_load, digits=3)

        batch = emptybatch(mod)
        batch[:,:,:,1] .= gpu(resizePadImage(IMG, mod))

        res = mod(batch) #run once
        t_run = @belapsed $mod($batch);
        table[i, 4] = true
        table[i, 5] = round(t_run, digits=4)
        table[i, 6] = round(1/t_run, digits=1)

        mod = nothing
        batch = nothing
        GC.gc()
    end
    pretty_table(table, header)
end


text:
using PyPlot, Glob, Printf

include("ConvergenceTests/ConvergenceTests.jl")

using .ConvergenceTests
using .ConvergenceTests.ForcedFlowFixedSlip: u

defaultcolors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
removespine(side) = gca().spines[side].set_visible(false)
removespines(sides...) = [removespine(side) for side in sides]

xy_filenames = glob("forced_fixed_slip_xy*", joinpath(@__DIR__, "data"))
xz_filenames = glob("forced_fixed_slip_xz*", joinpath(@__DIR__, "data"))

labels = ["(x, y)", "(x, z)"]

filenameses = [ xy_filenames,
                xz_filenames ]

errorses = [ ConvergenceTests.compute_errors((x, y, z, t) -> u(x, y, t), xy_filenames...),
             ConvergenceTests.compute_errors((x, y, z, t) -> u(x, z, t), xz_filenames...) ]

sizeses = [ ConvergenceTests.extract_sizes(xy_filenames...),
            ConvergenceTests.extract_sizes(xz_filenames...) ]

close("all")
fig, ax = subplots()

for j = 1:2

    filenames = filenameses[j]
    errors = errorses[j]
    sizes = sizeses[j]

    Nx = map(sz -> sz[1], sizes)

    L₁ = map(err -> err.L₁, errors)
    L∞ = map(err -> err.L∞, errors)

    @show size(L₁) size(Nx)

    ax.loglog(Nx, L₁, basex=2, color=defaultcolors[j], alpha=0.6, mfc="None",
            linestyle="None", marker="o", label="\$L_1\$-norm, $(labels[j])")

    ax.loglog(Nx, L∞, basex=2, color=defaultcolors[j], alpha=0.6, mfc="None",
            linestyle="None", marker="^", label="\$L_\\infty\$-norm, $(labels[j])")

    if j == 2
        L₁ = map(err -> err.L₁, errors)
        ii = sortperm(Nx)
        Nx = Nx[ii]
        L₁ = L₁[ii]
        ax.loglog(Nx, L₁[end] * (Nx[end] ./ Nx).^2, "k-", basex=2, linewidth=1, alpha=0.6, label=L"\sim N_x^{-2}")
    end
end

filenames = filenameses[1]
errors = errorses[1]
sizes = sizeses[1]
Nx = map(sz -> sz[1], sizes)

legend()

xlabel(L"N_x")
ylabel("Norms of the absolute error, \$ | u_{\\mathrm{sim}} - u_{\\mathrm{exact}} | \$")

removespines("top", "right")
title("Convergence for forced fixed slip")

filepath = joinpath(@__DIR__, "figs", "forced_fixed_slip_convergence.png")
savefig(filepath, dpi=480)

for (label, error) in zip(labels, errorses)
    L₁ = map(e -> e.L₁, error)
    L∞ = map(e -> e.L∞, error)
    name = "Forced flow fixed slip " * label
    test_rate_of_convergence(L₁, Nx, expected=-2.0, atol=0.05, name=name * " L₁")
    test_rate_of_convergence(L∞, Nx, expected=-2.0, atol=0.10, name=name * " L∞")
end

ext: jl


text:
using CSV
using DataFrames
using DelimitedFiles
using YAML
using SLiDE

DATA_DIR = joinpath("data", "input")
MAP_DIR = joinpath("data", "coremaps")

# Read DataFrames.
df_cbsa_map = read_file(joinpath(MAP_DIR, "scale", "census_cbsa.csv"));
df_cbsa_map[!,:state_code] .= df_cbsa_map[:,:state_desc]
df_cbsa_map = unique(df_cbsa_map[:,[:state_code,:cbsa_code,:csa_code,:cbsa_desc,:csa_desc]])

df_cfs_io = read_file(joinpath(DATA_DIR, "cfs.csv"));
df_gsp_io = dropmissing(read_file(joinpath(DATA_DIR, "gsp_metro.csv")));

# Isolate regional information.
df_cfs = copy(df_cfs_io)
df_gsp = copy(df_gsp_io)

df_cfs = sort(unique(DataFrame(
    state = [df_cfs[:,:orig_state]; df_cfs[:,:dest_state]],
    metro = [df_cfs[:,:orig_metro]; df_cfs[:,:dest_metro]])))
df_gsp = sort(unique(DataFrame(
    GSP_cbsa_code = df_gsp[:,:r],
    GSP_cbsa_desc = df_gsp[:,:r_desc])));

# # ******************************************************************************************
# QUESTION: Are any regions doubly represented?
df = sort(edit_with(copy(df_cfs), Drop(:metro, 99999, "==")));

# ******************************************************************************************
# QUESTIONS: Are there states with both CBSAs and CSAs represented?
df = copy(df_cfs)

x = [Map("scale/census_cbsa.csv", [:state_desc, :cbsa_code], [:cbsa_code, :cbsa_desc], [:state, :metro], [:cbsa_code,:cbsa_desc]),
     Map("scale/census_cbsa.csv", [:state_desc, :csa_code],  [:csa_code, :csa_desc],   [:state, :metro], [:csa_code,:csa_desc])]
df = edit_with(df, x)

df[!,:cbsa] .= .!ismissing.(df[:,:cbsa_desc]);
df[!,:csa]  .= .!ismissing.(df[:,:csa_desc]);

df_summary = innerjoin(by(df, :state, :csa => sum), by(df, :state, :cbsa => sum), on = :state);
df_summary[!,:both] .= sum.(eachrow(df_summary[:,[:cbsa_sum,:csa_sum]] .> 0))

if any(df_summary[:,:both] .> 1)
    println("Some states have both CSA and CBSA areas represented.")
end

# ******************************************************************************************
# QUESTION: What overlap
df = sort(edit_with(copy(df_cfs), Drop(:metro, 99999, "==")));

x = [Map("scale/census_cbsa.csv", [:state_desc, :cbsa_code], [:cbsa_code, :cbsa_desc, :csa_code, :csa_desc], [:state, :metro], [:cbsa_code, :cbsa_desc, :MAPPED_csa_code, :MAPPED_csa_desc]),
     Map("scale/census_cbsa.csv", [:state_desc, :csa_code],  [:csa_code, :csa_desc, :cbsa_code, :cbsa_desc], [:state, :metro], [:csa_code, :csa_desc, :MAPPED_cbsa_code, :MAPPED_cbsa_desc])]
df = edit_with(df, x)

# Reorder.
cols = [[:state, :metro]; propertynames(df)[occursin.(:code, propertynames(df))]; propertynames(df)[occursin.(:desc, propertynames(df))]]
df = df[:,cols]

# Overlap in CSA/CBSA codes?
intersect(df[:,:cbsa_code], df[:,:MAPPED_cbsa_code])
intersect(df[:,:csa_code], df[:,:MAPPED_csa_code])

# ******************************************************************************************
# CFS vs. GSP?
df[!,:cbsa_code_0] .= df[:,:cbsa_code]
df[!,:MAPPED_cbsa_code_0] .= df[:,:MAPPED_cbsa_code]

# df_gsp = leftjoin(df_gsp, unique(df[:,[:cbsa_code_0,:cbsa_code]]), on = Pair(:GSP_cbsa_code, :cbsa_code_0))
# df_gsp = leftjoin(df_gsp, unique(df[:,[:MAPPED_cbsa_code_0,:MAPPED_cbsa_code]]), on = Pair(:GSP_cbsa_code, :MAPPED_cbsa_code_0))

# QUESTIONS: Are there states with both CBSAs and CSAs represented?
# df = copy(df_cfs)

# # df_metro = read_file("../data/output/cfs_metro.csv");

# # df = sort(unique(DataFrame(ma = [df_metro[:,:orig_ma]; df_metro[:,:dest_ma]])));
# # joincols0 = [:cbsa_code, :csa_code];

# # df_temp = dropmissing(unique(df_cbsa_map[:,joincols0]));

# # for key in allkeys
# #     df_temp = unique(df_cbsa_map[:,joincols0]);
# #     df_temp[!,:ma] .= df_temp[:,Symbol(key,:_code)]

# #     joincols = Symbol.(uppercase(key),:_,joincols0);

# #     df_temp = edit_with(df_temp, Rename.(joincols0, joincols));

# #     global df = leftjoin(df, df_temp, on = :ma)
# # end

# # # df_metro_area = sort(unique(df_metro[:,[:orig_ma,:dest_ma]]));
# # # df_metro_area = edit_with(df_metro_area, Drop.([:orig_ma,:dest_ma], 0, "=="));
# # # first(df_metro_area,3)

# # # allflow = [:orig, :dest]
# # # allkeys = [:cbsa, :csa]
# # # for flow in allflow
# # #     for key in allkeys
# # #         joincols = [Symbol.(key, :_, [:code, :desc])];
# # #         df_temp = dropmissing(unique(df_cbsa_map[:,joincols]));

# # #         joincols = Symbol.(flow, :_, joincols)
# # #         df_temp = edit_with(df_temp, Rename.(propertynames(df_temp), joincols));

# # #         global df_metro_area = leftjoin(df_metro_area, df_temp,
# # #             on = Pair.(Symbol(flow, :_ma), joincols[1]));
# # #     end
# # # end

# # # df_metro_area[!,:found_csa] .= sum.(eachrow(ismissing.(df_metro_area[:,occursin.(:csa, propertynames(df_metro_area))])))
# # # df_metro_area[!,:found_cbsa] .= sum.(eachrow(ismissing.(df_metro_area[:,occursin.(:cbsa, propertynames(df_metro_area))])))

max_stars_repo_name: matbesancon/SLiDE

max_issues_repo_name: matbesancon/SLiDE

max_forks_repo_name: matbesancon/SLiDE


text:
# Workaround for https://github.com/JuliaLang/julia/issues/27429
# This fix only works on a single machine
function include_everywhere(filepath)
    fullpath = joinpath(@__DIR__, filepath)
    @sync for p in procs()
        @async remotecall_wait(include, p, fullpath)
    end
end


text:
import XLSX
using Base.Test, Missings

ef_blank_ptbr_1904 = XLSX.read("blank_ptbr_1904.xlsx")
ef_Book1 = XLSX.read("Book1.xlsx")
ef_Book_1904 = XLSX.read("Book_1904.xlsx")
ef_book_1904_ptbr = XLSX.read("book_1904_ptbr.xlsx")
ef_book_sparse = XLSX.read("book_sparse.xlsx")
ef_book_sparse_2 = XLSX.read("book_sparse_2.xlsx")

@test ef_Book1.filepath == "Book1.xlsx"
@test length(keys(ef_Book1.data)) > 0

@test ef_Book_1904.filepath == "Book_1904.xlsx"
@test length(keys(ef_Book_1904.data)) > 0

@test !XLSX.isdate1904(ef_Book1)
@test XLSX.isdate1904(ef_Book_1904)
@test XLSX.isdate1904(ef_blank_ptbr_1904)
@test XLSX.isdate1904(ef_book_1904_ptbr)

@test XLSX.sheetnames(ef_Book1) == [ "Sheet1", "Sheet2" ]
@test XLSX.sheetcount(ef_Book1) == 2
@test ef_Book1["Sheet1"].name == "Sheet1"
@test ef_Book1[1].name == "Sheet1"

@test XLSX.sst_unformatted_string(ef_Book1.workbook, 0) == "B2" # index is 0-based
@test XLSX.sst_unformatted_string(ef_Book1, 0) == "B2"
@test XLSX.sst_unformatted_string(ef_Book1, "0") == "B2"

@test_throws ErrorException XLSX.get_relationship_target_by_id(ef_Book1.workbook, "indalid_id")
@test_throws ErrorException XLSX.get_relationship_target_by_type(ef_Book1.workbook, "indalid_type")
@test !XLSX.has_relationship_by_type(ef_Book1.workbook, "invalid_type")

# Cell names
@test !XLSX.is_valid_cellname("A0")
@test XLSX.is_valid_cellname("A1")
@test XLSX.is_valid_cellname("XFD1048576")
@test !XLSX.is_valid_cellname("XFD1048577")
@test XLSX.is_valid_cellname("XFD1")
@test !XLSX.is_valid_cellname("ZFD1")
@test XLSX.is_valid_column_name("A")
@test XLSX.is_valid_column_name("AZ")
@test XLSX.is_valid_column_name("AAZ")
@test !XLSX.is_valid_column_name("AAAZ")
@test !XLSX.is_valid_column_name(":")
@test !XLSX.is_valid_column_name("É")

cn = XLSX.CellRef("A1")
@test string(cn) == "A1"
@test XLSX.column_name(cn) == "A"
@test XLSX.row_number(cn) == 1
@test XLSX.column_number(cn) == 1

cn = XLSX.CellRef("AB1")
@test string(cn) == "AB1"
@test XLSX.column_name(cn) == "AB"
@test XLSX.row_number(cn) == 1
@test XLSX.column_number(cn) == 28

cn = XLSX.CellRef("AMI1")
@test string(cn) == "AMI1"
@test XLSX.column_name(cn) == "AMI"
@test XLSX.row_number(cn) == 1
@test XLSX.column_number(cn) == 1023

cn = XLSX.CellRef("XFD1048576")
@test string(cn) == "XFD1048576"
@test XLSX.column_name(cn) == "XFD"
@test XLSX.row_number(cn) == 1048576
@test XLSX.column_number(cn) == 16384

@test XLSX.encode_column_number(1) == "A"
@test XLSX.encode_column_number(28) == "AB"
@test XLSX.encode_column_number(1023) == "AMI"
@test XLSX.encode_column_number(16384) == "XFD"

@test XLSX.CellRef(12, 2).name == "B12"

cr = XLSX.range"A1:C4"
@test string(cr) == "A1:C4"
@test XLSX.row_number(cr.start) == 1
@test XLSX.column_number(cr.start) == 1
@test XLSX.row_number(cr.stop) == 4
@test XLSX.column_number(cr.stop) == 3
@test size(cr) == (4, 3)

cr = XLSX.range"B2:C8"
@test XLSX.ref"B2" ∈ cr
@test XLSX.ref"B3" ∈ cr
@test XLSX.ref"C2" ∈ cr
@test XLSX.ref"C3" ∈ cr
@test XLSX.ref"C8" ∈ cr
@test XLSX.ref"A1" ∉ cr
@test XLSX.ref"C9" ∉ cr
@test XLSX.ref"D4" ∉ cr
@test size(cr) == (7, 2)

fullrng = XLSX.range"B2:E5"
@test fullrng ⊆ fullrng
@test XLSX.range"B3:D4" ⊆ fullrng
@test !issubset(XLSX.range"A1:E5", fullrng)

@test XLSX.is_valid_cellrange("B2:C8")
@test !XLSX.is_valid_cellrange("A:B")
@test_throws AssertionError XLSX.CellRange("Z10:A1")
@test_throws AssertionError XLSX.CellRange("Z1:A1")

# hashing and equality
@test XLSX.CellRef("AMI1") == XLSX.CellRef("AMI1")
@test hash(XLSX.CellRef("AMI1")) == hash(XLSX.CellRef("AMI1"))
@test XLSX.CellRange("A1:C4") == XLSX.CellRange("A1:C4")
@test hash(XLSX.CellRange("A1:C4")) == hash(XLSX.CellRange("A1:C4"))

# worksheet for book1
@test XLSX.dimension(ef_Book1["Sheet1"]) == XLSX.range"B2:C8"
@test XLSX.isdate1904(ef_Book1["Sheet1"]) == false

# relative cell position
rng = XLSX.range"B2:D4"
@test XLSX.relative_cell_position(XLSX.ref"C3", rng) == (2, 2)
@test XLSX.relative_cell_position(XLSX.ref"B2", rng) == (1, 1)
@test XLSX.relative_cell_position(XLSX.ref"C4", rng) == (3, 2)
@test XLSX.relative_cell_position(XLSX.ref"D4", rng) == (3, 3)

# getindex
f = XLSX.read("Book1.xlsx")
sheet1 = f["Sheet1"]
@test sheet1["B2"] == "B2"
@test isapprox(sheet1["C3"], 21.2)
@test sheet1["B5"] == Date(2018, 3, 21)
@test sheet1["B8"] == "palavra1"

XLSX.getcell(sheet1, "B2")
XLSX.getcellrange(sheet1, "B2:C3")

sheet2 = f[2]
sheet2_data = [ 1 2 3 ; 4 5 6 ; 7 8 9 ]
@test sheet2_data == sheet2["A1:C3"]
@test sheet2_data == sheet2[:]
@test sheet2[:] == XLSX.getdata(sheet2)

# Time and DateTime
@test XLSX._time(0.82291666666666663) == Dates.Time(Dates.Hour(19), Dates.Minute(45))
@test XLSX._datetime(43206.805447106482, false) == Date(2018, 4, 16) + Dates.Time(Dates.Hour(19), Dates.Minute(19), Dates.Second(51))

# General number formats
f = XLSX.read("general.xlsx")
sheet = f["general"]
@test sheet["A1"] == "text"
@test sheet["B1"] == "regular text"
@test sheet["A2"] == "integer"
@test sheet["B2"] == 102
@test sheet["A3"] == "float"
@test isapprox(sheet["B3"], 102.2)
@test sheet["A4"] == "date"
@test sheet["B4"] == Date(1983, 4, 16)
@test sheet["A5"] == "hour"
@test sheet["B5"] == Dates.Time(Dates.Hour(19), Dates.Minute(45))
@test sheet["A6"] == "datetime"
@test sheet["B6"] == Date(2018, 4, 16) + Dates.Time(Dates.Hour(19), Dates.Minute(19), Dates.Second(51))

# Book1.xlsx
f = XLSX.read("Book1.xlsx")
sheet = f["Sheet1"]
@test ismissing(sheet["A1"])
@test sheet["B2"] == "B2"
@test sheet["C2"] == "C2"
@test isapprox(sheet["B3"], 10.5)
@test isapprox(sheet["C3"], 21.2)
@test sheet["B4"] == Date(2018, 3, 21)
@test sheet["C4"] == Date(2018, 3, 22)
@test sheet["B5"] == Date(2018, 3, 21)
@test sheet["C5"] == Date(2018, 3, 22)
@test sheet["B6"] == true
@test sheet["C6"] == false
@test sheet["B7"] == 1
@test sheet["C7"] == 2
@test sheet["B8"] == "palavra1"
@test sheet["C8"] == "palavra2"

# book_1904_ptbr.xlsx
f = XLSX.read("book_1904_ptbr.xlsx")

@test f["Plan1"][:] == Any[ "Coluna A" "Coluna B" "Coluna C" "Coluna D";
                            10 10.5 Date(2018, 3, 22) "linha 2";
                            20 20.5 Date(2017, 12, 31) "linha 3";
                            30 30.5 Date(2018, 1, 1) "linha 4" ]

@test f["Plan2"]["A1"] == "Merge de A1:D1"
@test ismissing(f["Plan2"]["B1"])
@test f["Plan2"]["C2"] == "C2"
@test f["Plan2"]["D3"] == "D3"

# numbers.xlsx
f = XLSX.read("numbers.xlsx")
floats = f["float"][:]
for n in floats
    if !ismissing(n)
        @test isa(n, Float64)
    end
end

ints = f["int"][:]
for n in ints
    if !ismissing(n)
        @test isa(n, Int)
    end
end

error_sheet = f["error"]
@test error_sheet["A1"] == "errors"
@test !XLSX.iserror(XLSX.getcell(error_sheet, "A1"))
@test XLSX.iserror(XLSX.getcell(error_sheet, "A2"))
@test ismissing(error_sheet["A2"])
@test ismissing(error_sheet["A3"])
@test ismissing(error_sheet["A4"])
emptycell = XLSX.getcell(error_sheet, "B1")
@test !XLSX.iserror(emptycell)
@test ismissing(XLSX.getdata(error_sheet, emptycell))
@test XLSX.row_number(emptycell) == 1
@test XLSX.column_number(emptycell) == 2

# Column Range

cr = XLSX.ColumnRange("B:D")
@test cr.start == 2
@test cr.stop == 4
@test length(cr) == 3
@test_throws AssertionError XLSX.ColumnRange("B1:D3")
@test_throws AssertionError XLSX.ColumnRange("D:A")
@test collect(cr) == [ "B", "C", "D" ]

# CellRange iterator
rng = XLSX.CellRange("A2:C4")
@test collect(rng) == [ XLSX.CellRef("A2"), XLSX.CellRef("B2"), XLSX.CellRef("C2"), XLSX.CellRef("A3"), XLSX.CellRef("B3"), XLSX.CellRef("C3"), XLSX.CellRef("A4"), XLSX.CellRef("B4"), XLSX.CellRef("C4") ]

# Table

f = XLSX.read("book_sparse.xlsx")
s = f["Sheet1"]

report = Vector{String}()
for r in XLSX.eachrow(s)
    if !isempty(r)
        push!(report, string(XLSX.row_number(r), " - ", XLSX.column_bounds(r)))

        if XLSX.row_number(r) == 2
            @test XLSX.last_column_index(r, 2) == 2
        elseif XLSX.row_number(r) == 3
            @test XLSX.last_column_index(r, 3) == 4
        elseif XLSX.row_number(r) == 6
            @test XLSX.last_column_index(r, 1) == 4
            @test XLSX.last_column_index(r, 2) == 4
            @test XLSX.last_column_index(r, 3) == 4
            @test XLSX.last_column_index(r, 4) == 4
            @test_throws AssertionError XLSX.last_column_index(r, 5)
        elseif XLSX.row_number(r) == 9
            @test XLSX.last_column_index(r, 2) == 3
            @test XLSX.last_column_index(r, 3) == 3
            @test XLSX.last_column_index(r, 5) == 5
        end
    end
end
@test report == [ "2 - (2, 2)", "3 - (3, 4)", "6 - (1, 4)", "9 - (2, 5)"]

f = XLSX.read("general.xlsx")
s = f["table"]
data, col_names = XLSX.gettable(s)
@test col_names == [ Symbol("Column B"), Symbol("Column C"), Symbol("Column D"), Symbol("Column E"), Symbol("Column F"), Symbol("Column G")]

test_data = Vector{Any}(6)
test_data[1] = collect(1:8)
test_data[2] = [ "Str1", missing, "Str1", "Str1", "Str2", "Str2", "Str2", "Str2" ]
test_data[3] = [ Date(2018, 4, 21) + Dates.Day(i) for i in 0:7 ]
test_data[4] = [ missing, missing, missing, missing, missing, "a", "b", missing ]
test_data[5] = [ 0.2001132319, 0.2793987377, 0.0950591677, 0.0744023067, 0.8242278091, 0.6205883578, 0.9174151018, 0.6749604883 ]
test_data[6] = [ missing for i in 1:8 ]

function check_test_data(data::Vector{Any}, test_data::Vector{Any})

    @test length(data) == length(test_data)

    function size_of_data(d::Vector{Any})
        isempty(d) && return (0, 0)
        return length(d[1]), length(d)
    end

    rows, cols = size_of_data(test_data)

    for col in 1:cols
        @test length(data[col]) == length(test_data[col])
    end

    for row in 1:rows, col in 1:cols
        test_value = test_data[col][row]
        value = data[col][row]
        if ismissing(test_value)
            @test ismissing(value)
        else
            if isa(test_value, Float64)
                @test isapprox(value, test_value)
            else
                @test value == test_value
            end
        end
    end

    nothing
end

check_test_data(data, test_data)

@test XLSX.infer_eltype(data[1]) == Int
@test XLSX.infer_eltype(data[2]) == Union{Missing, String}
@test XLSX.infer_eltype(data[3]) == Date
@test XLSX.infer_eltype(data[4]) == Union{Missing, String}
@test XLSX.infer_eltype(data[5]) == Float64
@test XLSX.infer_eltype(data[6]) == Any
@test XLSX.infer_eltype([1, "1", 10.2]) == Any
@test XLSX.infer_eltype(Vector{Int}()) == Int

data_inferred, col_names = XLSX.gettable(s, infer_eltypes=true)
@test eltype(data_inferred[1]) == Int
@test eltype(data_inferred[2]) == Union{Missing, String}
@test eltype(data_inferred[3]) == Date
@test eltype(data_inferred[4]) == Union{Missing, String}
@test eltype(data_inferred[5]) == Float64
@test eltype(data_inferred[6]) == Any

s = f["table2"]
test_data = Vector{Any}(3)
test_data[1] = [ "A1", "A2", "A3", missing ]
test_data[2] = [ "B1", "B2", missing, "B4"]
test_data[3] = [ missing, missing, missing, missing ]

data, col_names = XLSX.gettable(s)

@test col_names == [:HA, :HB, :HC]
check_test_data(data, test_data)

for (ri, rowdata) in enumerate(XLSX.TableRowIterator(s))
    if ismissing(test_data[1][ri])
        @test ismissing(rowdata[:HA])
    else
        @test rowdata[:HA] == test_data[1][ri]
    end
end

override_col_names = [:ColumnA, :ColumnB, :ColumnC]
data, col_names = XLSX.gettable(s, column_labels=override_col_names)

@test col_names == override_col_names
check_test_data(data, test_data)

data, col_names = XLSX.gettable(s, "A:B", first_row=1)
test_data_AB_cols = Vector{Any}(2)
test_data_AB_cols[1] = test_data[1]
test_data_AB_cols[2] = test_data[2]
@test col_names == [:HA, :HB]
check_test_data(data, test_data_AB_cols)

data, col_names = XLSX.gettable(s, "A:B")
test_data_AB_cols = Vector{Any}(2)
test_data_AB_cols[1] = test_data[1]
test_data_AB_cols[2] = test_data[2]
@test col_names == [:HA, :HB]
check_test_data(data, test_data_AB_cols)

data, col_names = XLSX.gettable(s, "B:B", first_row=2)
@test col_names == [:B1]
@test length(data) == 1
@test length(data[1]) == 1
@test data[1][1] == "B2"

data, col_names = XLSX.gettable(s, "B:C")
@test col_names == [ :HB, :HC ]
test_data_BC_cols = Vector{Any}(2)
test_data_BC_cols[1] = ["B1", "B2"]
test_data_BC_cols[2] = [ missing, missing]
check_test_data(data, test_data_BC_cols)

data, col_names = XLSX.gettable(s, "B:C", first_row=2, header=false)
@test col_names == [ :B, :C ]
check_test_data(data, test_data_BC_cols)

s = f["table3"]
test_data = Vector{Any}(3)
test_data[1] = [ missing, missing, "B5" ]
test_data[2] = [ "C3", missing, missing ]
test_data[3] = [ missing, "D4", missing ]
data, col_names = XLSX.gettable(s)
@test col_names == [:H1, :H2, :H3]
check_test_data(data, test_data)
@test_throws ErrorException XLSX.find_row(XLSX.SheetRowIterator(s), 20)

s = f["table4"]
data, col_names = XLSX.gettable(s)
@test col_names == [:H1, :H2, :H3]
check_test_data(data, test_data)

empty_sheet = XLSX.getsheet("general.xlsx", "empty")
@test_throws ErrorException XLSX.gettable(empty_sheet)
itr = XLSX.SheetRowIterator(empty_sheet)
@test_throws ErrorException XLSX.find_row(itr, 1)

f = XLSX.read("general.xlsx")
tb5 = f["table5"]
test_data = Vector{Any}(1)
test_data[1] = [1, 2, 3, 4, 5]
data, col_names = XLSX.gettable(tb5)
@test col_names == [ :HEADER ]
check_test_data(data, test_data)
tb6 = f["table6"]
data, col_names = XLSX.gettable(tb6, first_row=3)
@test col_names == [ :HEADER ]
check_test_data(data, test_data)
tb7 = f["table7"]
data, col_names = XLSX.gettable(tb7, first_row=3)
@test col_names == [ :HEADER ]
check_test_data(data, test_data)

sheet_lookup = f["lookup"]
test_data = Vector{Any}(3)
test_data[1] = [ 10, 20, 30]
test_data[2] = [ "name1", "name2", "name3" ]
test_data[3] = [ 100, 200, 300 ]
data, col_names = XLSX.gettable(sheet_lookup)
@test col_names == [:ID, :NAME, :VALUE]
check_test_data(data, test_data)

header_error_sheet = f["header_error"]
@test_throws AssertionError XLSX.gettable(header_error_sheet)

#
# Helper functions
#

test_data = Vector{Any}(3)
test_data[1] = [ missing, missing, "B5" ]
test_data[2] = [ "C3", missing, missing ]
test_data[3] = [ missing, "D4", missing ]

data, col_names = XLSX.gettable("general.xlsx", "table4")
@test col_names == [:H1, :H2, :H3]
check_test_data(data, test_data)

@test XLSX.getdata("general.xlsx", "table4", "E12") == "H1"
test_data = Array{Any, 2}(2, 1)
test_data[1, 1] = "H2"
test_data[2, 1] = "C3"
@test XLSX.getdata("general.xlsx", "table4", "F12:F13") == test_data


text:
using Base.Test

@testset "batch_processing_helpers.jl" begin
    include("batch_processing_helpers/unit.jl")
    include("batch_processing_helpers/integration.jl")
end


text:
#!/usr/bin/env julia
using DocOpt
using Pope: LJH
doc = """
End Pope Watches Matter
This will write info to the matter sentinel file that will cause popewatches matter to end, if it it waiting on that file.

Usage:
  endpopewatchesmatter.jl
"""

arguments = docopt(doc, version=v"0.0.1")
LJH.write_sentinel_file("endpope",true)
println("End Pope Watches Matter wrote sentinel file: endpope, true")
sleep(1)
LJH.write_sentinel_file("endpope",false)
println("End Pope Watches Matter wrote sentinel file: endpope, false")


text:
include("image.jl")
include("image-utils.jl")
include("draw.jl")
include("sprite.jl")


text:
"""
Module controlling displaying of the text associated with the segmentation
- either text releted to all slices or just a single one currently displayed or both
"""
module DisplayWords
using FreeTypeAbstraction, ..ForDisplayStructs, ..DataStructs, ..ModernGlUtil , ModernGL, ColorTypes, ..PrepareWindowHelpers,   ..ShadersAndVerticies,  ..ShadersAndVerticiesForText

export getTextForCurrentSlice,textLinesFromStrings,renderSingleLineOfText,activateForTextDisp,bindAndActivateForText,reactivateMainObj, createTextureForWords,bindAndActivateForText, bindAndDisplayTexture

"""
First We need to bind fragment shader created to deal with text and supply the vertex shader with data for quad where this text needs to be displayed
    shader_program- reference to shader program
this function is intended to be invoked only once
    """
function bindAndActivateForText(shader_program_words::UInt32
                                ,fragment_shader_words::UInt32
                                ,vertex_shader::UInt32
                                ,vbo_words::Base.RefValue{UInt32}
                                ,calcDim::CalcDimsStruct)

    glLinkProgram(shader_program_words)
    glUseProgram(shader_program_words)

	glAttachShader(shader_program_words, fragment_shader_words)
	glAttachShader(shader_program_words, vertex_shader)

    glBindBuffer(GL_ARRAY_BUFFER, vbo_words[])
    glBufferData(GL_ARRAY_BUFFER,calcDim.mainQuadVertSize , calcDim.mainImageQuadVert, GL_STATIC_DRAW)

	encodeDataFromDataBuffer()
end #bindAndActivateForText

"""
In order to be able to display texture with text we need to activate main shader program and vbo
    shader_program- reference to shader program
    fragment_shader_words - reference to shader associated with text displaying
    calcDim - holds necessery constants holding for example window dimensions, texture sizes etc.
"""
function activateForTextDisp(shader_program_words::UInt32
                            ,vbo_words::Base.RefValue{UInt32}
                            ,calcDim::CalcDimsStruct)
   glUseProgram(shader_program_words)
    glBindBuffer(GL_ARRAY_BUFFER, vbo_words[])
    glBufferData(GL_ARRAY_BUFFER, calcDim.wordsQuadVertSize  ,calcDim.wordsImageQuadVert , GL_STATIC_DRAW)

	encodeDataFromDataBuffer()

end#activateForTextDisp

"""
Given  single SimpleLineTextStruct it will return matrix of data that will be used  by addTextToTexture function
    to display text
    texLine - source od data
    textureWidth - available width for a line
    fontFace - font we use
"""
function renderSingleLineOfText(texLine::SimpleLineTextStruct
                                ,textureWidth::Int32
                                ,fontFace::FTFont)
   return  renderstring!(zeros(UInt8,textureWidth,textureWidth) #::Matrix{UInt8}
                ,texLine.text
                ,fontFace
                ,texLine.fontSize
                ,texLine.fontSize
                ,texLine.fontSize
                ,valign = :vtop
                ,halign = :hleft) |>
    (matr)-> matr[1: Int(round(texLine.fontSize*2*texLine.extraLineSpace)),: ] |>
    (smallerMatr)->collect(transpose(reverse(smallerMatr; dims=(1)))) # getting proper text alignment

end #renderSingleLineOfText

"""
utility function that enables creating list of  text line structs from list of strings
"""
function  textLinesFromStrings(strs::Vector{String} ) ::Vector{SimpleLineTextStruct}
    return map(x->  SimpleLineTextStruct(text=x,fontSize= 120,extraLineSpace=1  ) ,strs)
end

"""
Finally in order to enable later proper display of the images we need to reactivate main quad and shaders
shader_program- reference to shader program
fragment_shader_main- reference to shader associated with main images
"""
function reactivateMainObj(shader_program::UInt32
                            ,vbo_main::UInt32
                            ,calcDim::CalcDimsStruct)

    glUseProgram(shader_program)
    glBindBuffer(GL_ARRAY_BUFFER, vbo_main[])
    glBufferData(GL_ARRAY_BUFFER,calcDim.mainQuadVertSize, calcDim.mainImageQuadVert, GL_STATIC_DRAW)
    encodeDataFromDataBuffer()

end #reactivateMainObj

"""
Creates and initialize texture that will be used for displaying text
   !!!! important we need to first bind shader program for text display before we will  invoke this function
    numberOfActiveTextUnits - number of textures already used - so we we will know what is still free
    widthh, heightt - size of the texture - the bigger the higher resolution, but higher computation cost
    actTextrureNumb -proper OpenGL active texture
    return fully initialized texture; also it assigne texture to appropriate sampler
    """
function createTextureForWords(numberOfActiveTextUnits::Int
                                ,widthh::Int32 =Int32(100)
                                ,heightt::Int32=Int32(1000)
                                ,actTextrureNumb::UInt32=UInt32(0) )::TextureSpec
    return  TextureSpec{UInt8}(
            name = "textText"
            ,color = RGB(0.0,0.0,1.0)
            #,ID=texId
            ,actTextrureNumb =actTextrureNumb
            ,OpGlType =GL_UNSIGNED_BYTE
        )
end#createTextureForWords

"""
we need to check wether scrolling dat contains some text that can be used for this particular slice display if not we will return only mainTextToDisp
"""
function getTextForCurrentSlice(scrollDat::FullScrollableDat, sliceNumb::Int32)::Vector{SimpleLineTextStruct}
    if( length(scrollDat.sliceTextToDisp)>=sliceNumb  )
        return  copy(vcat(scrollDat.mainTextToDisp ,  scrollDat.sliceTextToDisp[sliceNumb] ))
    end#if
        return copy(scrollDat.mainTextToDisp )
end#getTextForCurrentSlice

end#DisplayWords


text:
using DiffEqFlux, OrdinaryDiffEq, Test # , Plots

p = [1.5 1.0;3.0 1.0]
function lotka_volterra(du,u,p,t)
  du[1] = p[1,1]*u[1] - p[1,2]*u[1]*u[2]
  du[2] = -p[2,1]*u[2] + p[2,2]*u[1]*u[2]
end

u0 = [1.0,1.0]
tspan = (0.0,10.0)

prob = ODEProblem(lotka_volterra,u0,tspan,p)
sol = solve(prob,Tsit5())

# plot(sol)

p = [2.2 1.0;2.0 0.4] # Tweaked Initial Parameter Array
ps = Flux.params(p)

function predict_adjoint() # Our 1-layer neural network
  Array(solve(prob,Tsit5(),p=p,saveat=0.0:0.1:10.0))
end

loss_adjoint() = sum(abs2,x-1 for x in predict_adjoint())

data = Iterators.repeated((), 100)
opt = ADAM(0.1)
cb = function () #callback function to observe training
  display(loss_adjoint())
end

predict_adjoint()

# Display the ODE with the initial parameter values.
cb()
Flux.train!(loss_adjoint, ps, data, opt, cb = cb)

@test loss_adjoint() < 1


text:
export getLinearInterMatrix

function getLinearInterMatrix(M::RegularMesh,x::AbstractArray{Float64})
	return getLinearInterMatrix(M.domain,M.n,x)
end

"""
function A = getLinearInterpolationMatrix(domain,m,x)

builds interpolation matrix A, s.t. A(x)*I(:) = linearInter(I,domain,x)

Input:
 domain - description of computational domain
 m     - discretization size of image
 x     - interpolation points

Output:
 A     - linear interpolation matrix

see also linearInter

"""
function getLinearInterMatrix(domain::Array{Float64},n::Array{Int},xin::AbstractArray{Float64})

	h   = vec(domain[2:2:end]-domain[1:2:end])./vec(n)
	dim = round(Int,length(domain)/2)
	np  = round(Int,length(xin)/dim)
	x   = reshape(copy(xin),np,dim)

	# Convert x and y to the coordinate system 1:m, 1:n
	for i=1:dim
	    x[:,i] = (x[:,i]-domain[2*i-1])/h[i] + .5
	end

	Valid(j) = (0.<x[:,j]) .& (x[:,j].<(n[j]+1))

	if dim==1
		valid = find( Valid(1) )
	elseif dim===2
		valid = find( Valid(1) .& Valid(2) )
	elseif dim==3
		valid = find( Valid(1) .& Valid(2) .& Valid(3) )
	end
	if isempty(valid)
	    return spzeros(prod(n),np)
	end;
	P     = round.(Int,floor.(x)); x = x-P;    # split x into integer/remainder
	p(j)  = P[valid,j]
	xi(j) = x[valid,j]

	if dim==1
		error("nyi")
    elseif dim==2
        A    = getMatrix2D(valid, p(1)  , p(2)  , np, n, (1-xi(1)).*(1-xi(2)));
        A   += getMatrix2D(valid, p(1)+1, p(2)  , np, n, (xi(1))  .*(1-xi(2)));
        A   += getMatrix2D(valid, p(1)  , p(2)+1, np, n, (1-xi(1)).*(xi(2)));
        A   += getMatrix2D(valid, p(1)+1, p(2)+1, np, n, (xi(1))  .*(xi(2)));

    elseif dim== 3
		error("nyi")
    end
return A
end

function getMatrix2D(I::Vector{Int},p1::Vector{Int},p2::Vector{Int},np::Int,n::Array{Int},weight::Vector{Float64})
    valid =  find((p1.>=1) .& (p1.<=n[1]) .& (p2.>=1) .& (p2.<=n[2]));
    return sparse(I[valid],p1[valid]+(p2[valid]-1)*n[1],weight[valid],np,prod(n));
 end


text:
function add_missings(X, ratio=0.1)
    result = Matrix{Union{Float64, Missing}}(X)

    for i in 1:floor(Int, length(X) * ratio)
        result[rand(1:length(X))] = missing
    end

    return result
end

function add_missings_single(X, ratio=0.1)
    result = Matrix{Union{Float64, Missing}}(X)

    randcols = 1:floor(Int, size(X, 2) * ratio)
    for col in randcols
        result[rand(1:size(X, 1)), col] = missing
    end

    return result
end

# A sequential RNG for consistent testing across julia versions
mutable struct SequentialRNG <: AbstractRNG
    idx::Int
end
SequentialRNG(; start_idx=1) = SequentialRNG(start_idx)

function Base.rand(srng::SequentialRNG, x::Vector)
    srng.idx = srng.idx < length(x) ? srng.idx + 1 : 1
    return x[srng.idx]
end

struct ImputorTester{I<:Imputor}
    imp::Type{I}
    f::Function
    f!::Function
    kwargs::NamedTuple
end

function ImputorTester(imp::Type{<:Imputor}; kwargs...)
    fname = lowercase(string(nameof(imp)))

    return ImputorTester(
        imp,
        getfield(Impute, Symbol(fname)),
        getfield(Impute, Symbol(fname * "!")),
        NamedTuple{keys(kwargs)}(values(kwargs)),
    )
end

function test_all(tester::ImputorTester)
    test_hashing(tester)
    test_equality(tester)
    test_vector(tester)
    test_matrix(tester)
    test_cube(tester)
    test_dataframe(tester)
    test_groupby(tester)
    test_axisarray(tester)
    test_nameddimsarray(tester)
    test_keyedarray(tester)
    test_columntable(tester)
    test_rowtable(tester)
end

function test_hashing(tester::ImputorTester)
    @testset "Hashing" begin
        @test hash(tester.imp()) == hash(tester.imp())
    end
end

function test_equality(tester::ImputorTester)
    @testset "Equality" begin
        @test tester.imp() == tester.imp()
    end
end

function test_limited(tester::ImputorTester)
    @testset "Limited" begin
        a = allowmissing(1.0:1.0:20.0)
        a[[2, 3, 7, 11:15...]] .= missing

        all_imputed = impute(a, tester.imp(; tester.kwargs...))

        @testset "Limit equals missings" begin
            result = impute(a, tester.imp(; limit=5, tester.kwargs...))

            @test count(ismissing, result) < count(ismissing, a)

            @test isequal(result, all_imputed)
            @test isequal(result, tester.f(a; limit=5, tester.kwargs...))
        end

        @testset "Limit less than missings" begin
            result = impute(a, tester.imp(; limit=2, tester.kwargs...))

            @test count(ismissing, result) < count(ismissing, a)
            @test count(ismissing, all_imputed) < count(ismissing, result)

            @test isequal(result, tester.f(a; limit=2, tester.kwargs...))
        end

        @testset "In-place" begin
            # Test that the in-place function return the new results and logs whether it
            # successfully did it in-place
            a2 = deepcopy(a)
            result = tester.f(a2; limit=1, tester.kwargs...)

            a2_ = tester.f!(a2; limit=1, tester.kwargs...)
            @test isequal(a2_, result)
            if !isequal(a2, result)
                @warn "$(tester.f!) did not mutate input data when limited"
            end
        end
    end
end

function test_vector(tester::ImputorTester)
    @testset "Vector" begin
        if tester.imp != DropVars
            a = allowmissing(1.0:1.0:20.0)
            a[[2, 3, 7]] .= missing

            result = impute(a, tester.imp(; tester.kwargs...))

            @testset "Base" begin
                # Test that we have fewer missing values
                @test count(ismissing, result) < count(ismissing, a)
                @test isa(result, Vector)
                @test eltype(result) <: eltype(a)

                # Test that functional form behaves the same way
                @test result == tester.f(a; tester.kwargs...)
            end

            @testset "In-place" begin
                # Test that the in-place function return the new results and logs whether it
                # successfully did it in-place
                a2 = deepcopy(a)
                a2_ = tester.f!(a2; tester.kwargs...)
                @test a2_ == result
                if a2 != result
                    @warn "$(tester.f!) did not mutate input data of type Vector"
                end
            end

            @testset "No missing" begin
                # Test having no missing data
                b = allowmissing(1.0:1.0:20.0)
                @test impute(b, tester.imp(; tester.kwargs...)) == b
            end

            @testset "All missing" begin
                # Test having only missing data
                c = fill(missing, 10)
                @test isequal(impute(c, tester.imp(; tester.kwargs...)), c)
            end
        end
    end
end

function test_matrix(tester::ImputorTester)
    @testset "Matrix" begin
        a = allowmissing(1.0:1.0:20.0)
        a[[2, 3, 7]] .= missing
        m = collect(reshape(a, 5, 4))

        result = impute(m, tester.imp(; tester.kwargs...); dims=:cols)

        @testset "Base" begin
            # Test that we have fewer missing values
            @test count(ismissing, result) < count(ismissing, m)
            @test isa(result, Matrix)
            @test eltype(result) <: eltype(m)

            # Test that functional form behaves the same way
            @test result == tester.f(m; dims=:cols, tester.kwargs...)
        end

        @testset "In-place" begin
            # Test that the in-place function return the new results and logs whether it
            # successfully did it in-place
            m2 = deepcopy(m)
            m2_ = tester.f!(m2; dims=:cols, tester.kwargs...)
            @test m2_ == result
            if m2 != result
                @warn "$(tester.f!) did not mutate input data of type Matrix"
            end
        end

        @testset "Transpose" begin
            m_ = collect(m')
            result_ = collect(result')
            @test isequal(tester.f(m_; dims=:rows, tester.kwargs...), result_)

            if tester.imp != SRS
                @test isequal(tester.f!(m_; dims=:rows, tester.kwargs...), result_)
            end
        end

        @testset "No missing" begin
            # Test having no missing data
            b = collect(reshape(allowmissing(1.0:1.0:20.0), 5, 4))
            @test impute(b, tester.imp(; tester.kwargs...); dims=:cols) == b
        end

        @testset "All missing" begin
            # Test having only missing data
            c = missings(5, 2)
            @test isequal(impute(c, tester.imp(; tester.kwargs...); dims=:cols), c)
            c_ = impute!(deepcopy(c), tester.imp(; tester.kwargs...); dims=:cols)
            @test isequal(c_, c)
        end
    end
end

function test_cube(tester::ImputorTester)
    @testset "Cube" begin
        a = allowmissing(1.0:1.0:60.0)
        a[[2, 7, 18, 23, 34, 41, 55, 59, 60]] .= missing
        C = collect(reshape(a, 5, 4, 3))

        result = impute(C, tester.imp(; tester.kwargs...); dims=3)

        @testset "Base" begin
            # Test that we have fewer missing values
            @test count(ismissing, result) < count(ismissing, C)
            @test isa(result, Array{Union{Float64, Missing}, 3})
            @test eltype(result) <: eltype(C)

            # Test that functional form behaves the same way
            @test result == tester.f(C; dims=3, tester.kwargs...)
        end

        @testset "In-place" begin
            # Test that the in-place function return the new results and logs whether it
            # successfully did it in-place
            C2 = deepcopy(C)
            C2_ = tester.f!(C2; dims=3, tester.kwargs...)
            @test C2_ == result
            if C2 != result
                @warn "$(tester.f!) did not mutate input data of type Matrix"
            end
        end

        @testset "No missing" begin
            # Test having no missing data
            B = collect(reshape(allowmissing(1.0:1.0:60.0), 5, 4, 3))
            @test impute(B, tester.imp(; tester.kwargs...); dims=3) == B
        end

        @testset "All missing" begin
            # Test having only missing data
            M = missings(5, 4, 3)
            @test isequal(impute(M, tester.imp(; tester.kwargs...); dims=3), M)
            M_ = impute!(deepcopy(M), tester.imp(; tester.kwargs...); dims=3)
            @test isequal(M_, M)
        end
    end
end

function test_dataframe(tester::ImputorTester)
    @testset "DataFrame" begin
        table = DataFrame(
            :sin => allowmissing(sin.(1.0:1.0:20.0)),
            :cos => allowmissing(sin.(1.0:1.0:20.0)),
        )

        table.sin[[2, 3, 7, 12, 19]] .= missing

        result = impute(table, tester.imp(; tester.kwargs...))

        @testset "Base" begin
            # Test that we have fewer missing values
            @test count(ismissing, Matrix(result)) < count(ismissing, Matrix(table))
            @test isa(result, DataFrame)

            # Test that functional form behaves the same way
            @test result == tester.f(table; tester.kwargs...)
        end

        @testset "In-place" begin
            # Test that the in-place function return the new results and logs whether it
            # successfully did it in-place
            table2 = deepcopy(table)
            table2_ = tester.f!(table2; tester.kwargs...)
            @test table2_ == result
            if table2 != result
                @warn "$(tester.f!) did not mutate input data of type DataFrame"
            end
        end

        @testset "No missing" begin
            # Test having no missing data
            b = DataFrame(
                :sin => allowmissing(sin.(1.0:1.0:20.0)),
                :cos => allowmissing(sin.(1.0:1.0:20.0)),
            )
            @test impute(b, tester.imp(; tester.kwargs...)) == b
        end

        @testset "All missing" begin
            # Test having only missing data
            c = DataFrame(
                :sin => fill(missing, 10),
                :cos => fill(missing, 10),
            )
            @test isequal(impute(c, tester.imp(; tester.kwargs...)), c)
        end
    end
end

function test_groupby(tester::ImputorTester)
    @testset "GroupBy" begin
        T = NamedTuple{(:hod, :obj, :val), Tuple{Int, Int, Union{Float64, Missing}}}

        rows = map(Iterators.product(1:24, 1:8, 0:19)) do t
            hod, obj, x = t
            # Deterministically return some `missing`s per hod/obj pair
            return if x in (0, 5, 12, 19)
                T((hod, obj, missing))
            else
                T((hod, obj, sin(hod) * cos(x) + obj))
            end
        end

        df = DataFrame(rows)

        # Deleting variables in a groupby doesn't really make sense
        if tester.imp != DropVars
            result = mapreduce(tester.f, vcat, groupby(df, [:hod, :obj]))
            @test !isequal(df, result)

            @test size(result) == size(df)

            # Test that we successfully imputed something.
            # We expect LOCF and NOCB to leave `missing`s at the start and end of each
            # group respectively.
            @test count(ismissing, Tables.matrix(result)) < count(ismissing, df.val)
            @test isequal(
                mapreduce(tester.f!, vcat, groupby(deepcopy(df), [:hod, :obj])),
                result
            )
        end
    end
end

function test_axisarray(tester::ImputorTester)
    @testset "AxisArray" begin
        a = allowmissing(1.0:1.0:20.0)
        a[[2, 3, 7]] .= missing
        m = collect(reshape(a, 5, 4))
        aa = AxisArray(
            deepcopy(m),
            Axis{:time}(DateTime(2017, 6, 5, 5):Hour(1):DateTime(2017, 6, 5, 9)),
            Axis{:id}(1:4)
        )
        result = impute(aa, tester.imp(; tester.kwargs...); dims=:cols)

        @testset "Base" begin
            # Test that we have fewer missing values
            @test count(ismissing, result) < count(ismissing, aa)
            @test isa(result, AxisArray)
            @test eltype(result) <: eltype(aa)

            # Test that functional form behaves the same way
            @test result == tester.f(aa; tester.kwargs..., dims=:cols)
        end

        @testset "In-place" begin
            # Test that the in-place function return the new results and logs whether it
            # successfully did it in-place
            aa2 = deepcopy(aa)
            aa2_ = tester.f!(aa2; tester.kwargs..., dims=:cols)
            @test aa2_ == result
            if aa2 != result
                @warn "$(tester.f!) did not mutate input data of type AxisArray"
            end
        end
    end
end

function test_nameddimsarray(tester::ImputorTester)
    @testset "NamedDimsArray" begin
        a = allowmissing(1.0:1.0:20.0)
        a[[2, 3, 7]] .= missing
        m = collect(reshape(a, 5, 4))
        nda = NamedDimsArray(deepcopy(m), (:time, :id))
        result = impute(nda, tester.imp(; tester.kwargs...); dims=:id)

        @testset "Base" begin
            # Test that we have fewer missing values
            @test count(ismissing, result) < count(ismissing, nda)
            @test isa(result, NamedDimsArray)
            @test eltype(result) <: eltype(nda)

            # Test that functional form behaves the same way
            @test result == tester.f(nda; tester.kwargs..., dims=:id)

            # Test using cols still works
            @test result == tester.f(nda; tester.kwargs..., dims=:cols)
        end

        @testset "In-place" begin
            # Test that the in-place function return the new results and logs whether it
            # successfully did it in-place
            nda2 = deepcopy(nda)
            nda2_ = tester.f!(nda2; tester.kwargs..., dims=:cols)
            @test nda2_ == result
            if nda2 != result
                @info "$(tester.f!) did not mutate input data of type NamedDimsArray"
            end
        end
    end
end

function test_keyedarray(tester::ImputorTester)
    @testset "KeyedArray" begin
        a = allowmissing(1.0:1.0:20.0)
        a[[2, 3, 7]] .= missing
        m = collect(reshape(a, 5, 4))
        ka = KeyedArray(
            deepcopy(m);
            time=DateTime(2017, 6, 5, 5):Hour(1):DateTime(2017, 6, 5, 9),
            id=1:4,
        )
        result = impute(ka, tester.imp(; tester.kwargs...); dims=:id)

        @testset "Base" begin
            # Test that we have fewer missing values
            @test count(ismissing, result) < count(ismissing, ka)
            @test isa(result, KeyedArray)
            @test eltype(result) <: eltype(ka)

            # Test that functional form behaves the same way
            @test result == tester.f(ka; tester.kwargs..., dims=:id)

            # Test using cols still works
            @test result == tester.f(ka; tester.kwargs..., dims=:cols)
        end

        @testset "In-place" begin
            # Test that the in-place function return the new results and logs whether it
            # successfully did it in-place
            ka2 = deepcopy(ka)
            ka2_ = tester.f!(ka2; tester.kwargs..., dims=:cols)
            @test ka2_ == result
            if ka2 != result
                @info "$(tester.f!) did not mutate input data of type KeyedArray"
            end
        end
    end
end

function test_columntable(tester::ImputorTester)
    @testset "Column Table" begin
        coltab = (
            sin = allowmissing(sin.(1.0:1.0:20.0)),
            cos = allowmissing(sin.(1.0:1.0:20.0)),
        )

        coltab.sin[[2, 3, 7, 12, 19]] .= missing

        result = impute(coltab, tester.imp(; tester.kwargs...))

        @testset "Base" begin
            # Test that we have fewer missing values
            @test count(ismissing, Tables.matrix(result)) < count(ismissing, Tables.matrix(coltab))
            @test isa(result, NamedTuple)

            # Test that functional form behaves the same way
            @test result == tester.f(coltab; tester.kwargs...)
        end

        @testset "In-place" begin
            # Test that the in-place function return the new results and logs whether it
            # successfully did it in-place
            coltab2 = deepcopy(coltab)
            coltab2_ = tester.f!(coltab2; tester.kwargs...)
            @test coltab2_ == result
            if coltab2 != result
                @warn "$(tester.f!) did not mutate input data of column table"
            end
        end
    end
end

function test_rowtable(tester::ImputorTester)
    @testset "Row Table" begin
        table = DataFrame(
            :sin => allowmissing(sin.(1.0:1.0:20.0)),
            :cos => allowmissing(sin.(1.0:1.0:20.0)),
        )

        table.sin[[2, 3, 7, 12, 19]] .= missing
        rowtab = Tables.rowtable(table)

        result = impute(rowtab, tester.imp(; tester.kwargs...))

        @testset "Base" begin
            # Test that we have fewer missing values
            @test count(ismissing, Tables.matrix(result)) < count(ismissing, Tables.matrix(rowtab))
            @test isa(result, Vector)

            # Test that functional form behaves the same way
            @test result == tester.f(rowtab; tester.kwargs...)
        end

        @testset "In-place" begin
            # Test that the in-place function returns the new results and logs whether it
            # successfully did it in-place
            rowtab2 = deepcopy(rowtab)
            rowtab2_ = tester.f!(rowtab2; tester.kwargs...)
            @test rowtab2_ == result
            if !isequal(rowtab2, result)
                @warn "$(tester.f!) did not mutate input data of row table"
            end
        end
    end
end


text:
using JLD2
using BenchmarkTools
using Benchmarks

# Benchmark parameters

decomposition = Slab()
# decomposition = Pencil()

ranks = Dict(
    Slab() => (1, 2, 4, 8, 16),
    Pencil() => (1, 4, 16)
)[decomposition]

grid_size(R, decomposition) = (4096, 4096)

rank_size(R, ::Slab) = (1, R)
rank_size(R, ::Pencil) = Int.((√R, √R))

# Run benchmarks

print_system_info()

for R in ranks
    Nx, Ny = grid_size(R, decomposition)
    Rx, Ry = rank_size(R, decomposition)
    @info "Benchmarking distributed shallow water model strong scaling with $(typeof(decomposition)) decomposition [N=($Nx, $Ny), ranks=($Rx, $Ry)]..."
    julia = Base.julia_cmd()
    run(`mpiexec -np $R $julia --project strong_scaling_shallow_water_model_single.jl $(typeof(decomposition)) $Nx $Ny $Rx $Ry`)
end

# Collect and merge benchmarks from all ranks

suite = BenchmarkGroup(["size", "ranks"])

for R in ranks
    Nx, Ny = grid_size(R, decomposition)
    Rx, Ry = rank_size(R, decomposition)
    case = ((Nx, Ny), (Rx, Ry))

    for local_rank in 0:R-1
        filename = string("strong_scaling_shallow_water_model_$(R)ranks_$(typeof(decomposition))_$local_rank.jld2")
        jldopen(filename, "r") do file
            if local_rank == 0
                suite[case] = file["trial"]
            else
                merged_trial = suite[case]
                local_trial = file["trial"]
                append!(merged_trial.times, local_trial.times)
                append!(merged_trial.gctimes, local_trial.gctimes)
            end
        end
    end
end

# Summarize benchmarks

df = benchmarks_dataframe(suite)
sort!(df, :ranks)
benchmarks_pretty_table(df, title="Shallow water model strong scaling benchmark")

base_case = (grid_size(1, decomposition), rank_size(1, decomposition))
suite_Δ = speedups_suite(suite, base_case=base_case)
df_Δ = speedups_dataframe(suite_Δ, efficiency=:strong, base_case=base_case, key2rank=k->prod(k[2]))
sort!(df_Δ, :ranks)
benchmarks_pretty_table(df_Δ, title="Shallow water model strong scaling speedup")


text:
@testset "Transition priority tests" begin

@testset "OUT before IN priority test" begin
    mpSim = ManpowerSimulation()

    attribute = Attribute( "Level" )
    setPossibleAttributeValues!( attribute, ["A", "B"] )
    addSimulationAttribute!( mpSim, attribute )

    node = BaseNode( "A" )
    setNodeRequirements!( node, ["Level"], ["A"] )
    addSimulationBaseNode!( mpSim, node )

    node = BaseNode( "B" )
    setNodeRequirements!( node, ["Level"], ["B"] )
    setNodeTarget!( node, 40 )
    addSimulationBaseNode!( mpSim, node )

    recruitment = Recruitment( "EW" )
    setRecruitmentSchedule!( recruitment, 12 )
    setRecruitmentTarget!( recruitment, "A" )
    setRecruitmentFixed!( recruitment, 10 )
    setRecruitmentAgeFixed!( recruitment, 18 * 12 )
    addSimulationRecruitment!( mpSim, recruitment )

    transition = Transition( "trans", "A", "B" )
    setTransitionSchedule!( transition, 12 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 36 ) )
    addSimulationTransition!( mpSim, transition )

    transition = Transition( "trans", "B" )
    setTransitionSchedule!( transition, 12 )
    addTransitionCondition!( transition,
        MPcondition( "tenure", >=, 120 ) )
    addSimulationTransition!( mpSim, transition )

    retirement = Retirement()
    setRetirementCareerLength!( retirement, 120 )
    setRetirementSchedule!( retirement, 12 )
    setSimulationRetirement!( mpSim, retirement )

    setSimulationLength!( mpSim, 300.0 )

    @test verifySimulation!( mpSim )
    run( mpSim, saveConfig = false )

    report = nodePopReport( mpSim, 12, "A", "B" )
    @test all( report[:, :A] .==
        vcat( 10, 20, fill( 30, 5 ), 40, 50, fill( 60, 17 ) ) ) &&
        all( report[:, :B] .== vcat( 0, 0, 0, 10:10:30, fill( 40, 20 ) ) )

    clearSimulationTransitions!( mpSim )

    transition = Transition( "trans1", "A", "B" )
    setTransitionSchedule!( transition, 12 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 36 ) )
    addSimulationTransition!( mpSim, transition )

    transition = Transition( "trans2", "B" )
    setTransitionSchedule!( transition, 12 )
    addTransitionCondition!( transition,
        MPcondition( "tenure", >=, 120 ) )
    addSimulationTransition!( mpSim, transition )

    setSimulationTransitionTypeOrder!( mpSim,
        Dict( "trans1" => 1, "trans2" => 2 ) )

    run( mpSim, saveConfig = false )
    report = nodePopReport( mpSim, 12, "A", "B" )
    @test all( report[:, :A] .==
        vcat( 10, 20, fill( 30, 5 ), 40, 50, fill( 60, 17 ) ) ) &&
        all( report[:, :B] .== vcat( 0, 0, 0, 10:10:30, fill( 40, 20 ) ) )
end  # @testset "OUT before IN priority test"

@testset "OUT transitions priority test" begin
    mpSim = ManpowerSimulation()

    attribute = Attribute( "Level" )
    setPossibleAttributeValues!( attribute, ["A", "B"] )
    addSimulationAttribute!( mpSim, attribute )

    attribute = Attribute( "Branch" )
    setPossibleAttributeValues!( attribute, ["none", "A", "B"] )
    setInitialAttributeValues!( attribute, Dict( "none" => 1.0 ) )
    addSimulationAttribute!( mpSim, attribute )

    node = BaseNode( "A" )
    setNodeRequirements!( node, ["Level"], ["A"] )
    addSimulationBaseNode!( mpSim, node )

    node = BaseNode( "B" )
    setNodeRequirements!( node, ["Level", "Branch"], ["B", "A"] )
    setNodeTarget!( node, -1 )
    addSimulationBaseNode!( mpSim, node )

    node = BaseNode( "C" )
    setNodeRequirements!( node, ["Level", "Branch"], ["B", "B"] )
    setNodeTarget!( node, -1 )
    addSimulationBaseNode!( mpSim, node )

    recruitment = Recruitment( "EW" )
    setRecruitmentSchedule!( recruitment, 12 )
    setRecruitmentTarget!( recruitment, "A" )
    setRecruitmentFixed!( recruitment, 6 )
    setRecruitmentAgeFixed!( recruitment, 18 * 12 )
    addSimulationRecruitment!( mpSim, recruitment )

    transition = Transition( "trans1", "A", "B" )
    setTransitionSchedule!( transition, 12 )
    setTransitionFluxLimits!( transition, 4, 8 )
    setTransitionMaxAttempts!( transition, 1 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 36 ) )
    addSimulationTransition!( mpSim, transition )

    transition = Transition( "trans2", "A", "C" )
    setTransitionSchedule!( transition, 12 )
    setTransitionFluxLimits!( transition, 4, 8 )
    setTransitionMaxAttempts!( transition, 1 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 36 ) )
    addSimulationTransition!( mpSim, transition )

    setSimulationTransitionTypeOrder!( mpSim,
        Dict( "trans1" => 2, "trans2" => 1 ) )

    retirement = Retirement()
    setRetirementCareerLength!( retirement, 120 )
    setRetirementSchedule!( retirement, 12 )
    setSimulationRetirement!( mpSim, retirement )

    setSimulationLength!( mpSim, 300.0 )

    @test verifySimulation!( mpSim )
    run( mpSim, saveConfig = false )

    report = nodeEvolutionReport( mpSim, 12, "A", "B", "C" )[2]["A"][2]
    @test all( report[:, Symbol( "trans1: A => B" )] .==
        vcat( 0, 0, 0, fill( 2, 23 ) ) ) &&
        all( report[:, Symbol( "trans2: A => C" )] .==
        vcat( 0, 0, 0, fill( 4, 23 ) ) )

    clearSimulationRecruitment!( mpSim )
    setRecruitmentFixed!( recruitment, 10 )
    addSimulationRecruitment!( mpSim, recruitment )

    run( mpSim, saveConfig = false )
    report = nodeEvolutionReport( mpSim, 12, "A", "B", "C" )[2]["A"][2]
    @test all( report[:, Symbol( "trans1: A => B" )] .==
        vcat( 0, 0, 0, fill( 4, 23 ) ) ) &&
        all( report[:, Symbol( "trans2: A => C" )] .==
        vcat( 0, 0, 0, fill( 6, 23 ) ) )

    clearSimulationRecruitment!( mpSim )
    setRecruitmentFixed!( recruitment, 14 )
    addSimulationRecruitment!( mpSim, recruitment )

    run( mpSim, saveConfig = false )
    report = nodeEvolutionReport( mpSim, 12, "A", "B", "C" )[2]["A"][2]
    @test all( report[:, Symbol( "trans1: A => B" )] .==
        vcat( 0, 0, 0, fill( 6, 23 ) ) ) &&
        all( report[:, Symbol( "trans2: A => C" )] .==
        vcat( 0, 0, 0, fill( 8, 23 ) ) )

    clearSimulationRecruitment!( mpSim )
    setRecruitmentFixed!( recruitment, 18 )
    addSimulationRecruitment!( mpSim, recruitment )

    run( mpSim, saveConfig = false )
    report = nodeEvolutionReport( mpSim, 12, "A", "B", "C" )[2]["A"][2]
    @test all( report[:, Symbol( "trans1: A => B" )] .==
        vcat( 0, 0, 0, fill( 8, 23 ) ) ) &&
        all( report[:, Symbol( "trans2: A => C" )] .==
        vcat( 0, 0, 0, fill( 8, 23 ) ) )

    clearSimulationRecruitment!( mpSim )
    setRecruitmentFixed!( recruitment, 10 )
    addSimulationRecruitment!( mpSim, recruitment )

    clearSimulationTransitions!( mpSim )
    setSimulationBaseNodeOrder!( mpSim, Dict( "B" => 2, "C" => 1 ) )
    transition = Transition( "trans", "A", "B" )
    setTransitionSchedule!( transition, 12 )
    setTransitionFluxLimits!( transition, 4, 8 )
    setTransitionMaxAttempts!( transition, 1 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 36 ) )
    addSimulationTransition!( mpSim, transition )

    transition = Transition( "trans", "A", "C" )
    setTransitionSchedule!( transition, 12 )
    setTransitionFluxLimits!( transition, 4, 8 )
    setTransitionMaxAttempts!( transition, 1 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 36 ) )
    addSimulationTransition!( mpSim, transition )

    run( mpSim, saveConfig = false )
    report = nodeEvolutionReport( mpSim, 12, "A", "B", "C" )[2]["A"][2]
    @test all( report[:, Symbol( "trans: A => B" )] .==
        vcat( 0, 0, 0, fill( 4, 23 ) ) ) &&
        all( report[:, Symbol( "trans: A => C" )] .==
        vcat( 0, 0, 0, fill( 6, 23 ) ) )
end  # @testset ""OUT transitions priority test""

@testset "IN transition priority test" begin
    mpSim = ManpowerSimulation()

    attribute = Attribute( "Level" )
    setPossibleAttributeValues!( attribute, ["A", "B"] )
    addSimulationAttribute!( mpSim, attribute )

    attribute = Attribute( "Branch" )
    setPossibleAttributeValues!( attribute, ["A", "B"] )
    addSimulationAttribute!( mpSim, attribute )

    node = BaseNode( "A" )
    setNodeRequirements!( node, ["Level", "Branch"], ["A", "A"] )
    addSimulationBaseNode!( mpSim, node )

    node = BaseNode( "B" )
    setNodeRequirements!( node, ["Level", "Branch"], ["A", "B"] )
    addSimulationBaseNode!( mpSim, node )

    node = BaseNode( "C" )
    setNodeRequirements!( node, ["Level"], ["B"] )
    setNodeTarget!( node, 6 )
    addSimulationBaseNode!( mpSim, node )

    recruitment = Recruitment( "EW" )
    setRecruitmentSchedule!( recruitment, 12 )
    setRecruitmentTarget!( recruitment, "A" )
    setRecruitmentFixed!( recruitment, 10 )
    setRecruitmentAgeFixed!( recruitment, 18 * 12 )
    addSimulationRecruitment!( mpSim, recruitment )

    recruitment = Recruitment( "EW" )
    setRecruitmentSchedule!( recruitment, 12 )
    setRecruitmentTarget!( recruitment, "B" )
    setRecruitmentFixed!( recruitment, 10 )
    setRecruitmentAgeFixed!( recruitment, 18 * 12 )
    addSimulationRecruitment!( mpSim, recruitment )

    transition = Transition( "trans1", "A", "C" )
    setTransitionSchedule!( transition, 12 )
    setTransitionFluxLimits!( transition, 4, 8 )
    setTransitionMaxAttempts!( transition, 1 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 36 ) )
    addSimulationTransition!( mpSim, transition )

    transition = Transition( "trans2", "B", "C" )
    setTransitionSchedule!( transition, 12 )
    setTransitionFluxLimits!( transition, 4, 8 )
    setTransitionMaxAttempts!( transition, 1 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 36 ) )
    addSimulationTransition!( mpSim, transition )

    transition = Transition( "PE", "C" )
    setTransitionSchedule!( transition, 12 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 12 ) )
    addSimulationTransition!( mpSim, transition )

    setSimulationTransitionTypeOrder!( mpSim,
    Dict( "trans1" => 2, "trans2" => 1 ) )

    retirement = Retirement()
    setRetirementCareerLength!( retirement, 120 )
    setRetirementSchedule!( retirement, 12 )
    setSimulationRetirement!( mpSim, retirement )

    setSimulationLength!( mpSim, 300.0 )

    @test verifySimulation!( mpSim )
    run( mpSim, saveConfig = false )

    report = nodeEvolutionReport( mpSim, 12, "A", "B", "C" )[2]["C"][1]
    @test all( report[:, Symbol( "trans1: A => C" )] .==
        vcat( 0, 0, 0, fill( 4, 23 ) ) ) &&
        all( report[:, Symbol( "trans2: B => C" )] .==
        vcat( 0, 0, 0, fill( 4, 23 ) ) )

    setNodeTarget!( node, 10 )
    addSimulationBaseNode!( mpSim, node )

    run( mpSim, saveConfig = false )
    report = nodeEvolutionReport( mpSim, 12, "A", "B", "C" )[2]["C"][1]
    @test all( report[:, Symbol( "trans1: A => C" )] .==
        vcat( 0, 0, 0, fill( 4, 23 ) ) ) &&
        all( report[:, Symbol( "trans2: B => C" )] .==
        vcat( 0, 0, 0, fill( 6, 23 ) ) )

    setNodeTarget!( node, 14 )
    addSimulationBaseNode!( mpSim, node )

    run( mpSim, saveConfig = false )
    report = nodeEvolutionReport( mpSim, 12, "A", "B", "C" )[2]["C"][1]
    @test all( report[:, Symbol( "trans1: A => C" )] .==
        vcat( 0, 0, 0, fill( 6, 23 ) ) ) &&
        all( report[:, Symbol( "trans2: B => C" )] .==
        vcat( 0, 0, 0, fill( 8, 23 ) ) )

    setNodeTarget!( node, 18 )
    addSimulationBaseNode!( mpSim, node )

    run( mpSim, saveConfig = false )
    report = nodeEvolutionReport( mpSim, 12, "A", "B", "C" )[2]["C"][1]
    @test all( report[:, Symbol( "trans1: A => C" )] .==
        vcat( 0, 0, 0, fill( 8, 23 ) ) ) &&
        all( report[:, Symbol( "trans2: B => C" )] .==
        vcat( 0, 0, 0, fill( 8, 23 ) ) )

    clearSimulationTransitions!( mpSim )
    setSimulationBaseNodeOrder!( mpSim, Dict( "A" => 2, "B" => 1 ) )
    setNodeTarget!( node, 10 )

    transition = Transition( "trans", "A", "C" )
    setTransitionSchedule!( transition, 12 )
    setTransitionFluxLimits!( transition, 4, 8 )
    setTransitionMaxAttempts!( transition, 1 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 36 ) )
    addSimulationTransition!( mpSim, transition )

    transition = Transition( "trans", "B", "C" )
    setTransitionSchedule!( transition, 12 )
    setTransitionFluxLimits!( transition, 4, 8 )
    setTransitionMaxAttempts!( transition, 1 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 36 ) )
    addSimulationTransition!( mpSim, transition )

    transition = Transition( "PE", "C" )
    setTransitionSchedule!( transition, 12 )
    addTransitionCondition!( transition, MPcondition( "time in node", >=, 12 ) )
    addSimulationTransition!( mpSim, transition )

    run( mpSim, saveConfig = false )
    report = nodeEvolutionReport( mpSim, 12, "A", "B", "C" )[2]["C"][1]
    @test all( report[:, Symbol( "trans: A => C" )] .==
        vcat( 0, 0, 0, fill( 4, 23 ) ) ) &&
        all( report[:, Symbol( "trans: B => C" )] .==
        vcat( 0, 0, 0, fill( 6, 23 ) ) )
end  # @testset "IN transition priority test"

end  # @testset "Transition priority tests"


text:
abstract type AbstractServiceFormulation end

mutable struct ServiceModel{D<:PSY.Service,
                            B<:AbstractServiceFormulation}
    service_type::Type{D}
    formulation::Type{B}
end

struct ServiceExpressionKey
    name::String
    device_type::Type{<:PSY.Device}
end


text:
export BrickletParticulateMatterPMConcentration
struct BrickletParticulateMatterPMConcentration
    pm10::Integer
    pm25::Integer
    pm100::Integer
end

export BrickletParticulateMatterPMCount
struct BrickletParticulateMatterPMCount
    greater03um::Integer
    greater05um::Integer
    greater10um::Integer
    greater25um::Integer
    greater50um::Integer
    greater100um::Integer
end

export BrickletParticulateMatterSensorInfo
struct BrickletParticulateMatterSensorInfo
    sensor_version::Integer
    last_error_code::Integer
    framing_error_count::Integer
    checksum_error_count::Integer
end

export BrickletParticulateMatterPMConcentrationCallbackConfiguration
struct BrickletParticulateMatterPMConcentrationCallbackConfiguration
    period::Integer
    value_has_to_change::Bool
end

export BrickletParticulateMatterPMCountCallbackConfiguration
struct BrickletParticulateMatterPMCountCallbackConfiguration
    period::Integer
    value_has_to_change::Bool
end

export BrickletParticulateMatterSPITFPErrorCount
struct BrickletParticulateMatterSPITFPErrorCount
    error_count_ack_checksum::Integer
    error_count_message_checksum::Integer
    error_count_frame::Integer
    error_count_overflow::Integer
end

export BrickletParticulateMatterIdentity
struct BrickletParticulateMatterIdentity
    uid::String
    connected_uid::String
    position::Char
    hardware_version::Vector{Integer}
    firmware_version::Vector{Integer}
    device_identifier::Integer
end

export BrickletParticulateMatter
"""
Measures Particulate Matter concentration (PM1.0, PM2.5 and PM10)
"""
mutable struct BrickletParticulateMatter <: TinkerforgeDevice
    ipcon::IPConnection
    deviceInternal::PyObject

    """
    Creates an object with the unique device ID *uid* and adds it to
    the IP Connection *ipcon*.
    """
    function BrickletParticulateMatter(uid::String, ipcon::IPConnection)
        package = pyimport("tinkerforge.bricklet_particulate_matter")
        deviceInternal = package.BrickletParticulateMatter(uid, ipcon.ipconInternal)

        return new(ipcon, deviceInternal)
    end
end

export get_pm_concentration
"""
    $(SIGNATURES)

Returns the particulate matter concentration, broken down as:

* PM\\ :sub:`1.0`\\ ,
* PM\\ :sub:`2.5`\\  and
* PM\\ :sub:`10.0`\\ .

If the sensor is disabled (see :func:`Set Enable`) then the last known good
values from the sensor are returned.

If you want to get the values periodically, it is recommended to use the
:cb:`PM Concentration` callback. You can set the callback configuration
with :func:`Set PM Concentration Callback Configuration`.
"""
function get_pm_concentration(device::BrickletParticulateMatter)
    return device.deviceInternal.get_pm_concentration()
end

export get_pm_count
"""
    $(SIGNATURES)

Returns the number of particulates in 100 ml of air, broken down by their
diameter:

* greater 0.3µm,
* greater 0.5µm,
* greater 1.0µm,
* greater 2.5µm,
* greater 5.0µm and
* greater 10.0µm.

If the sensor is disabled (see :func:`Set Enable`) then the last known good
value from the sensor is returned.

If you want to get the values periodically, it is recommended to use the
:cb:`PM Count` callback. You can set the callback configuration
with :func:`Set PM Count Callback Configuration`.
"""
function get_pm_count(device::BrickletParticulateMatter)
    return device.deviceInternal.get_pm_count()
end

export set_enable
"""
    $(SIGNATURES)

Enables/Disables the fan and the laser diode of the sensors.

The sensor takes about 30 seconds after it is enabled to settle and produce stable
values.

The laser diode has a lifetime of about 8000 hours. If you want to measure in
an interval with a long idle time (e.g. hourly) you should turn the
laser diode off between the measurements.
"""
function set_enable(device::BrickletParticulateMatter, enable)
    device.deviceInternal.set_enable(enable)
end

export get_enable
"""
    $(SIGNATURES)

Returns the state of the sensor as set by :func:`Set Enable`.
"""
function get_enable(device::BrickletParticulateMatter)
    return device.deviceInternal.get_enable()
end

export get_sensor_info
"""
    $(SIGNATURES)

Returns information about the sensor:

* the sensor version number,
* the last error code reported by the sensor (0 means no error) and
* the number of framing and checksum errors that occurred in the communication
  with the sensor.
"""
function get_sensor_info(device::BrickletParticulateMatter)
    return device.deviceInternal.get_sensor_info()
end

export set_pm_concentration_callback_configuration
"""
    $(SIGNATURES)

The period is the period with which the :cb:`PM Concentration`
callback is triggered periodically. A value of 0 turns the callback off.

If the `value has to change`-parameter is set to true, the callback is only
triggered after the value has changed. If the value didn't change within the
period, the callback is triggered immediately on change.

If it is set to false, the callback is continuously triggered with the period,
independent of the value.
"""
function set_pm_concentration_callback_configuration(device::BrickletParticulateMatter, period, value_has_to_change)
    device.deviceInternal.set_pm_concentration_callback_configuration(period, value_has_to_change)
end

export get_pm_concentration_callback_configuration
"""
    $(SIGNATURES)

Returns the callback configuration as set by
:func:`Set PM Concentration Callback Configuration`.
"""
function get_pm_concentration_callback_configuration(device::BrickletParticulateMatter)
    return device.deviceInternal.get_pm_concentration_callback_configuration()
end

export set_pm_count_callback_configuration
"""
    $(SIGNATURES)

The period is the period with which the :cb:`PM Count` callback
is triggered periodically. A value of 0 turns the callback off.

If the `value has to change`-parameter is set to true, the callback is only
triggered after the value has changed. If the value didn't change within the
period, the callback is triggered immediately on change.

If it is set to false, the callback is continuously triggered with the period,
independent of the value.
"""
function set_pm_count_callback_configuration(device::BrickletParticulateMatter, period, value_has_to_change)
    device.deviceInternal.set_pm_count_callback_configuration(period, value_has_to_change)
end

export get_pm_count_callback_configuration
"""
    $(SIGNATURES)

Returns the callback configuration as set by
:func:`Set PM Count Callback Configuration`.
"""
function get_pm_count_callback_configuration(device::BrickletParticulateMatter)
    return device.deviceInternal.get_pm_count_callback_configuration()
end

export get_spitfp_error_count
"""
    $(SIGNATURES)

Returns the error count for the communication between Brick and Bricklet.

The errors are divided into

* ACK checksum errors,
* message checksum errors,
* framing errors and
* overflow errors.

The errors counts are for errors that occur on the Bricklet side. All
Bricks have a similar function that returns the errors on the Brick side.
"""
function get_spitfp_error_count(device::BrickletParticulateMatter)
    return device.deviceInternal.get_spitfp_error_count()
end

export set_bootloader_mode
"""
    $(SIGNATURES)

Sets the bootloader mode and returns the status after the requested
mode change was instigated.

You can change from bootloader mode to firmware mode and vice versa. A change
from bootloader mode to firmware mode will only take place if the entry function,
device identifier and CRC are present and correct.

This function is used by Brick Viewer during flashing. It should not be
necessary to call it in a normal user program.
"""
function set_bootloader_mode(device::BrickletParticulateMatter, mode)
    return device.deviceInternal.set_bootloader_mode(mode)
end

export get_bootloader_mode
"""
    $(SIGNATURES)

Returns the current bootloader mode, see :func:`Set Bootloader Mode`.
"""
function get_bootloader_mode(device::BrickletParticulateMatter)
    return device.deviceInternal.get_bootloader_mode()
end

export set_write_firmware_pointer
"""
    $(SIGNATURES)

Sets the firmware pointer for :func:`Write Firmware`. The pointer has
to be increased by chunks of size 64. The data is written to flash
every 4 chunks (which equals to one page of size 256).

This function is used by Brick Viewer during flashing. It should not be
necessary to call it in a normal user program.
"""
function set_write_firmware_pointer(device::BrickletParticulateMatter, pointer)
    device.deviceInternal.set_write_firmware_pointer(pointer)
end

export write_firmware
"""
    $(SIGNATURES)

Writes 64 Bytes of firmware at the position as written by
:func:`Set Write Firmware Pointer` before. The firmware is written
to flash every 4 chunks.

You can only write firmware in bootloader mode.

This function is used by Brick Viewer during flashing. It should not be
necessary to call it in a normal user program.
"""
function write_firmware(device::BrickletParticulateMatter, data)
    return device.deviceInternal.write_firmware(data)
end

export set_status_led_config
"""
    $(SIGNATURES)

Sets the status LED configuration. By default the LED shows
communication traffic between Brick and Bricklet, it flickers once
for every 10 received data packets.

You can also turn the LED permanently on/off or show a heartbeat.

If the Bricklet is in bootloader mode, the LED is will show heartbeat by default.
"""
function set_status_led_config(device::BrickletParticulateMatter, config)
    device.deviceInternal.set_status_led_config(config)
end

export get_status_led_config
"""
    $(SIGNATURES)

Returns the configuration as set by :func:`Set Status LED Config`
"""
function get_status_led_config(device::BrickletParticulateMatter)
    return device.deviceInternal.get_status_led_config()
end

export get_chip_temperature
"""
    $(SIGNATURES)

Returns the temperature as measured inside the microcontroller. The
value returned is not the ambient temperature!

The temperature is only proportional to the real temperature and it has bad
accuracy. Practically it is only useful as an indicator for
temperature changes.
"""
function get_chip_temperature(device::BrickletParticulateMatter)
    return device.deviceInternal.get_chip_temperature()
end

export reset
"""
    $(SIGNATURES)

Calling this function will reset the Bricklet. All configurations
will be lost.

After a reset you have to create new device objects,
calling functions on the existing ones will result in
undefined behavior!
"""
function reset(device::BrickletParticulateMatter)
    device.deviceInternal.reset()
end

export write_uid
"""
    $(SIGNATURES)

Writes a new UID into flash. If you want to set a new UID
you have to decode the Base58 encoded UID string into an
integer first.

We recommend that you use Brick Viewer to change the UID.
"""
function write_uid(device::BrickletParticulateMatter, uid)
    device.deviceInternal.write_uid(uid)
end

export read_uid
"""
    $(SIGNATURES)

Returns the current UID as an integer. Encode as
Base58 to get the usual string version.
"""
function read_uid(device::BrickletParticulateMatter)
    return device.deviceInternal.read_uid()
end

export get_identity
"""
    $(SIGNATURES)

Returns the UID, the UID where the Bricklet is connected to,
the position, the hardware and firmware version as well as the
device identifier.

The position can be 'a', 'b', 'c', 'd', 'e', 'f', 'g' or 'h' (Bricklet Port).
A Bricklet connected to an :ref:`Isolator Bricklet <isolator_bricklet>` is always at
position 'z'.

The device identifier numbers can be found :ref:`here <device_identifier>`.
|device_identifier_constant|
"""
function get_identity(device::BrickletParticulateMatter)
    return device.deviceInternal.get_identity()
end

export register_callback
"""
Registers the given *function* with the given *callback_id*.
"""
function register_callback(device::BrickletParticulateMatter, callback_id, function_)
    if isnothing(function_)
        device.registered_callbacks.pop(callback_id, None)
    else
        device.registered_callbacks[callback_id] = function_
    end
end


text:
"""
    Planet(optimizer, eager::Bool)

Planet integrates a SAT solver (`PicoSAT.jl`) to find an activation pattern that maps a feasible input to an infeasible output.

# Problem requirement
1. Network: any depth, ReLU activation
2. Input: hyperrectangle or bounded hpolytope
3. Output: PolytopeComplement

# Return
`BasicResult`

# Method
Binary search of activations (0/1) and pruning by optimization. Our implementation is non eager.
- `optimizer` default `GLPKSolverMIP()`;
- `eager` default `false`;

# Property
Sound and complete.

# Reference
[R. Ehlers, "Formal Verification of Piece-Wise Linear Feed-Forward Neural Networks,"
in *International Symposium on Automated Technology for Verification and Analysis*, 2017.](https://arxiv.org/abs/1705.01320)

[https://github.com/progirep/planet](https://github.com/progirep/planet)
"""
@with_kw struct Planet <: Solver
    optimizer = GLPK.Optimizer
    eager::Bool = false
end

function solve(solver::Planet, problem::Problem)
    @assert ~solver.eager "Eager implementation not supported yet"
    isbounded(problem.input) || UnboundedInputError("Planet does not accept unbounded input sets.")
    # Refine bounds. The bounds are values after activation
    status, bounds = tighten_bounds(problem, solver.optimizer)
    status == OPTIMAL || return CounterExampleResult(:holds)
    ψ = init_ψ(problem.network, bounds)
    δ = PicoSAT.solve(ψ)
    opt = solver.optimizer
    # Main loop to compute the SAT problem
    while δ != :unsatisfiable
        status, conflict = elastic_filtering(problem, δ, bounds, opt)
        status == INFEASIBLE || return CounterExampleResult(:violated, conflict)
        push!(ψ, conflict)
        δ = PicoSAT.solve(ψ)
    end
    return CounterExampleResult(:holds)
end

function init_ψ(nnet::Network, bounds::Vector{Hyperrectangle})
    ψ = Vector{Vector{Int64}}()
    index = 0
    for i in 1:length(bounds)-1
        index = set_activation_pattern!(ψ, nnet.layers[i], bounds[i], index)
    end
    return ψ
end
function set_activation_pattern!(ψ::Vector{Vector{Int64}}, L::Layer{ReLU}, bound::Hyperrectangle, index::Int64)
    before_act_bound = approximate_affine_map(L, bound)
    lower = low(before_act_bound)
    upper = high(before_act_bound)
    for j in 1:length(lower)
        index += 1
        lower[j] > 0 && push!(ψ, [index])
        upper[j] < 0 && push!(ψ, [-index])
        lower[j] <= 0 <= upper[j] && push!(ψ, [index, -index])
    end
    return index
end
function set_activation_pattern!(ψ::Vector{Vector{Int64}}, L::Layer{Id}, bound::Hyperrectangle, index::Int64)
    n = n_nodes(L)
    for j in 1:n
        index += 1
        push!(ψ, [index])
    end
    return index
end

function elastic_filtering(problem::Problem, δ::Vector{Vector{Bool}}, bounds::Vector{Hyperrectangle}, optimizer)
    model = Model(optimizer)
    model[:bounds] = bounds
    model[:δ] = δ
    z = init_vars(model, problem.network, :z, with_input=true)
    slack = init_vars(model, problem.network, :slack)

    add_set_constraint!(model, problem.input, first(z))
    add_complementary_set_constraint!(model, problem.output, last(z))
    encode_network!(model, problem.network, TriangularRelaxedLP())
    encode_network!(model, problem.network, SlackLP())
    min_sum!(model, slack)

    conflict = Vector{Int64}()
    act = get_activation(problem.network, bounds)
    while true
        optimize!(model)
        termination_status(model) == OPTIMAL || return (INFEASIBLE, conflict)
        (m, index) = max_slack(slack, act)
        m > 0.0 || return (:Feasible, value(first(z)))
        # activated z get a factor of (-1)
        coeff = δ[index[1]][index[2]] ? -1 : 1
        node = coeff * get_node_id(problem.network, index)
        push!(conflict, node)
        @constraint(model, slack[index[1]][index[2]] == 0.0)
    end
end

function elastic_filtering(problem::Problem,
                           list::Vector{Int64},
                           bounds::Vector{Hyperrectangle},
                           optimizer)
    return elastic_filtering(problem,
                             get_assignment(problem.network, list),
                             bounds,
                             optimizer)
end

function max_slack(x::Vector{<:Vector}, act)
    m = 0.0
    index = (0, 0)
    for i in 1:length(x), j in 1:length(x[i])
        if act[i][j] == 0 # Only return undetermined nodes
            val = value(x[i][j])
            if val > m
                m = val
                index = (i, j)
            end
        end
    end
    return (m, index)
end

# Only use tighten_bounds for feasibility check
function tighten_bounds(problem::Problem, optimizer)
    model = Model(optimizer)
    model[:bounds] = bounds = get_bounds(problem)
    z = init_vars(model, problem.network, :z, with_input=true)
    add_set_constraint!(model, problem.input, first(z))
    add_complementary_set_constraint!(model, problem.output, last(z))
    encode_network!(model, problem.network, TriangularRelaxedLP())

    new_bounds = Vector{Hyperrectangle}(undef, length(z))
    for i in 1:length(z)
        lower = low(bounds[i])
        upper = high(bounds[i])
        for j in 1:length(z[i])
            zᵢⱼ = z[i][j]
            @objective(model, Min, zᵢⱼ)
            optimize!(model)
            termination_status(model) == OPTIMAL || return (INFEASIBLE, bounds)
            lower[j] = value(zᵢⱼ)

            @objective(model, Max, zᵢⱼ)
            optimize!(model)
            upper[j] = value(zᵢⱼ)
        end
        new_bounds[i] = Hyperrectangle(low = lower, high = upper)
    end
    return (OPTIMAL, new_bounds)
end

function get_assignment(nnet::Network, list::Vector{Int64})
    p = Vector{Vector{Bool}}(undef, length(nnet.layers))
    ℓ_start = 1
    for (i, layer) in enumerate(nnet.layers)
        ℓ_next = ℓ_start + n_nodes(layer)
        p[i] = get_assignment(layer, list[ℓ_start:ℓ_next-1])
        ℓ_start = ℓ_next
    end
    return p
end
get_assignment(L::Layer{ReLU}, list::Vector{Int64}) = list .> 0
get_assignment(L::Layer{Id},   list::Vector{Int64}) = trues(length(list))

function get_node_id(nnet::Network, x::Tuple{Int64, Int64})
    # All the nodes in the previous layers
    n = sum(n_nodes.(nnet.layers[1:x[1]-1]))

    # Plus the previous nodes in the current layer
    return n + x[2]
end

# NOTE: not used -
# function get_node_id(nnet::Network, n::Int64)
#     i = 0
#     j = n
#     while j > length(nnet.layers[i+1].bias)
#         i += 1
#         j = j - length(nnet.layers[i].bias)
#     end
#     return (i, j)
# end


text:
function (::GeometricToAlgebraic)(𝛏::AbstractVector)
    A, B, H, K, τ = 𝛏

    a = cos(τ)^2/A^2 + sin(τ)^2/B^2
    b = (1/A^2 - 1/B^2)*sin(2*τ)
    c = (cos(τ)^2/B^2) + sin(τ)^2/A^2;
    d = (2*sin(τ)*(K*cos(τ)-H*sin(τ))) / B^2 - (2*cos(τ)^2*(H+K*tan(τ))) / A^2
    e = (2*cos(τ)*(H*sin(τ) - K*cos(τ))) / B^2 - (2*sin(τ)*(H*cos(τ) + K*sin(τ))) / A^2
    f = (H*cos(τ) + K*sin(τ))^2 / A^2 + (K*cos(τ) - H*sin(τ))^2 / B^2 - 1

    𝛉 = SVector(a, b, c, d, e, f)
    return 𝛉
end

function (::AlgebraicToGeometric)(𝛉::AbstractVector)
    a, b, c, d, e, f  = 𝛉
    Δ = b^2 - 4*a*c
    λ₊ = 0.5*(a + c - (b^2 + (a - c)^2)^0.5)
    λ₋ = 0.5*(a + c + (b^2 + (a - c)^2)^0.5)

    ψ = b*d*e - a*e^2 - b^2*f + c*(4*a*f - d^2)
    V₊ = (ψ/(λ₊*Δ))^0.5
    V₋ = (ψ/(λ₋*Δ))^0.5

    # major semi-axis
    A = max(V₊, V₋)
    # minor semi-axis
    B = min(V₊, V₋)

    # determine x-coordinate of ellipse centroid
    H = (2*c*d - b*e)/(Δ)
    # determine y-coordinate of ellipse centroid
    K = (2*a*e - b*d)/(Δ)

    # angle between x-axis and major axis
    τ = 0
    # determine tilt of ellipse in radians
    if V₊ >= V₋
        if (b == 0 && a < c)
            τ = 0
        elseif (b == 0 && a >= c)
            τ  = 0.5*π
        elseif (b < 0 && a < c)
            τ  = 0.5*acot((a - c)/b)
        elseif (b < 0 && a == c)
            τ = π/4
        elseif (b < 0 && a > c)
            τ = 0.5*acot((a - c)/b) + π/2
        elseif (b > 0 && a < c)
            τ = 0.5*acot((a - c)/b) + π
        elseif (b > 0 && a == c)
            τ = π*(3/4)
        elseif (b > 0 && a > c)
            τ = 0.5*acot((a - c)/b) + π/2
        end
    elseif V₊ < V₋
        if (b == 0 && a < c)
            τ = π/2
        elseif (b == 0 && a >= c)
            τ = 0
        elseif (b < 0 && a < c)
            τ = 0.5*acot((a - c)/b) + π/2
        elseif (b < 0 && a == c)
            τ = π*(3/4)
        elseif (b < 0 && a > c)
            τ = 0.5*acot((a - c)/b) + π
        elseif (b > 0 && a < c)
            τ = 0.5*acot((a - c)/b) + π/2
        elseif (b > 0 && a == c)
            τ = π/4
        elseif (b > 0 && a > c)
            τ = 0.5*acot((a - c)/b)
        end
    end
    𝛏 = SVector(A, B, H, K, τ)
    return 𝛏
end


text:
function binary_search(array, value, low=1, high=length(array))
    if isempty(array)
        return 0
    end

    if low >= high
        if low > high || array[low] != value
            return 0
        end

        return low
    end

    mid = (low + high) ÷ 2

    if array[mid] > value
        return binary_search(array, value, low, mid-1)
    elseif array[mid] < value
        return binary_search(array, value, mid+1, high)
    end

    return mid
end

max_stars_repo_name: landrumb/julia

max_issues_repo_name: landrumb/julia

max_forks_repo_name: landrumb/julia


text:
module testrepo

using Printf
greet() = "Hello"
print_greet() = (@printf "%s\n" greet())
export print_greet

end

max_stars_repo_name: mfherbst/testrepo

max_issues_repo_name: mfherbst/testrepo

max_forks_repo_name: mfherbst/testrepo