using DrWatson
@quickactivate("HyperspectraWithNeXL")
# Load the necessary libraries
using HyperspectraWithNeXL
using NeXLSpectrum
using Gadfly
using Images, FileIO, Unitful
# Load the HyperSpectrum from disk
lt = 0.72*4.0*18.0*3600.0/(1024*1024) # 18.0 hours on 4 detectors
hs = NeXLSpectrum.compress(HyperSpectrum(
    LinearEnergyScale(0.0,10.0),
    Dict{Symbol,Any}(
      :TakeOffAngle => deg2rad(35.0),
      :ProbeCurrent => 1.0,
      :LiveTime => lt,
      :BeamEnergy => 20.0e3,
      :Name => "Mn Nodule"
    ),
    readrplraw(joinpath(datadep"MnNodule","map[15]")),
    fov = [ 4.096u"mm", 4.096u"mm"], offset= [ 0.0u"mm", 0.0u"mm" ]
))
# hs = hs[16:8:1024, 16:8:1024]
# Build the fitting references
refpath = datadep"MnNodule_Standards"
refs = references( [
    reference(n"C", joinpath(refpath, "C std.msa") ),
    reference(n"Ag", joinpath(refpath, "Ag std.msa") ),
    reference(n"Al", joinpath(refpath, "Al std.msa") ),
    reference(n"C", joinpath(refpath, "C std.msa") ),
    reference(n"Ca", joinpath(refpath, "CaF2 std.msa") ),
    reference(n"Ce", joinpath(refpath, "CeO2 std.msa") ),
    reference(n"Cl", joinpath(refpath, "NaCl std.msa") ),
    reference(n"Cr", joinpath(refpath, "Cr std.msa") ),
    reference(n"Cu", joinpath(refpath, "Cu std.msa") ),
    reference(n"Fe", joinpath(refpath, "Fe std.msa") ),
    reference(n"S", joinpath(refpath, "FeS2 std.msa") ),
    reference(n"P", joinpath(refpath, "GaP std.msa") ),
    reference(n"K", joinpath(refpath, "KBr std.msa") ),
    reference(n"Mg", joinpath(refpath, "Mg std.msa") ),
    reference(n"O", joinpath(refpath, "MgO std.msa") ),
    reference(n"Mn", joinpath(refpath, "Mn std.msa") ),
    reference(n"Na", joinpath(refpath, "NaCl std.msa") ),
    reference(n"Ni", joinpath(refpath, "Ni std.msa") ),
    reference(n"Si", joinpath(refpath, "Si std.msa") ),
    reference(n"Ti", joinpath(refpath, "Ti std.msa") ),
    reference(n"Zn", joinpath(refpath, "Zn std.msa") ) ],
    132.0
)
# Fit the reference spectra to the HyperSpectrum unknown
resf = fit_spectrum(hs, refs, mode = :Fast)
# Select the optimal k-ratio (K, L? or M?) for each element
bestks = optimizeks(SimpleKRatioOptimizer(2.0), resf)
# Convert compound k-ratios to pure element equivalents (matrix correction)
pureks = aspure.(bestks)
# Normalize the k-ratios at each point in the HyperSpectrum
bestf = normalizek(pureks)

# Save the resulting images
path = joinpath(plotsdir(),"Table K-ratio Map")
mkpath(path)
for bf in bestf
  FileIO.save(File{format"PNG"}(joinpath(path,"k_norm[$(bf.xrays)].png")),Log3Band.(bf.kratios))
end

li = labeledimages([ "$(bf.xrays)" for bf in bestf],[Log3Band.(bf.kratios)
                for bf in bestf ], ncols=4)

li |> SVG(joinpath(plotsdir(),"Labeled k-ratio maps.svg"), 8inch, 10inch)

# Clean up
hs, resf, bestks, pureks, bestf = nothing, nothing, nothing, nothing, nothing


# This file is a part of Julia. License is MIT: https://julialang.org/license

module RangesTest

import Dates
using Test
using ExtendedDates

using InteractiveUtils: subtypes

let
    YD = YearDate
    SD = SemesterDate
    QD = QuarterDate
    MD = MonthDate
    WD = WeekDate
    DD = DayDate
    UD = UndatedDate
    for (f1, l1, f2, l2, f3, l3, f4, l4, period) in (
        (
            YD(2014),
            YD(2013),
            YD(2014),
            YD(2014),
            YD(-20),
            YD(20),
            typemin(YD),
            typemax(YD),
            (Dates.Year,),
        ),
        (
            SD(2014, 1),
            SD(2013, 1),
            SD(2014, 1),
            SD(2014, 1),
            SD(-2000, 1),
            SD(2000, 1),
            typemin(SD),
            typemax(SD),
            (Dates.Year, Semester),
        ),
        (
            QD(2014, 1),
            QD(2013, 1),
            QD(2014, 1),
            QD(2014, 1),
            QD(-2000, 1),
            QD(2000, 1),
            typemin(QD),
            typemax(QD),
            (Dates.Year, Semester, Dates.Quarter),
        ),
        (
            MD(2014, 1),
            MD(2013, 1),
            MD(2014, 1),
            MD(2014, 1),
            MD(-2000, 1),
            MD(2000, 1),
            typemin(MD),
            typemax(MD),
            (Dates.Year, Semester, Dates.Quarter, Dates.Month),
        ),
        (
            WD(2014, 1),
            WD(2013, 1),
            WD(2014, 1),
            WD(2014, 1),
            WD(1, 1),
            WD(2000, 1),
            typemin(WD),
            typemax(WD),
            (Dates.Week,),
        ),
        (
            DD(2014, 1, 1),
            DD(2013, 1, 1),
            DD(2014, 1, 1),
            DD(2014, 1, 1),
            DD(-2000, 1, 1),
            DD(2000, 1, 1),
            typemin(DD),
            typemax(DD),
            (Dates.Year, Semester, Dates.Quarter, Dates.Month, Dates.Week, Dates.Day),
        ),
        (
            UD(2014),
            UD(2013),
            UD(2014),
            UD(2014),
            UD(-2000),
            UD(2000),
            typemin(UD),
            typemax(UD),
            (Undated,),
        ),
    )

        for P in period
            for pos_step in (P(1), P(2), P(50), P(2048), P(10000))
                # empty range
                dr = f1:pos_step:l1
                len = length(dr)
                @test len == 0
                @test isa(len, Int64)
                @test isempty(dr)
                @test first(dr) == f1
                @test last(dr) < f1
                @test length([i for i in dr]) == 0
                @test_throws ArgumentError minimum(dr)
                @test_throws ArgumentError maximum(dr)
                @test_throws BoundsError dr[1]
                @test findall(in(dr), dr) == Int64[]
                @test [dr;] == typeof(f1)[]
                @test isempty(reverse(dr))
                @test length(reverse(dr)) == 0
                @test first(reverse(dr)) < f1
                @test last(reverse(dr)) >= f1
                @test issorted(dr)
                @test sortperm(dr) === StepRange{Int64,Int}(1:1:0)
                @test !(f1 in dr)
                @test !(l1 in dr)
                @test !(f1 - pos_step in dr)
                @test !(l1 + pos_step in dr)
                @test dr == []
                @test hash(dr) == hash([])

                for (f, l) in ((f2, l2), (f3, l3))# , (f4, l4))
                    dr = f:pos_step:l
                    len = length(dr)
                    @test len > 0
                    @test isa(len, Int64)
                    @test !isempty(dr)
                    @test first(dr) == f
                    @test last(dr) <= l
                    @test minimum(dr) == first(dr)
                    @test maximum(dr) == last(dr)
                    @test dr[1] == f
                    @test dr[end] <= l
                    @test iterate(dr) == (first(dr), (length(dr), 1))
                    if len < 10#000
                        dr1 = [i for i in dr]
                        @test length(dr1) == len
                        @test findall(in(dr), dr) == [1:len;]
                        @test length([dr;]) == len
                        @test dr == dr1
                        @test hash(dr) == hash(dr1)
                    end
                    @test !isempty(reverse(dr))
                    @test length(reverse(dr)) == len
                    @test last(reverse(dr)) == f
                    @test issorted(dr)
                    @test f in dr
                end
            end

            for neg_step in (P(-1), P(-2), P(-50), P(-2048), P(-10000))
                # empty range
                dr = l1:neg_step:f1
                len = length(dr)
                @test len == 0
                @test isa(len, Int64)
                @test isempty(dr)
                @test first(dr) == l1
                @test last(dr) > l1
                @test length([i for i in dr]) == 0
                @test_throws ArgumentError minimum(dr)
                @test_throws ArgumentError maximum(dr)
                @test_throws BoundsError dr[1]
                @test findall(in(dr), dr) == Int64[]
                @test [dr;] == typeof(f1)[]
                @test isempty(reverse(dr))
                @test length(reverse(dr)) == 0
                @test first(reverse(dr)) > l1
                @test last(reverse(dr)) <= l1
                @test issorted(dr)
                @test sortperm(dr) === StepRange{Int64,Int}(1:1:0)
                @test !(l1 in dr)
                @test !(l1 in dr)
                @test !(l1 - neg_step in dr)
                @test !(l1 + neg_step in dr)
                @test dr == []
                @test hash(dr) == hash([])

                for (f, l) in ((f2, l2), (f3, l3))#, (f4, l4))
                    dr = l:neg_step:f
                    len = length(dr)
                    @test len > 0
                    @test isa(len, Int64)
                    @test !isempty(dr)
                    @test first(dr) == l
                    @test last(dr) >= f
                    @test minimum(dr) == last(dr)
                    @test maximum(dr) == first(dr)
                    @test dr[1] == l
                    @test dr[end] >= f
                    @test iterate(dr) == (first(dr), (length(dr), 1))

                    if len < 10000
                        dr1 = [i for i in dr]
                        @test length(dr1) == len
                        @test findall(in(dr), dr) == [1:len;]
                        @test length([dr;]) == len
                        @test dr == dr1
                        @test hash(dr) == hash(dr1)
                    end
                    @test !isempty(reverse(dr))
                    @test length(reverse(dr)) == len
                    @test issorted(dr) == (len <= 1)
                    @test l in dr
                end
            end
        end
    end
end

# Dates are physical units, and ranges should require an explicit step.
# See #19896 and https://discourse.julialang.org/t/type-restriction-on-unitrange/6557/12
if VERSION >= v"1.7.0"
    @test_throws MethodError DayDate(2013, 1, 1):DayDate(2013, 2, 1)
end

# All the range representations we want to test
# Date ranges
dr = DayDate(2013, 1, 1):Day(1):DayDate(2013, 2, 1)
dr1 = DayDate(2013, 1, 1):Day(1):DayDate(2013, 1, 1)
dr2 = DayDate(2013, 1, 1):Day(1):DayDate(2012, 2, 1) # empty range
dr3 = DayDate(2013, 1, 1):Day(-1):DayDate(2012) # negative step
# Big ranges
dr4 = DayDate(0):Day(1):DayDate(20000, 1, 1)
dr5 = DayDate(0):Day(1):DayDate(200000, 1, 1)
dr6 = DayDate(0):Day(1):DayDate(2000000, 1, 1)
dr7 = DayDate(0):Day(1):DayDate(20000000, 1, 1)
dr8 = DayDate(0):Day(1):DayDate(200000000, 1, 1)
dr9 = typemin(DayDate):Day(1):typemax(DayDate)
# Other steps
dr10 = typemax(DayDate):Day(-1):typemin(DayDate)
dr11 = typemin(DayDate):Week(1):typemax(DayDate)

dr12 = typemin(DayDate):Month(1):typemax(DayDate)
dr13 = typemin(DayDate):Year(1):typemax(DayDate)

dr14 = typemin(DayDate):Week(10):typemax(DayDate)
dr15 = typemin(DayDate):Month(100):typemax(DayDate)
dr16 = typemin(DayDate):Year(1000):typemax(DayDate)
dr17 = typemax(DayDate):Week(-10000):typemin(DayDate)
dr18 = typemax(DayDate):Month(-100000):typemin(DayDate)
dr19 = typemax(DayDate):Year(-1000000):typemin(DayDate)
dr20 = typemin(DayDate):Day(2):typemax(DayDate)

drs = Any[
    dr,
    dr1,
    dr2,
    dr3,
    dr4,
    dr5,
    dr6,
    dr7,
    dr8,
    dr9,
    dr10,
    dr11,
    dr12,
    dr13,
    dr14,
    dr15,
    dr16,
    dr17,
    dr18,
    dr19,
    dr20,
]
drs2 = map(x -> DayDate(first(x)):step(x):DayDate(last(x)), drs)

@test map(length, drs) == map(x -> size(x)[1], drs)
@test map(length, drs) == map(x -> length(DayDate(first(x)):step(x):DayDate(last(x))), drs)
@test map(length, drs) == map(x -> length(reverse(x)), drs)
@test all(x -> findall(in(x), x) == [1:length(x);], drs[1:4])
@test isempty(dr2)
@test all(x -> reverse(x) == range(last(x), step = -step(x), length = length(x)), drs)
@test all(x -> minimum(x) == (step(x) < zero(step(x)) ? last(x) : first(x)), drs[4:end])
@test all(x -> maximum(x) == (step(x) < zero(step(x)) ? first(x) : last(x)), drs[4:end])
@test all(drs[1:3]) do dd
    for (i, d) in enumerate(dd)
        @test d == (first(dd) + Day(i - 1))
    end
    true
end
@test_throws MethodError dr .+ 1
a = DayDate(2013, 1, 1)
b = DayDate(2013, 2, 1)
@test map!(x -> x + Day(1), Vector{DayDate}(undef, 32), dr) ==
      [(a+Day(1)):Day(1):(b+Day(1));]
@test map(x -> x + Day(1), dr) == [(a+Day(1)):Day(1):(b+Day(1));]

@test map(x -> a in x, drs[1:4]) == [true, true, false, true]
@test a in dr
@test b in dr
@test DayDate(2013, 1, 3) in dr
@test DayDate(2013, 1, 15) in dr
@test DayDate(2013, 1, 26) in dr
@test !(DayDate(2012, 1, 1) in dr)

@test all(x -> sort(x) == (step(x) < zero(step(x)) ? reverse(x) : x), drs)
@test all(x -> step(x) < zero(step(x)) ? issorted(reverse(x)) : issorted(x), drs)

@test length(b:Day(-1):a) == 32
@test length(b:Day(1):a) == 0
@test length(b:Day(1):a) == 0
@test length(a:Day(2):b) == 16
@test last(a:Day(2):b) == DayDate(2013, 1, 31)
@test length(a:Day(7):b) == 5
@test last(a:Day(7):b) == DayDate(2013, 1, 29)
@test length(a:Day(32):b) == 1
@test last(a:Day(32):b) == DayDate(2013, 1, 1)
@test (a:Day(1):b)[1] == DayDate(2013, 1, 1)
@test (a:Day(1):b)[2] == DayDate(2013, 1, 2)
@test (a:Day(1):b)[7] == DayDate(2013, 1, 7)
@test (a:Day(1):b)[end] == b
@test first(a:Day(1):DayDate(20000, 1, 1)) == a
@test first(a:Day(1):DayDate(200000, 1, 1)) == a
@test first(a:Day(1):DayDate(2000000, 1, 1)) == a
@test first(a:Day(1):DayDate(20000000, 1, 1)) == a
@test first(a:Day(1):DayDate(200000000, 1, 1)) == a
@test first(a:Day(1):typemax(DayDate)) == a
@test first(typemin(DayDate):Day(1):typemax(DayDate)) == typemin(DayDate)

@test length(typemin(DayDate):Week(1):typemax(DayDate)) == 26351950414948059
# Big Month/Year ranges
@test length(typemin(DayDate):Month(1):typemax(DayDate)) == 6060531933867600
@test length(typemin(DayDate):Year(1):typemax(DayDate)) == 505044327822300

c = DayDate(2013, 6, 1)
@test length(a:Month(1):c) == 6
@test [a:Month(1):c;] == [a + Month(1) * i for i = 0:5]
@test [a:Month(2):DayDate(2013, 1, 2);] == [a]
@test [c:Month(-1):a;] == reverse([a:Month(1):c;])

@test length(range(Date(2000), step = Day(1), length = 366)) == 366
let n = 100000
    local a = DayDate(2000)
    for i = 1:n
        @test length(range(a, step = Day(1), length = i)) == i
    end
    return a + Day(n)
end

@test typeof(step(DayDate(2000):Day(1):DayDate(2001))) == Day

a = DayDate(2013, 1, 1)
b = DayDate(2013, 2, 1)
d = DayDate(2020, 1, 1)
@test length(a:Year(1):d) == 8
@test first(a:Year(1):d) == a
@test last(a:Year(1):d) == d
@test length(a:Month(12):d) == 8
@test first(a:Month(12):d) == a
@test last(a:Month(12):d) == d
@test length(a:Week(52):d) == 8
@test first(a:Week(52):d) == a
@test last(a:Week(52):d) == DayDate(2019, 12, 24)
@test length(a:Day(365):d) == 8
@test first(a:Day(365):d) == a
@test last(a:Day(365):d) == DayDate(2019, 12, 31)

a = DayDate(2013, 1, 1)
b = DayDate(2013, 2, 1)
@test length(a:Year(1):DayDate(2020, 2, 1)) == 8
@test length(a:Year(1):DayDate(2020, 6, 1)) == 8
@test length(a:Year(1):DayDate(2020, 11, 1)) == 8
@test length(a:Year(1):DayDate(2020, 12, 31)) == 8
@test length(a:Year(1):DayDate(2021, 1, 1)) == 9
@test length(DayDate(2000):Year(-10):DayDate(1900)) == 11
@test length(DayDate(2000, 6, 23):Year(-10):DayDate(1900, 2, 28)) == 11
@test length(DayDate(2000, 1, 1):Year(1):DayDate(2000, 2, 1)) == 1

let n = 100000
    local a, b
    a = b = DayDate(0)
    for i = 1:n
        @test length(a:Year(1):b) == i
        b += Year(1)
    end
end

let n = 10000,
    a = DayDate(1985, 12, 5),
    b = DayDate(1986, 12, 27),
    c = DayDate(1985, 12, 5),
    d = DayDate(1986, 12, 27)

    for i = 1:n
        @test length(a:Month(1):b) == 13
        @test length(a:Year(1):b) == 2
        @test length(c:Month(1):d) == 13
        @test length(c:Year(1):d) == 2
        a += Day(1)
        b += Day(1)
    end
end

let n = 100000
    local a, b
    a = b = DayDate(2000)
    for i = 1:n
        @test length(a:Month(1):b) == i
        b += Month(1)
    end
end

@test length(Year(1):Year(1):Year(10)) == 10
@test length(Year(10):Year(-1):Year(1)) == 10
@test length(Year(10):Year(-2):Year(1)) == 5
if VERSION >= v"1.7.0"
    @test length(typemin(Year):Year(1):typemax(Year)) == 0 # overflow
    @test_throws MethodError DayDate(0):DayDate(2000)
end
@test_throws MethodError DayDate(0):Year(10)
@test length(range(DayDate(2000), step = Day(1), length = 366)) == 366
@test last(range(DayDate(2000), step = Day(1), length = 366)) == DayDate(2000, 12, 31)
@test last(range(DayDate(2001), step = Day(1), length = 365)) == DayDate(2001, 12, 31)
@test last(range(DayDate(2000), step = Day(1), length = 367)) ==
      last(range(DayDate(2000), step = Month(12), length = 2)) ==
      last(range(DayDate(2000), step = Year(1), length = 2))

# Issue 5
lastdaysofmonth = [DayDate(2014, i, daysinmonth(2014, i)) for i = 1:12]
@test [DayDate(2014, 1, 31):Month(1):DayDate(2015);] == lastdaysofmonth

# Range addition/subtraction:
let d = Day(1)
    @test (DayDate(2000):d:DayDate(2001)) + d == (DayDate(2000)+d:d:DayDate(2001)+d)
    @test (DayDate(2000):d:DayDate(2001)) - d == (DayDate(2000)-d:d:DayDate(2001)-d)
end

end  # RangesTest module


module OnionSockets

using Sockets
using DiffieHellman

import Serialization
import SecureIO
import Multiplexers

import Multiplexers: Line
import SecureIO: SecureSerializer

serialize(io::Union{TCPSocket,IOBuffer},msg) = Serialization.serialize(io,msg)
deserialize(io::Union{TCPSocket,IOBuffer}) = Serialization.deserialize(io)

serialize(io::Line,msg) = Multiplexers.serialize(io,msg)
deserialize(io::Line) = Multiplexers.deserialize(io)

serialize(io::SecureSerializer,msg) = SecureIO.serialize(io,msg)
deserialize(io::SecureSerializer) = SecureIO.deserialize(io)

BallotIOs = Union{TCPSocket,IOBuffer,Line,SecureSerializer}

Multiplexers.serialize(io::BallotIOs,msg) = serialize(io,msg)
Multiplexers.deserialize(io::BallotIOs) = deserialize(io)

SecureIO.serialize(io::BallotIOs,msg) = serialize(io,msg)
SecureIO.deserialize(io::BallotIOs) = deserialize(io)

import Sockets.connect
import Sockets.accept
import Sockets.TCPServer

function connect(socket::BallotIOs,id,dh::DH)
    send = x-> serialize(socket,x)
    get = () -> deserialize(socket)

    key,ballotid = diffie(send,get,dh)
    sroutersocket = SecureSerializer(socket,key)
    return sroutersocket
end

connect(port,id,dh::DH) = connect(connect(port),id,dh)

function accept(socket::BallotIOs,members,dh::DH)
    send = x -> serialize(socket,x)
    get = () -> deserialize(socket)

    key,unknownid = hellman(send,get,dh)
    #@assert unknownid==nothing

    securesocket = SecureSerializer(socket,key)
    return securesocket
end

accept(socket::BallotIOs,dh::DH) = accept(socket,nothing,dh)
accept(server::TCPServer,members,dh::DH) = accept(accept(server),members,dh)

export connect, accept, serialize, deserialize

end # module


# Cheke, L. G. and Clayton, N. S., Eurasian jays (Garrulus glandarius) overcome
# their current desires to anticipate two distinct future needs and plan for
# them appropriately, Biology Letters 8:171-175, 2011,
# http://dx.doi.org/10.1098/rsbl.2011.0909
#
# id-bird mapping: 1: Hoy, 2: Wiggins, 3: Hunter, 4: Ainsley
# Wiggins and Ainsley were fed with raisins instead of suet pellets. For
# simplicity I use suet pellets for all birds in this protocol.

function _summarize(::Experiment{:Cheke11_specsat}, data)
	combine(groupby(data, [:prefed, :action, :foodtype]),
				df -> DataFrame(μ = mean(df.items),
                                sem = sem(float.(df.items)),
								n = length(df.items)))
end
function statistical_tests(exp::Experiment{:Cheke11_specsat}, data)
    tests = Test[]
    push!(tests, Test("overall",
                      splitplotanova(data, exp.tests["overall"].locals,
                                     betweenfactors = [],
                                     withinfactors = [:prefed, :foodtype],
                                     id = :id, data = :items),
                      exp.tests))
    for action in ("cache", "eat")
        push!(tests, Test(action,
                          splitplotanova(@where(data, :action .== action),
                                         exp.tests[action].locals,
                                         betweenfactors = [],
                                         withinfactors = [:prefed, :foodtype],
                                         id = :id, data = :items),
                          exp.tests))
    end
    TestCollection(tests)
end
function run!(exp::Experiment{:Cheke11_specsat}, models; N = 4)
	results = DataFrame(prefed = String[], action = String[],
					    foodtype = String[], id = Int[], items = Float64[])
	for id in 1:N
		m = models[id]
		add!(m, MaintenanceDiet)
		for food in (PowderedPeanut, PowderedSuetPellet)
			remove!(m, MaintenanceDiet)
			wait!(m, 2u"hr")
			add!(m, food, 20)
			wait!(m, 15u"minute")
			remove!(m, Any)
			add!(m, Peanut, 40)
			add!(m, SuetPellet, 40)
			tray = Tray(0, rand(1:100))
			add!(m, tray)
			wait!(m, 15u"minute")
			prefed = lowercase(string(food))
			for typ in (Peanut, SuetPellet)
                push!(results, [prefed, "eat", lowercase(string(typ)), id,
								40 - countfooditems(m, typ)])
                push!(results, [prefed, "cache", lowercase(string(typ)), id,
								countcache(tray, typ)])
			end
			remove!(m, Any)
			add!(m, MaintenanceDiet)
			wait!(m, 21.5u"hr")
		end
	end
    results
end

function getfraction_Cheke11_planning(data)
	groups = groupby(data, :foodtype)
	df = join(groups..., on = [:trial, :tray, :id])
	total = df.items .+ df.items_1
	df.fractionprefed = zeros(length(df.items))
	for (i, t) in enumerate(total)
		if t == 0
			df.fractionprefed[i] = NA
		else
			df.fractionprefed[i] = df.items_1[i] / t
		end
	end
	df
end
function _summarize(::Experiment{:Cheke11_planning}, data)
	combine(groupby(data, [:trial, :foodtype, :tray, :action]),
				df -> DataFrame(μ = mean(df.items),
                                sem = sem(float.(df.items)),
								n = length(df.items)))
end
function statistical_tests(exp::Experiment{:Cheke11_planning}, data)
    tests = Test[]
    push!(tests, Test("foodtype & tray",
                      splitplotanova(data,
                                     exp.tests["foodtype & tray"].locals,
                                     betweenfactors = [],
                                     withinfactors = [:tray, :foodtype],
                                     id = :id, data = :items),
                      exp.tests))
    for trial in 1:3
        id = "Tray × foodtype interaction in trial $trial"
        push!(tests, Test(id,
                          splitplotanova(@where(data, :trial .== trial),
                                         exp.tests[id].locals,
                                         betweenfactors = [],
                                         withinfactors = [:tray, :foodtype],
                                         id = :id, data = :items),
                          exp.tests))
    end
    id = "Tray × foodtype interaction in trial 3 without hunter"
    push!(tests, Test(id,
                      splitplotanova(@where(data, :trial .== 3, :id .!= 3),
                                     exp.tests[id].locals,
                                     betweenfactors = [],
                                     withinfactors = [:tray, :foodtype],
                                     id = :id, data = :items),
                      exp.tests))
    TestCollection(tests)
end
function run!(exp::Experiment{:Cheke11_planning}, models; N = 4)
    results = DataFrame(trial = Int[], tray = String[], action = String[],
                        foodtype = String[],
						id = Int[], items = Float64[])
	for id in 1:N
		m = models[id]
		add!(m, MaintenanceDiet)
		for trial in 1:3
			# stage 1
		    trays = [Tray(traypos) for traypos in 1:2]
			remove!(m, MaintenanceDiet)
			wait!(m, 1u"hr")
			if trial == 1
				add!(m, MaintenanceDiet)
			elseif id == 2
				add!(m, PowderedSuetPellet, 20)
			else
				add!(m, PowderedPeanut, 20)
			end
            samefirst = true
			if id <= 2 # Hoy and Wiggins
				samefirst = false
			end
			foodtypeorder = [Peanut, SuetPellet]
			if id == 2
				reverse!(foodtypeorder)
			end
			wait!(m, 15u"minute")
            remove!(m, Any)
			add!(m, Peanut, 40)
			add!(m, SuetPellet, 40)
			add!(m, trays)
			wait!(m, 15u"minute")
			remove!(m, Any)
			add!(m, MaintenanceDiet)
			for traypos in 1:2
				for foodid in 1:2
					push!(results, [trial,
                                    ((samefirst && traypos == 1) || (!samefirst && traypos == 2)) ? "same" : "different",
                                    "cache",
                                    foodid == 1 ? "prefed" : "nonprefed",
									id, countcache(trays[traypos],
													foodtypeorder[foodid])])
				end
			end
			wait!(m, 3u"hr")
			# stage 2 & 3
			for stage in 2:3
				remove!(m, MaintenanceDiet)
				wait!(m, 1u"hr")
				if id == 1 && stage == 2 || id != 1 && stage == 3
					add!(m, PowderedSuetPellet, 20)
				else
					add!(m, PowderedPeanut, 20)
				end
				wait!(m, 15u"minute")
                remove!(m, Any)
                # cover tray 2 in stage 2 and tray 1 in stage 3
				cover!(trays[stage == 2 ? 2 : 1])
				add!(m, trays)
                # retrieve from tray 1 in stage 2 and tray 2 in stage 3
				wait!(m, 15u"minute")
				remove!(m, Any)
				add!(m, MaintenanceDiet)
				uncover!(trays[stage == 2 ? 2 : 1])
				wait!(m, 22.5u"hr")
			end
            pilfer!.(trays)
			wait!(m, 19.5u"hr")
		end
	end
    results
end


# Plot results

export
    plotpcs,
    plotfluctuations

using Gadfly
using Colors
import Cairo

"Dictionary of default colours for plotting."
const default_colours = Dict{String, String}(
    "exprose"=> "#3498DB", # Blue
    "input_1"=> "#E74C3C", # Red
    "input_2"=> "#2ECC71", # Green
    "mod"    => "#FF9900", # Orange
    "extra"  => "#000000", # Black
)

"`Theme` for plots."
plottheme() = Theme(
    background_color=colorant"white",
    panel_stroke=colorant"black",
    major_label_color=colorant"black",
    minor_label_color=colorant"black",
    major_label_font_size=20pt,
    minor_label_font_size=16pt,
    grid_line_width=0mm,
)

"`Theme` for plot points."
pointtheme(colour::AbstractString) = Theme(
    default_color=parse(Colorant, colour),
    point_size=1.5mm,
    highlight_width=0mm,
)

"Plot the projections of an ensemble onto the principal components of the motion."
function plotpcs(out_prefix::AbstractString,
                    pcs::Array{Float64};
                    pcs_ref_one::Array{Float64}=Float64[],
                    pcs_ref_two::Array{Float64}=Float64[],
                    pcs_mod::Array{Float64}=Float64[],
                    pcs_extra::Array{Float64}=Float64[],
                    comps_to_plot::Array{Tuple{Int,Int},1}=[(1,2), (1,3), (2,3)])
    checkfilepath(out_prefix)
    for (i, j) in comps_to_plot
        out_filepath = "$(out_prefix)_$(i)_$j.png"
        @assert length(pcs) > 0 "No projections in array"
        @assert max(i, j) <= size(pcs, 1) "Chose a PC number higher than those available"
        layers = layer(
            x=pcs[i,:],
            y=pcs[j,:],
            Geom.point,
            pointtheme(default_colours["exprose"]),
        )
        pcsaddlayer!(layers, pcs_ref_one, (i, j), default_colours["input_1"])
        pcsaddlayer!(layers, pcs_ref_two, (i, j), default_colours["input_2"])
        pcsaddlayer!(layers, pcs_mod, (i, j), default_colours["mod"])
        pcsaddlayer!(layers, pcs_extra, (i, j), default_colours["extra"])
        p = plot(
            layers...,
            Guide.xlabel("Projections onto PC $i / Å"),
            Guide.ylabel("Projections onto PC $j / Å"),
            plottheme,
        )
        draw(PNG(out_filepath, 20cm, 20cm), p)
    end
    println("Plotted principal components to file(s) \"$(out_prefix)_x_y.png\"")
end

"Adds a layer of points to an existing layer for PC plotting."
function pcsaddlayer!(layers::Array{Layer,1}, pcs::Array{Float64}, comps::Tuple{Int,Int}, colour::AbstractString)
    if length(pcs) > 0
        i, j = comps
        @assert max(i, j) <= size(pcs, 1) "Chose a PC number higher than those available"
        push!(layers, layer(
            x=pcs[i,:],
            y=pcs[j,:],
            Geom.point,
            pointtheme(colour),
        )[1])
    end
end

"""
Plot root mean square fluctuations of an aligned ensemble.
Arguments are the output filepath and the array of fluctuations.
"""
function plotfluctuations(out_filepath::AbstractString,
                    flucs::Array{Float64,1};
                    flucs_mod::Array{Float64,1}=Float64[])
    checkfilepath(out_filepath)
    n_res = length(flucs)
    @assert n_res > 0 "Fluctuation array is empty"
    layers = layer(
        x=collect(1:n_res),
        y=flucs,
        Geom.line,
        pointtheme(default_colours["exprose"]),
    )
    if length(flucs_mod) > 0
        @assert length(flucs_mod) == n_res "Number of fluctuations differ"
        push!(layers, layer(
            x=collect(1:n_res),
            y=flucs_mod,
            Geom.line,
            pointtheme(default_colours["mod"]),
        )[1])
    end
    p = plot(
        layers...,
        Guide.xlabel("Residue index"),
        Guide.ylabel("RMSF / Å"),
        plottheme,
    )
    draw(PNG(out_filepath, 20cm, 15cm), p)
    println("Plotted RMSFs to file \"$out_filepath\"")
end


@testset "test_writer" begin
    @testset "write latitude" begin
        lat = IGCLatitude(51.29971666666667)
        s_expected_lat = "5117983N"
        @test string(lat) == s_expected_lat

        lat = IGCLatitude(-33.94611666666667)
        s_expected_lat = "3356767S"
        @test string(lat) == s_expected_lat
    end

    @testset "write longitude" begin
        long = IGCLongitude(6.956383333333333)
        s_expected_long = "00657383E"
        @test string(long) == s_expected_long

        long = IGCLongitude(-99.71176666666666)
        s_expected_long = "09942706W"
        @test string(long) == s_expected_long
    end

    @testset "write time" begin
        t = Time(16, 2, 45)
        s_expected_time = "160245"
        @test string(t, IGCTimeFormat) == s_expected_time
    end

    @testset "write date" begin
        d = Date(2019, 8, 20)
        s_expected_date = "200819"
        @test string(d, IGCDateFormat) == s_expected_date
    end

    @testset "write pressure altitude" begin
        alt = IGCPressureAltitude(288)
        s_expected_alt = "00288"
        @test string(alt) == s_expected_alt
    end

    @testset "write GPS altitude" begin
        alt = IGCGpsAltitude(429)
        s_expected_alt = "00429"
        @test string(alt) == s_expected_alt
    end

    @testset "write FixValidity" begin
        fv = FixValidity.Fix3D
        io = IOBuffer()
        FixValidity.write(io, fv)
        @test String(take!(io)) == "A"
        @test FixValidity.string(fv) == "A"

        fv = FixValidity.Fix2D
        io = IOBuffer()
        FixValidity.write(io, fv)
        @test String(take!(io)) == "V"
        @test FixValidity.string(fv) == "V"
    end

    @testset "write A record" begin
        rec = A_record(
            "XXX",
            "ABC",
            "FLIGHT:1"
        )
        s_expected_rec = "AXXXABC FLIGHT:1"
        @test string(rec) == s_expected_rec
    end

    @testset "write B record" begin
        rec = B_record(
            Time(16, 2, 45),
            IGCLatitude(51.118766666666666),
            IGCLongitude(-1.8216666666666668),
            FixValidity.parse('A'),
            IGCPressureAltitude(288),
            IGCGpsAltitude(429),
            36,
            "19509020"
        )
        s_expected_rec = "B1602455107126N00149300WA002880042919509020"
        @test string(rec) == s_expected_rec
    end

    @testset "write C record task info" begin
        rec = C_record_task_info(
            Date(2001, 7, 15),  # declaration_date
            Time(21, 38, 41),  # declaration_time
            Date(2001, 7, 16),  # flight_date
            "0001",  # number
            2,  # num_turnpoints
            "500K Tri"  # description
        )
        s_expected_rec = "C150701213841160701000102 500K Tri"
        @test string(rec) == s_expected_rec
    end

    @testset "write C record waypoint info" begin
        rec = C_record_waypoint_info(
            IGCLatitude(51.18931666666667),  # latitude
            IGCLongitude(-1.03165),  # longitude
            "Lasham Clubhouse"  # description
        )
        s_expected_rec = "C5111359N00101899W Lasham Clubhouse"
        @test string(rec) == s_expected_rec
    end

    @testset "write GpsQualifier" begin
        qual = GpsQualifier.GPS
        io = IOBuffer()
        GpsQualifier.write(io, qual)
        @test String(take!(io)) == "1"

        qual = GpsQualifier.DGPS
        io = IOBuffer()
        GpsQualifier.write(io, qual)
        @test String(take!(io)) == "2"
    end

    @testset "write D record" begin
        rec = D_record(
            GpsQualifier.DGPS,  # qualifier
            "0331"  # station_id
        )
        s_expected_rec = "D20331"
        @test string(rec) == s_expected_rec

        rec = D_record(
            GpsQualifier.GPS,  # qualifier
            "9999"  # station_id
        )
        s_expected_rec = "D19999"
        @test string(rec) == s_expected_rec
    end

    @testset "write E record" begin
        rec = E_record(
            Time(16, 2, 45),  # time
            "PEV",  # tlc
            ""  # extension_string
        )
        s_expected_rec = "E160245PEV"
        @test string(rec) == s_expected_rec
    end

    @testset "write F record" begin
        rec = F_record(
            Time(16, 2, 40),  # time
            ["04", "06", "09", "12", "36", "24", "22", "18", "21"]  # satellites
        )
        s_expected_rec = "F160240040609123624221821"
        @test string(rec) == s_expected_rec
    end

    @testset "write G record" begin
        rec = G_record(
            "REJNGJERJKNJKRE31895478537H43982FJN9248F942389T433T"
        )
        s_expected_rec = "GREJNGJERJKNJKRE31895478537H43982FJN9248F942389T433T"
        @test string(rec) == s_expected_rec
    end

    @testset "write I record" begin
        rec = I_record([
            IGCExtension(36:38, "FXA"),
            IGCExtension(39:40, "SIU"),
            IGCExtension(41:43, "ENL")
        ])
        s_expected_rec = "I033638FXA3940SIU4143ENL"
        @test string(rec) == s_expected_rec
    end

    @testset "write J record" begin
        rec = J_record([
            IGCExtension(8:12, "HDT")
        ])
        s_expected_rec = "J010812HDT"
        @test string(rec) == s_expected_rec
    end

    @testset "write K record" begin
        rec = K_record(
            Time(16, 2, 48),
            "00090",
            7  # tofix: is it really necessary?
        )
        s_expected_rec = "K16024800090"
        @test string(rec) == s_expected_rec
    end

    @testset "write L record" begin
        rec = L_record(
            "XXX",
            "RURITANIAN STANDARD NATIONALS DAY 1"
        )
        s_expected_rec = "LXXXRURITANIAN STANDARD NATIONALS DAY 1"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record fix accuracy" begin
        rec = H_record_FiXAccuracy(35)
        s_expected_rec = "HFFXA035"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record utc date" begin
        rec = H_record_DaTE(2001, 7, 16)
        s_expected_rec = "HFDTE160701"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record pilot" begin
        rec = H_record_PiLoT("Bloggs Bill D")
        s_expected_rec = "HFPLTPILOTINCHARGE:Bloggs Bill D"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record copilot" begin
        rec = H_record_Copilot("Smith-Barry John A")
        s_expected_rec = "HFCM2CREW2:Smith-Barry John A"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record glider type" begin
        rec = H_record_GliderType("Schleicher ASH-25")
        s_expected_rec = "HFGTYGLIDERTYPE:Schleicher ASH-25"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record glider registration" begin
        rec = H_record_GliderRegistration("ABCD-1234")
        s_expected_rec = "HFGIDGLIDERID:ABCD-1234"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record gps datum" begin
        rec = H_record_GpsDatum("WGS-1984")
        s_expected_rec = "HFDTM100GPSDATUM:WGS-1984"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record firmware revision" begin
        rec = H_record_FirmwareRevision("6.4")
        s_expected_rec = "HFRFWFIRMWAREVERSION:6.4"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record hardware revision" begin
        rec = H_record_HardwareRevision("3.0")
        s_expected_rec = "HFRHWHARDWAREVERSION:3.0"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record manufacturer model" begin
        rec = H_record_ManufacturerModel("Manufacturer", "Model")
        s_expected_rec = "HFFTYFRTYPE:Manufacturer,Model"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record gps receiver" begin
        rec = H_record_GpsReceiver(
            "MarconiCanada",
            "Superstar",
            12,
            MaxAlt(10000, "m")
        )
        s_expected_rec = "HFGPS:MarconiCanada,Superstar,12ch,max10000m"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record pressure sensor" begin
        rec = H_record_PressureAltitudeSensor(
            "Sensyn",
            "XYZ1111",
            MaxAlt(11000, "m")
        )
        s_expected_rec = "HFPRSPRESSALTSENSOR:Sensyn,XYZ1111,max11000m"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record competition id" begin
        rec = H_record_CompetitionId("XYZ-78910")
        s_expected_rec = "HFCIDCOMPETITIONID:XYZ-78910"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record competition class" begin
        rec = H_record_CompetitionClass("15m Motor Glider")
        s_expected_rec = "HFCCLCOMPETITIONCLASS:15m Motor Glider"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record time zone offset" begin
        rec = H_record_TimeZoneOffset(3 * 60)
        s_expected_rec = "HFTZNTIMEZONE:3"
        @test string(rec) == s_expected_rec

        rec = H_record_TimeZoneOffset(-(3 * 60 + 30))
        s_expected_rec = "HFTZNTIMEZONE:-3.30"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record mop sensor" begin
        rec = H_record_MeansOfPropulsion("MOP-(SN:1,ET=1375,0,1375,0,3.05V,p=0),Ver:0")
        s_expected_rec = "HFMOPSENSOR:MOP-(SN:1,ET=1375,0,1375,0,3.05V,p=0),Ver:0"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record site" begin
        rec = H_record_Site("lk15comp")
        s_expected_rec = "HFSITSite:lk15comp"
        @test string(rec) == s_expected_rec
    end

    @testset "write H record units of measure" begin
        rec = H_record_UnitsOfMeasure(["km", "ft", "kt"])
        s_expected_rec = "HFUNTUnits:km,ft,kt"
        @test string(rec) == s_expected_rec
    end

    @testset "write IGCDocument" begin
        io = IOBuffer()  # this buffer is filled at each update! call

        igcdoc = IGCDocument(stream = io, parsing_mode = IGC.ParsingMode.STRICT)

        update!(igcdoc, A_record("XXX", "ABC", "FLIGHT:1"))
        s_expected = """AXXXABC FLIGHT:1"""

        update!(igcdoc, H_record_FiXAccuracy('F', 35))
        update!(igcdoc, H_record_DaTE('F', Date(2001, 07, 16)))
        update!(igcdoc, H_record_PiLoT('F', "Bloggs Bill D"))
        update!(igcdoc, H_record_Copilot('F', "Smith-Barry John A"))
        update!(igcdoc, H_record_GliderType('F', "Schleicher ASH-25"))
        update!(igcdoc, H_record_GliderRegistration('F', "ABCD-1234"))
        update!(igcdoc, H_record_GpsDatum('F', "WGS-1984"))
        update!(igcdoc, H_record_FirmwareRevision('F', "6.4"))
        update!(igcdoc, H_record_HardwareRevision('F', "3.0"))
        update!(igcdoc, H_record_ManufacturerModel('F', "Manufacturer", "Model"))
        update!(igcdoc, H_record_GpsReceiver('F', "MarconiCanada", "Superstar", 12, MaxAlt(10000, "m")))
        update!(igcdoc, H_record_PressureAltitudeSensor('F', "Sensyn", "XYZ1111", MaxAlt(11000, "m")))
        update!(igcdoc, H_record_CompetitionId('F', "XYZ-78910"))
        update!(igcdoc, H_record_CompetitionClass('F', "15m Motor Glider"))
        s_expected = s_expected * EOL_DEFAULT * """HFFXA035
HFDTE160701
HFPLTPILOTINCHARGE:Bloggs Bill D
HFCM2CREW2:Smith-Barry John A
HFGTYGLIDERTYPE:Schleicher ASH-25
HFGIDGLIDERID:ABCD-1234
HFDTM100GPSDATUM:WGS-1984
HFRFWFIRMWAREVERSION:6.4
HFRHWHARDWAREVERSION:3.0
HFFTYFRTYPE:Manufacturer,Model
HFGPS:MarconiCanada,Superstar,12ch,max10000m
HFPRSPRESSALTSENSOR:Sensyn,XYZ1111,max11000m
HFCIDCOMPETITIONID:XYZ-78910
HFCCLCOMPETITIONCLASS:15m Motor Glider"""

        rec = I_record([IGCExtension(36:38, "FXA"), IGCExtension(39:40, "SIU"), IGCExtension(41:43, "ENL")])
        update!(igcdoc, rec)
        s_expected = s_expected * EOL_DEFAULT * "I033638FXA3940SIU4143ENL"

        rec = J_record([IGCExtension(8:12, "HDT")])
        update!(igcdoc, rec)
        s_expected = s_expected * EOL_DEFAULT * "J010812HDT"

        rec = C_record_task_info(Date(2001, 7, 15), Time(21, 38, 41), Date(2001, 7, 16), "0001", 2, "500K Tri")
        update!(igcdoc, rec)
        s_expected = s_expected * EOL_DEFAULT * """C150701213841160701000102 500K Tri"""

        records = [
            C_record_waypoint_info(IGCLatitude(51.18931666666667), IGCLongitude(-1.03165), "Lasham Clubhouse"),
            C_record_waypoint_info(IGCLatitude(51.16965), IGCLongitude(-1.0440666666666667), "Lasham Start S, Start"),
            C_record_waypoint_info(IGCLatitude(52.15153333333333), IGCLongitude(-2.9204499999999998), "Sarnesfield, TP1"),
            C_record_waypoint_info(IGCLatitude(52.50245), IGCLongitude(-0.2935333333333333), "Norman Cross, TP2"),
            C_record_waypoint_info(IGCLatitude(51.16965), IGCLongitude(-1.0440666666666667), "Lasham Start S, Finish"),
            C_record_waypoint_info(IGCLatitude(51.18931666666667), IGCLongitude(-1.03165), "Lasham Clubhouse")
        ]
        update!(igcdoc, records)
        s_expected = s_expected * EOL_DEFAULT * """C5111359N00101899W Lasham Clubhouse
C5110179N00102644W Lasham Start S, Start
C5209092N00255227W Sarnesfield, TP1
C5230147N00017612W Norman Cross, TP2
C5110179N00102644W Lasham Start S, Finish
C5111359N00101899W Lasham Clubhouse"""

        rec = F_record(Time(16, 02, 40), ["04", "06", "09", "12", "36", "24", "22", "18", "21"])
        update!(igcdoc, rec)
        s_expected = s_expected * EOL_DEFAULT * "F160240040609123624221821"

        records = [
            B_record(Time(16, 02, 40), IGCLatitude(54.11868333333334), IGCLongitude(-2.8223666666666665), FixValidity.Fix3D, IGCPressureAltitude(280), IGCGpsAltitude(421), 36, "20509950"),
            D_record(GpsQualifier.DGPS, "0331"),
            E_record(Time(16, 02, 45), "PEV", ""),
            B_record(Time(16, 02, 45), IGCLatitude(51.118766666666666), IGCLongitude(-1.8216666666666668), FixValidity.Fix3D, IGCPressureAltitude(288), IGCGpsAltitude(429), 36, "19509020"),
            B_record(Time(16, 02, 50), IGCLatitude(51.1189), IGCLongitude(-1.8213833333333334), FixValidity.Fix3D, IGCPressureAltitude(290), IGCGpsAltitude(432), 36, "21009015"),
            B_record(Time(16, 02, 55), IGCLatitude(51.119), IGCLongitude(-1.82035), FixValidity.Fix3D, IGCPressureAltitude(290), IGCGpsAltitude(430), 36, "20009012"),
            F_record(Time(16, 03, 00), ["06", "09", "12", "36", "24", "22", "18", "21"]),
            B_record(Time(16, 03, 00), IGCLatitude(51.119166666666665), IGCLongitude(-1.8200333333333334), FixValidity.Fix3D, IGCPressureAltitude(291), IGCGpsAltitude(432), 36, "25608009"),
            E_record(Time(16, 03, 05), "PEV", ""),
            B_record(Time(16, 03, 05), IGCLatitude(51.11966666666667), IGCLongitude(-1.81975), FixValidity.Fix3D, IGCPressureAltitude(291), IGCGpsAltitude(435), 36, "21008015"),
            B_record(Time(16, 03, 10), IGCLatitude(51.1202), IGCLongitude(-1.8195666666666668), FixValidity.Fix3D, IGCPressureAltitude(293), IGCGpsAltitude(435), 36, "19608024"),
            K_record(Time(16, 02, 48), "00090", 7),
            B_record(Time(16, 02, 48), IGCLatitude(51.120333333333335), IGCLongitude(-1.8191666666666666), FixValidity.Fix3D, IGCPressureAltitude(494), IGCGpsAltitude(436), 36, "19008018"),
            B_record(Time(16, 02, 52), IGCLatitude(51.122166666666665), IGCLongitude(-1.8187833333333334), FixValidity.Fix3D, IGCPressureAltitude(496), IGCGpsAltitude(439), 36, "19508015")
        ]
        update!(igcdoc, records)
        s_expected = s_expected * EOL_DEFAULT * """B1602405407121N00249342WA002800042120509950
D20331
E160245PEV
B1602455107126N00149300WA002880042919509020
B1602505107134N00149283WA002900043221009015
B1602555107140N00149221WA002900043020009012
F1603000609123624221821
B1603005107150N00149202WA002910043225608009
E160305PEV
B1603055107180N00149185WA002910043521008015
B1603105107212N00149174WA002930043519608024
K16024800090
B1602485107220N00149150WA004940043619008018
B1602525107330N00149127WA004960043919508015"""

        records = [
            L_record("XXX", "RURITANIAN STANDARD NATIONALS DAY 1"),
            L_record("XXX", "FLIGHT TIME: 4:14:25, TASK SPEED:58.48KTS")
        ]
        update!(igcdoc, records)
        s_expected = s_expected * EOL_DEFAULT * """LXXXRURITANIAN STANDARD NATIONALS DAY 1
LXXXFLIGHT TIME: 4:14:25, TASK SPEED:58.48KTS"""

        records = [
            G_record("REJNGJERJKNJKRE31895478537H43982FJN9248F942389T433T"),
            G_record("JNJK2489IERGNV3089IVJE9GO398535J3894N358954983O0934"),
            G_record("SKTO5427FGTNUT5621WKTC6714FT8957FGMKJ134527FGTR6751"),
            G_record("K2489IERGNV3089IVJE39GO398535J3894N358954983FTGY546"),
            G_record("12560DJUWT28719GTAOL5628FGWNIST78154INWTOLP7815FITN")
        ]
        update!(igcdoc, records)
        s_expected = s_expected * EOL_DEFAULT * """GREJNGJERJKNJKRE31895478537H43982FJN9248F942389T433T
GJNJK2489IERGNV3089IVJE9GO398535J3894N358954983O0934
GSKTO5427FGTNUT5621WKTC6714FT8957FGMKJ134527FGTR6751
GK2489IERGNV3089IVJE39GO398535J3894N358954983FTGY546
G12560DJUWT28719GTAOL5628FGWNIST78154INWTOLP7815FITN"""
        @test String(take!(io)) == s_expected

        # this buffer is filled only at end of file
        io_at_end = IOBuffer()
        write(io_at_end, igcdoc)
        #@test String(take!(io_at_end)) == s_expected
    end

end


# point = Point(locaslID, x, y, left, right, flag_1, flag_2, nbhs, conn, nx, ny, prim, flux_res, q, dq, entropy, xpos_nbhs, xneg_nbhs, ypos_nbhs, yneg_nbhs, xpos_conn, xneg_conn, ypos_conn, yneg_conn, delta)

function setNormals(self::Point, n)
    self.nx = n[1]
    self.ny = n[2]
    return nothing
end

function getxy(self)
    return (self.x, self.y)
end

function setConnectivity(self, conn)
    self.xpos_conn = conn[1]
    self.xpos_nbhs = length(conn[1])
    self.xneg_conn = conn[2]
    self.xneg_nbhs = length(conn[2])
    self.ypos_conn = conn[3]
    self.ypos_nbhs = length(conn[3])
    self.yneg_conn = conn[4]
    self.yneg_nbhs = length(conn[4])
    return nothing
end

# function setSmallestPointDistance(self::Point, distance)
#     self.short_distance = distance
# end


module BanditOpt

"""
BanditOpt is a Julia package for experimenting with various Multi-Armed Bandit algorithms.
"""
BanditOpt

"""
BanditOpt.Agents module provides implementations of various multi-arm bandit algorithms and an
unified API to access those algorithms. This includes base types for implementing various types
of agents and the common interface.
"""
module Agents
    using Distributions
    using LaTeXStrings
    using Printf
    include( "Agents/AgentBase.jl" )
    include( "Agents/StationaryAgentBase.jl" )

    export
        # Methods
        get_arm_index, update_reward!, reset!, info_str,
        show,
        # Agents
        AgentBase,
        # Stationary Agents
        StationaryAgentBase,
        epsGreedy, epsNGreedy
end

"""
    Arms

`Arms` module includes the different types of reward processes available for
Bandit algorithms. It define the following base types for creating new reward
processes.
- [Arms.ArmBase](@ref)
- [Arms.StationaryArmBase](@ref)
- [Arms.NonstationaryArmBase](@ref)
The types and methods in `Arms` are not exported and need to be called e.g as
`Arms.pull!()`
"""
module Arms
    using Distributions
    include( "Arms/ArmBase.jl" )
    include( "Arms/StationaryArms.jl" )
    include( "Arms/NonStationaryArms.jl" )

    # export
    #     # Methods
    #     pull!, tick!, reset!,
    #     # Arm Models
    #     ArmBase,
    #     # Stationary Arm Models
    #     StationaryArmBase,
    #     # Non-stationary Arm Models
    #     NonstationaryArmBase
end

# -----------------------------  Exports ----------------------------------
using .Agents
using .Arms

export
    # Agents Module
    Agents,
        # Methods
        get_arm_index, update_reward!, reset!, info_str, show,
        # Agents
        AgentBase,
        # Stationary Agents
        StationaryAgentBase,
        epsGreedy, epsNGreedy,
    # Arms Module
    Arms
        # # Methods
        # pull!, tick!, reset!,
        # # Arm Models
        # ArmBase,
        # # Stationary Arm Models
        # StationaryArmBase,
        # # Non Staionary Arm Models
        # NonstationaryArmBase

end # module


struct SimulationResults
    global_index::MAPPING_DICT
    bus_lookup::Dict{Int, Int}
    system::PSY.System
    time_log::Dict{Symbol, Any}
    solution::SciMLBase.AbstractODESolution
    function SimulationResults(
        inputs::SimulationInputs,
        system::PSY.System,
        time_log,
        solution,
    )
        new(make_global_state_map(inputs), get_lookup(inputs), system, time_log, solution)
    end
end

get_global_index(res::SimulationResults) = res.global_index
get_bus_count(res::SimulationResults) = get_n_buses(res.system)
get_bus_lookup(res::SimulationResults) = res.bus_lookup
get_system(res::SimulationResults) = res.system
get_solution(res::SimulationResults) = res.solution

"""
Internal function to obtain as a Vector of Float64 of a specific state. It receives the solution and the
global index for a state.

"""
function _post_proc_state_series(solution, ix::Int, dt::Union{Nothing, Float64})
    if dt === nothing
        ix_t = unique(i -> solution.t[i], eachindex(solution.t))
        ts = solution.t[ix_t]
        state = solution[ix, ix_t]
    else
        ts = range(0, stop = solution.t[end], step = dt)
        state = solution(collect(ts); idxs = ix)
    end
    return ts, state
end

"""
Function to obtain the state time series of a specific state. It receives the simulation, and a tuple
containing the name of the Dynamic Device and the symbol of the state.
"""
function post_proc_state_series(
    res::SimulationResults,
    ref::Tuple{String, Symbol},
    dt::Union{Nothing, Float64},
)
    global_state_index = get_global_index(res)
    ix = get(global_state_index[ref[1]], ref[2], 0)
    return _post_proc_state_series(get_solution(res), ix, dt)
end

"""
Function to obtain voltage and output currents for a dynamic device. It receives the simulation, and the name
of the Dynamic Device.
"""
function post_proc_voltage_current_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)::NTuple{5, Vector{Float64}}
    #Note: Type annotation since get_dynamic_injector is type unstable and solution is Union{Nothing, DAESol}
    system = get_system(res)
    bus_lookup = get_bus_lookup(res)
    n_buses = length(bus_lookup)
    solution = res.solution
    device = PSY.get_component(PSY.StaticInjection, system, name)
    if isnothing(device)
        error("Device $(name) not found in the system")
    end
    bus_ix = get(bus_lookup, PSY.get_number(PSY.get_bus(device)), -1)
    ts, V_R, V_I = post_proc_voltage_series(solution, bus_ix, n_buses, dt)
    dyn_device = PSY.get_dynamic_injector(device)
    _, I_R, I_I = compute_output_current(res, dyn_device, V_R, V_I, dt)
    return ts, V_R, V_I, I_R, I_I
end

"""
Function to obtain voltage using the bus index (and not the bus number). It receives the solution, the bus index and
the total number of buses.

"""
function post_proc_voltage_series(
    solution,
    bus_ix::Int,
    n_buses::Int,
    dt::Union{Nothing, Float64},
)
    bus_ix < 0 && error("Bus number $(bus_number) not found.")
    ts, V_R = _post_proc_state_series(solution, bus_ix, dt)
    _, V_I = _post_proc_state_series(solution, bus_ix + n_buses, dt)
    return collect(ts), collect(V_R), collect(V_I)
end

"""
Function to compute the real current output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_real_current_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    ts, _, _, I_R, _ = post_proc_voltage_current_series(res, name, dt)
    return ts, I_R
end
"""
Function to compute the imaginary current output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_imaginary_current_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    ts, _, _, _, I_I = post_proc_voltage_current_series(res, name, dt)
    return ts, I_I
end

"""
Function to compute the active power output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_activepower_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    ts, V_R, V_I, I_R, I_I = post_proc_voltage_current_series(res, name, dt)
    return ts, V_R .* I_R + V_I .* I_I
end

"""
Function to compute the active power output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_reactivepower_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    ts, V_R, V_I, I_R, I_I = post_proc_voltage_current_series(res, name, dt)
    return ts, V_I .* I_R - V_R .* I_I
end

"""
Function to compute the field current output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_field_current_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    system = get_system(res)
    bus_lookup = get_bus_lookup(res)
    n_buses = length(bus_lookup)
    solution = res.solution
    device = PSY.get_component(PSY.StaticInjection, system, name)
    bus_ix = get(bus_lookup, PSY.get_number(PSY.get_bus(device)), -1)
    ts, V_R, V_I = post_proc_voltage_series(solution, bus_ix, n_buses, dt)
    dyn_device = PSY.get_dynamic_injector(device)
    _, I_fd = compute_field_current(res, dyn_device, V_R, V_I, dt)
    return ts, I_fd
end

"""
Function to compute the field voltage output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_field_voltage_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    system = get_system(res)
    device = PSY.get_component(PSY.StaticInjection, system, name)
    dyn_device = PSY.get_dynamic_injector(device)
    ts, Vf = compute_field_voltage(res, dyn_device, dt)
    return ts, Vf
end

"""
    get_state_series(
        res::SimulationResults,
        ref::Tuple{String, Symbol};
        dt::Union{Nothing, Float64} = nothing
    )
    end

Function to obtain series of states out of DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `ref:Tuple{String, Symbol}` : Tuple used to identify the dynamic device, via its name, as a `String`, and the associated state as a `Symbol`.
"""
function get_state_series(res::SimulationResults, ref::Tuple{String, Symbol}; dt = nothing)
    return post_proc_state_series(res, ref, dt)
end

"""
    get_voltage_magnitude_series(
        res::SimulationResults,
        bus_number::Int
    )

Function to obtain the voltage magnitude series out of the DAE Solution.

# Arguments:

- `res::SimulationResults` : Simulation Results object that contains the solution
- `bus_number::Int` : Bus number identifier
"""
function get_voltage_magnitude_series(res::SimulationResults, bus_number::Int; dt = nothing)
    n_buses = get_bus_count(res)
    bus_ix = get(get_bus_lookup(res), bus_number, 0)
    ts, V_R, V_I = post_proc_voltage_series(res.solution, bus_ix, n_buses, dt)
    return ts, sqrt.(V_R .^ 2 .+ V_I .^ 2)
end

"""
    get_voltage_angle_series(
        res::SimulationResults,
        bus_number::Int
    )

Function to obtain the voltage angle series out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `bus_number::Int` : Bus number identifier
"""
function get_voltage_angle_series(res::SimulationResults, bus_number::Int; dt = nothing)
    n_buses = get_bus_count(res)
    bus_ix = get(get_bus_lookup(res), bus_number, 0)
    ts, V_R, V_I = post_proc_voltage_series(res.solution, bus_ix, n_buses, dt)
    return ts, atan.(V_I, V_R)
end

"""
    get_real_current_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the real current time series of a Dynamic Injection series out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_real_current_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_real_current_series(res, name, dt)
end

"""
    get_imaginary_current_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the imaginary current time series of a Dynamic Injection series out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_imaginary_current_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_imaginary_current_series(res, name, dt)
end

"""
    get_activepower_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the active power output time series of a Dynamic Injection series out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_activepower_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_activepower_series(res, name, dt)
end

"""
    get_reactivepower_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the reactive power output time series of a Dynamic Injection series out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_reactivepower_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_reactivepower_series(res, name, dt)
end

"""
    get_field_current_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the field current time series of a Dynamic Generator out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_field_current_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_field_current_series(res, name, dt)
end

"""
    get_field_voltage_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the field voltage time series of a Dynamic Generator out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_field_voltage_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_field_voltage_series(res, name, dt)
end

"""
    show_states_initial_value(res::SimulationResults)

Function to print initial states.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
"""
function show_states_initial_value(res::SimulationResults)
    bus_size = get_bus_count(res)
    system = get_system(res)
    x0_init = res.solution.u[1]
    println("Voltage Variables")
    println("====================")
    buses_sorted =
        sort(collect(PSY.get_components(PSY.Bus, system)); by = x -> PSY.get_number(x))
    for bus in buses_sorted
        name = PSY.get_name(bus)
        println(name)
        println("====================")
        bus_n = PSY.get_number(bus)
        bus_ix = get_bus_lookup(res)[bus_n]
        V_R = x0_init[bus_ix]
        V_I = x0_init[bus_ix + bus_size]
        Vm = sqrt(V_R^2 + V_I^2)
        θ = angle(V_R + V_I * 1im)
        print("Vm ", round(Vm, digits = 4), "\n")
        print("θ ", round(θ, digits = 4), "\n")
        println("====================")
    end
    println("====================")
    for device in PSY.get_components(PSY.DynamicInjection, system)
        states = PSY.get_states(device)
        name = PSY.get_name(device)
        println("Differential States")
        println(name)
        println("====================")
        global_index = get_global_index(res)[name]
        for s in states
            print(s, " ", round(x0_init[global_index[s]], digits = 4), "\n")
        end
        println("====================")
    end
    dyn_branches = PSY.get_components(PSY.DynamicBranch, system)
    if !isempty(dyn_branches)
        println("====================")
        println("Line Current States")
        for br in dyn_branches
            states = PSY.get_states(br)
            name = PSY.get_name(br)
            printed_name = "Line " * name
            println("====================")
            println(printed_name)
            global_index = get_global_index(res)[name]
            for (i, s) in enumerate(states)
                print(s, " ", round(x0_init[global_index[s]], digits = 5), "\n")
            end
            println("====================")
        end
    end
    return
end


"""
genetrap.jl

Fit GRSM models to live cell and smFISH data
"""

"""
Gene information
"""
const genes = ["CANX";"DNAJC5";"ERRFI1";"KPNB1";"MYH9";"Rab7a";"RHOA";"RPAP3";"Sec16A";"SLC2A1"]
const genelength = Dict([("Sec16A", 42960);("SLC2A1", 33802);("ERRFI1", 14615);("RHOA", 52948);("KPNB1", 33730);("MYH9", 106741);("DNAJC5", 40930);("CANX", 32710);("Rab7a", 88663);("RPAP3", 44130)])
const MS2end = Dict([("Sec16A", 5220);("SLC2A1", 26001);("ERRFI1", 5324);("RHOA", 51109);("KPNB1", 24000);("MYH9", 71998);("DNAJC5", 14857);("CANX", 4861);("Rab7a", 83257);("RPAP3", 38610)])
const halflife = Dict([("CANX", 50.),("DNAJC5", 5.),("ERRFI1", 1.35),("KPNB1", 9.),("MYH9", 10.),("Rab7a", 50.),("RHOA", 50.),("RPAP3", 7.5),("Sec16A", 8.),("SLC2A1", 5.)])

"""
genetrap()

Load data, model and option structures for metropolis-hastings fit
root is the folder containing data and results

FISH counts is divided by tempfish to adjust relative weights of data
set tempfish = 0 to equalize FISH and live cell counts
"""
function genetrap(root::String,infolder::String,rinchar::String,label::String,gene::String,G::Int,R::Int,nalleles::Int,type::String,maxtime::Float64,samplesteps::Int,method=1,temp=1.,tempfish=1.,warmupsteps=0,annealsteps=0)
    r = readrates_genetrap(root,infolder,rinchar,gene,"$G$R",type)
    println(r)
    genetrap(root,r,label,gene,G,R,nalleles,type,maxtime,samplesteps,method,temp,tempfish,annealsteps,warmupsteps)
end

function genetrap(root,r,label::String,gene::String,G::Int,R::Int,nalleles::Int,type::String,maxtime::Float64,samplesteps::Int,method,temp=1.,tempfish=1.,warmupsteps=0,annealsteps=0)
    data = data_genetrap(root,label,gene,tempfish)
    model = model_genetrap(r,gene,G,R,nalleles,type,method)
    options = MHOptions(samplesteps,annealsteps,warmupsteps,maxtime,temp)
    return data,model,options
end

function data_genetrap(root,label,gene,tempfish=1.)
    LC = readLCPDF_genetrap(root,gene)
    if tempfish == 0
        counts = Int(div(sum(LC[:,2]+LC[:,3]),2))
        println(counts)
        # set FISH counts to mean of live cell counts
        histFISH = readFISH_genetrap(root,gene,counts)
    else
        histFISH = readFISH_genetrap(root,gene,tempfish)
    end
    RNALiveCellData(label,gene,LC[:,1],LC[:,3],LC[:,2],length(histFISH),histFISH)
end

"""
model_genetrap

load model structure
"""
function model_genetrap(r,gene::String,G::Int,R::Int,nalleles::Int,type::String,method,propcv=[.02*ones(2*(G-1));1e-4;1e-4*ones(R);.02*ones(R)],fittedparam=collect(1:num_rates(G,R)-1),Gprior=(.01,.5),Sprior=(.1,.5),Rcv=1e-2)
    rm,rcv = prior_rates_genetrap(G,R,gene,Gprior,Sprior,Rcv)
    # d = priordistributionLogNormal_genetrap(rm,rcv,G,R)
    d = priordistributionGamma_genetrap(rm,rcv,G,R)
    if r == 0
        r = rm
    else
        r = r[:,1]
    end
    GRSMmodel{typeof(r),typeof(d),typeof(propcv),typeof(fittedparam),typeof(method)}(G,R,nalleles,type,r,d,propcv,fittedparam,method)
    # GRSMmodel(G,R,nalleles,type,r,d,prop,fittedparam,method)
end

"""
setpriorrate(G,R,gene,Gprior::Tuple,Sprior::Tuple,Rcv::Float64)
Set prior distribution for mean and cv of rates
"""
function prior_rates_genetrap(G,R,gene,Gprior::Tuple,Sprior::Tuple,Rcv::Float64)
    n = G-1
    rm = [fill(Gprior[1],2*n);2000/(genelength[gene]-MS2end[gene]);fill(2000/MS2end[gene]*R,R);fill(Sprior[1], R);log(2.)/(60 .* halflife[gene])]
    rcv = [fill(Gprior[2],2*n);Rcv;Rcv;fill(Sprior[2], R);Rcv]
    return rm,rcv
end

"""
priordistributionLogNormal_genetrap(r,cv,G,R)

LogNormal Prior distribution
"""
function priordistributionLogNormal_genetrap(r,cv,G,R)
    sigma = sigmalognormal(cv)
    d = []
    j = 1
    #  G rates
    for i in Grange(G)
        push!(d,Distributions.LogNormal(log(r[i]),sigma[j]))
        j += 1
    end
    # initiation rate
    i = initiation(G)
    push!(d,Distributions.LogNormal(log(r[i]),sigma[j]))
    j += 1
    # Step rates are bounded by length of insertion site to end of gene, i.e sum of reciprocal rates is bounded
    t = sum(1 ./ r[Rrange(G,R)])
    push!(d,Distributions.LogNormal(-log(t),sigma[j]))
    j += 1
    # priors for splice rates
    rs = 0.
    for i in Srange(G,R)
        rs += r[i]
        push!(d,Distributions.LogNormal(log(rs),sigma[j]))
        j += 1
    end
    return d
end

"""
priordistributionGamma_genetrap(r,cv,G,R)

Gamma Prior distribution
"""
function priordistributionGamma_genetrap(rm,cv,G,R)
    n = G-1
    zet = R
    d = []
    rsig = cv .^ 2
    j = 1
    # priors for G rates
    for i in Grange(G)
        theta = rsig[j]*rm[i]
        alpha = 1/rsig[j]
        push!(d,Distributions.Gamma(alpha,theta))
        j += 1
    end
    # prior for nu1 is upper bounded by rm, i.e. distance is bounded by distance to insertion site
    theta = rsig[j]*rm[initiation(G)]
    alpha = 1/rsig[j]
    push!(d,Distributions.Gamma(alpha,theta))
    j += 1
    # priors for nu rates are bounded by length of insertion site to end of gene, i.e sum of reciprocal rates is bounded
    tm = sum(1 ./ rm[Rrange(G,R)])
    # for i in 2*n + 2 : 2*n + zet + 1
    #     tm += 1/rm[i]
    # end
    theta = rsig[j]/tm
    alpha = 1/rsig[j]
    push!(d,Distributions.Gamma(alpha,theta))
    j += 1
    # priors for ejection rates
    rs = 0
    for i in Srange(G,R)
        rs += rm[i]
        theta = rsig[j]*rs
        alpha = 1/rsig[j]
        push!(d,Distributions.Gamma(alpha,theta))
        j += 1
    end
    return d
end

"""
readLCPDF_genetrap(root,gene)

Read in dwell time PDF
"""
function readLCPDF_genetrap(root,gene)
    infile = joinpath(root,"DwellTimePDF/$(gene)_PDF.csv")
    if isfile(infile)
        LC = readdlm(infile,',')
        x = truncate_histogram(LC[:,2],.999,1000)
        LC[1:length(x),:]
    else
        println("No data for gene: ", gene)
    end
end

"""
readFISH_genetrap(root,gene,temp=1.,clone=true)

Read and assemble smFISH data
"""
function readFISH_genetrap(root::String,gene::String,temp::Float64=1.,clone=true)
    counts = countsFISH_genetrap(root,gene,clone)
    readFISH_genetrap(root,gene,Int(div(counts,temp)),clone)
end

function readFISH_genetrap(root::String,gene::String,counts::Int,clone=true)
    fishfile=joinpath(root,"Fish_4_Genes/$(gene)_steady_state_mRNA.csv")
    col = clone ? 3 : 2
    # Get smFISH RNA histograms
    if isfile(fishfile)
        x = readdlm(fishfile,',')[3:end,col]
        x = x[x .!= ""]
        x = truncate_histogram(x,.99,1000)
        x /= sum(x)
        x *= counts
        return x
    else
        println("No smFISH data for gene: ", gene)
    end
    nothing
end

"""
countsFISH_genetrap(root,gene,x,counts,clone=true)

read in smFISH counts
"""
function countsFISH_genetrap(root,gene,clone=true)
    countfile = joinpath(root,"counts/total_counts.csv")
    wttext = "WT_"
    clonetext = "_clone"
    if isfile(countfile)
        countsdata = readdlm(countfile,',')[:,:]
        if clone
            counts=(countsdata[findall(uppercase("$(gene)$clonetext") .== uppercase.(countsdata[:,1])),2])[1]
        else
            counts=(countsdata[findall("$wttext$(gene)" .== countsdata[:,1]),2])[1]
        end
    else
        counts = 1000
    end
    return counts
end

"""
readrates_genetrap(infolder::String,rinchar::String,gene::String,model::String,type::String)

Read in initial rates from previous runs
"""
function readrates_genetrap(root::String,infolder::String,rinchar::String,gene::String,model::String,type::String)
    if rinchar == "rml" || rinchar == "rlast"
        rskip = 1
        rstart = 1
    elseif rinchar == "rmean"
        rskip = 2
        rstart = 1
    elseif rinchar == "rquant"
        rskip = 3
        rstart = 2
    end
    if type == "offeject" || type == "on"
        type = ""
    end
    # infile = "$resultpath/$infolder/$(gene)/$rinchar$model$type$txtstr"
    infile = getratefolder_genetrap(root,infolder,rinchar,gene,model,type)
    print(gene," ",model," ",type)
    readrates_genetrap(infile,rstart,rskip)
end

function readrates_genetrap(infile::String,rstart::Int,rskip::Int)
    if isfile(infile) && ~isempty(read(infile))
        r = readdlm(infile)
        r = r[:,rstart:rskip:end]
        println("")
        return r
    else
        println(" no prior")
        return 0
    end
    nothing
end

function getratefolder_genetrap(root::String,infolder::String,rinchar::String,gene::String,model::String,type::String)
    results = joinpath(root,"Results")
    infolder = joinpath(infolder,gene)
    infolder = joinpath(infolder,rinchar * model * type * txtstr)
    joinpath(results,infolder)

end


module SCRIPT

BUFFER = zeros(Float32,128^3*400)

function init(inputs::Dict{Symbol,Any})
	FILE = Base.@__FILE__
  println("Script: $(basename(FILE))")
  global BUFFER = zeros(Float32,128^3*6*40)
end

function update()
end

function render()
end

end #SCRIPT


export BrickletAnalogInV3VoltageCallbackConfiguration
struct BrickletAnalogInV3VoltageCallbackConfiguration
    period::Integer
    value_has_to_change::Bool
    option::Char
    min::Integer
    max::Integer
end

export BrickletAnalogInV3Calibration
struct BrickletAnalogInV3Calibration
    offset::Integer
    multiplier::Integer
    divisor::Integer
end

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

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

export BrickletAnalogInV3
"""
Measures DC voltage between 0V and 42V
"""
mutable struct BrickletAnalogInV3 <: TinkerforgeDevice
    ipcon::IPConnection
    deviceInternal::PyObject

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

        return new(ipcon, deviceInternal)
    end
end

export get_voltage
"""
    $(SIGNATURES)

Returns the measured voltage. The resolution is approximately 10mV to 1mV
depending on the oversampling configuration (:func:`Set Oversampling`).

If you want to get the value periodically, it is recommended to use the
:cb:`Voltage` callback. You can set the callback configuration
with :func:`Set Voltage Callback Configuration`.
"""
function get_voltage(device::BrickletAnalogInV3)
    return device.deviceInternal.get_voltage()
end

export set_voltage_callback_configuration
"""
    $(SIGNATURES)

The period is the period with which the :cb:`Voltage` 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.

It is furthermore possible to constrain the callback with thresholds.

The `option`-parameter together with min/max sets a threshold for the :cb:`Voltage` callback.

The following options are possible:

.. csv-table::
 :header: "Option", "Description"
 :widths: 10, 100

 "'x'",    "Threshold is turned off"
 "'o'",    "Threshold is triggered when the value is *outside* the min and max values"
 "'i'",    "Threshold is triggered when the value is *inside* or equal to the min and max values"
 "'<'",    "Threshold is triggered when the value is smaller than the min value (max is ignored)"
 "'>'",    "Threshold is triggered when the value is greater than the min value (max is ignored)"

If the option is set to 'x' (threshold turned off) the callback is triggered with the fixed period.
"""
function set_voltage_callback_configuration(device::BrickletAnalogInV3, period, value_has_to_change, option, min, max)
    device.deviceInternal.set_voltage_callback_configuration(period, value_has_to_change, option, min, max)
end

export get_voltage_callback_configuration
"""
    $(SIGNATURES)

Returns the callback configuration as set by :func:`Set Voltage Callback Configuration`.
"""
function get_voltage_callback_configuration(device::BrickletAnalogInV3)
    return device.deviceInternal.get_voltage_callback_configuration()
end

export set_oversampling
"""
    $(SIGNATURES)

Sets the oversampling between 32x and 16384x. The Bricklet
takes one 12bit sample every 17.5µs. Thus an oversampling
of 32x is equivalent to an integration time of 0.56ms and
a oversampling of 16384x is equivalent to an integration
time of 286ms.

The oversampling uses the moving average principle. A
new value is always calculated once per millisecond.

With increased oversampling the noise decreases. With decreased
oversampling the reaction time increases (changes in voltage will be
measured faster).
"""
function set_oversampling(device::BrickletAnalogInV3, oversampling)
    device.deviceInternal.set_oversampling(oversampling)
end

export get_oversampling
"""
    $(SIGNATURES)

Returns the oversampling value as set by :func:`Set Oversampling`.
"""
function get_oversampling(device::BrickletAnalogInV3)
    return device.deviceInternal.get_oversampling()
end

export set_calibration
"""
    $(SIGNATURES)

Sets a calibration for the measured voltage value.
The formula for the calibration is as follows::

 Calibrated Value = (Value + Offset) * Multiplier / Divisor

We recommend that you use the Brick Viewer to calibrate
the Bricklet. The calibration will be saved internally and only
has to be done once.
"""
function set_calibration(device::BrickletAnalogInV3, offset, multiplier, divisor)
    device.deviceInternal.set_calibration(offset, multiplier, divisor)
end

export get_calibration
"""
    $(SIGNATURES)

Returns the calibration as set by :func:`Set Calibration`.
"""
function get_calibration(device::BrickletAnalogInV3)
    return device.deviceInternal.get_calibration()
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::BrickletAnalogInV3)
    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::BrickletAnalogInV3, 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::BrickletAnalogInV3)
    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::BrickletAnalogInV3, 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::BrickletAnalogInV3, 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::BrickletAnalogInV3, 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::BrickletAnalogInV3)
    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::BrickletAnalogInV3)
    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::BrickletAnalogInV3)
    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::BrickletAnalogInV3, 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::BrickletAnalogInV3)
    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::BrickletAnalogInV3)
    return device.deviceInternal.get_identity()
end

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


function minimize!(objective::ObjWrapper, x0, scheme, options=OptOptions())
    minimize!(objective, (x0, nothing), (scheme, NWI()), options)
end
function minimize!(objective::ObjWrapper, x0, approach::Tuple{<:Any, <:TRSPSolver}, options=OptOptions())
    minimize!(objective, (x0, nothing), approach, options)
end
function minimize!(objective::ObjWrapper, state0::Tuple, scheme, options=OptOptions())
    minimize!(objective, state0, (scheme, NWI()), options)
end
function minimize!(objective::ObjWrapper, state0::Tuple, approach::Tuple{<:Any, <:TRSPSolver}, options=OptOptions())
    tr_minimize!(objective, state0, approach, options)
end
include("optimize/inplace_loop.jl")
include("optimize/outofplace_loop.jl")


#===================================
Define GNNGraph type as a subtype of Graphs' AbstractGraph.
For the core methods to be implemented by any AbstractGraph, see
https://juliagraphs.org/Graphs.jl/latest/types/#AbstractGraph-Type
https://juliagraphs.org/Graphs.jl/latest/developing/#Developing-Alternate-Graph-Types
=============================================#

const COO_T = Tuple{T, T, V} where {T <: AbstractVector{<:Integer}, V}
const ADJLIST_T = AbstractVector{T} where T <: AbstractVector{<:Integer}
const ADJMAT_T = AbstractMatrix
const SPARSE_T = AbstractSparseMatrix # subset of ADJMAT_T
const CUMAT_T = Union{CUDA.AnyCuMatrix, CUDA.CUSPARSE.CuSparseMatrix}

"""
    GNNGraph(data; [graph_type, ndata, edata, gdata, num_nodes, graph_indicator, dir])
    GNNGraph(g::GNNGraph; [ndata, edata, gdata])

A type representing a graph structure that also stores
feature arrays associated to nodes, edges, and the graph itself.

A `GNNGraph` can be constructed out of different `data` objects
expressing the connections inside the graph. The internal representation type
is determined by `graph_type`.

When constructed from another `GNNGraph`, the internal graph representation
is preserved and shared. The node/edge/graph features are retained
as well, unless explicitely set by the keyword arguments
`ndata`, `edata`, and `gdata`.

A `GNNGraph` can also represent multiple graphs batched togheter
(see [`Flux.batch`](@ref) or [`SparseArrays.blockdiag`](@ref)).
The field `g.graph_indicator` contains the graph membership
of each node.

`GNNGraph`s are always directed graphs, therefore each edge is defined
by a source node and a target node (see [`edge_index`](@ref)).
Self loops (edges connecting a node to itself) and multiple edges
(more than one edge between the same pair of nodes) are supported.

A `GNNGraph` is a Graphs.jl's `AbstractGraph`, therefore it supports most
functionality from that library.

# Arguments

- `data`: Some data representing the graph topology. Possible type are
    - An adjacency matrix
    - An adjacency list.
    - A tuple containing the source and target vectors (COO representation)
    - A Graphs' graph.
- `graph_type`: A keyword argument that specifies
                the underlying representation used by the GNNGraph.
                Currently supported values are
    - `:coo`. Graph represented as a tuple `(source, target)`, such that the `k`-th edge
              connects the node `source[k]` to node `target[k]`.
              Optionally, also edge weights can be given: `(source, target, weights)`.
    - `:sparse`. A sparse adjacency matrix representation.
    - `:dense`. A dense adjacency matrix representation.
    Defaults to `:coo`, currently the most supported type.
- `dir`: The assumed edge direction when given adjacency matrix or adjacency list input data `g`.
        Possible values are `:out` and `:in`. Default `:out`.
- `num_nodes`: The number of nodes. If not specified, inferred from `g`. Default `nothing`.
- `graph_indicator`: For batched graphs, a vector containing the graph assigment of each node. Default `nothing`.
- `ndata`: Node features. A named tuple of arrays whose last dimension has size `num_nodes`.
- `edata`: Edge features. A named tuple of arrays whose last dimension has size `num_edges`.
- `gdata`: Graph features. A named tuple of arrays whose last dimension has size `num_graphs`.

# Usage.

```julia
using Flux, GraphNeuralNetworks

# Construct from adjacency list representation
data = [[2,3], [1,4,5], [1], [2,5], [2,4]]
g = GNNGraph(data)

# Number of nodes, edges, and batched graphs
g.num_nodes  # 5
g.num_edges  # 10
g.num_graphs # 1

# Same graph in COO representation
s = [1,1,2,2,2,3,4,4,5,5]
t = [2,3,1,4,5,3,2,5,2,4]
g = GNNGraph(s, t)

# From a Graphs' graph
g = GNNGraph(erdos_renyi(100, 20))

# Add 2 node feature arrays
g = GNNGraph(g, ndata = (x=rand(100, g.num_nodes), y=rand(g.num_nodes)))

# Add node features and edge features with default names `x` and `e`
g = GNNGraph(g, ndata = rand(100, g.num_nodes), edata = rand(16, g.num_edges))

g.ndata.x
g.ndata.e

# Send to gpu
g = g |> gpu

# Collect edges' source and target nodes.
# Both source and target are vectors of length num_edges
source, target = edge_index(g)
```
"""
struct GNNGraph{T<:Union{COO_T,ADJMAT_T}} <: AbstractGraph{Int}
    graph::T
    num_nodes::Int
    num_edges::Int
    num_graphs::Int
    graph_indicator       # vector of ints or nothing
    ndata::NamedTuple
    edata::NamedTuple
    gdata::NamedTuple
end

@functor GNNGraph

function GNNGraph(data;
                        num_nodes = nothing,
                        graph_indicator = nothing,
                        graph_type = :coo,
                        dir = :out,
                        ndata = (;),
                        edata = (;),
                        gdata = (;),
                        )

    @assert graph_type ∈ [:coo, :dense, :sparse] "Invalid graph_type $graph_type requested"
    @assert dir ∈ [:in, :out]

    if graph_type == :coo
        graph, num_nodes, num_edges = to_coo(data; num_nodes, dir)
    elseif graph_type == :dense
        graph, num_nodes, num_edges = to_dense(data; dir)
    elseif graph_type == :sparse
        graph, num_nodes, num_edges = to_sparse(data; dir)
    end

    num_graphs = !isnothing(graph_indicator) ? maximum(graph_indicator) : 1

    ndata = normalize_graphdata(ndata, default_name=:x, n=num_nodes)
    edata = normalize_graphdata(edata, default_name=:e, n=num_edges, duplicate_if_needed=true)
    gdata = normalize_graphdata(gdata, default_name=:u, n=num_graphs)

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

function GNNGraph(n::T; graph_type=:coo, kws...) where {T<:Integer}
    s, t = T[], T[]
    return GNNGraph(s, t; graph_type, num_nodes=n, kws...)
end

# COO convenience constructors
GNNGraph(s::AbstractVector, t::AbstractVector, v = nothing; kws...) = GNNGraph((s, t, v); kws...)
GNNGraph((s, t)::NTuple{2}; kws...) = GNNGraph((s, t, nothing); kws...)

# GNNGraph(g::AbstractGraph; kws...) = GNNGraph(adjacency_matrix(g, dir=:out); kws...)

function GNNGraph(g::AbstractGraph; kws...)
    s = Graphs.src.(Graphs.edges(g))
    t = Graphs.dst.(Graphs.edges(g))
    if !Graphs.is_directed(g)
        # add reverse edges since GNNGraph is directed
        s, t = [s; t], [t; s]
    end
    GNNGraph((s, t); num_nodes=Graphs.nv(g), kws...)
end

function GNNGraph(g::GNNGraph; ndata=g.ndata, edata=g.edata, gdata=g.gdata, graph_type=nothing)

    ndata = normalize_graphdata(ndata, default_name=:x, n=g.num_nodes)
    edata = normalize_graphdata(edata, default_name=:e, n=g.num_edges, duplicate_if_needed=true)
    gdata = normalize_graphdata(gdata, default_name=:u, n=g.num_graphs)

    if !isnothing(graph_type)
        if graph_type == :coo
            graph, num_nodes, num_edges = to_coo(g.graph; g.num_nodes)
        elseif graph_type == :dense
            graph, num_nodes, num_edges = to_dense(g.graph)
        elseif graph_type == :sparse
            graph, num_nodes, num_edges = to_sparse(g.graph)
        end
        @assert num_nodes == g.num_nodes
        @assert num_edges == g.num_edges
    else
        graph = g.graph
    end
    GNNGraph(graph,
            g.num_nodes, g.num_edges, g.num_graphs,
            g.graph_indicator,
            ndata, edata, gdata)
end

function Base.show(io::IO, g::GNNGraph)
    println(io, "GNNGraph:
    num_nodes = $(g.num_nodes)
    num_edges = $(g.num_edges)")
    g.num_graphs > 1 && println("num_graphs = $(g.num_graphs)")
    if !isempty(g.ndata)
        println(io, "    ndata:")
        for k in keys(g.ndata)
            println(io, "        $k => $(size(g.ndata[k]))")
        end
    end
    if !isempty(g.edata)
        println(io, "    edata:")
        for k in keys(g.edata)
            println(io, "        $k => $(size(g.edata[k]))")
        end
    end
    if !isempty(g.gdata)
        println(io, "    gdata:")
        for k in keys(g.gdata)
            println(io, "        $k => $(size(g.gdata[k]))")
        end
    end
end

### StatsBase/LearnBase compatibility
StatsBase.nobs(g::GNNGraph) = g.num_graphs
LearnBase.getobs(g::GNNGraph, i) = getgraph(g, i)

# Flux's Dataloader compatibility. Related PR https://github.com/FluxML/Flux.jl/pull/1683
Flux.Data._nobs(g::GNNGraph) = g.num_graphs
Flux.Data._getobs(g::GNNGraph, i) = getgraph(g, i)

#########################
Base.:(==)(g1::GNNGraph, g2::GNNGraph) = all(k -> getfield(g1,k)==getfield(g2,k), fieldnames(typeof(g1)))


using MIPforQP

using MathOptInterface
const MOI = MathOptInterface
const MOIU = MathOptInterface.Utilities

using JuMP, BARON, Gurobi, LinearAlgebra

const TIME_LIMIT = 5 * 60.0

const NUM_LAYERS = 3
const SOC_LOWER_BOUND = false
const EPIGRAPH = false
const FAMILIES = ("small", "basic", "extended", "extended2")

const RESULT_PATH = joinpath(@__DIR__, "result.csv")
const ROOT_DIR = joinpath(@__DIR__, "box_qp", "data")

function gurobi_direct_factory(cutoff)
    return JuMP.optimizer_with_attributes(
        Gurobi.Optimizer,
        "TimeLimit" => TIME_LIMIT,
        "NonConvex" => 2,
        "Cutoff" => cutoff
    )
end
function gurobi_direct_mipfocus_factory(cutoff, mipfocus::Int)
    return JuMP.optimizer_with_attributes(
        Gurobi.Optimizer,
        "TimeLimit" => TIME_LIMIT,
        "NonConvex" => 2,
        "MIPFocus" => mipfocus,
        "Cutoff" => cutoff
    )
end
function gurobi_mip_factory(cutoff)
    return JuMP.optimizer_with_attributes(
        Gurobi.Optimizer,
        "TimeLimit" => TIME_LIMIT,
        "MIPFocus" => 3,
        "Cutoff" => cutoff
    )
end
function baron_direct_factory(cutoff)
    return JuMP.optimizer_with_attributes(
        BARON.Optimizer,
        # NOTE: To plug in CPLEX as LP/MIP solver, uncomment the following line and fill in the correct path.
        # "CplexLibName" => "/path/to/cplex/library",
        "MaxTime" => TIME_LIMIT,
        "CutOff" => cutoff
    )
end
const primal_factory = JuMP.optimizer_with_attributes(
    Gurobi.Optimizer,
    "TimeLimit" => TIME_LIMIT,
    "NonConvex" => 2,
    "MIPFocus" => 1
)

function nn_bridge(model)
    MIPforQP.formulate_quadratics!(
        model,
        MIPforQP.Reformulation(MIPforQP.NeuralNetRelaxation(NUM_LAYERS), SOC_LOWER_BOUND)
    )
    return
end
function hongbo_bridge(model)
    MIPforQP.formulate_quadratics!(
        model,
        MIPforQP.Reformulation(MIPforQP.HongboRelaxation(NUM_LAYERS), SOC_LOWER_BOUND)
    )
    return
end

const FACTORIES_AND_BRIDGES = Dict(
    ("gurobi", "direct") => (gurobi_direct_factory, identity),
    ("gurobi_mf_3", "direct") => (co -> gurobi_direct_mipfocus_factory(co, 3), identity),
    ("gurobi", "nn")     => (gurobi_mip_factory, nn_bridge),
    ("gurobi", "hongbo") => (gurobi_mip_factory, hongbo_bridge),
    ("baron", "direct")  => (baron_direct_factory, identity),
)

const CONFIGURATIONS = collect(keys(FACTORIES_AND_BRIDGES))

const CONFIGS_TO_ANALYZE = [
    ("gurobi", "direct"),
    ("gurobi_mf_3", "direct"),
    ("gurobi", "nn"),
    ("gurobi", "hongbo"),
    ("baron", "direct"),
]


# Readinfo types
# A container for storage of extracted fastq info
struct FastqInfo
	quality::Float64
	length::Int64
	gc::Float64
end

# A container for storage of extracted BAM info
struct BAMInfo
	quality::Float64
	length::Int64
	identity::Float64
	gc::Float64
end


#=

running tribe model from the command line

Ignacio Quintero Mächler

t(-_-t)

August 17 2017
=#

"""
    parse_commandline()

Parses command line arguments to be used in compete
"""
function parse_commandline()

  s = ArgParseSettings(prog        = "Tapestree",
                       description = "Inference of competition on trait evolution along biogeographic history",
                       version     = "0.1",
                       add_version = true)

  @add_arg_table s begin

    # optional arguments
    "--niter", "-i"
      help = "number of mcmc iterations"
      arg_type = Int64
      default = 500000

    "--nburn", "-b"
      help = "number of iterations in burning and tuning phase"
      arg_type = Int64
      default = 500000

    "--nthin", "-t"
      help = "sample every nthin iterations"
      arg_type = Int64
      default = 1000

    "--min_dt", "-d"
      help = "minimum percentage of discrete sampling"
      arg_type = Float64
      default = 0.01

    "--comp_x_prior"
      help = "gaussian prior for trait competition"
      arg_type = Tuple{Float64,Float64}
      default = 0.0, 10.0
      dest_name = "ωxprior"

    "--comp_gain_prior"
      help = "gaussian prior for effect of competition on area gain"
      arg_type = Tuple{Float64,Float64}
      default = 0.0, 10.0
      dest_name = "ω1prior"

    "--comp_loss_prior"
      help = "gaussian prior for effect of competition on area loss"
      arg_type = Tuple{Float64,Float64}
      default = 0.0, 10.0
      dest_name = "ω0prior"

    "--sigma_prior"
      help = "exponential prior for rate of evolution"
      arg_type = Float64
      default = 0.1
      dest_name = "σ²prior"

    "--lambda_prior"
      help = "exponential prior for gain and loss rates"
      arg_type = Float64
      default = 0.1
      dest_name = "λprior"

    "--stbrl"
      help = "stem branch length"
      arg_type = Float64
      default = 1.0

    # flags
    "--fix_biocomp", "-f"
      help = "fix to 0 competition effect on gain and losses"
      action = :store_true
      dest_name = "fix_ω1_ω0"

    # required arguments
    "tree_file"
      help = "full path to phylogenetic tree file"
      required = true
    "data_file"
      help = "full path to data file"
      required = true
    "out_file"
      help = "full path to write file"
      required = true

  end

  parsed_args = parse_args(s, as_symbols = true)

  println("Parsed args:")
  for (arg,val) in parsed_args
      println("  $arg  =>  $val")
  end

  return parsed_args
end

# parse commands
comp_args = parse_commandline()

using Tapestree

# run compete
tribe(comp_args[:tree_file],
      comp_args[:data_file],
      comp_args[:out_file];
      filter((u,v) -> u != :tree_file &&
                      u != :data_file &&
                      u != :out_file, comp_args)...)