module RNGTest

    using Random

    import Base: convert, getindex, pointer

    import Distributed: pmap

    include("../deps/deps.jl")

    swrite = Ref{Ptr{Ptr{Bool}}}()

    function __init__()
        swrite[] = cglobal(("swrite_Basic", libtestu01), Ptr{Bool})
        unsafe_store!(swrite[], 0, 1)
    end

    # WrappedRNG

    # TestU01 expects a standard function as input (created via
    # cfunction here). When one wants to test the random stream of
    # an AbstractRNG, a little more work has to be done before
    # passing it to TestU01. The type WrappedRNG wraps an
    # AbstractRNG into an object which knows which type of random
    # numbers to produce, and also whether the scalar or array API
    # should be used (this is useful when different algorithms are
    # used for each case, which results in different streams which
    # should be tested separately). Such an object can then be
    # passed to the Unif01 constructor.

    const TestableNumbers = Union{Int8, UInt8, Int16, UInt16, Int32, UInt32,
        Int64, UInt64, Int128, UInt128, Float16, Float32, Float64}

    mutable struct WrappedRNG{T<:TestableNumbers, RNG<:AbstractRNG}
        rng::RNG
        cache::Vector{T}
        fillarray::Bool
        vals::Vector{UInt32}
        idx::Int
    end

    function WrappedRNG(rng::RNG, ::Type{T}, fillarray = true, cache_size = 3*2^11 ÷ sizeof(T)) where {RNG, T}
        if T <: Integer && cache_size*sizeof(T) % sizeof(UInt32) != 0
            error("cache_size must be a multiple of $(Int(4/sizeof(T))) (for type $T)")
        elseif T === Float16 && cache_size % 6 != 0 || T === Float32 && cache_size % 3 != 0
            error("cache_size must be a multiple of 3 (resp. 6) for Float32 (resp. Float16)")
        end
        cache = Vector{T}(undef, cache_size)
        fillcache(WrappedRNG{T, RNG}(rng, cache, fillarray,
            unsafe_wrap(Array, convert(Ptr{UInt32}, pointer(cache)), sizeof(cache)÷sizeof(UInt32)),
            0)) # 0 is a dummy value, which will be set correctly by fillcache
    end

    # The ability to play with the cache size and the fillarray option is for advanced uses,
    # when one wants to test different code path of the particular RNG implementations, like
    # MersenneTwister from Base.
    # For now let's document only the type parameter in the wrap function:
    wrap(rng::AbstractRNG, ::Type{T}) where {T<:TestableNumbers} = WrappedRNG(rng, T)

    function fillcache(g::WrappedRNG{T}) where T
        if g.fillarray
            rand!(g.rng, g.cache)
        else
            for i = 1:length(g.cache)
                @inbounds g.cache[i] = rand(g.rng, T)
            end
        end
        g.idx = 0
        return g
    end

    function (g::WrappedRNG{T})() where T<:Integer
        g.idx+1 > length(g.vals) && fillcache(g)
        @inbounds return g.vals[g.idx+=1]
    end

    function (g::WrappedRNG{Float64})()
        g.idx+1 > length(g.cache) && fillcache(g)
        @inbounds return g.cache[g.idx+=1]
    end

    function (g::WrappedRNG{Float32})()
        g.idx+3 > length(g.cache) && fillcache(g)
        @inbounds begin
            f = Float64(g.cache[g.idx+1])
            # a Float32 has 24 bits of precision, but only 23 bit of entropy
            f += Float64(g.cache[g.idx+2])/exp2(23)
            f += Float64(g.cache[g.idx+=3])/exp2(46)
            return f % 1.0
        end
    end

    function (g::WrappedRNG{Float16})()
        g.idx+6 > length(g.cache) && fillcache(g)
        @inbounds begin
            f = Float64(g.cache[g.idx+1])
            # a Float16 has 10 bits of entropy
            f += Float64(g.cache[g.idx+2])/exp2(10)
            f += Float64(g.cache[g.idx+3])/exp2(20)
            f += Float64(g.cache[g.idx+4])/exp2(30)
            f += Float64(g.cache[g.idx+5])/exp2(40)
            f += Float64(g.cache[g.idx+=6])/exp2(50)
            return f % 1.0
        end
    end

    # RNGGenerator struct
    mutable struct Unif01
        ptr::Ptr{Cvoid}
        gentype::Type
        name::String
        function Unif01(f::Function, genname)
            for i in 1:100
                tmp = f()
                if typeof(tmp) != Float64 error("Function must return Float64") end
                if tmp < 0 || tmp > 1 error("Function must return values on [0,1]") end
            end
            cf = @cfunction($f, Float64, ())
            # TestU01 crashed if two unif01 object are generated. The only safe thing is to explicitly delete the object when used instead of using finalizers
            return new(ccall((:unif01_CreateExternGen01, libtestu01), Ptr{Cvoid}, (Ptr{UInt8}, Ptr{Cvoid}), genname, cf), Float64, genname)
        end

        function Unif01(g::WrappedRNG{T}, genname) where {T<:AbstractFloat}
            # we assume that g being created out of an AbstractRNG, it produces Floats in the interval [0,1)
            cf = @cfunction($g, Float64, ())
            return new(ccall((:unif01_CreateExternGen01, libtestu01), Ptr{Cvoid}, (Ptr{UInt8}, Ptr{Cvoid}), genname, cf), Float64, genname)
        end

        function Unif01(g::WrappedRNG{T}, genname) where {T<:Integer}
            @assert Cuint === UInt32
            cf = @cfunction($g, UInt32, ())
            return new(ccall((:unif01_CreateExternGenBits, libtestu01), Ptr{Cvoid}, (Ptr{UInt8}, Ptr{Cvoid}), genname, cf), UInt32, genname)
        end
    end
    function delete(obj::Unif01)
            if obj.gentype === Float64
                ccall((:unif01_DeleteExternGen01, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
            else
                ccall((:unif01_DeleteExternGenBits, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
            end
    end

    const RNGGenerator = Union{Function, WrappedRNG}

    # Result types

    ## gofw
    struct Gotw_TestArray
        data::Vector{Float64}
        Gotw_TestArray() = new(Vector{Float64}(undef, 11))
    end
    function getindex(obj::Gotw_TestArray, i::Symbol)
        i == :KSP && return obj.data[1]
        i == :KSM && return obj.data[2]
        i == :KS && return obj.data[3]
        i == :AD && return obj.data[4]
        i == :CM && return obj.data[5]
        i == :WG && return obj.data[6]
        i == :WU  && return obj.data[7]
        i == :Mean && return obj.data[8]
        i == :Var && return obj.data[9]
        i == :Cor && return obj.data[10]
        i == :Sum && return obj.data[11]
        throw(BoundsError())
    end

    for (t, sCreate, sDelete, sPval) in
        ((:ResPoisson, :sres_CreatePoisson, :sres_DeletePoisson, :getPValPoisson),
         (:MarsaRes, :smarsa_CreateRes, :smarsa_DeleteRes, :getPValSmarsa),
         (:KnuthRes2, :sknuth_CreateRes2, :sknuth_DeleteRes2, :getPValRes2))
        @eval begin
            # The types
            mutable struct $t
                ptr::Ptr{Cvoid}
                function $(t)()
                    res = new(ccall(($(string(sCreate)), libtestu01), Ptr{Cvoid}, (), ))
                    finalizer(delete, res)
                    return res
                end
            end
            # Finalizers
            function delete(obj::$t)
                ccall(($(string(sDelete)), libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
            end
            # pvalue extractors
            pvalue(obj::$t) = ccall(($(string(sPval)), libtestu01extractors), Float64, (Ptr{Cvoid},), obj.ptr)
        end
    end

    # Sres
    # Basic
    ## Type
    mutable struct ResBasic
        ptr::Ptr{Cvoid}
        function ResBasic()
            res = new(ccall((:sres_CreateBasic, libtestu01), Ptr{Cvoid}, (), ))
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::ResBasic)
        ccall((:sres_DeleteBasic, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::ResBasic)
        res = Gotw_TestArray()
        ccall((:getPValBasic, libtestu01extractors), Cvoid, (Ptr{Cvoid}, Ptr{Float64}), obj.ptr, res.data)
        return res
    end

    # Chi2
    ## Type
    mutable struct ResChi2
        ptr::Ptr{Cvoid}
        N::Int
        function ResChi2(N::Integer)
            res = new(ccall((:sres_CreateChi2, libtestu01), Ptr{Cvoid}, (), ), N)
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::ResChi2)
        ccall((:sres_DeleteChi2, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::ResChi2)
        res = Gotw_TestArray()
        ccall((:getPValChi2, libtestu01extractors), Cvoid, (Ptr{Cvoid}, Ptr{Float64}), obj.ptr, res.data)
        return res[obj.N == 1 ? :Mean : :Sum]
    end

    # sknuth
    ## Type
    mutable struct KnuthRes1
        ptr::Ptr{Cvoid}
        N::Int
        function KnuthRes1(N::Integer)
            res = new(ccall((:sknuth_CreateRes1, libtestu01), Ptr{Cvoid}, (), ), N)
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::KnuthRes1)
        ccall((:sknuth_DeleteRes1, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::KnuthRes1)
        chi = Gotw_TestArray()
        bas = Gotw_TestArray()
        ccall((:getPValRes1, libtestu01extractors), Cvoid, (Ptr{Cvoid}, Ptr{Float64}, Ptr{Float64}), obj.ptr, chi.data, bas.data)
        return chi[obj.N == 1 ? :Mean : :Sum], bas[obj.N == 1 ? :Mean : :AD]
    end

    # smarsa
    ## Type
    mutable struct MarsaRes2
        ptr::Ptr{Cvoid}
        N::Int
        function MarsaRes2(N::Integer)
            res = new(ccall((:smarsa_CreateRes2, libtestu01), Ptr{Cvoid}, (), ), N)
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::MarsaRes2)
        ccall((:smarsa_DeleteRes2, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::MarsaRes2)
        res = Gotw_TestArray()
        ccall((:getPValSmarsa2, libtestu01extractors), Cvoid, (Ptr{Cvoid}, Ptr{Float64}), obj.ptr, res.data)
        return obj.N == 1 ? res[:Mean] : res[:Sum]
    end

    # Walk
    ## Type
    mutable struct WalkRes
        ptr::Ptr{Cvoid}
        N::Int
        function WalkRes(N::Integer)
            res = new(ccall((:swalk_CreateRes, libtestu01), Ptr{Cvoid}, (), ), N)
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::WalkRes)
        ccall((:swalk_DeleteRes, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::WalkRes)
        pH = Gotw_TestArray()
        pM = Gotw_TestArray()
        pJ = Gotw_TestArray()
        pR = Gotw_TestArray()
        pC = Gotw_TestArray()
        ccall((:getPVal_Walk, libtestu01extractors), Cvoid, (Ptr{Cvoid}, Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{Float64}), obj.ptr, pH.data, pM.data, pJ.data, pR.data, pC.data)
        return obj.N == 1 ? (pH[:Mean], pM[:Mean], pJ[:Mean], pR[:Mean], pC[:Mean]) : (pH[:Sum], pM[:Sum], pJ[:Sum], pR[:Sum], pC[:Sum])
    end

    # Npairs
    struct Snpair_StatArray
        data::Vector{Float64}
        Snpair_StatArray() = new(Vector{Float64}(undef, 11))
    end
    function getindex(obj::Snpair_StatArray, i::Symbol)
        i == :NP && return obj.data[1]
        i == :NPS && return obj.data[2]
        i == :NPPR && return obj.data[3]
        i == :mNP && return obj.data[4]
        i == :mNP1 && return obj.data[5]
        i == :mNP1S && return obj.data[6]
        i == :mNP2  && return obj.data[7]
        i == :mNP2S && return obj.data[8]
        i == :NJumps && return obj.data[9]
        i == :BB && return obj.data[10]
        i == :BM && return obj.data[11]
        throw(BoundsError())
    end
    ## Type
    mutable struct NpairRes
        ptr::Ptr{Cvoid}
        N::Int
        function NpairRes(N::Integer)
            res = new(ccall((:snpair_CreateRes, libtestu01), Ptr{Cvoid}, (), ), N)
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::NpairRes)
        ccall((:snpair_DeleteRes, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::NpairRes)
        res = Snpair_StatArray()
        ccall((:getPVal_Npairs, libtestu01extractors), Cvoid, (Ptr{Cvoid}, Ptr{Float64}), obj.ptr, res.data)
        return obj.N == 1 ? (res[:NP], res[:mNP]) : (res[:NP], res[:mNP1], res[:mNP2], res[:NJumps])
    end

    # scomp
    ## Type
    mutable struct CompRes
        ptr::Ptr{Cvoid}
        N::Int
        function CompRes(N::Integer)
            res = new(ccall((:scomp_CreateRes, libtestu01), Ptr{Cvoid}, (), ), N)
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::CompRes)
        ccall((:scomp_DeleteRes, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::CompRes)
        num = Gotw_TestArray()
        size = Gotw_TestArray()
        ccall((:getPValScomp, libtestu01extractors), Cvoid, (Ptr{Cvoid}, Ptr{Float64}, Ptr{Float64}), obj.ptr, num.data, size.data)
        return num[obj.N == 1 ? :Mean : :Sum], size[obj.N == 1 ? :Mean : :Sum]
    end

    # sspectral
    ## Type
    mutable struct SpectralRes
        ptr::Ptr{Cvoid}
        function SpectralRes()
            res = new(ccall((:sspectral_CreateRes, libtestu01), Ptr{Cvoid}, (), ))
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::SpectralRes)
        ccall((:sspectral_DeleteRes, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::SpectralRes)
        res = Gotw_TestArray()
        ccall((:getPValSspectral, libtestu01extractors), Cvoid, (Ptr{Cvoid}, Ptr{Float64}), obj.ptr, res.data)
        return res[:AD]
    end

    # sstring
    ## Type
    mutable struct StringRes
        ptr::Ptr{Cvoid}
        function StringRes()
            res = new(ccall((:sstring_CreateRes, libtestu01), Ptr{Cvoid}, (), ))
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::StringRes)
        ccall((:sstring_DeleteRes, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::StringRes)
        res = Gotw_TestArray()
        ccall((:getPValStringRes, libtestu01extractors), Cvoid, (Ptr{Cvoid}, Ptr{Float64}), obj.ptr, res.data)
        return res
    end

    ## Type
    mutable struct StringRes2
        ptr::Ptr{Cvoid}
        N::Int
        function StringRes2(N::Integer)
            res = new(ccall((:sstring_CreateRes2, libtestu01), Ptr{Cvoid}, (), ), N)
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::StringRes2)
        ccall((:sstring_DeleteRes2, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::StringRes2)
        res = Gotw_TestArray()
        res2 = ccall((:getPValStringRes2, libtestu01extractors), Float64, (Ptr{Cvoid}, Ptr{Float64}), obj.ptr, res.data)
        return res[obj.N == 1 ? :Mean : :Sum], res2
    end

    ## Type
    mutable struct StringRes3
        ptr::Ptr{Cvoid}
        N::Int
        function StringRes3(N::Integer)
            res = new(ccall((:sstring_CreateRes3, libtestu01), Ptr{Cvoid}, (), ), N)
            finalizer(delete, res)
            return res
        end
    end
    ## Finalizers
    function delete(obj::StringRes3)
        ccall((:sstring_DeleteRes3, libtestu01), Cvoid, (Ptr{Cvoid},), obj.ptr)
    end
    ## pvalue extractors
    function pvalue(obj::StringRes3)
        res1 = Gotw_TestArray()
        res2 = Gotw_TestArray()
        ccall((:getPValStringRes3, libtestu01extractors), Float64, (Ptr{Cvoid}, Ptr{Float64}, Ptr{Float64}), obj.ptr, res1.data, res2.data)
        return res1[obj.N == 1 ? :Mean : :Sum], res2[obj.N == 1 ? :Mean : :Sum]
    end

    #########
    # Tests #
    #########
    ## smarsa
    function smarsa_BirthdaySpacings(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, d::Integer, t::Integer, p::Integer)
        unif01 = Unif01(gen, "")
        sres = ResPoisson()
        try
            ccall((:smarsa_BirthdaySpacings, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Clong, Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, d, t, p)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function smarsa_GCD(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer)
        unif01 = Unif01(gen, "")
        sres = MarsaRes2(N)
        try
            ccall((:smarsa_GCD, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, s)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function smarsa_CollisionOver(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, d::Integer, t::Integer)
        unif01 = Unif01(gen, "")
        sres = MarsaRes()
        try
            ccall((:smarsa_CollisionOver, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Clong, Cint),
             unif01.ptr, sres.ptr, N, n,
             r, d, t)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function smarsa_Savir2(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, m::Integer, t::Integer)
        unif01 = Unif01(gen, "")
        sres = ResChi2(N)
        try
            ccall((:smarsa_Savir2, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Clong, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, m, t)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function smarsa_SerialOver(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, d::Integer, t::Integer)
        unif01 = Unif01(gen, "")
        sres = ResBasic()
        try
            ccall((:smarsa_SerialOver, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Clong, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, d, t)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function smarsa_MatrixRank(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer, L::Integer, k::Integer)
        unif01 = Unif01(gen, "")
        sres = ResChi2(N)
        try
            ccall((:smarsa_MatrixRank, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint, Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, s, L, k)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end

    ## sknuth
    function sknuth_Collision(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, d::Integer, t::Integer)
        unif01 = Unif01(gen, "")
        sres = KnuthRes2()
        try
            ccall((:sknuth_Collision, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Clong, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, d, t)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sknuth_CollisionPermut(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, t::Integer)
        unif01 = Unif01(gen, "")
        sres = KnuthRes2()
        try
            ccall((:sknuth_CollisionPermut, libtestu01), Cvoid,
            (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                Cint, Cint),
            unif01.ptr, sres.ptr, N, n,
            r, t)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sknuth_CouponCollector(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, d::Integer)
        unif01 = Unif01(gen, "")
        sres = ResChi2(N)
        try
            ccall((:sknuth_CouponCollector, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, d)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sknuth_Gap(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, Alpha::Real, Beta::Real)
        unif01 = Unif01(gen, "")
        sres = ResChi2(N)
        try
            ccall((:sknuth_Gap, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Float64, Float64),
                unif01.ptr, sres.ptr, N, n,
                r, Alpha, Beta)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sknuth_MaxOft(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, d::Integer, t::Integer)
        unif01 = Unif01(gen, "")
        sres = KnuthRes1(N)
        try
            ccall((:sknuth_MaxOft, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, d, t)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sknuth_Permutation(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, t::Integer)
        unif01 = Unif01(gen, "")
        sres = ResChi2(N)
        try
            ccall((:sknuth_Permutation, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, t)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sknuth_Run(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, up::Integer)
        unif01 = Unif01(gen, "")
        sres = ResChi2(N)
        try
            ccall((:sknuth_Run, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, up)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sknuth_SimpPoker(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, d::Integer, k::Integer)
        unif01 = Unif01(gen, "")
        sres = ResChi2(N)
        try
            ccall((:sknuth_SimpPoker, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, d, k)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end

    ## svaria
    function svaria_AppearanceSpacings(gen::RNGGenerator, N::Integer, Q::Integer, K::Integer, r::Integer, s::Integer, L::Integer)
        unif01 = Unif01(gen, "")
        sres = ResBasic()
        try
            ccall((:svaria_AppearanceSpacings, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Clong, Cint, Cint, Cint),
                unif01.ptr, sres.ptr, N, Q,
                K, r, s, L)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function svaria_SampleProd(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, t::Integer)
        unif01 = Unif01(gen, "")
        sres = ResBasic()
        try
            ccall((:svaria_SampleProd, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, t)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function svaria_SampleMean(gen::RNGGenerator, N::Integer, n::Integer, r::Integer)
        unif01 = Unif01(gen, "")
        sres = ResBasic()
        try
            ccall((:svaria_SampleMean, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint),
                unif01.ptr, sres.ptr, N, n,
                r)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function svaria_SampleCorr(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, k::Integer)
        unif01 = Unif01(gen, "")
        sres = ResBasic()
        try
            ccall((:svaria_SampleCorr, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, k)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function svaria_SumCollector(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, g::Float64)
        unif01 = Unif01(gen, "")
        sres = ResChi2(N)
        try
            ccall((:svaria_SumCollector, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cdouble),
                unif01.ptr, sres.ptr, N, n,
                r, g)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function svaria_WeightDistrib(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, k::Integer, alpha::Real, beta::Real)
        unif01 = Unif01(gen, "")
        sres = ResChi2(N)
        try
            ccall((:svaria_WeightDistrib, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Clong, Float64, Float64),
                unif01.ptr, sres.ptr, N, n,
                r, k, alpha, beta)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end

    ## sstring
    function sstring_AutoCor(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer, d::Integer)
        unif01 = Unif01(gen, "")
        sres = ResBasic()
        try
            ccall((:sstring_AutoCor, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, s, d)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sstring_HammingCorr(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer, L::Integer)
        unif01 = Unif01(gen, "")
        sres = StringRes()
        try
            ccall((:sstring_HammingCorr, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, s, L)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sstring_HammingIndep(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer, L::Integer, d::Integer)
        unif01 = Unif01(gen, "")
        sres = StringRes()
        try
            ccall((:sstring_HammingIndep, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint, Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, s, L, d)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sstring_HammingWeight2(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer, L::Integer)
        unif01 = Unif01(gen, "")
        sres = ResBasic()
        try
            ccall((:sstring_HammingWeight2, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint, Clong),
                unif01.ptr, sres.ptr, N, n,
                r, s, L)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sstring_LongestHeadRun(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer, L::Integer)
        unif01 = Unif01(gen, "")
        sres = StringRes2(N)
        try
            ccall((:sstring_LongestHeadRun, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint, Clong),
                unif01.ptr, sres.ptr, N, n,
                r, s, L)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sstring_PeriodsInStrings(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer)
        unif01 = Unif01(gen, "")
        sres = ResChi2(N)
        try
            ccall((:sstring_PeriodsInStrings, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, s)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function sstring_Run(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer)
        unif01 = Unif01(gen, "")
        sres = StringRes3(N)
        try
            ccall((:sstring_Run, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, s)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end

    ## swalk
    function swalk_RandomWalk1(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer, L0::Integer, L1::Integer)
        unif01 = Unif01(gen, "")
        sres = WalkRes(N)
        try
            ccall((:swalk_RandomWalk1, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint, Clong, Clong),
                unif01.ptr, sres.ptr, N, n,
                r, s, L0, L1)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end

    ## snpair
    function snpair_ClosePairs(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, t::Integer, p::Integer, m::Integer)
        unif01 = Unif01(gen, "")
        sres = NpairRes(N)
        try
            ccall((:snpair_ClosePairs, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint, Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, t, p, m)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end

    ## scomp
    function scomp_LempelZiv(gen::RNGGenerator, N::Integer, k::Integer, r::Integer, s::Integer)
        unif01 = Unif01(gen, "")
        sres = ResBasic()
        try
            ccall((:scomp_LempelZiv, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Cint,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, k,
                r, s)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end
    function scomp_LinearComp(gen::RNGGenerator, N::Integer, n::Integer, r::Integer, s::Integer)
        unif01 = Unif01(gen, "")
        sres = CompRes(N)
        try
            ccall((:scomp_LinearComp, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Clong,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, n,
                r, s)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end

    # sspectral
    function sspectral_Fourier3(gen::RNGGenerator, N::Integer, k::Integer, r::Integer, s::Integer)
        unif01 = Unif01(gen, "")
        sres = SpectralRes()
        try
            ccall((:sspectral_Fourier3, libtestu01), Cvoid,
                (Ptr{Cvoid}, Ptr{Cvoid}, Clong, Cint,
                    Cint, Cint),
                unif01.ptr, sres.ptr, N, k,
                r, s)
        finally
            delete(unif01)
        end
        return pvalue(sres)
    end

    ##################
    # Test Batteries #
    ##################
    for (snm, fnm) in ((:SmallCrush, :smallcrushTestU01), (:Crush, :crushTestU01), (:BigCrush, :bigcrushTestU01), (:pseudoDIEHARD, :diehardTestU01), (:FIPS_140_2, :fips_140_2TestU01))
        @eval begin
            function $(fnm)(f::RNGGenerator, fname::String)
                unif01 = Unif01(f, fname)
                try
                    ccall(($(string("bbattery_", snm)), libtestu01),
                        Cvoid, (Ptr{Cvoid},), unif01.ptr)
                finally
                    delete(unif01)
                end
            end
            $(fnm)(f::RNGGenerator) = $(fnm)(f::RNGGenerator, "")
        end
    end
     function smallcrushJulia(f::RNGGenerator)
         # @everywhere f = ()->g()
         testnames = [g->smarsa_BirthdaySpacings(g, 1, 5000000, 0, 1073741824, 2, 1),
                      g->sknuth_Collision(g, 1, 5000000, 0, 65536, 2),
                      g->sknuth_Gap(g, 1, 200000, 22, 0.0, .00390625),
                      g->sknuth_SimpPoker(g, 1, 400000, 24, 64, 64),
                      g->sknuth_CouponCollector(g, 1, 500000, 26, 16),
                      g->sknuth_MaxOft(g, 1, 2000000, 0, 100000, 6),
                      g->svaria_WeightDistrib(g, 1, 200000, 27, 256, 0.0, 0.125),
                      g->smarsa_MatrixRank(g, 1, 20000, 20, 10, 60, 60),
                      g->sstring_HammingIndep(g, 1, 500000, 20, 10, 300, 0)[:Mean],
                      g->swalk_RandomWalk1(g, 1, 1000000, 0, 30, 150, 150)]
         return pmap(t->t(f), testnames)
     end
     function bigcrushJulia(f::RNGGenerator)
         testnames = [g->smarsa_SerialOver(g, 1, 10^9, 0, 2^8, 3)[:Mean],
                      g->smarsa_SerialOver(g, 1, 10^9, 22, 2^8, 3)[:Mean],
                      g->smarsa_CollisionOver(g, 30, 2*10^7, 0, 2^21, 2),
                      g->smarsa_CollisionOver(g, 30, 2*10^7, 9, 2^21, 2),
                      g->smarsa_CollisionOver(g, 30, 2*10^7, 0, 2^14, 3),
                      g->smarsa_CollisionOver(g, 30, 2*10^7, 16, 2^14, 3),
                      g->smarsa_CollisionOver(g, 30, 2*10^7, 0, 64, 7),
                      g->smarsa_CollisionOver(g, 30, 2*10^7, 24, 64, 7),
                      g->smarsa_CollisionOver(g, 30, 2*10^7, 0, 8, 14),
                      g->smarsa_CollisionOver(g, 30, 2*10^7, 27, 8, 14),
                      g->smarsa_CollisionOver(g, 30, 2*10^7, 0, 4, 21),
                      g->smarsa_CollisionOver(g, 30, 2*10^7, 28, 4, 21),
                      g->smarsa_BirthdaySpacings(g, 100, 10^7, 0, 2^31, 2, 1),
                      g->smarsa_BirthdaySpacings(g, 20, 2*10^7, 0, 2^21, 3, 1),
                      g->smarsa_BirthdaySpacings(g, 20, 3*10^7, 14, 2^16, 4, 1),
                      g->smarsa_BirthdaySpacings(g, 20, 2*10^7, 0, 2^9, 7, 1),
                      g->smarsa_BirthdaySpacings(g, 20, 2*10^7, 7, 2^9, 7, 1),
                      g->smarsa_BirthdaySpacings(g, 20, 3*10^7, 14, 2^8, 8, 1),
                      g->smarsa_BirthdaySpacings(g, 20, 3*10^7, 22, 2^8, 8, 1),
                      g->smarsa_BirthdaySpacings(g, 20, 3*10^7, 0, 2^4, 16, 1),
                      g->smarsa_BirthdaySpacings(g, 20, 3*10^7, 26, 2^4, 16, 1),
                      g->snpair_ClosePairs(g, 30, 6*10^6, 0, 3, 0, 30),
                      g->snpair_ClosePairs(g, 20, 4*10^6, 0, 5, 0, 30),
                      g->snpair_ClosePairs(g, 10, 3*10^6, 0, 9, 0, 30),
                      g->snpair_ClosePairs(g, 5, 2*10^6, 0, 16, 0, 30),
                      g->sknuth_SimpPoker(g, 1, 4*10^8, 0, 8, 8),
                      g->sknuth_SimpPoker(g, 1, 4*10^8, 27, 8, 8),
                      g->sknuth_SimpPoker(g, 1, 10^8, 0, 32, 32),
                      g->sknuth_SimpPoker(g, 1, 10^8, 25, 32, 32),
                      g->sknuth_CouponCollector(g, 1, 2*10^8, 0, 8),
                      g->sknuth_CouponCollector(g, 1, 2*10^8, 10, 8),
                      g->sknuth_CouponCollector(g, 1, 2*10^8, 20, 8),
                      g->sknuth_CouponCollector(g, 1, 2*10^8, 27, 8),
                      g->sknuth_Gap(g, 1, 5*10^8, 0, 0.0, 1/16),
                      g->sknuth_Gap(g, 1, 3*10^8, 25, 0.0, 1/32),
                      g->sknuth_Gap(g, 1, 10^8, 0, 0.0, 1/128),
                      g->sknuth_Gap(g, 1, 10^7, 20, 0.0, 1/1024),
                      g->sknuth_Run(g, 5, 10^9, 0, 0),
                      g->sknuth_Run(g, 5, 10^9, 15, 1),
                      g->sknuth_Permutation(g, 1, 10^9, 0, 3),
                      g->sknuth_Permutation(g, 1, 10^9, 0, 5),
                      g->sknuth_Permutation(g, 1, 5*10^8, 0, 7),
                      g->sknuth_Permutation(g, 1, 5*10^8, 10, 10),
                      g->sknuth_CollisionPermut(g, 20, 2*10^7, 0, 14),
                      g->sknuth_CollisionPermut(g, 20, 2*10^7, 10, 14),
                      g->sknuth_MaxOft(g, 40, 10^7, 0, 10^5, 8),
                      g->sknuth_MaxOft(g, 30, 10^7, 0, 10^5, 16),
                      g->sknuth_MaxOft(g, 20, 10^7, 0, 10^5, 24),
                      g->sknuth_MaxOft(g, 20, 10^7, 0, 10^5, 32),
                      g->svaria_SampleProd(g, 40, 10^7, 0, 8)[:AD],
                      g->svaria_SampleProd(g, 20, 10^7, 0, 16)[:AD],
                      g->svaria_SampleProd(g, 20, 10^7, 0, 24)[:AD],
                      g->svaria_SampleMean(g, 2*10^7, 30, 0)[:AD],
                      g->svaria_SampleMean(g, 2*10^7, 30, 10)[:AD],
                      g->svaria_SampleCorr(g, 1, 2*10^9, 0, 1)[:Mean],
                      g->svaria_SampleCorr(g, 1, 2*10^9, 0, 2)[:Mean],
                      g->svaria_AppearanceSpacings(g, 1, 10^7, 10^9, 0, 3, 15)[:Mean],
                      g->svaria_AppearanceSpacings(g, 1, 10^7, 10^9, 27, 3, 15)[:Mean],
                      g->svaria_WeightDistrib(g, 1, 2*10^7, 0, 256, 0.0, 1/4),
                      g->svaria_WeightDistrib(g, 1, 2*10^7, 20, 256, 0.0, 1/4),
                      g->svaria_WeightDistrib(g, 1, 2*10^7, 28, 256, 0.0, 1/4),
                      g->svaria_WeightDistrib(g, 1, 2*10^7, 0, 256, 0.0, 1/16),
                      g->svaria_WeightDistrib(g, 1, 2*10^7, 10, 256, 0.0, 1/16),
                      g->svaria_WeightDistrib(g, 1, 2*10^7, 26, 256, 0.0, 1/16),
                      g->svaria_SumCollector(g, 1, 5*10^8, 0, 10.0),
                      g->smarsa_MatrixRank(g, 10, 10^6, 0, 5, 30, 30),
                      g->smarsa_MatrixRank(g, 10, 10^6, 25, 5, 30, 30),
                      g->smarsa_MatrixRank(g, 1, 5000, 0, 4, 1000, 1000),
                      g->smarsa_MatrixRank(g, 1, 5000, 26, 4, 1000, 1000),
                      g->smarsa_MatrixRank(g, 1, 80, 15, 15, 5000, 5000),
                      g->smarsa_MatrixRank(g, 1, 80, 0, 30, 5000, 5000),
                      g->smarsa_Savir2(g, 10, 10^7, 10, 2^30, 30),
                      g->smarsa_GCD(g, 10, 5*10^7, 0, 30),
                      g->swalk_RandomWalk1(g, 1, 10^8, 0, 5, 50, 50),
                      g->swalk_RandomWalk1(g, 1, 10^8, 25, 5, 50, 50),
                      g->swalk_RandomWalk1(g, 1, 10^7, 0, 10, 1000, 1000),
                      g->swalk_RandomWalk1(g, 1, 10^7, 20, 10, 1000, 1000),
                      g->swalk_RandomWalk1(g, 1, 10^6, 0, 15, 1000, 1000),
                      g->swalk_RandomWalk1(g, 1, 10^6, 15, 15, 10000, 10000),
                      g->scomp_LinearComp(g, 1, 400000, 0, 1),
                      g->scomp_LinearComp(g, 1, 400000, 29, 1),
                      g->scomp_LempelZiv(g, 10, 27, 0, 30)[:Sum],
                      g->scomp_LempelZiv(g, 10, 27, 15, 15)[:Sum],
                      g->sspectral_Fourier3(g, 100000, 14, 0, 3),
                      g->sspectral_Fourier3(g, 100000, 14, 27, 3),
                      g->sstring_LongestHeadRun(g, 1, 1000, 0, 3, 10^7),
                      g->sstring_LongestHeadRun(g, 1, 1000, 27, 3, 10^7),
                      g->sstring_PeriodsInStrings(g, 10, 5*10^8, 0, 10),
                      g->sstring_PeriodsInStrings(g, 10, 5*10^8, 20, 10),
                      g->sstring_HammingWeight2(g, 10, 10^9, 0, 3, 10^6)[:Sum],
                      g->sstring_HammingWeight2(g, 10, 10^9, 27, 3, 10^6)[:Sum],
                      g->sstring_HammingCorr(g, 1, 10^9, 10, 10, 30)[:Mean],
                      g->sstring_HammingCorr(g, 1, 10^8, 10, 10, 300)[:Mean],
                      g->sstring_HammingCorr(g, 1, 10^8, 10, 10, 1200)[:Mean],
                      g->sstring_HammingIndep(g, 10, 3*10^7, 0, 3, 30, 0)[:Sum],
                      g->sstring_HammingIndep(g, 10, 3*10^7, 27, 3, 30, 0)[:Sum],
                      g->sstring_HammingIndep(g, 1, 3*10^7, 0, 4, 300, 0)[:Mean],
                      g->sstring_HammingIndep(g, 1, 3*10^7, 26, 4, 300, 0)[:Mean],
                      g->sstring_HammingIndep(g, 1, 10^7, 0, 5, 1200, 0)[:Mean],
                      g->sstring_HammingIndep(g, 1, 10^7, 25, 5, 1200, 0)[:Mean],
                      g->sstring_Run(g, 1, 2*10^9, 0, 3),
                      g->sstring_Run(g, 1, 2*10^9, 27, 3),
                      g->sstring_AutoCor(g, 10, 10^9, 0, 3, 1)[:Sum],
                      g->sstring_AutoCor(g, 10, 10^9, 0, 3, 3)[:Sum],
                      g->sstring_AutoCor(g, 10, 10^9, 27, 3, 1)[:Sum],
                      g->sstring_AutoCor(g, 10, 10^9, 27, 3, 3)[:Sum]]
         return pmap(t->t(f), testnames)
     end
end


using Combinatorics

using dataset_pb2

using BondLengthDistributions
using SmuMolecules
using SmuUtilities

const THRESHOLD = 2.0

"""Generate a BondTopology that joins each Hydrogen atom to its nearest
    heavy atom.
Args:
  bond_topology:
  distances:
Returns:
"""
function hydrogen_to_nearest_atom(bond_topology::dataset_pb2.BondTopology,
                             distances::Array{Float32,2})::Union{dataset_pb2.BondTopology, Nothing}
  result = dataset_pb2.BondTopology()
  # Not sure if this is safe or not.
  setproperty!(result, :atoms, bond_topology.atoms)

  single_bond = 1

  natoms = length(bond_topology.atoms)
  for a1 in 1:natoms
    bond_topology.atoms[a1] == dataset_pb2.BondTopology_AtomType.ATOM_H || continue

    shortest_distance = 1.0e+30
    closest_heavy_atom = -1
    for a2 in 1:natoms
      bond_topology.atoms[a2] == dataset_pb2.BondTopology_AtomType.ATOM_H && continue

      distances[a1, a2] >= THRESHOLD && continue

      if distances[a1, a2] < shortest_distance
        shortest_distance = distances[a1, a2]
        closest_heavy_atom = a2
      end
    end

    closest_heavy_atom < 0 && return nothing

    add_bond!(a1 - 1, closest_heavy_atom - 1, single_bond, result)
  end

  return result
end

# Enable sorting of BondTopology's
import Base: isless
Base.isless(bt1::BondTopology, bt2::BondTopology) = isless(bt1.score, bt2.score)

# Convenenience function for filtering scores.
nonzero = x->x>0.0

# Convenenience function for selecting non hydrogen atoms
not_hydrogen = x->x != BondTopology_AtomType.ATOM_H

"""Return all BondTopology's that are plausible.

  Given a molecule described by `bond_topology` and `geometry`, return all possible
  BondTopology that are consistent with that.
  Note that `bond_topology` will be put in a canonical form.
  Args:
    bond_length_distribution:
    bond_topology:
    geometry:
  Returns:
    TopologyMatches
"""
function bond_topologies_from_geom(
    bond_lengths::AllBondLengthDistributions,
    bond_topology::dataset_pb2.BondTopology,
    geometry::dataset_pb2.Geometry,
    matching_parameters::MatchingParameters)::dataset_pb2.TopologyMatches
  result = dataset_pb2.TopologyMatches()    # To be returned.
  length(bond_topology.atoms) == 1 && return result  # return an empty result

  canonical_bond_topology!(bond_topology)
  distances = SmuUtilities.distances(geometry)

  # First join each Hydrogen to its nearest heavy atom, thereby
  # creating a starting BondTopology from which all others can grow
  starting_bond_topology = hydrogen_to_nearest_atom(bond_topology, distances)
  starting_bond_topology === nothing && return result

  heavy_atom_indices = findall(not_hydrogen, bond_topology.atoms)
  length(heavy_atom_indices) < 2 && return result

  # For each atom pair, a list of possible bond types.
  # Key is a tuple of the two atom numbers, value is a Vector
  # with the score for each bond type.

  bonds_to_scores = Dict{Tuple{Int32,Int32}, Vector{Float32}}()

  for (i, j) in Combinatorics.combinations(heavy_atom_indices, 2)  # All pairs
    (dist = distances[i, j]) > THRESHOLD && continue

    scores = [pdf(bond_lengths, bond_topology.atoms[i], bond_topology.atoms[j], btype, dist)
              for btype in 0:4]

    any(nonzero, scores) && (bonds_to_scores[(i, j)] = scores)
  end

  @debug("bonds_to_scores $(bonds_to_scores)")
  isempty(bonds_to_scores) && return result

  found_topologies = Vector{BondTopology}()  # Will be set into `result`.

  mol = SmuMolecule(starting_bond_topology, bonds_to_scores, matching_parameters)

  search_space = generate_search_state(mol)
  for s in Iterators.product(search_space...)
    @debug("Placing state $s")
    (bt = place_bonds!(s, mol)) == nothing && continue
    is_single_fragment(bt) || continue

    canonical_bond_topology!(bt)
    same_bond_topology(bond_topology, bt) && (bt.is_starting_topology = true)
    # smiles not set
    push!(found_topologies, bt)
  end

  isempty(found_topologies) && return result

  length(found_topologies) > 1 && sort!(found_topologies, rev=true)

  setproperty!(result, :bond_topology, found_topologies)

  return result
end


function main(args)
    return args
end
# Load inline arguments for worker unit
num_workers, this = main(ARGS)
num_workers = parse(Int64, num_workers)
this        = parse(Int64, this)

#
# Important directories
simdir          = "/remote/ceph2/group/gerda/data/mpik/gerda-simulations/gerda-gems-db/pss-dev"
simdir_old      = "/remote/ceph/group/gerda/data/simulation/gerda-mage-sim"
mapping_file    = simdir_old*"/UTILS/det-data/ged-mapping.json"
parameters_file = simdir_old*"/UTILS/det-data/ged-parameters.json"
config_dir      = "../../../2020-02-06_8380701d_st_ffischer/res/Impurity_Scan/config-dep/"

#
# Load packages and functions
include("../src/init.jl")
include("../src/fct.jl")
include("../src/worker_fct.jl")

#
# Output paths and filter settings
plots_base_path = "../../waveforms/sim/plots/raw/"
base_path_raw   = "../../waveforms/sim/raw_wf/"
base_path       = "../../waveforms/sim/raw_wf/"
sampling_time   = 1u"ns";
no_new_files    = 0
parameters = JSON.parsefile("../dicts/electronics_parameters.json")

channels = []
for c in 0:1:36
    files = glob(base_path_raw * lpad(c, 2, "0") * "-" * channel_to_name[c] * "/cl-wf/raw*/*.h5");
    if length(files) > 0
        push!(channels, c)
    end
end
channels = get_share_for_worker(channels, num_workers, this)

for ch in channels
    Base.run(`clear`)
    @info("Start Ch" * lpad(ch, 2, "0") * " | " * channel_to_name[ch])
    @info(">---------------------------------------------------------------<")

    files = glob(base_path_raw * lpad(ch, 2, "0") * "-" * channel_to_name[ch] * "/cl-wf/raw*/*.h5")
    pro = Progress(length(files), dt=0.5,
        barglyphs=BarGlyphs('|','█', ['▁' ,'▂' ,'▃' ,'▄' ,'▅' ,'▆', '▇'],' ','|',),
        barlen=10)
    for file in files
        file_nr = findall(x->x==file, files)[1]
        filename  = split(file, basename(file))[1]
        filename *= channel_to_name[ch] * "_w_filter" * split(basename(file), channel_to_name[ch])[2]
        filename = split(filename, "/cl-wf/")[1] * "/w_filter/" * split(filename, "/cl-wf/")[2]
        if !isfile(filename)
            data_raw = HDF5.h5open(file, "r") do h5f
                LegendHDF5IO.readdata(h5f, "generated_waveforms")
            end
            first_half = 1:Int(size(data_raw,1) / 2)
            waveform = add_baseline_and_extend_tail.(data_raw.waveform[first_half], 6000, 12000)

            #
            # Reduce time resolution to fit data
            waveform_w_bl = []
            map(x-> push!(waveform_w_bl, RDWaveform(x.time[1:10:end], x.value[1:10:end])), waveform)
            GBP = parameters[channel_to_name[ch]]["par"]["GBP"]*1e6
            tau = parameters[channel_to_name[ch]]["par"]["tau"]*1e-6
            Cd  = parameters[channel_to_name[ch]]["par"]["Cd"]*1e-12
            Cf  = parameters[channel_to_name[ch]]["par"]["Cf"]*1e-12

            data = Table(
                energy       = [],
                multiplicity = [],
                timestamp    = [],
                run          = [],
                channel      = [],
                waveform     = []
            )
            for i in eachindex(waveform_w_bl)
                pulse = waveform_w_bl[i].value
                if sum(pulse) > 0
                    filtered_pulse = applyElectronics(pulse; Ts = 10e-9, GBP = GBP, tau = tau, Kv = 150e3, Cd = Cd, Cf = Cf, Rf = 500e6)
                    inter = find_intersect(filtered_pulse, maximum(filtered_pulse)/2, 5)
                    cut   = filtered_pulse[(inter-399):1:(inter+400)]
                    time  = (0:10:7990)u"ns"
                    cut_pulse = RDWaveform(time, cut)
                    append!(data, Table(
                            energy       = [data_raw[i].edep],
                            multiplicity = [data_raw[i].event_mult],
                            timestamp    = [data_raw[i].evtno],
                            run          = [0],
                            channel      = [unique(data_raw[i].detno)[1]],
                            waveform     = [cut_pulse]
                        )
                    )
                end

            end
            events = Table(
                energy       = VectorOfArrays(Array{typeof(data[1].energy),1}(data.energy)),
                multiplicity = Int32.(data.multiplicity),
                timestamp    = Int32.(data.timestamp),
                run          = Int32.(data.run),
                channel      = Int32.(data.channel),
                waveform     = ArrayOfRDWaveforms( Array{typeof(data[1].waveform), 1}(data.waveform) )
            )

            !isdir(dirname(filename)) ? mkpath(dirname(filename)) : ""
            HDF5.h5open(filename, "w") do h5f
                LegendHDF5IO.writedata(h5f, "data", events)
            end
        end
        next!(pro)
    end
end


# Automatically generated
struct VirtualForce{F1<:Function,F2<:Function,F3<:Function,F4<:Function}
    vrx1::F1
    vry1::F2
    vrx2::F3
    vry2::F4
end

struct Params{T,F1<:Function,F2<:Function,F3<:Function,F4<:Function,F5<:Function,F6<:Function,F7<:Function,F8<:Function,F9<:Function,F10<:Function,F11<:Function,F12<:Function,F13<:Function,F14<:Function,F15<:Function}
    z::Vector{Float64}
    ea::F1
    fa::F2
    gs::F3
    ha::F4
    ia::F5
    eap::F6
    fap::F7
    gsp::F8
    hap::F9
    iap::F10
    eapp::F11
    fapp::F12
    gspp::F13
    happ::F14
    iapp::F15
    ae::TorqueGenerator
    af::TorqueGenerator
    footang::T
    g::T
    he::TorqueGenerator
    hf::TorqueGenerator
    ina::T
    inb::T
    inc::T
    ind::T
    ine::T
    inf::T
    ing::T
    inh::T
    ini::T
    k1::T
    k2::T
    k3::T
    k4::T
    k5::T
    k6::T
    k7::T
    k8::T
    ke::TorqueGenerator
    kf::TorqueGenerator
    l1::T
    l10::T
    l11::T
    l2::T
    l3::T
    l4::T
    l5::T
    l6::T
    l7::T
    l8::T
    l9::T
    ma::T
    mb::T
    mc::T
    md::T
    me::T
    mf::T
    mg::T
    mh::T
    mi::T
    mtpb::T
    mtpk::T
    pop1xi::T
    pop2xi::T
    mt::T
    u8::T
    u9::T
    u10::T
    u11::T
    virtual_force::VirtualForce
end

# initialise with constant values
function Params(ea, fa, gs, ha, ia, eap, fap, gsp, hap, iap, eapp, fapp, gspp, happ, iapp, ae, af, footang, g, he, hf, ina, inb, inc, ind, ine, inf, ing, inh, ini, k1, k2, k3, k4, k5, k6, k7, k8, ke, kf, l1, l10, l11, l2, l3, l4, l5, l6, l7, l8, l9, ma, mb, mc, md, me, mf, mg, mh, mi, mtpb, mtpk, pop1xi, pop2xi)
    z = Vector{Float64}(undef, 884)
    mt = ma + mb + mc + md + me + mf + mg
    u8 = 0.0
    u9 = 0.0
    u10 = 0.0
    u11 = 0.0

    virtual_force = VirtualForce(t -> 0.0, t -> 0.0, t -> 0.0, t -> 0.0)

    z[21] = l10 - l9
    z[394] = g * me
    z[749] = z[21] * z[394]
    z[846] = inf + inh + ini
    z[22] = l8 - l7
    z[677] = mf * z[22]
    z[857] = inh + ini
    z[26] = l2 - l1
    z[727] = mi * z[26]
    z[7] = cos(footang)
    z[8] = sin(footang)
    z[23] = l6 - l4
    z[24] = 0.5l6 + 0.5 * z[23]
    z[25] = 0.5l3 - 0.5l5
    z[75] = ma + mb + mc + md + me + mf + mg + mh + mi
    z[76] = (l1 * ma + l2 * mb + l2 * mc + l2 * md + l2 * me + l2 * mf + l2 * mg + l2 * mh + l2 * mi) / z[75]
    z[77] = (l4 * mb + 2 * l6 * mc + 2 * l6 * md + 2 * l6 * me + 2 * l6 * mf + 2 * l6 * mg + 2 * l6 * mh + 2 * l6 * mi) / z[75]
    z[78] = (l7 * mc + l8 * md + l8 * me + l8 * mf + l8 * mg + l8 * mh + l8 * mi) / z[75]
    z[79] = (l10 * me + l10 * mf + l10 * mg + l10 * mh + l10 * mi + l9 * md) / z[75]
    z[80] = (l10 * mf + l10 * mh + l10 * mi + me * z[21]) / z[75]
    z[81] = (l8 * mh + l8 * mi + mf * z[22]) / z[75]
    z[83] = (l6 * mi + mh * z[24]) / z[75]
    z[84] = (mi * z[26]) / z[75]
    z[85] = (mh * z[25]) / z[75]
    z[86] = (l3 * mb) / z[75]
    z[87] = ma + mb + mc + md
    z[88] = (l1 * ma + l2 * mb + l2 * mc + l2 * md) / z[87]
    z[89] = (l4 * mb + 2 * l6 * mc + 2 * l6 * md) / z[87]
    z[90] = (l7 * mc + l8 * md) / z[87]
    z[91] = (l9 * md) / z[87]
    z[92] = (l3 * mb) / z[87]
    z[93] = me + mf + mh + mi
    z[94] = (l2 * (me + mf + mh + mi)) / z[93]
    z[95] = (l6 * (me + mf + mh + mi)) / z[93]
    z[96] = (l8 * (me + mf + mh + mi)) / z[93]
    z[97] = (l10 * (me + mf + mh + mi)) / z[93]
    z[98] = (l10 * mf + l10 * mh + l10 * mi + me * z[21]) / z[93]
    z[99] = (l8 * mh + l8 * mi + mf * z[22]) / z[93]
    z[100] = (l6 * mi + mh * z[24]) / z[93]
    z[101] = (mi * z[26]) / z[93]
    z[102] = (mh * z[25]) / z[93]
    z[390] = g * ma
    z[391] = g * mb
    z[392] = g * mc
    z[393] = g * md
    z[395] = g * mf
    z[396] = g * mg
    z[397] = g * mh
    z[398] = g * mi
    z[438] = z[76] - l1
    z[443] = z[76] - l2
    z[444] = 0.5l4 - 0.5 * z[77]
    z[445] = 0.5l3 - 0.5 * z[86]
    z[516] = 2l6 - z[77]
    z[517] = l2 - z[76]
    z[538] = l8 - z[78]
    z[554] = l10 - z[79]
    z[562] = l10 - z[80]
    z[567] = l6 - 0.5 * z[77]
    z[569] = l8 - z[81]
    z[570] = z[24] - z[83]
    z[571] = z[25] - z[85]
    z[578] = l6 - z[83]
    z[587] = z[444] + z[7] * z[445]
    z[589] = z[445] + z[7] * z[444]
    z[600] = z[7] * z[86]
    z[610] = 2 * z[570] + 2 * z[7] * z[571]
    z[612] = 2 * z[571] + 2 * z[7] * z[570]
    z[618] = l1 * ma
    z[623] = l2 * mb
    z[626] = l4 * mb
    z[627] = l3 * mb
    z[659] = me * z[21]
    z[687] = l2 * mg
    z[688] = l6 * mg
    z[689] = l8 * mg
    z[690] = l10 * mg
    z[702] = l8 * mh
    z[712] = mh * z[24]
    z[714] = mh * z[25]
    z[741] = l1 * z[390]
    z[743] = l2 * z[391]
    z[744] = l2 * z[396]
    z[751] = z[22] * z[395]
    z[754] = z[26] * z[398]
    z[787] = ina + inb + inc + ind + ine + inf + ing + inh + ini + ma * l1^2
    z[788] = l3^2 + l4^2 + 4 * l2^2 + 2 * l3 * l4 * z[7]
    z[789] = l2 * l3
    z[790] = l2 * l4
    z[791] = z[24]^2
    z[792] = z[25]^2
    z[793] = z[7] * z[24] * z[25]
    z[794] = l10 * l2
    z[795] = l2 * l6
    z[796] = l2 * l8
    z[797] = l10^2
    z[798] = l2^2
    z[799] = l6^2
    z[800] = l8^2
    z[801] = l10 * l6
    z[802] = l10 * l8
    z[803] = l6 * l8
    z[805] = ma * l1^2
    z[810] = l3 * z[8]
    z[811] = l4 * z[8]
    z[812] = z[7] * z[24]
    z[813] = z[7] * z[25]
    z[814] = z[8] * z[25]
    z[815] = z[8] * z[24]
    z[817] = ina + ma * l1^2 + mb * l2^2 + mg * l2^2
    z[819] = ina + ma * l1^2
    z[824] = ina + inb + ma * l1^2
    z[825] = l2^2 + l6^2
    z[830] = ina + inb + inc + ma * l1^2
    z[834] = ina + inb + inc + ind + ma * l1^2
    z[837] = ine + inf + inh + ini
    z[847] = l8 * z[24]
    z[848] = l8 * z[25]

    return Params(z, ea, fa, gs, ha, ia, eap, fap, gsp, hap, iap, eapp, fapp, gspp, happ, iapp, ae, af, footang, g, he, hf, ina, inb, inc, ind, ine, inf, ing, inh, ini, k1, k2, k3, k4, k5, k6, k7, k8, ke, kf, l1, l10, l11, l2, l3, l4, l5, l6, l7, l8, l9, ma, mb, mc, md, me, mf, mg, mh, mi, mtpb, mtpk, pop1xi, pop2xi, mt, u8, u9, u10, u11, virtual_force)
end


#=
Type definition and methods containing the overarching backtesting object fueling the engine
=#

mutable struct Strategy
    universe::Universe
    indicator::Indicator
    rules::Tuple{Vararg{Rule}}
    portfolio::Portfolio
    results::Results
    function Strategy(universe::Universe,
                      indicator::Indicator,
                      rules::Tuple{Vararg{Rule}},
                      portfolio::Portfolio=Portfolio(universe))
        return new(universe, indicator, rules, portfolio, Results())
    end
end

function generate_trades(strat::Strategy; verbose::Bool=true)::Dict{String,TS}
    all_trades = Dict{String,TS}()
    for asset in strat.universe.assets
        verbose ? print("Generating trades for asset $asset...") : nothing
        trades = TS(falses(size(strat.universe.data[asset],1), length(strat.rules)),
                    strat.universe.data[asset].index)
        local indicator_data = calculate(strat.indicator, strat.universe.data[asset])
        for (i,rule) in enumerate(strat.rules);
            trades[:,i] = rule.trigger.fun(indicator_data)
        end
        all_trades[asset] = trades
        verbose ? print("Done.\n") : nothing
    end
    return all_trades
end

function generate_trades!(strat::Strategy; args...)::Void
    strat.results.trades = generate_trades(strat; args...)
    return nothing
end

function backtest(strat::Strategy; px_trade::Symbol=:Open, px_close::Symbol=:Settle, verbose::Bool=true)::Dict{String,TS{Float64}}
    if isempty(strat.results.trades)
        generate_trades!(strat, verbose=verbose)
    end
    result = Dict{String,TS}()
    for asset in strat.universe.assets
        verbose ? print("Running backtest for asset $asset...") : nothing
        trades = strat.results.trades[asset].values
        N = size(trades, 1)
        summary_ts = strat.universe.data[asset]
        #TODO: add setindex! method for TS objects using Symbol and Vector to assign inplace
        #TODO: generalize this logic to incorporate order types
        #FIXME: generalize this logic to use the actual rules (this is a temporary quickfix)
        trade_price = summary_ts[px_trade].values
        close_price = summary_ts[px_close].values
        pos = zeros(Float64, N)
        pnl = zeros(Float64, N)
        do_trade = false
        for t in 2:N
            for (i,rule) in enumerate(strat.rules)
                if trades[t-1,i] != 0
                    do_trade = true
                    #TODO: fill out this logic with the various order types
                    order_side = rule.action in (long,buy) ? 1 : rule.action in (short,sell) ? -1 : 0
                    #TODO: add logic here for the int vs. float argument type to order function
                    (order_qty,) = rule.args
                    #if isa(order_qty, Int); else FIXME: portfolio adjustment logic; end
                    pos[t] = order_qty * order_side
                    pnl[t] = pos[t] * (close_price[t] - trade_price[t])
                end
            end
            if !do_trade
                pos[t] = pos[t-1]
                pnl[t] = pos[t] * (close_price[t]-close_price[t-1])
            end
            do_trade = false
        end
        summary_ts = [summary_ts TS([pos pnl cumsum(pnl)], summary_ts.index, [:Pos,:PNL,:CumPNL])]
        result[asset] = summary_ts
        verbose ? print("Done.\n") : nothing
    end
    return result
end

function backtest!(strat::Strategy; args...)::Void
    strat.results.backtest = backtest(strat; args...)
    return nothing
end

Base.copy(strat::Strategy) = Strategy(strat.universe, strat.indicator, strat.rules)

# define matrix row iterator protocol
# this allows us to `enumerate(EachRow(M))`
# thereby getting the count of the iteration as well as the row
immutable EachRow{T<:AbstractMatrix}
    A::T
end
Base.start(::EachRow) = 1
Base.next(itr::EachRow, s) = (itr.A[s,:], s+1)
Base.done(itr::EachRow, s) = s > size(itr.A,1)

#TODO: more meaningful progres information
#TODO: parallel processing
#TODO: streamline this so that it doesnt run so slow (seems to be recompiling at each run)
function optimize(strat::Strategy; samples::Int=0, seed::Int=0, verbose::Bool=true, summary_fun::Function=cum_pnl, args...)::Matrix
    strat_save = copy(strat)
    n_runs = get_n_runs(strat.indicator.paramset)
    idx_samples::Vector{Int} = collect(1:n_runs)
    if samples > 0
        srand(seed)
        idx_samples = rand(idx_samples, samples)
    else
        samples = n_runs
    end
    combos = get_param_combos(strat.indicator.paramset, n_runs=n_runs)[idx_samples,:]
    result = zeros(samples)
    for (run, combo) in enumerate(idx_samples)
        verbose ? println("Run $run/$samples") : nothing
        strat.indicator.paramset.arg_defaults = combo
        generate_trades!(strat, verbose=false)
        backtest!(strat, verbose=false; args...)
        result[run] = summary_fun(strat.results.backtest)
    end
    # prevent out-of-scope alteration of strat object
    strat = strat_save
    return result
end

# TODO: implement function to edit results member of strat in place
function optimize!(strat::Strategy; samples::Int=0, seed::Int=0, verbose::Bool=true, summary_fun::Function=cum_pnl, args...)::Void
    n_runs = get_n_runs(strat.indicator.paramset)
    idx_samples::Vector{Int} = collect(1:n_runs)
    if samples > 0
        if seed >= 0
            srand(seed)
        end
        idx_samples = rand(idx_samples, samples)
    else
        samples = n_runs
    end
    combos = get_param_combos(strat.indicator.paramset, n_runs=n_runs)[idx_samples,:]
    strat.results.optimization = zeros(samples,1)
    for (run, combo) in enumerate(EachRow(combos))
        verbose ? println("Run $run/$samples") : nothing
        strat.indicator.paramset.arg_defaults = combo
        generate_trades!(strat, verbose=false)
        backtest!(strat, verbose=false; args...)
        strat.results.optimization[run] = summary_fun(strat.results)
    end
    strat.results.optimization = [combos strat.results.optimization]
    return nothing
end


using KernelAbstractions: NoneEvent
using OffsetArrays: OffsetArray
using CUDA

using Oceananigans.Operators: Δzᵃᵃᶜ
using Oceananigans.BoundaryConditions: left_gradient, right_gradient, linearly_extrapolate, FBC, VBC, GBC
using Oceananigans.BoundaryConditions: fill_bottom_halo!, fill_top_halo!, apply_z_bottom_bc!, apply_z_top_bc!
using Oceananigans.Grids: Flat, Bounded
using Oceananigans.Coriolis: AbstractRotation
using Oceananigans.Architectures: device_event
using Oceananigans.TurbulenceClosures: AbstractTurbulenceClosure
using Oceananigans.TurbulenceClosures.CATKEVerticalDiffusivities: _top_tke_flux, CATKEVDArray

import Oceananigans.Utils: launch!
import Oceananigans.Grids: validate_size, validate_halo
import Oceananigans.BoundaryConditions: fill_halo_regions!
import Oceananigans.TurbulenceClosures: time_discretization, calculate_diffusivities!
import Oceananigans.TurbulenceClosures: ∂ⱼ_τ₁ⱼ, ∂ⱼ_τ₂ⱼ, ∂ⱼ_τ₃ⱼ, ∇_dot_qᶜ
import Oceananigans.TurbulenceClosures.CATKEVerticalDiffusivities: top_tke_flux
import Oceananigans.Coriolis: x_f_cross_U, y_f_cross_U, z_f_cross_U

#####
##### Implements a "single column model mode" for HydrostaticFreeSurfaceModel
#####

const SingleColumnGrid = AbstractGrid{<:AbstractFloat, <:Flat, <:Flat, <:Bounded}

#####
##### Model constructor utils
#####

PressureField(::SingleColumnGrid) = (; pHY′ = nothing)
FreeSurface(free_surface::ExplicitFreeSurface{Nothing}, velocities, arch, ::SingleColumnGrid) = nothing
FreeSurface(free_surface::ImplicitFreeSurface{Nothing}, velocities, arch, ::SingleColumnGrid) = nothing

validate_momentum_advection(momentum_advection, ::SingleColumnGrid) = nothing
validate_tracer_advection(tracer_advection::AbstractAdvectionScheme, ::SingleColumnGrid) = nothing, NamedTuple()
validate_tracer_advection(tracer_advection::Nothing, ::SingleColumnGrid) = nothing, NamedTuple()

#####
##### Time-step optimizations
#####

calculate_free_surface_tendency!(arch, ::SingleColumnGrid, args...) = NoneEvent()

# Fast state update and halo filling

function update_state!(model::HydrostaticFreeSurfaceModel, grid::SingleColumnGrid)

    fill_halo_regions!(prognostic_fields(model), model.architecture, model.clock, fields(model))

    compute_auxiliary_fields!(model.auxiliary_fields)

    # Calculate diffusivities
    calculate_diffusivities!(model.diffusivity_fields, model.closure, model)

    fill_halo_regions!(model.diffusivity_fields,
                       model.architecture,
                       model.clock,
                       fields(model))

    return nothing
end

const ClosureArray = AbstractArray{<:AbstractTurbulenceClosure}

@inline function ∂ⱼ_τ₁ⱼ(i, j, k, grid::SingleColumnGrid, closure_array::ClosureArray, args...)
    @inbounds closure = closure_array[i, j]
    return ∂ⱼ_τ₁ⱼ(i, j, k, grid, closure, args...)
end

@inline function ∂ⱼ_τ₂ⱼ(i, j, k, grid::SingleColumnGrid, closure_array::ClosureArray, args...)
    @inbounds closure = closure_array[i, j]
    return ∂ⱼ_τ₂ⱼ(i, j, k, grid, closure, args...)
end

@inline function ∇_dot_qᶜ(i, j, k, grid::SingleColumnGrid, closure_array::ClosureArray, c, tracer_index, args...)
    @inbounds closure = closure_array[i, j]
    return ∇_dot_qᶜ(i, j, k, grid, closure, c, tracer_index, args...)
end

struct ColumnEnsembleSize{C<:Tuple{Int, Int}}
    ensemble :: C
    Nz :: Int
    Hz :: Int
end

ColumnEnsembleSize(; Nz, ensemble=(0, 0), Hz=1) = ColumnEnsembleSize(ensemble, Nz, Hz)

validate_size(TX, TY, TZ, e::ColumnEnsembleSize) = tuple(e.ensemble[1], e.ensemble[2], e.Nz)
validate_halo(TX, TY, TZ, e::ColumnEnsembleSize) = tuple(0, 0, e.Hz)

@inline function time_discretization(closure_array::AbstractArray)
    first_closure = CUDA.@allowscalar first(closure_array) # assumes all closures have same time-discretization
    return time_discretization(first_closure)
end

#####
##### CATKEVerticalDiffusivity helpers
#####

@inline tracer_tendency_kernel_function(model::HydrostaticFreeSurfaceModel, closure::CATKEVDArray, ::Val{:e}) =
    hydrostatic_turbulent_kinetic_energy_tendency

""" Compute the flux of TKE through the surface / top boundary. """
@inline function top_tke_flux(i, j, grid::SingleColumnGrid, clock, fields, parameters, closure_array::CATKEVDArray, buoyancy)
    top_tracer_bcs = parameters.top_tracer_boundary_conditions
    top_velocity_bcs = parameters.top_velocity_boundary_conditions
    @inbounds closure = closure_array[i, j]

    return _top_tke_flux(i, j, grid, closure.surface_TKE_flux, closure,
                         buoyancy, fields, top_tracer_bcs, top_velocity_bcs, clock)
end

@inline function hydrostatic_turbulent_kinetic_energy_tendency(i, j, k, grid::SingleColumnGrid,
                                                               val_tracer_index::Val{tracer_index},
                                                               advection,
                                                               closure_array::CATKEVDArray, args...) where tracer_index

    @inbounds closure = closure_array[i, j]
    return hydrostatic_turbulent_kinetic_energy_tendency(i, j, k, grid, val_tracer_index, advection, closure, args...)
end

#####
##### Arrays of Coriolises
#####

const CoriolisMatrix = AbstractMatrix{<:AbstractRotation}

@inline function x_f_cross_U(i, j, k, grid::SingleColumnGrid, coriolis_array::CoriolisMatrix, U)
    @inbounds coriolis = coriolis_array[i, j]
    return x_f_cross_U(i, j, k, grid, coriolis, U)
end

@inline function y_f_cross_U(i, j, k, grid::SingleColumnGrid, coriolis_array::CoriolisMatrix, U)
    @inbounds coriolis = coriolis_array[i, j]
    return y_f_cross_U(i, j, k, grid, coriolis, U)
end

@inline function z_f_cross_U(i, j, k, grid::SingleColumnGrid, coriolis_array::CoriolisMatrix, U)
    @inbounds coriolis = coriolis_array[i, j]
    return z_f_cross_U(i, j, k, grid, coriolis, U)
end


module Jax

using PyCall
using ChainRulesCore
using DLPack
using Adapt

using ..PyCallChainRules: PyAdaptor, fmap

const inspect = PyNULL()
const jax = PyNULL()
const dlpack = PyNULL()
const stax = PyNULL()
const numpy = PyNULL()

const ispysetup = Ref{Bool}(false)

pyto_dlpack(x) = @pycall dlpack.to_dlpack(x)::PyObject
pyfrom_dlpack(x) = @pycall dlpack.from_dlpack(x)::PyObject

struct JaxFunctionWrapper
    jaxfn::PyObject
end

function (wrap::JaxFunctionWrapper)(args...; kwargs...)
    out = wrap.jaxfn(fmap(x->DLPack.share(x, PyObject, pyfrom_dlpack), args)...)
    return fmap(x->DLPack.wrap(x, pyto_dlpack), out)
end

function ChainRulesCore.rrule(wrap::JaxFunctionWrapper, args...; kwargs...)
    T = typeof(first(args))
    project = ProjectTo(args)
    pyargs = fmap(x->DLPack.share(x, PyObject, pyfrom_dlpack), args)
    jax_primal, jax_vjpfun = jax.vjp(wrap.jaxfn, pyargs...; kwargs...)
    function JaxFunctionWrapper_pullback(Δ)
        cΔ = fmap(x->Adapt.adapt(PyAdaptor{T}(), x), Δ)
        dlΔ = fmap(x->DLPack.share(x, PyObject, pyfrom_dlpack), cΔ)
        tangent_vals = fmap(x->DLPack.wrap(x, pyto_dlpack), jax_vjpfun(dlΔ))
        return (NoTangent(), project(tangent_vals)...)
    end
    return fmap(x->DLPack.wrap(x, pyto_dlpack), jax_primal), JaxFunctionWrapper_pullback
end

function __init__()
    try
        copy!(jax, pyimport("jax"))
        copy!(dlpack, pyimport("jax.dlpack"))
        copy!(numpy, pyimport("numpy"))
        copy!(stax, pyimport("jax.example_libraries.stax"))
        copy!(inspect, pyimport("inspect"))
        ispysetup[] = true
    catch err
        @warn """PyCallChainRules.jl has failed to import jax from Python.
                 Please make sure these are installed.
        """
        @debug err
        ispysetup[] = false
        #rethrow(err)
    end
end

end


# modified by N Laohakunakorn
# University of Edinburgh 2020

using Statistics
# ----------------------------------------------------------------------------------- #
# Copyright (c) 2017 Varnerlab
# School of Chemical Engineering Purdue University
# W. Lafayette IN 46907 USA

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# ----------------------------------------------------------------------------------- #
function Control(t,x,rate_vector,data_dictionary)
# ---------------------------------------------------------------------- #
# Control.jl was generated using the Kwatee code generation system.
# Username: nicholas
# Type: CFPS-JULIA
# Version: 1.0
# Generation timestamp: 03-24-2017 13:01:36
#
# Arguments:
# t  - current time
# x  - state vector
# rate_vector - vector of reaction rates
# data_dictionary  - Data dictionary instance (holds model parameters)
# ---------------------------------------------------------------------- #

# Set a default value for the allosteric control variables -
number_of_reactions = length(rate_vector);
control_vector = ones(number_of_reactions);
control_parameter_array = data_dictionary["CONTROL_PARAMETER_ARRAY"];

# Alias the species vector -
M_g6p_c = x[1];
M_f6p_c = x[2];
M_fdp_c = x[3];
M_dhap_c = x[4];
M_g3p_c = x[5];
M_13dpg_c = x[6];
M_3pg_c = x[7];
M_2pg_c = x[8];
M_oaa_c = x[9];
M_coa_c = x[10];
M_accoa_c = x[11];
M_6pgl_c = x[12];
M_6pgc_c = x[13];
M_ru5p_D_c = x[14];
M_xu5p_D_c = x[15];
M_r5p_c = x[16];
M_s7p_c = x[17];
M_e4p_c = x[18];
M_2ddg6p_c = x[19];
M_cit_c = x[20];
M_icit_c = x[21];
M_akg_c = x[22];
M_succoa_c = x[23];
M_q8_c = x[24];
M_fum_c = x[25];
M_q8h2_c = x[26];
M_mql8_c = x[27];
M_mqn8_c = x[28];
M_h_e = x[29];
M_ppi_c = x[30];
M_glx_c = x[31];
M_actp_c = x[32];
M_etoh_c = x[33];
M_for_c = x[34];
M_nh3_c = x[35];
M_arg_L_c = x[36];
M_h2s_c = x[37];
M_thf_c = x[38];
M_mlthf_c = x[39];
M_aicar_c = x[40];
M_5mthf_c = x[41];
M_chor_c = x[42];
M_h2o2_c = x[43];
M_mglx_c = x[44];
M_prop_c = x[45];
M_indole_c = x[46];
M_cadav_c = x[47];
M_gaba_c = x[48];
M_glycoA_c = x[49];
M_78mdp_c = x[50];
M_4adochor_c = x[51];
M_4abz_c = x[52];
M_78dhf_c = x[53];
M_dhf_c = x[54];
M_methf_c = x[55];
M_10fthf_c = x[56];
M_prpp_c = x[57];
M_hco3_c = x[58];
M_clasp_c = x[59];
M_or_c = x[60];
M_omp_c = x[61];
M_5pbdra = x[62];
M_gar_c = x[63];
M_fgar_c = x[64];
M_fgam_c = x[65];
M_air_c = x[66];
M_cair_c = x[67];
M_saicar_c = x[68];
M_faicar_c = x[69];
M_imp_c = x[70];
M_xmp_c = x[71];
GENE_CAT = x[72];
RNAP = x[73];
OPEN_GENE_CAT = x[74];
mRNA_CAT = x[75];
RIBOSOME = x[76];
RIBOSOME_START_CAT = x[77];
M_ala_L_c_tRNA = x[78];
M_arg_L_c_tRNA = x[79];
M_asn_L_c_tRNA = x[80];
M_asp_L_c_tRNA = x[81];
M_cys_L_c_tRNA = x[82];
M_glu_L_c_tRNA = x[83];
M_gln_L_c_tRNA = x[84];
M_gly_L_c_tRNA = x[85];
M_his_L_c_tRNA = x[86];
M_ile_L_c_tRNA = x[87];
M_leu_L_c_tRNA = x[88];
M_lys_L_c_tRNA = x[89];
M_met_L_c_tRNA = x[90];
M_phe_L_c_tRNA = x[91];
M_pro_L_c_tRNA = x[92];
M_ser_L_c_tRNA = x[93];
M_thr_L_c_tRNA = x[94];
M_trp_L_c_tRNA = x[95];
M_tyr_L_c_tRNA = x[96];
M_val_L_c_tRNA = x[97];
PROTEIN_CAT = x[98];
tRNA = x[99];
M_glc_D_c = x[100];
M_pep_c = x[101];
M_pyr_c = x[102];
M_ac_c = x[103];
M_lac_D_c = x[104];
M_mal_L_c = x[105];
M_atp_c = x[106];
M_adp_c = x[107];
M_amp_c = x[108];
M_gtp_c = x[109];
M_gdp_c = x[110];
M_gmp_c = x[111];
M_utp_c = x[112];
M_udp_c = x[113];
M_ump_c = x[114];
M_ctp_c = x[115];
M_cdp_c = x[116];
M_cmp_c = x[117];
M_succ_c = x[118];
M_asp_L_c = x[119];
M_gly_L_c = x[120];
M_ile_L_c = x[121];
M_asn_L_c = x[122];
M_cys_L_c = x[123];
M_lys_L_c = x[124];
M_his_L_c = x[125];
M_ala_L_c = x[126];
M_phe_L_c = x[127];
M_pro_L_c = x[128];
M_ser_L_c = x[129];
M_thr_L_c = x[130];
M_trp_L_c = x[131];
M_tyr_L_c = x[132];
M_val_L_c = x[133];
M_met_L_c = x[134];
M_leu_L_c = x[135];
M_glu_L_c = x[136];
M_gln_L_c = x[137];
M_o2_c = x[138];
M_co2_c = x[139];
M_pi_c = x[140];
M_nh4_c = x[141];
M_so4_c = x[142];
M_h_c = x[143];
M_h2o_c = x[144];
M_nad_c = x[145];
M_nadh_c = x[146];
M_nadp_c = x[147];
M_nadph_c = x[148];
E_R_glk_atp = x[149];
E_R_pgi = x[150];
E_R_pgi_R = x[151];
E_R_pfk = x[152];
E_R_fdp = x[153];
E_R_fbaA = x[154];
E_R_fbaA_R = x[155];
E_R_tpiA = x[156];
E_R_tpiA_R = x[157];
E_R_gapA = x[158];
E_R_gapA_R = x[159];
E_R_pgk = x[160];
E_R_pgk_R = x[161];
E_R_gpm = x[162];
E_R_gpm_R = x[163];
E_R_eno = x[164];
E_R_eno_R = x[165];
E_R_pyk = x[166];
E_R_pck = x[167];
E_R_ppc = x[168];
E_R_pdh = x[169];
E_R_pps = x[170];
E_R_zwf = x[171];
E_R_zwf_R = x[172];
E_R_pgl = x[173];
E_R_gnd = x[174];
E_R_rpe = x[175];
E_R_rpe_R = x[176];
E_R_rpi = x[177];
E_R_rpi_R = x[178];
E_R_talAB = x[179];
E_R_talAB_R = x[180];
E_R_tkt1 = x[181];
E_R_tkt1_R = x[182];
E_R_tkt2 = x[183];
E_R_tkt2_R = x[184];
E_R_edd = x[185];
E_R_eda = x[186];
E_R_gltA = x[187];
E_R_acn = x[188];
E_R_acn_R = x[189];
E_R_icd = x[190];
E_R_icd_R = x[191];
E_R_sucAB = x[192];
E_R_sucCD = x[193];
E_R_sdh = x[194];
E_R_frd = x[195];
E_R_fum = x[196];
E_R_fum_R = x[197];
E_R_mdh = x[198];
E_R_mdh_R = x[199];
E_R_cyd = x[200];
E_R_cyo = x[201];
E_R_app = x[202];
E_R_atp = x[203];
E_R_nuo = x[204];
E_R_pnt1 = x[205];
E_R_pnt2 = x[206];
E_R_ndh1 = x[207];
E_R_ndh2 = x[208];
E_R_ppa = x[209];
E_R_aceA = x[210];
E_R_aceB = x[211];
E_R_maeA = x[212];
E_R_maeB = x[213];
E_R_pta = x[214];
E_R_pta_R = x[215];
E_R_ackA = x[216];
E_R_ackA_R = x[217];
E_R_acs = x[218];
E_R_adhE = x[219];
E_R_adhE_R = x[220];
E_R_ldh = x[221];
E_R_ldh_R = x[222];
E_R_pflAB = x[223];
E_R_alaAC = x[224];
E_R_alaAC_R = x[225];
E_R_arg = x[226];
E_R_aspC = x[227];
E_R_asnB = x[228];
E_R_asnA = x[229];
E_R_cysEMK = x[230];
E_R_gltBD = x[231];
E_R_gdhA = x[232];
E_R_gdhA_R = x[233];
E_R_glnA = x[234];
E_R_glyA = x[235];
E_R_his = x[236];
E_R_ile = x[237];
E_R_leu = x[238];
E_R_lys = x[239];
E_R_met = x[240];
E_R_phe = x[241];
E_R_pro = x[242];
E_R_serABC = x[243];
E_R_thr = x[244];
E_R_trp = x[245];
E_R_tyr = x[246];
E_R_val = x[247];
E_R_arg_deg = x[248];
E_R_asp_deg = x[249];
E_R_asn_deg = x[250];
E_R_gly_deg = x[251];
E_R_mglx_deg = x[252];
E_R_ser_deg = x[253];
E_R_pro_deg = x[254];
E_R_thr_deg1 = x[255];
E_R_thr_deg2 = x[256];
E_R_thr_deg3 = x[257];
E_R_trp_deg = x[258];
E_R_cys_deg = x[259];
E_R_lys_deg = x[260];
E_R_gln_deg = x[261];
E_R_glu_deg = x[262];
E_R_gaba_deg1 = x[263];
E_R_gaba_deg2 = x[264];
E_R_chor = x[265];
E_R_fol_e = x[266];
E_R_fol_1 = x[267];
E_R_fol_2a = x[268];
E_R_fol_2b = x[269];
E_R_fol_3 = x[270];
E_R_fol_4 = x[271];
E_R_gly_fol = x[272];
E_R_gly_fol_R = x[273];
E_R_mthfd = x[274];
E_R_mthfd_R = x[275];
E_R_mthfc = x[276];
E_R_mthfc_R = x[277];
E_R_mthfr2a = x[278];
E_R_mthfr2b = x[279];
E_R_prpp_syn = x[280];
E_R_or_syn_1 = x[281];
E_R_or_syn_2 = x[282];
E_R_omp_syn = x[283];
E_R_ump_syn = x[284];
E_R_ctp_1 = x[285];
E_R_ctp_2 = x[286];
E_R_A_syn_1 = x[287];
E_R_A_syn_2 = x[288];
E_R_A_syn_3 = x[289];
E_R_A_syn_4 = x[290];
E_R_A_syn_5 = x[291];
E_R_A_syn_6 = x[292];
E_R_A_syn_7 = x[293];
E_R_A_syn_8 = x[294];
E_R_A_syn_9 = x[295];
E_R_A_syn_10 = x[296];
E_R_A_syn_12 = x[297];
E_R_xmp_syn = x[298];
E_R_gmp_syn = x[299];
E_R_atp_amp = x[300];
E_R_utp_ump = x[301];
E_R_ctp_cmp = x[302];
E_R_gtp_gmp = x[303];
E_R_atp_adp = x[304];
E_R_utp_adp = x[305];
E_R_ctp_adp = x[306];
E_R_gtp_adp = x[307];
E_R_udp_utp = x[308];
E_R_cdp_ctp = x[309];
E_R_gdp_gtp = x[310];
E_R_atp_ump = x[311];
E_R_atp_cmp = x[312];
E_R_atp_gmp = x[313];
E_R_adk_atp = x[314];
E_Import_o2 = x[315];
E_Import_co2 = x[316];
E_Import_pi = x[317];
E_Import_nh4 = x[318];
E_Import_so4 = x[319];
E_Import_h2o = x[320];
E_Export_o2 = x[321];
E_Export_co2 = x[322];
E_Export_pi = x[323];
E_Export_nh4 = x[324];
E_Export_so4 = x[325];
E_Export_h2o = x[326];
E_Proton_gradient = x[327];
E_transcriptional_initiation_CAT = x[328];
E_transcription_CAT = x[329];
E_mRNA_degradation_CAT = x[330];
E_translation_initiation_CAT = x[331];
E_translation_CAT = x[332];
E_tRNA_charging_M_ala_L_c_CAT = x[333];
E_tRNA_charging_M_arg_L_c_CAT = x[334];
E_tRNA_charging_M_asn_L_c_CAT = x[335];
E_tRNA_charging_M_asp_L_c_CAT = x[336];
E_tRNA_charging_M_cys_L_c_CAT = x[337];
E_tRNA_charging_M_glu_L_c_CAT = x[338];
E_tRNA_charging_M_gln_L_c_CAT = x[339];
E_tRNA_charging_M_gly_L_c_CAT = x[340];
E_tRNA_charging_M_his_L_c_CAT = x[341];
E_tRNA_charging_M_ile_L_c_CAT = x[342];
E_tRNA_charging_M_leu_L_c_CAT = x[343];
E_tRNA_charging_M_lys_L_c_CAT = x[344];
E_tRNA_charging_M_met_L_c_CAT = x[345];
E_tRNA_charging_M_phe_L_c_CAT = x[346];
E_tRNA_charging_M_pro_L_c_CAT = x[347];
E_tRNA_charging_M_ser_L_c_CAT = x[348];
E_tRNA_charging_M_thr_L_c_CAT = x[349];
E_tRNA_charging_M_trp_L_c_CAT = x[350];
E_tRNA_charging_M_tyr_L_c_CAT = x[351];
E_tRNA_charging_M_val_L_c_CAT = x[352];

transfer_function_vector = Float64[];
# type: inhibition actor: M_pep_c target: R_pfk
push!(transfer_function_vector,1.0 - control_parameter_array[1,1]*M_pep_c^control_parameter_array[1,2]/(1+control_parameter_array[1,1]*M_pep_c^control_parameter_array[1,2]));
control_vector[4] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: activation actor: M_pep_c target: R_fdp
push!(transfer_function_vector,control_parameter_array[2,1]*M_pep_c^control_parameter_array[2,2]/(1+control_parameter_array[2,1]*M_pep_c^control_parameter_array[2,2]));
control_vector[5] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: activation actor: M_fdp_c target: R_pyk
push!(transfer_function_vector,control_parameter_array[3,1]*M_fdp_c^control_parameter_array[3,2]/(1+control_parameter_array[3,1]*M_fdp_c^control_parameter_array[3,2]));
control_vector[18] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_pep_c target: R_pck
push!(transfer_function_vector,1.0 - control_parameter_array[4,1]*M_pep_c^control_parameter_array[4,2]/(1+control_parameter_array[4,1]*M_pep_c^control_parameter_array[4,2]));
control_vector[19] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: activation actor: M_fdp_c target: R_ppc
push!(transfer_function_vector,control_parameter_array[5,1]*M_fdp_c^control_parameter_array[5,2]/(1+control_parameter_array[5,1]*M_fdp_c^control_parameter_array[5,2]));
control_vector[20] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_pyr_c target: R_pdh
push!(transfer_function_vector,1.0 - control_parameter_array[6,1]*M_pyr_c^control_parameter_array[6,2]/(1+control_parameter_array[6,1]*M_pyr_c^control_parameter_array[6,2]));
control_vector[21] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_pep_c target: R_pps
push!(transfer_function_vector,1.0 - control_parameter_array[7,1]*M_pep_c^control_parameter_array[7,2]/(1+control_parameter_array[7,1]*M_pep_c^control_parameter_array[7,2]));
control_vector[22] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_akg_c target: R_gltA
push!(transfer_function_vector,1.0 - control_parameter_array[8,1]*M_akg_c^control_parameter_array[8,2]/(1+control_parameter_array[8,1]*M_akg_c^control_parameter_array[8,2]));
control_vector[39] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_pep_c target: R_icd
push!(transfer_function_vector,1.0 - control_parameter_array[9,1]*M_pep_c^control_parameter_array[9,2]/(1+control_parameter_array[9,1]*M_pep_c^control_parameter_array[9,2]));
control_vector[42] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_akg_c target: R_aceA
push!(transfer_function_vector,1.0 - control_parameter_array[10,1]*M_akg_c^control_parameter_array[10,2]/(1+control_parameter_array[10,1]*M_akg_c^control_parameter_array[10,2]));
# type: inhibition actor: M_pep_c target: R_aceA
push!(transfer_function_vector,1.0 - control_parameter_array[11,1]*M_pep_c^control_parameter_array[11,2]/(1+control_parameter_array[11,1]*M_pep_c^control_parameter_array[11,2]));
# type: inhibition actor: M_3pg_c target: R_aceA
push!(transfer_function_vector,1.0 - control_parameter_array[12,1]*M_3pg_c^control_parameter_array[12,2]/(1+control_parameter_array[12,1]*M_3pg_c^control_parameter_array[12,2]));
control_vector[62] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_akg_c target: R_aceB
push!(transfer_function_vector,1.0 - control_parameter_array[13,1]*M_akg_c^control_parameter_array[13,2]/(1+control_parameter_array[13,1]*M_akg_c^control_parameter_array[13,2]));
# type: inhibition actor: M_pep_c target: R_aceB
push!(transfer_function_vector,1.0 - control_parameter_array[14,1]*M_pep_c^control_parameter_array[14,2]/(1+control_parameter_array[14,1]*M_pep_c^control_parameter_array[14,2]));
# type: inhibition actor: M_3pg_c target: R_aceB
push!(transfer_function_vector,1.0 - control_parameter_array[15,1]*M_3pg_c^control_parameter_array[15,2]/(1+control_parameter_array[15,1]*M_3pg_c^control_parameter_array[15,2]));
control_vector[63] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: inhibition actor: M_accoa_c target: R_maeB
push!(transfer_function_vector,1.0 - control_parameter_array[16,1]*M_accoa_c^control_parameter_array[16,2]/(1+control_parameter_array[16,1]*M_accoa_c^control_parameter_array[16,2]));
control_vector[65] = mean(transfer_function_vector);
transfer_function_vector = 0;

transfer_function_vector = Float64[];
# type: activation actor: M_pyr_c target: R_ldh_R
push!(transfer_function_vector,control_parameter_array[17,1]*M_pyr_c^control_parameter_array[17,2]/(1+control_parameter_array[17,1]*M_pyr_c^control_parameter_array[17,2]));
control_vector[74] = mean(transfer_function_vector);
transfer_function_vector = 0;

# Modify the rate_vector with the control variables -
rate_vector = rate_vector.*control_vector;

# Return the modified rate vector -
return rate_vector;
end