module ibm2ieee

const IBM32_SIGN = (UInt32(0x80000000))
const IBM32_EXPT = (UInt32(0x7f000000))
const IBM32_FRAC = (UInt32(0x00ffffff))
const IBM32_TOP =  (UInt32(0x00f00000))
const TIES_TO_EVEN_MASK32 = (UInt32(0xfffffffd))

const IBM64_SIGN = (UInt64(0x8000000000000000))
const IBM64_EXPT = (UInt64(0x7f00000000000000))
const IBM64_FRAC = (UInt64(0x00ffffffffffffff))
const IBM64_TOP =  (UInt64(0x00f0000000000000))
const TIES_TO_EVEN_MASK64 = (UInt64(0xfffffffffffffffd))

# Masks used for 3-bit and 32-bit rounded right-shifts of a 64-bit quantity.
#   The masks comprise the parity bit and the trailing bits for the shift.
const TIES_TO_EVEN_RSHIFT3 =  (UInt64(0x000000000000000b))
const TIES_TO_EVEN_RSHIFT32 = (UInt64(0x000000017fffffff))

const IEEE32_MAXEXP = 254     # Maximum biased exponent for finite values.
const IEEE32_INFINITY = (UInt32(0x7f800000))

# Constant used to count number of leading bits in a nonzero hex digit
# via `(BITCOUNT_MAGIC >> (hex_digit*2)) & 3U`. */
const BITCOUNT_MAGIC = (UInt32(0x000055af))

"""
IBM single-precision bit pattern to IEEE single-precision bit pattern.
"""

function ibm32ieee32(ibm::UInt32)::UInt32
    # Overflow and underflow possible; rounding can only happen
    # in subnormal cases.
    # int ibm_expt, ieee_expt, leading_zeros;
    # npy_uint32 ibm_frac, top_digit;
    # npy_uint32 ieee_sign, ieee_frac;

    ieee_sign = ibm & IBM32_SIGN
    ibm_frac = ibm & IBM32_FRAC

    # Quick return for zeros.
    if ibm_frac == 0
        return ieee_sign
    end

    # Reduce shift by 2 to get a binary exponent from the hex exponent.
    ibm_expt = Int((ibm & IBM32_EXPT) >> 22)

    # Normalise significand, then count leading zeros in top hex digit.
    top_digit = ibm_frac & IBM32_TOP
    while top_digit == 0
        ibm_frac <<= 4
        ibm_expt -= 4
        top_digit = ibm_frac & IBM32_TOP
    end
    leading_zeros = Int32((BITCOUNT_MAGIC >> (top_digit >> 19)) & 0x3)
    ibm_frac <<= leading_zeros

    # Adjust exponents for the differing biases of the formats: the IBM bias
    # is 64 hex digits, or 256 bits. The IEEE bias is 127. The difference is
    # -129; we get an extra -1 from the different significand representations
    # (0.f for IBM versus 1.f for IEEE), and another -1 to compensate for an
    # evil trick that saves an operation: on the fast path: we don't remove the
    # hidden 1-bit from the IEEE significand, so in the final addition that
    # extra bit ends in incrementing the exponent by one. */
    ieee_expt = ibm_expt - 131 - leading_zeros

    if ieee_expt >= 0 && ieee_expt < IEEE32_MAXEXP
        # normal case; no shift needed
        ieee_frac = ibm_frac;
        return ieee_sign + (UInt32(ieee_expt) << 23) + ieee_frac;
    elseif ieee_expt >= IEEE32_MAXEXP
        # overflow
        return ieee_sign + IEEE32_INFINITY;
    elseif ieee_expt >= -32
        # possible subnormal result; shift significand right by -ieee_expt
        # bits, rounding the result with round-ties-to-even.
        #
        # The round-ties-to-even code deserves some explanation: out of the
        # bits we're shifting out, let's call the most significant bit the
        # "rounding bit", and the rest the "trailing bits". We'll call the
        # least significant bit that *isn't* shifted out the "parity bit".
        # So for an example 5-bit shift right, we'd label the bits as follows:
        #
        # Before the shift:
        #
        #         ...xxxprtttt
        #                    ^
        #    msb            lsb
        #
        # After the shift:
        #
        #              ...xxxp
        #                    ^
        #    msb            lsb
        #
        # with the result possibly incremented by one.
        #
        # For round-ties-to-even, we need to round up if both (a) the rounding
        # bit is 1, and (b) either the parity bit is 1, or at least one of the
        # trailing bits is 1. We construct a mask that has 1-bits in the
        # parity bit position and trailing bit positions, and use that to
        # check condition (b). So for example in the 5-bit shift right, the
        # mask looks like this:
        #
        #         ...000101111 : mask
        #         ...xxxprtttt : ibm_frac
        #                    ^
        #    msb            lsb
        #
        # We then shift right by (shift - 1), add 1 if (ibm & mask) is
        # nonzero, and then do a final shift by one to get the rounded
        # value. Note that this approach avoids the possibility of
        # trying to shift a width-32 value by 32, which would give
        # undefined behaviour (see C99 6.5.7p3).
        #
        mask = ~(TIES_TO_EVEN_MASK32 << (-1 - ieee_expt))
        round_up = (ibm_frac & mask) > 0x0
        ieee_frac = ((ibm_frac >> (-1 - ieee_expt)) + round_up) >> 1
        return ieee_sign + ieee_frac
    else
        # underflow to zero
        return ieee_sign;
    end
end

"""
IBM double-precision bit pattern to IEEE single-precision bit pattern.
"""

function ibm64ieee32( ibm::UInt32)::UInt32
    # Overflow and underflow possible; rounding can occur in both
    # normal and subnormal cases.
    # int ibm_expt, ieee_expt, leading_zeros;
    # npy_uint64 ibm_frac, top_digit;
    # npy_uint32 ieee_sign, ieee_frac;

    ieee_sign = (ibm & IBM64_SIGN) >> 32
    ibm_frac = ibm & IBM64_FRAC

    # Quick return for zeros.
    if ibm_frac==0
        return ieee_sign
    end

    # Reduce shift by 2 to get a binary exponent from the hex exponent.
    ibm_expt = Int((ibm & IBM64_EXPT) >> 54)

    # Normalise significand, then count leading zeros in top hex digit.
    top_digit = ibm_frac & IBM64_TOP
    while top_digit == 0
        ibm_frac <<= 4
        ibm_expt -= 4
        top_digit = ibm_frac & IBM64_TOP
    end
    leading_zeros = Int((BITCOUNT_MAGIC >> (top_digit >> 51)) & 0x3)

    ibm_frac <<= leading_zeros;
    ieee_expt = ibm_expt - 131 - leading_zeros;

    if ieee_expt >= 0 && ieee_expt < IEEE32_MAXEXP
        # normal case; shift right 32, with round-ties-to-even
        round_up = (ibm_frac & TIES_TO_EVEN_RSHIFT32) > 0x0
        ieee_frac = ((npy_uint32)(ibm_frac >> 31) + round_up) >> 1
        return ieee_sign + ((npy_uint32)ieee_expt << 23) + ieee_frac
    elseif ieee_expt >= IEEE32_MAXEXP
        # overflow
        return ieee_sign + IEEE32_INFINITY
    elseif ieee_expt >= -32
        # possible subnormal; shift right with round-ties-to-even
        mask = ~(TIES_TO_EVEN_MASK64 << (31 - ieee_expt))
        round_up = (ibm_frac & mask) > 0x0
        ieee_frac = UInt32((ibm_frac >> (31 - ieee_expt)) + round_up) >> 1
        return ieee_sign + ieee_frac
    else
        # underflow to zero
        return ieee_sign
    end
end

"""
IBM single-precision bit pattern to IEEE double-precision bit pattern.

This is the simplest of the four cases: there's no need to check for
overflow or underflow, no possibility of subnormal output, and never
any rounding.

"""

function ibm32ieee64(ibm::UInt32)::UInt64
    ieee_sign = UInt64((ibm & IBM32_SIGN)) << 32
    ibm_frac = ibm & IBM32_FRAC

    # Quick return for zeros.
    if ibm_frac == 0
        return ieee_sign
    end

    # Reduce shift by 2 to get a binary exponent from the hex exponent.
    ibm_expt = UInt64((ibm & IBM32_EXPT) >> 22)

    # Normalise significand, then count leading zeros in top hex digit.
    top_digit = ibm_frac & IBM32_TOP
    while top_digit == 0
        ibm_frac <<= 4
        ibm_expt -= 4
        top_digit = ibm_frac & IBM32_TOP
    end
    leading_zeros = Int32((BITCOUNT_MAGIC >> (top_digit >> 19)) & 0x3)
    ibm_frac <<= leading_zeros

    # Adjust exponents for the differing biases of the formats: the IBM bias
    # is 64 hex digits, or 256 bits. The IEEE bias is 1023. The difference is
    # 767; we get an extra -1 from the different significand representations
    # (0.f for IBM versus 1.f for IEEE), and another -1 to compensate for an
    # evil trick that saves an operation: we don't remove the hidden 1-bit
    # from the IEEE significand, so in the final addition that extra bit ends
    # in incrementing the exponent by one. */
    ieee_expt = ibm_expt + 765 - leading_zeros
    ieee_frac = UInt64(ibm_frac) << (29 + leading_zeros)
    ieee_frac = UInt64(ibm_frac) << (29)
    return ieee_sign + (UInt64(ieee_expt) << 52) + ieee_frac
end

"""
IBM double-precision bit pattern to IEEE double-precision bit pattern.
"""

function ibm64ieee64(ibm::UInt64)::UInt64
    # No overflow or underflow possible, but the precision of the
    # IBM double-precision format exceeds that of its IEEE counterpart,
    # so we'll frequently need to round. */
    # int ibm_expt, ieee_expt, leading_zeros;
    # npy_uint64 ibm_frac, top_digit;
    # npy_uint64 ieee_sign, ieee_frac, round_up;

    ieee_sign = ibm & IBM64_SIGN
    ibm_frac = ibm & IBM64_FRAC

    # Quick return for zeros.
    if ibm_frac == 0
        return ieee_sign;
    end

    # Reduce shift by 2 to get a binary exponent from the hex exponent.
    ibm_expt = Int((ibm & IBM64_EXPT) >> 54);

    # Normalise significand, then count leading zeros in top hex digit.
    top_digit = ibm_frac & IBM64_TOP
    while top_digit == 0
        ibm_frac <<= 4
        ibm_expt -= 4
        top_digit = ibm_frac & IBM64_TOP
    end
    leading_zeros = Int((BITCOUNT_MAGIC >> (top_digit >> 51)) & 0x3)

    ibm_frac <<= leading_zeros
    ieee_expt = ibm_expt + 765 - leading_zeros

    # Right-shift by 3 bits (the difference between the IBM and IEEE
    # significand lengths), rounding with round-ties-to-even.
    round_up = (ibm_frac & TIES_TO_EVEN_RSHIFT3) > 0x0;
    ieee_frac = ((ibm_frac >> 2) + round_up) >> 1
    return ieee_sign + (UInt64(ieee_expt) << 52) + ieee_frac;
end

end # module


@testset "TabularQApproximator" begin
    A = TabularQApproximator([1 2; 3 4])

    @test A(1) == [1.0, 2.0]
    @test A(2) == [3.0, 4.0]
    @test A(2, 2) == 4

    update!(A, (2, 2) => -1.0)
    @test A(2) == [3.0, 3.0]
    @test A(2, 2) == 3

    update!(A, 1 => [-1.0, -1.0])
    @test A(1) == [0.0, 1.0]
    @test A(1, 1) == 0
    @test A(1, 2) == 1
end


# Random sub-space ensemble module
module RandomSubspace

	using j4pr.ClassifierCombiner
	using MLDataPattern: getobs, datasubset, ObsDim
	export SubspaceEnsemble, randomsubspace_train, randomsubspace_exec

	"""
	Random subspace ensemble object (it describes the ensemble).

	# Fields
	 * `idx::Vector{Vector{Int}}` is a vector of variable index vectors, 1 vector/ensemble member
	 * `f_train::Function` is the training function
	 * `f_exec::Function` is the execution function
	 * `members::Vector` is a vector containing the trained classifiers
	 * `combiner::ClassifierCombiner.AbstractCombiner` the means by which ensemble outputs are combined. Non-trainable combiners should be used. If a trainable
	    combiner is to be used, it has to be trained outside the ensemble
	* `parallel_train::Bool` specifies whether to parallelize training or not
	* `parallel_execution::Bool` specifies whether to parallelize execution or not

	"""
	struct SubspaceEnsemble{F, T<:Vector{S} where S, U<:AbstractCombiner}
		idx::Vector{Vector{Int}}	# vector of vectors of variable indices
		f_train::Function		# training function
		f_exec::F			# execution function
		members::T			# ensemble members (trained)
		combiner::U			# combiner (only non-trainable combiners supported)
		parallel_train::Bool		# whether to parallelize ensemble training or not
		parallel_execution::Bool	# whether to parallelize ensemble execution or not
	end

	# Aliases
	const SubspaceEnsembleCell{F<:Void, T, U} = SubspaceEnsemble{F,T,U}		# no execution function, it is dynamically generated in training
	const SubspaceEnsembleClassic{F<:Function, T, U} = SubspaceEnsemble{F,T,U} 	# the execution function is specified

	# Printers
	Base.show(io::IO, m::SubspaceEnsembleCell) = print(io, "Sub-space ensemble, $(length(m.idx)) members, Cell version")
	Base.show(io::IO, m::SubspaceEnsembleClassic) = print(io, "Sub-space ensemble, $(length(m.idx)) members")

	"""
		subspace_indices(n, L, M, replace)

	Function that generates a `Vector{Vector{Int}` containing the `L` random subsets of variable indices for a given dataset of `n` variables using `M` variables.
	If `replace` is `true`, the same variable can appear in multiple subsets and the number of subsets has no upper bound.
	"""
	function subspace_indices(n::Int, L::Int, M::Int, replace::Bool)
		@assert n > 1 "The dataset has to have more than 1 variable."
		@assert L > 0 "The random subset ensemble has to have at least 1 member."
		@assert M > 0 "The number of random variables has to be at least 1. "

		# Generate V based on n,L,M
		if replace
			V=Vector{Vector{Int}}(L)
			@inbounds for i in 1:L
				V[i] = randperm(n)[1:M]
			end
			return V
		else
			@assert L*M <=n "Cannot split $n variables to an ensemble of $L elements with $M distinct variables each."
			ro = randperm(n) # random order
			V=Vector{Vector{Int}}(L)
			k=1
			@inbounds for i in 1:M:L*M
				V[k] = ro[i:i+M-1]
				k+=1
			end
			return V
		end
	end

	"""
		randomsubspace_train(X, y, L, M, f_train, combiner, replace; parallel_train=true, parallel_execution=false)

	Trains a random subspace classifier. The function runs `f_train` on subspaces of `X` and returns `RandomSubspace` object.

	# Arguments
	 * `X::AbstractMatrix` is the training data; columns are assumed to be samples, lines variables
	 * `y::AbstractVector` are the labels
	 * `L::Int` is the ensemble size
	 * `M::Int` is the number of variables for each ensemble member
	 * `f_train::Function` is the training function for the ensemble members
	 * `f_exec::Union{Function,Void}` is the execution function for the ensemble members
	 * `combiner::ClassifierCombiner.AbstractCombiner` is the combiner
	 * `replace::Bool` specifies whether the variable subsets can contain (`true`) of not (`false`) the same variable. If `true`, any number of subsets can be generated; if
	 `false` a finite maximum number can be generated but all subsets will be distinct

	# Keyword arguments
	 * `parallel_train::Bool=false` specifies whether to paralellize or not the training. Generally a good ideea except for lazy classifiers (e.g. kNN).
	 * `parallel_execution::Bool=false` specifies whether to paralellize or not the execution. Unless the execution is time consuming (e.g. Parzen classifier), it should be left to `false`.

	It is important to note that the training method calls internally `x->f_train((x,y))` where `x` are the subsets of `X` constructed using `L`, `M` and the total number of variables of `X`.
	Considering a generic training function `g` desired to be used in the ensemble, of the following forms:
	 * `g(X, y, arg1, arg2)`, one should provide as argument something as `f_train = (x)->g_train(x[1], x[2], arg1, arg2) # uses labels`
	 * `g(X, arg1, arg2)`, one should provide as argument something as `f_train = (x)->g_train(x[1], arg1, arg2) # ignores labels`
	 * `g(X, arg1, arg2)` where `X==(data,labels)`, one should provide as argument something as `f_train = (x)->g_train(x, arg1, arg2) # MLDataUtils container case`
	 * `g(args...)::CellFunU`, one can simply provide `g.f` (i.e. the training function of the untrained function cell) since it already contains the training arguments.

	 During execution, `f_exec(ensemble.members[i], Xᵢ)` will be called for all subspace subsets `Xᵢ` of `X` corresponding to the members of the ensemble hence `f_exec` must have
	 a signature `f_exec(M, X)` where `M` is represents the learned model and `X` represents the data.
	"""
	function randomsubspace_train(X::T where T<:AbstractMatrix, y::S where S<:AbstractVector, L::Int, M::Int, f_train::Function, f_exec::Union{Function,Void},
			       		combiner::S where S<:AbstractCombiner, replace::Bool;
					parallel_train::Bool=true, parallel_execution::Bool=false)

		# Calculate the number of variables
		n = size(X,1)

		# Calculate subspace indices
		idx = subspace_indices(n, L, M, replace)

		# Return data subsets (Vector of datasubsets of X) and V
		if parallel_train
			members = pmap(x->f_train((x,y)), (getobs(datasubset(X,i,ObsDim.First())) for i in idx) )
		else
			members = map(x->f_train((x,y)), (getobs(datasubset(X,i,ObsDim.First())) for i in idx) )
		end

		return SubspaceEnsemble(idx, f_train, f_exec, members, combiner, parallel_train, parallel_execution)
	end

	"""
		randomsubspace_exec(ensemble, X)

	Executes the random subspace ensemble.

	# Arguments
	 * `ensemble::RandomSubspace.SubspaceEnsemble` the subspace ensemble
	 * `X::AbstractMatrix` is the data; columns are assumed to be samples, lines variables
	"""
	function randomsubspace_exec(ensemble::SubspaceEnsembleClassic, X::T where T<:AbstractMatrix)
		if ensemble.parallel_execution
			out = @sync @parallel (vcat) for i in eachindex(ensemble.members)
				ensemble.f_exec(ensemble.members[i], getobs(datasubset(X, ensemble.idx[i], ObsDim.First())))
			end
		else
			out = [ensemble.f_exec(ensemble.members[i], getobs(datasubset(X, ensemble.idx[i], ObsDim.First()))) for i in eachindex(ensemble.members)]

		end

		return combiner_exec(ensemble.combiner, out)
	end

	# j4pr case: no execution function, the ensemble members contain it already ('f_exec' is not present at all)
	function randomsubspace_exec(ensemble::SubspaceEnsembleCell, X::T where T<:AbstractMatrix )
		if ensemble.parallel_execution
			out = @sync @parallel (vcat) for i in eachindex(ensemble.members)
				ensemble.members[i]( getobs(datasubset(X, ensemble.idx[i], ObsDim.First())) )
			end
		else
			out = [ensemble.members[i](getobs(datasubset(X, ensemble.idx[i],ObsDim.First()))) for i in eachindex(ensemble.members)]
		end

		return combiner_exec(ensemble.combiner, out)
	end
end

##########################
# FunctionCell Interface #
##########################
"""
	randomsubspace(f, L, M, combiner, replace; parallel_train=true, parallel_execution=false)

Creates an untrained random subspace ensemble.

# Arguments
 * `f::CellFunU` is a untrained cell corresponding to the classifer/transform that the ensemble will contain
 * `L::Int` is the ensemble size
 * `M::Int` is the number of variables for each ensemble member
 * `f_train::Function` is the training function
 * `combiner::ClassifierCombiner.AbstractCombiner` is the combiner
 * `replace::Bool` specifies whether the variable subsets can contain (`true`) or not (`false`) the same variable. If `true`, any number of subsets can be generated; if
 `false` a finite maximum number can be generated but all subsets will be distinct (default: `true`)

# Keyword arguments
 * `parallel_train::Bool=true` specifies whether to paralellize or not the training.
 * `parallel_execution::Bool=false` specifies whether to paralellize or not the execution.

For more information:
	[1] T.K. Ho "The Random Subspace Method for Constructing Decision Forests" 1998 IEEE Transactions on Pattern Analysis and Machine Intelligence 20(8): 832-844.
 	[2] L. Kuncheva et al. "Random Subspace Ensembles for fMRI Classification" IEEE Transactions on Medical Imaging 29 (2): 531-542.
	[3] L. Kuncheva "Combining Pattern Classifiers 2'nd Ed." 2014, ISBN 978-1-118-31523-1

Try `?j4pr.RandomSubspace.randomsubspace_train` and `?j4pr.RandomSubspace.randomsubspace_exec` and for more details.

# Examples
```
julia> using j4pr

julia> D=DataGenerator.iris() # Get the iris dataset
Iris Dataset, 150 obs, 4 vars, 1 target(s)/obs, 3 distinct values: "virginica"(50),"setosa"(50),"versicolor"(50)

julia> w=j4pr.knn(5,smooth=:ml) # 5-NN classifier, maximum likelihood posterior smoothing
5-NN classifier: smooth=ml, no I/O size information, untrained

julia> we=randomsubspace(w, 5, 2,j4pr.ClassifierCombiner.GeneralizedMeanCombiner(5,3,1.0)) # ensemble of 5 members, 2 variables; the combiner uses 3 classes
Sub-space ensemble (5-NN classifier: smooth=ml, 5 members × 2 vars), no I/O size information, untrained

julia> we_trained = D |>we # train ensemble
Sub-space ensemble (5-NN classifier: smooth=ml, 5 members × 2 vars), 4->3, trained

julia> +D |> we_trained # run ensemble on training data
3×150 Array{Float64,2}:
 1.0          1.0          1.0          1.0          1.0          1.0          1.0          …  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16
 2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16     2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  0.12
 2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16  2.22045e-16     1.0          1.0          1.0          1.0          1.0          1.0          0.88
```
"""
randomsubspace(f::F where F<:CellFunU, L::Int, M::Int,
	       combiner::ClassifierCombiner.AbstractCombiner=ClassifierCombiner.NoCombiner(),
	       replace::Bool=true; parallel_train::Bool=true, parallel_execution::Bool=false) =
	FunctionCell(randomsubspace, (f, L, M, combiner, replace), ModelProperties(), "Sub-space ensemble ($(f.tinfo), $L members × $M vars)";
			parallel_train=parallel_train, parallel_execution=parallel_execution) # untrained function cell

############################
# DataCell/Array Interface #
############################
"""
	randomsubspace(x, f, L, M, combiner, replace; parallel_train=true, parallel_execution=false)

Trains a random subspace ensemble using the data `x`.
"""
# Training
randomsubspace(x::T where T<:CellDataL, f::F where F<:CellFunU, L::Int, M::Int, combiner::ClassifierCombiner.AbstractCombiner=ClassifierCombiner.NoCombiner(),
	       replace::Bool=true; parallel_train::Bool=true, parallel_execution::Bool=false) =
	randomsubspace((getx!(x), gety(x)), f, L, M, combiner, replace; parallel_train=parallel_train, parallel_execution=parallel_execution)

randomsubspace(x::Tuple{T,S} where T<:AbstractVector where S<:AbstractVector, f::F where F<:CellFunU, L::Int, M::Int,
	combiner::ClassifierCombiner.AbstractCombiner=ClassifierCombiner.NoCombiner(), replace::Bool=true; parallel_train::Bool=true, parallel_execution::Bool=false) =
	randomsubspace((mat(x[1], LearnBase.ObsDim.Constant{2}()), x[2]), f, L, M, combiner, replace; parallel_train=parallel_train, parallel_execution=parallel_execution)

randomsubspace(x::Tuple{T,S} where T<:AbstractMatrix where S<:AbstractVector, f::F where F<:CellFunU, L::Int, M::Int,
	       combiner::ClassifierCombiner.AbstractCombiner=ClassifierCombiner.NoCombiner(), replace::Bool=true;
	       parallel_train::Bool=true, parallel_execution::Bool=false) =
begin

	@assert nobs(x[1]) == nobs(x[2]) "[randomsubspace] Expected $(nobs(x[1])) labels/values, got $(nobs(x[2]))."

	# Transform labels first
	enc = labelencn(x[2])
	yenc = label2ind.(x[2],enc)

	# Train model
	ensembledata = RandomSubspace.randomsubspace_train(x[1], yenc, L, M, f.f, nothing, combiner, replace;
						    parallel_train=parallel_train, parallel_execution=parallel_execution)

	# Build model properties
	modelprops = ModelProperties(nvars(x[1]), length(enc.label), enc)

	FunctionCell(randomsubspace, Model(ensembledata, modelprops),
	      "Sub-space ensemble ($(f.tinfo), $L members × $M vars)" ) # trained function cell
end

# Execution
randomsubspace(x::T where T<:CellData, model::Model{<:RandomSubspace.SubspaceEnsemble}) =
	datacell(randomsubspace(getx!(x), model), gety(x))
randomsubspace(x::T where T<:AbstractVector, model::Model{<:RandomSubspace.SubspaceEnsemble}) =
	randomsubspace(mat(x, LearnBase.ObsDim.Constant{2}()), model)
randomsubspace(x::T where T<:AbstractMatrix, model::Model{<:RandomSubspace.SubspaceEnsemble}) =
	RandomSubspace.randomsubspace_exec(model.data,x)


# SDPA file format reader-writer

# Redefine the following method to branch on the filename extension of `optimizer` supports more formats
MOI.write(optimizer::SOItoMOIBridge, filename::String) = MOI.write(optimizer.sdoptimizer, filename)
MOI.write(optimizer::AbstractSDOptimizer, filename::String) = writesdpa(optimizer, filename)
MOI.read!(optimizer::SOItoMOIBridge, filename::String) = MOI.read!(optimizer.sdoptimizer, filename)
MOI.read!(optimizer::AbstractSDOptimizer, filename::String) = readsdpa!(optimizer, filename)

function writesdpa(optimizer::AbstractSDOptimizer, filename::String)
    endswith(filename, ".sdpa") || error("Filename must end with .sdpa")
    open(filename, "w") do io
        nconstrs = getnumberofconstraints(optimizer)
        println(io, nconstrs)
        nblocks = getnumberofblocks(optimizer)
        println(io, nblocks)
        for blk in 1:nblocks
            print(io, getblockdimension(optimizer, blk))
            if blk != nblocks
                print(io, ' ')
            end
        end
        println(io)
        for c in 1:nconstrs
            print(io, getconstraintconstant(optimizer, c))
            if c != nconstrs
                print(io, ' ')
            end
        end
        println(io)
        for (val, blk, i, j) in getobjectivecoefficients(optimizer)
            println(io, "0 $blk $i $j $val")
        end
        for c in 1:nconstrs
            for (val, blk, i, j) in getconstraintcoefficients(optimizer, c)
                println(io, "$c $blk $i $j $val")
            end
        end
    end
end

nextline(io::IO) = chomp(readline(io))

function readsdpa!(optimizer::AbstractSDOptimizer, filename::String)
    endswith(filename, ".sdpa") || error("Filename '$filename' must end with .sdpa")
    open(filename, "r") do io
        line = nextline(io)
        while line[1] == '"' || line[1] == '*' # Comment
            line = nextline(io)
        end
        nconstrs = parse(Int, line)
        nblocks = parse(Int, nextline(io))
        blkdims = parse.(Int, split(nextline(io)))
        init!(optimizer, blkdims, nconstrs)
        T = coefficienttype(optimizer)
        constraint_constants = parse.(T, split(nextline(io)))
        for c in 1:nconstrs
            setconstraintconstant!(optimizer, constraint_constants[c], c)
        end
        while !eof(io)
            line = nextline(io)
            isempty(line) && break
            s = split(line)
            c = parse(Int, s[1])
            0 ≤ c ≤ nconstrs || error("Invalid constraint index $c in '$filename', it should be an integer between 0 and $nconstrs")
            blk = parse(Int, s[2])
            1 ≤ blk ≤ nblocks || error("Invalid block index $blk in '$filename', it should be an integer between 0 and $nblocks")
            i = parse(Int, s[3])
            j = parse(Int, s[4])
            val = parse(T, s[5])
            if iszero(c)
                setobjectivecoefficient!(optimizer, val, blk, i, j)
            else
                setconstraintcoefficient!(optimizer, val, c, blk, i, j)
            end
        end
    end
end


"""
    basicExperiment(; genj::GeneticJuliaStructure = GenJ)

Runs a single experiment with the tools and methods specified in `genj`.

# Keyword Arguments
- `genj::GeneticJuliaStructure = GenJ`: the main structure. For `code user`: don't modify the default value unless multiple experiments are going to be run.

See also: [`run`](@ref)
"""
function basicExperiment(; genj::GeneticJuliaStructure = GenJ)

    initTime(genj)

    genPopulation!(genj)
    evaluate!(genj, genj._population)

    initBestFitness(genj)

    while !(reached(genj))

        saveResults(genj)

        selectedParents = selectParents(genj)

        offspring = cross(genj, selectedParents)
        mutatedOffspring = mutate(genj, offspring)

        evaluate!(genj, mutatedOffspring)

        replacePopulation!(genj, mutatedOffspring)
    end

    saveResults(genj)
end # function

"""
    SPEA(archiveSize::Integer, distanceFunction::Function=euclideanDistance; genj::GeneticJuliaStructure = GenJ)

documentation
"""
function SPEA(archiveSize::Integer, distanceFunction::Function=euclideanDistance; genj::GeneticJuliaStructure = GenJ)

    getDominant(population::Array{Individual}) = filter(x->getGlobalFitness(x)==1, population)

    function truncateArchive(archive::Array{Individual})

        k = round(Integer, sqrt(archiveSize + genj._generator._popSize))
        rest = length(archive) - archiveSize
        fitnesses = getFitness(archive)
        distanceMatrix = pairwiseDistance(fitnesses, fitnesses, distanceFunction)

        for i=1:rest
            nearestDistances = sum(distanceMatrix[1, getKnearest(distanceMatrix[1,:], k)])
            bestIndex = 1
            for j=2:size(distanceMatrix)[1]
                current = sum(distanceMatrix[j, getKnearest(distanceMatrix[j,:], k)])
                if current < nearestDistances
                    nearestDistances = current
                    bestIndex = j
                end
            end
            distanceMatrix = distanceMatrix[1:end .!= bestIndex, 1:end .!= bestIndex]
            archive = archive[1:end .!= bestIndex]
        end

        return archive
    end

    initTime(genj)
    genPopulation!(genj)
    evaluate!(genj, genj._population)

    initBestFitness(genj)

    archive = getDominant(genj._population)

    while !reached(genj)

        saveResults(genj)

        if length(archive) > archiveSize
            archive=truncateArchive(archive)
        end
        selectedParents = selectParents(genj, union(genj._population, archive))

        offspring = cross(genj, selectedParents)
        mutatedOffspring = mutate(genj, offspring)

        evaluate!(genj, mutatedOffspring)

        replacePopulation!(genj, mutatedOffspring)

        archive = getDominant(union(genj._population, archive))
    end

    saveResults(genj)
end # function


Variable(vidx) = Variable(
    vidx,
    0,
    0,
    0,
    0,
    [],
    [],
    [],
    [],
    0,
    0,
    0,
    Vector{Vector{Tuple{Symbol,Int,Int,Int}}}(),
    false,
    false,
    false,
    false,
    0.0,
    Vector{Bool}()
)

MatchingInit() = MatchingInit(0, Int[], Int[], Int[], Int[], Int[], Int[], Bool[], Bool[])
SCCInit() = SCCInit(Int[], Int[], Int[], Bool[], Int[])

function ConstraintInternals(cidx::Int, fct, set, indices::Vector{Int})
    return ConstraintInternals(
        cidx,
        fct,
        set,
        indices,
        Int[],
        ImplementedConstraintFunctions(),
        false,
        false,
        false,
        Vector{BoundRhsVariable}(undef, 0),
    )
end

function ImplementedConstraintFunctions()
    return ImplementedConstraintFunctions([
        false for f in fieldnames(ImplementedConstraintFunctions)
    ]...)
end

function LinearConstraint(
    cidx,
    indices::Vector,
    coeffs::Vector{T},
    constant,
    set::MOI.AbstractScalarSet,
) where {T}
    @assert length(indices) == length(coeffs)
    len = length(coeffs)
    scalar_terms = Vector{MOI.ScalarAffineTerm{T}}(undef, len)
    for (i, idx, coeff) in zip(1:len, indices, coeffs)
        scalar_terms[i] = MOI.ScalarAffineTerm{T}(coeff, MOI.VariableIndex(idx))
    end
    saf = MOI.ScalarAffineFunction(scalar_terms, constant)
    return LinearConstraint(cidx, saf, set, indices)
end

function LinearConstraint(
    cidx::Int,
    fct::MOI.ScalarAffineFunction,
    set::MOI.AbstractScalarSet,
    indices::Vector{Int},
)
    # get common type for rhs and coeffs
    # use the first value (can be .upper, .lower, .value) and subtract left constant
    rhs = -fct.constant
    if isa(set, Union{MOI.EqualTo,CS.NotEqualTo})
        rhs += set.value
    elseif isa(set, MOI.LessThan)
        rhs += set.upper
    end
    coeffs = [t.coefficient for t in fct.terms]
    promote_T = promote_type(typeof(rhs), eltype(coeffs))
    if promote_T != eltype(coeffs)
        coeffs = convert.(promote_T, coeffs)
    end
    if promote_T != typeof(rhs)
        rhs = convert(promote_T, rhs)
    end
    maxs = zeros(promote_T, length(indices))
    mins = zeros(promote_T, length(indices))
    pre_maxs = zeros(promote_T, length(indices))
    pre_mins = zeros(promote_T, length(indices))
    # this can be changed later in `set_in_all_different!` but needs to be initialized with false
    in_all_different = false

    is_strict = !(isa(set, MOI.LessThan) || isa(set, MOI.EqualTo))
    is_equal = isa(set, MOI.EqualTo)
    internals = ConstraintInternals(cidx, fct, set, indices)
    # the rhs is filled in init_constraint
    lc = LinearConstraint(
        internals,
        in_all_different,
        is_strict,
        is_equal,
        false, # the rhs is not a strong rhs yet
        zero(promote_T),
        zero(promote_T),
        coeffs,
        mins,
        maxs,
        pre_mins,
        pre_maxs,
    )
    return lc
end

function ReifiedConstraint(std, act_std, inner_constraint, anti_constraint)
    return ReifiedConstraint(std, act_std, inner_constraint, anti_constraint, false, 0)
end

"""
    ConstraintSolverModel(T::DataType=Float64)

Create the constraint model object and specify the type of the solution
"""
function ConstraintSolverModel(::Type{T} = Float64) where {T<:Real}
    ConstraintSolverModel(
        nothing, # lp_model
        Vector{VariableRef}(), # lp_x
        Vector{Variable}(), # init_search_space
        Vector{Variable}(), # search_space
        Vector{Tuple{Int,Int}}(), # init_fixes
        Vector{Vector{Int}}(), # subscription
        Vector{Constraint}(), # constraints
        Vector{VarAndVal}(), # root_infeasible_vars
        Vector{Int}(), # bt_infeasible
        1, # c_backtrack_idx
        1, # c_step_nr
        Vector{BacktrackObj{T}}(), # backtrack_vec
        PriorityQueue{Int,Priority}(Base.Order.Reverse), # priority queue for `get_next_node`
        MOI.FEASIBILITY_SENSE, #
        NoObjective(), #
        Vector{Bool}(), # var_in_obj
        Val(:DFS),
        get_branch_strategy(),
        get_branch_split(),
        true,
        ActivityObj(),
        zero(T), # best_sol,
        zero(T), # best_bound
        Vector{Solution}(), # all solution objects
        CSInfo(0, false, 0, 0, 0, NumberConstraintTypes()), # info
        Dict{Symbol,Any}(), # input
        Vector{TreeLogNode{T}}(), # logs
        SolverOptions(), # options,
        -1.0, # solve start time
        -1.0, # solve time will be overwritten
    )
end

@deprecate init() ConstraintSolverModel()

function NumberConstraintTypes()
    return NumberConstraintTypes(zeros(Int, length(fieldnames(NumberConstraintTypes)))...)
end

function new_BacktrackObj(com::CS.CoM, parent_idx, vidx, lb, ub)
    parent = com.backtrack_vec[parent_idx]
    return BacktrackObj{parametric_type(com)}(
        length(com.backtrack_vec) + 1, # idx
        -1, # step_nr
        parent_idx,
        parent.depth + 1,
        :Open, # status
        true, # is feasible
        vidx,
        lb, # lb and ub only take effect if vidx != 0
        ub, # ub
        parent.best_bound,
        parent.solution,
        zeros(length(com.search_space)), # solution values of bound computation
    )
end

function BacktrackObj(com::CS.CoM)
    return BacktrackObj(
        1, # idx
        -1, # step_nr
        0, # parent_idx
        0, # depth
        :Closed, # status
        true, # is feasible until proven otherwise
        0, # vidx
        0, # lb and ub only take effect if vidx != 0
        0, # ub
        com.sense == MOI.MIN_SENSE ? typemax(com.best_bound) : typemin(com.best_bound),
        zeros(length(com.search_space)),
        zeros(length(com.search_space)),
    )
end

function BoolConstraintInternals()
    return BoolConstraintInternals(false, 0, false, 0)
end

function AndConstraint(internals::ConstraintInternals, lhs::Constraint, rhs::Constraint)
    return AndConstraint(internals, BoolConstraintInternals(), lhs, rhs)
end

function OrConstraint(internals::ConstraintInternals, lhs::Constraint, rhs::Constraint)
    return OrConstraint(internals, BoolConstraintInternals(), lhs, rhs)
end


function partial_operand(n)

  reps = parse(Int64, ENV["LAMP_REPS"])

  A = randn(n,n)
  B = randn(n,n)

  Benchmarker.add_data(csv, "part_op_col_mult_nai", Benchmarker.measure(reps, part_op_col_mult_nai, A, B))
  Benchmarker.add_data(csv, "part_op_col_add_nai", Benchmarker.measure(reps, part_op_col_add_nai, A, B))
  Benchmarker.add_data(csv, "part_op_col_add_rec", Benchmarker.measure(reps, part_op_col_add_rec, A, B))

  Benchmarker.add_data(csv, "part_op_ele_mult_nai", Benchmarker.measure(reps, part_op_ele_mult_nai, A, B))
  Benchmarker.add_data(csv, "part_op_ele_add_nai", Benchmarker.measure(reps, part_op_ele_add_nai, A, B))
  Benchmarker.add_data(csv, "part_op_ele_add_rec", Benchmarker.measure(reps, part_op_ele_add_rec, A, B))

  @test isapprox(implicit_diagonal(A, B)[2], explicit_diagonal(A, B)[2])
end

function part_op_col_mult_nai(A, B)
  result = zeros(size(A,1), 1)
  start::Float64 = 0.0
  finish::Float64 = 0.0
  Benchmarker.cachescrub()
  Benchmarker.gcscrub()
  start = time_ns()

  result = (A*B)[:,2]

  finish = time_ns()
  return ((finish-start)*1e-9, result)
end

function part_op_col_add_nai(A, B)
  result = zeros(size(A,1), 1)
  start::Float64 = 0.0
  finish::Float64 = 0.0
  Benchmarker.cachescrub()
  Benchmarker.gcscrub()
  start = time_ns()

  result = (A+B)[:,2]

  finish = time_ns()
  return ((finish-start)*1e-9, result)
end

function part_op_col_add_rec(A, B)
  result = zeros(size(A,1), 1)
  start::Float64 = 0.0
  finish::Float64 = 0.0
  Benchmarker.cachescrub()
  Benchmarker.gcscrub()
  start = time_ns()

  result = A[:,2] + B[:,2]

  finish = time_ns()
  return ((finish-start)*1e-9, result)
end

function part_op_ele_mult_nai(A, B)
  result = zeros(size(A,1), 1)
  start::Float64 = 0.0
  finish::Float64 = 0.0
  Benchmarker.cachescrub()
  Benchmarker.gcscrub()
  start = time_ns()

  result = (A*B)[2,2]

  finish = time_ns()
  return ((finish-start)*1e-9, result)
end

function part_op_ele_add_nai(A, B)
  result = zeros(size(A,1), 1)
  start::Float64 = 0.0
  finish::Float64 = 0.0
  Benchmarker.cachescrub()
  Benchmarker.gcscrub()
  start = time_ns()

  result = (A+B)[2,2]

  finish = time_ns()
  return ((finish-start)*1e-9, result)
end

function part_op_ele_add_rec(A, B)
  result = zeros(size(A,1), 1)
  start::Float64 = 0.0
  finish::Float64 = 0.0
  Benchmarker.cachescrub()
  Benchmarker.gcscrub()
  start = time_ns()

  result = A[2,2] + B[2,2]

  finish = time_ns()
  return ((finish-start)*1e-9, result)
end


## deprecated functions..

function abc_mcmc_dbscan_full(dfcart::GaiaClustering.Df, params::abcfull)
    let
        println("# abc_mcmc_dbscan_full is deprecated ...")

        Random.seed!()
        println("## ABC/MCMC for DBSCAN FULL (parameters+weighting)...")
        mci = mcfull(zeros(Float64,0),zeros(Int32,0),zeros(Int32,0) , zeros(Float64,0), zeros(Int32,0),
        zeros(Float64,0), zeros(Float64,0), zeros(Float64,0))

        initial = true
        th = []
        mi = modelfull(0.0,0,0, 0., 0., 0.)
        micurrent = modelfull(0.0,0,0, 0., 0., 0.)
        probi = 0.
        probcurrent = 0.

        minimumQ = params.minQ
        minstars = params.minstars
        niter = params.niter
        nburn = params.nburnout

        maxstars= 5000

        println("### Minimum Q : $minimumQ")
        println("### Minimum nstars : $minstars")
        println("### Maximum nstars : $maxstars")
        minimumQ , minstars = check_qminqstar_full(dfcart, params, minimumQ, minstars)
        println("### Minimum Q : $minimumQ")
        println("### Minimum nstars : $minstars")
        if minstars < params.forcedminstars
            minstars = params.forcedminstars
            println("### Minimum nstars forced to : $minstars")
        end

        while initial
            mi, probi = theta_full(params)
            dfcartnorm = getDfcartnorm(dfcart , mi)
            qres , nstars = find_clusters(dfcartnorm, dfcart, mi)
            if qres > minimumQ && nstars >= minstars && nstars <= maxstars
                println("### init done ...")
                initial = false
                push!(mci.eps, mi.eps)
                push!(mci.mne, mi.min_nei)
                push!(mci.mcl, mi.min_cl)
                push!(mci.w3d, mi.w3d)
                push!(mci.wvel, mi.wvel)
                push!(mci.whrd, mi.whrd)
                push!(mci.qc,  qres)
                push!(mci.qn,  nstars)
            end
        end

        ministats_full(100, dfcart, mi , params)

        nchain = 1
        loopAgain = true
        burndone = false

        while loopAgain
            micurrent, probcurrent = thetaiter_full(mi , params)
            dfcartnorm = getDfcartnorm(dfcart , micurrent)
            qres , nstars = find_clusters(dfcartnorm, dfcart, micurrent)

            if qres > minimumQ && nstars >= minstars && nstars <= maxstars
            ### Metropolis-Hasting
                α = probcurrent / probi
                if α > rand()
                    mi = micurrent
                    probi = probcurrent
                    nchain += 1
                    if (nchain%1000 == 0) println("### chain:",nchain) end
                    if nchain > nburn && !burndone println("### burnout done...") ; nchain = 0 ; burndone = true end
                    if nchain > niter loopAgain = false end
                    if burndone
                        push!(mci.eps, mi.eps)
                        push!(mci.mne, mi.min_nei)
                        push!(mci.mcl, mi.min_cl)
                        push!(mci.w3d, mi.w3d)
                        push!(mci.wvel, mi.wvel)
                        push!(mci.whrd, mi.whrd)
                        push!(mci.qc , qres)
                        push!(mci.qn,  nstars)
                    end
                else
                    nchain += 1
                    if (nchain%1000 == 0) println("### chain:",nchain) end
                    if nchain > nburn && !burndone println("### burnout done...") ; nchain = 0 ; burndone = true end
                    if nchain > niter loopAgain = false end
                    if burndone
                        push!(mci.eps, mi.eps)
                        push!(mci.mne, mi.min_nei)
                        push!(mci.mcl, mi.min_cl)
                        push!(mci.w3d, mi.w3d)
                        push!(mci.wvel, mi.wvel)
                        push!(mci.whrd, mi.whrd)
                        push!(mci.qc , qres)
                        push!(mci.qn,  nstars)
                    end
                end
            end
        end
        println("## ABC/MCMC FULL done")
        println("##")
        return(mci)
    end
end

### function to adapt the minimum condition to evaluate the  solution
###
function check_qminqstar_full(dfcart::GaiaClustering.Df,
        params::abcfull, minimumQ, minstars)
    let
        new_minq = minimumQ
        new_minstars = minstars
        notfound = true

        ## standard sofar 50
        mingoodsolution = 10
        niter = 500

        println("#### Checking the minQ and minStars conditions...")
        println("#### Minimum good solutions $mingoodsolution")

        while notfound
            goodsolutions = 0
            for i in 1:niter
                mi, probi = theta_full(params)
                dfcartnorm = getDfcartnorm(dfcart , mi)
                qres , nstars = find_clusters(dfcartnorm, dfcart, mi)
                if qres > new_minq && nstars >= new_minstars
                    goodsolutions += 1
                end
                if goodsolutions > mingoodsolution
                    notfound = false
                    return(new_minq, new_minstars)
                end
            end
            if notfound
                new_minq *= 0.95
                new_minstars = trunc(Int, 0.95 * new_minstars)
                println("#### MinQ not reached yet... testing with $new_minq")
            end

            if new_minstars == 0
                println("#### Problem with minStars, return(0.5,5)")
                return(0.5,5)
            end
        end
        return(new_minq, new_minstars)
    end
end
function find_clusters(df::GaiaClustering.Df, dfcart::GaiaClustering.Df , m::GaiaClustering.model,
    aperture2d = 1.5, maxaperture2d = 15, aperturev = 3.0, maxaperturev = 20, nboot = 50 ,
    aperture3d = 3., maxaperture3d = 20)
    let
        println("Warning...find_clusters deprecated, find_clusters2 should be used instead")
        labels = clusters(df.data , m.eps , 20, m.min_nei, m.min_cl)
        nsol= length(labels)
        if nsol == 0
            return(0, 0)
        end
        if nsol > 1000
            println("### Warning... $nsol clusters found in find_clusters ")
        end

    ### metrics of the clusters
        q2d = metric2(dfcart, labels, "spatial2d" , aperture2d, maxaperture2d, nboot)
        q3d = metric2(dfcart, labels, "spatial3d" , aperture3d, maxaperture3d, nboot)     #### Added
        qv  = metric2(dfcart, labels, "velocity" , aperturev, maxaperturev, nboot)
        qp, qa = metric2(dfcart, labels, "HRD" )

        nlab = []
        for ilab in labels
            push!(nlab,length(ilab))
        end

    #### metric for the number of stars in the cluster
        qn = []
        for nl in nlab
            push!(qn,log10(nl))
        end

        qc = 0.
        qstar = 0
        for i in 1:length(nlab)
            k1 = q2d[i][1]
            k1bis = q3d[i][1]
            k2 = qv[i][1]
            k3 = qa[i][1]
            k4 = qn[i]
            ############### Composite metric ###
            qq = (2k1 + k1bis + 3k2 + k3 + k4) / 8.0
            # qq = (3k1 + k1bis + 3k2 + k4) / 8.0
            ###############
            if qq > qc
                qc = qq
                qstar = nlab[i]
            end
        end
        return(qc, qstar)
    end
end

## broadcasting modelfull ..
##
function find_clusters(df::GaiaClustering.Df, dfcart::GaiaClustering.Df , m::GaiaClustering.modelfull,
    aperture2d = 1.5, maxaperture2d = 15, aperturev = 3.0, maxaperturev = 20, nboot = 50,
    aperture3d = 3., maxaperture3d = 20)
    let
        println("Warning...find_clusters deprecated, find_clusters2 should be used instead")
        labels = clusters(df.data , m.eps , 20, m.min_nei, m.min_cl)
        nsol= length(labels)
        if nsol == 0 || nsol > 1000
            println("### Warning... $nsol clusters found in find_clusters2 ")
            return(0, 0)
        end

    ### metrics of the clusters
        q2d = metric2(dfcart, labels, "spatial2d" , aperture2d, maxaperture2d, nboot)
        q3d = metric2(dfcart, labels, "spatial3d" , aperture3d, maxaperture3d, nboot)
        qv  = metric2(dfcart, labels, "velocity" , aperturev, maxaperturev, nboot)
        qp, qa = metric2(dfcart, labels, "HRD" )

        nlab = []
        for ilab in labels
            push!(nlab,length(ilab))
        end

    #### metric for the number of stars in the cluster
        qn = []
        for nl in nlab
            push!(qn,log10(nl))
        end

        qc = 0.
        qstar = 0
        for i in 1:length(nlab)
            k1 = q2d[i][1]
            k1bis = q3d[i][1]
            k2 = qv[i][1]
            k3 = qa[i][1]
            k4 = qn[i]
            ############### Composite metric ###
            qq = (2k1 + k1bis + 3k2 + k3 + k4) / 8.0
            # qq = (3k1 + k1bis + 3k2 + k4) / 8.0
            ###############
            if qq > qc
                qc = qq
                qstar = nlab[i]
            end
        end
        return(qc, qstar)
    end
end


import Base: *, sort
using Random, Statistics, DataFrames, CSV
using BioSequences, FASTX, XLSX, CPUTime

#########
#The following functions are the base set of tools that this program uses

# Overload the concatenation operator to combine a sequence and
# a single base, i.e. dna"AGCGTGC" * DNA_T
function *(seq::LongDNASeq, base::DNA)
    seq * LongDNASeq([base])
end

# Overload sort to order DNA codes by degeneracy:
#   A,G,C,T,M,R,W,S,Y,K,V,H,D,B,N
function sort(seq::LongSequence{DNAAlphabet{4}}; rev::Bool=false)
    sort(convert(Vector{DNA}, seq), by=degeneracy, rev=rev)
end

function is_palindrome(randomer)
    return occursin(randomer,reverse_complement(randomer))
end
"""
    isvalid(seq, sites)

Check if any of the sites appear in the sequence. Also check for palindromes by calling is palindrom

"""
function isvalid(seq, sites)
    if is_palindrome(seq)
        return false
    end
    for site in sites
        if occursin(site, seq)
            return false
        end
    end
    return true
end

"""
    find_valid_randomer(prefix, bases, sites)

Randomly select bases to add the prefix, ensuring no site
appears in the randomer.
"""
function find_valid_randomer(prefix, bases, sites)
    n = length(bases)
    pre_n = length(prefix)
    randomer = prefix * (dna"-" ^ n)
    for i = 1:n
        candidates=shuffle(bases[i])
        for cand in candidates
            randomer[pre_n+i] = cand
            if isvalid(randomer[1:pre_n+i], sites)
                break
            else
                randomer[pre_n+i] = DNA_Gap
            end
        end
        if randomer[pre_n+i] == DNA_Gap
            break
        end
    end
    return randomer
end

"""
    degeneracy(base::DNA[, uselog2=true])

Calculate the degeneracy of a single base. The degeneracy
is the number of bases in the code, so
    degeneracy(DNA_N) == 4
and
    degeneracy(DNA_A) == 1

If `uselog2`, the degeneracy is log2 transformed. By convention,
the degneracy of `DNA_Gap` is zero in both the log2-transformed
and the untransformed case.
"""
function degeneracy(base::DNA; uselog2=true)
    deg = 0.0
    if base == DNA_N
        deg = 4.0
    elseif base == DNA_A || base == DNA_C || base == DNA_G || base == DNA_T
        deg = 1.0
    elseif base == DNA_B || base == DNA_D || base == DNA_H || base == DNA_V
        deg = 3.0
    elseif (base == DNA_K || base == DNA_Y || base == DNA_S || base == DNA_W
            || base == DNA_R || base == DNA_M)
        deg = 2.0
    else # DNA_Gap
        deg = 0.0
    end

    if uselog2
        if base == DNA_Gap
            deg = 2^-100  #log2(0) is negative infinity, so we use something close to zero
        end

        return log2(deg)

    else
        return deg
    end
end

"""
    degeneracy(seq::LongSequence{DNAAlphabet{4}}; uselog2=true)

Calculate the degeneracy of a DNA sequence.
"""
function degeneracy(seq::LongSequence{DNAAlphabet{4}}; uselog2=true)
    if uselog2
        return sum(degeneracy.(seq, uselog2=true))
    else
        return prod(degeneracy.(seq, uselog2=false))
    end
end
function iscompatible_sequence(seqa,seqb)
    n=length(seqa)
    count=0
    for i=1:n
        if iscompatible(seqa[i],seqb[i])
            count+=1;
        end
    end
    return count==n
end

function decompress_oligo(comp_oligo)
    basedict=Dict(DNA_A => dna"A", DNA_T => dna"T", DNA_C => dna"C", DNA_G => dna"G",DNA_R =>dna"AG",DNA_Y=>dna"CT",DNA_S=>dna"GC",
    DNA_W=>dna"AT",DNA_K=>dna"GT",DNA_M=>dna"AC",DNA_B=>dna"CGT",DNA_D=>dna"AGT",DNA_H=>dna"ACT",DNA_V=>dna"ACG",DNA_N=>dna"ACGT",DNA_Gap=>dna"-")
    nseqs=trunc(Int128,degeneracy(comp_oligo;uselog2=false))
    randomerlen=length(comp_oligo)
    blockarray=zeros(Int128,randomerlen+1)
    blockarray[1]=nseqs
    degsarray=zeros(Int128,randomerlen)
    repsarray=zeros(Int128,randomerlen+1)
    repsarray[1]=1
    uncomp_oligos=[dna"-"^randomerlen for _ in 1:nseqs]
    for i = 1:randomerlen
        nucleotides=basedict[comp_oligo[i]]
        degsarray[i]=length(nucleotides)
        blockarray[i+1]=trunc(Int128,blockarray[i]/degsarray[i]);
        bases=dna"-"^blockarray[i];
        repsarray[i+1]=trunc(Int128,prod(degsarray[1:i]));
            for  k in 1:degsarray[i]
                for l in 1:blockarray[i+1]
                    bases[l+blockarray[i+1]*(k-1)]=nucleotides[k]
                end
            end

        bases=bases^repsarray[i];
        for m in 1:nseqs
            uncomp_oligos[m][i]=bases[m];
        end
    end
    return uncomp_oligos
end
function decompress_pool(compressed_pool)
    uncomppool=decompress_oligo(compressed_pool[1])
    for i = 2:length(compressed_pool)
        subset=decompress_oligo(compressed_pool[i])
        for i in subset
            push!(uncomppool,i)
        end
    end
    return uncomppool
end

#####################
#Define the action space
#####################
function generate_random_pool(;randomerlen=6,randpoolsize=100,nucleotides=dna"ACGT")
    randpool=[]
    randomer=dna"-"^randomerlen
    for i = 1:randomerlen
        bases=shuffle(nucleotides)
        randomer[i]=bases[1];
    end
    push!(randpool,randomer)
    while length(randpool)<randpoolsize
        randomer=dna"-"^randomerlen
        for i = 1:randomerlen
            bases=shuffle(nucleotides)
            randomer[i]=bases[1];
        end
        check=true
        for j = 1:length(randpool)
            if randomer==randpool[j]
                check=false
                break
            end
        end
        if check
            push!(randpool,randomer)
        end
        if length(randpool)==length(nucleotides)^randomerlen
            break
        end
    end

    return randpool
end

function valid_candidates(sites)
    n=length(sites[1])
    nsites=length(sites)
    candidates=generate_random_pool(;randomerlen=n,randpoolsize=4^n);
    for i=1:nsites
        candidates=candidates[findall(j -> !iscompatible_sequence(j,sites[i]),candidates)]
    end
    return candidates
end

function define_action_space(sites)
    if length(sites)==0
        bases=[dna"" for i in 1:6]
        return bases
    else
        randomerlen=length(sites[1]);
        nsites=length(sites)
        bases=[];
        for i=1:randomerlen
            states=dna"-"^nsites;
            nucleotides=dna"----"
            #println("TEST: $nucleotides")
            for j=1:nsites
                states[j]=sites[j][i];
            end
            if !occursin(dna"A",states)
                nucleotides[1]=DNA_A
            else
                nucleotides[1]=DNA_Gap
            end
            if !occursin(dna"T",states)
                nucleotides[2]=DNA_T
            else
                nucleotides[2]=DNA_Gap
            end
            if !occursin(dna"C",states)
                nucleotides[3]=DNA_C
            else
            nucleotides[3]=DNA_Gap
            end
            if !occursin(dna"G",states)
                nucleotides[4]=DNA_G
            else
                nucleotides[4]=DNA_Gap
            end
            #println("TEST: $states")
            push!(bases,omit_codes(nucleotides))
            #println("Test: $nucleotides")
        end
    end
    return bases
end
function omit_codes(bases)
    testcodes=dna"AGCTMRWSYKVHDBN"
    testcodes_copy=deepcopy(testcodes)
    indexarray=[]
    for i= 1:length(testcodes)
        count=0;
        for nucleotide in bases
            if iscompatible(nucleotide,testcodes[i])
                count+=1;
            end
        end
        if count >0
            push!(indexarray,i)
        end
    end
    check=sort(indexarray,rev=true)
    for index in check
        deleteat!(testcodes_copy,index)
    end

    return testcodes_copy
end

#######################
#The follwoing functions are used as part of the various reward systems
######################

#SCORING SYSTEM FOR HITTING ALL mRNAs. Not used in the weighted scoring system
function mRNA_coverage(randomer,mRNA)
    hits=0;
    for seq in mRNA
        if occursin(randomer,seq)
            hits+=1;
        end
    end
    return hits
end

#Weighted Scoring System for mRNA coverage
function mRNA_weighted(randomer,mRNA,weights)
score=0;
for i=1:length(mRNA)
    if occursin(randomer,mRNA[i])
        score+=weights[i];
    end
end
return score
end

# Keeps track of the number of times a given gene has been hit. It is used to compute the weights above
function mRNA_hitcounter(randomer,mRNA,hitcounts)
    for i=1:length(mRNA)
        if occursin(randomer,mRNA[i])
            hitcounts[i]+=1;
        end
    end
    return hitcounts
end
function mRNA_uniform_coverage(randomer,mRNAs;hitcounts=zeros(length(mRNAs)),uniformity_weight=1,total_hits_weight=1,overwrite_hitcounts=false, kwargs...)
    mRNAnum=length(mRNAs);
    gene_scores=zeros(mRNAnum);
    dist_score=0;
    randomerlen=length(randomer);
    new_hitcounts=zeros(mRNAnum)
    #println("hitcounts: $hitcounts")
    for i=1:mRNAnum
        counter=0;
        indexcounter=0
        index=approxsearchindex(mRNAs[i][1:end],randomer,0);
        indexcounter+=index;
        if index > 0
            counter +=1
        end
        while index >0
            index=approxsearchindex(mRNAs[i][indexcounter+1:end],randomer,0);
            indexcounter+=index
            if index==0
                break
            else
                counter+=1;
            end
        end

        new_hitcounts[i]=counter
        hit_score=hitcounts[i]+new_hitcounts[i]
        if hit_score==0
            gene_scores[i]=0;
        else
            gene_scores[i]=1+total_hits_weight*log10(hit_score)
        end
    end
    scored_hits=hitcounts+new_hitcounts;
    if sum(scored_hits)==0
        final_score=0
    else
        sorted_hits=sort(scored_hits);
        DUavg=mean(sorted_hits)
        lq=round(Integer,mRNAnum/4);
        DUlq=mean(sorted_hits[1:lq])
        dist_score=DUlq/DUavg;
        final_score=sum(gene_scores)+uniformity_weight*mRNAnum*dist_score;
    end
    if overwrite_hitcounts
        return final_score,scored_hits,dist_score, uniformity_weight,total_hits_weight
    else
        return final_score,hitcounts,dist_score,uniformity_weight,total_hits_weight
    end
end

function mRNA_multiobjective(randomer, mRNAs; indicies=[[] for i=1:10], b1=1,b2=1,overwrite_indicies=false, return_indicies=false,kwargs...)
    # If overwrite_indicies=false, the funciton will score the randomer as if the randomer were to be added, but will not actually add the randomer
    # If overwrite_indicies=true, then the function will add the randomer hit sites to the offical count
    # b1 = weight for uniformity score
    # b2 = weight for hits per unit length
    mRNAnum=length(mRNAs)
    gene_scores=zeros(mRNAnum);
    means=zeros(mRNAnum);
    stds=ones(mRNAnum);
    randomerlen=length(randomer);
    indicies_copy=deepcopy(indicies);
    for i=1:mRNAnum
        iteration=0
        index=approxsearchindex(mRNAs[i][1:end],randomer,0);
        if index > 0
            push!(indicies_copy[i],index)
        end
        iteration = iteration+1;
        while index > 0
            index=approxsearchindex(mRNAs[i][((indicies_copy[i][end]+1):end)],randomer,0);
            iteration=iteration+1;
            if index == 0
                break
            else
                push!(indicies_copy[i],index+indicies_copy[i][end])

            end
        end
        if length(indicies_copy[i])==0
            continue
        end
        dists=calculate_gap_distances(indicies_copy[i],length(mRNAs[i]),randomerlen)
        stds[i]=std(dists)/length(mRNAs[i])
        gene_scores[i]=1+b1*(1-stds[i])+b2*length(indicies_copy[i])/length(mRNAs[i]);
    end

    if overwrite_indicies && return_indicies
    return indicies_copy, b1, b2
    elseif ~overwrite_indicies &&  return_indicies
    return  indicies
    else  # by having return_indicies=false, you can just return the gene scores. This just makes the output of the function more flexible.
        return sum(gene_scores)
    end
end

function calculate_gap_distances(hit_indicies,genelen,randomerlen)
    dists=[]
    hit_indicies=sort(hit_indicies)
    push!(dists,hit_indicies[1])
    if length(hit_indicies)>1
        for i=2:length(hit_indicies)
            push!(dists,(hit_indicies[i]-hit_indicies[i-1]-randomerlen))
        end
    end
    push!(dists,(genelen-hit_indicies[end]-randomerlen))
    return dists
end

# Rollout reward systems
#3 factor scoring policy. This is the default reward system
function simulate_random_mRNA_uniform_coverage(prefix,bases,sites,mRNAs; nsims=1000, horizon=length(bases), kwargs...)
    mRNA_score=zeros(nsims)
    true_horizon = min(horizon, length(bases))
    n=length(prefix)+true_horizon;
    sim_randomer_list=[dna"-"^n for _ in 1:nsims];

    Threads.@threads for i = 1:nsims   #This is where multithreading is incorporated
        sim_randomer_list[i]=find_valid_randomer(prefix,bases[1:true_horizon],sites);
        mRNA_score[i],b,c,d,e= mRNA_uniform_coverage(sim_randomer_list[i],mRNAs;overwrite_hitcounts=false, kwargs...)
    end
    max_sim_score,max_index=findmax(mRNA_score);
    max_sim_randomer=sim_randomer_list[max_index];
    return mean(mRNA_score),max_sim_randomer,max_sim_score

end
function simulate_random_mRNA_multiobjective(prefix,bases,sites,mRNAs; nsims=1000, horizon=length(bases), kwargs...)
    mRNA_score=zeros(nsims)
    true_horizon = min(horizon, length(bases))
    n=length(prefix)+true_horizon;
    sim_randomer_list=[dna"-"^n for _ in 1:nsims];
    Threads.@threads for i = 1:nsims   #This is where multithreading is incorporated
        sim_randomer_list[i]=find_valid_randomer(prefix,bases[1:true_horizon],sites);
        mRNA_score[i]= mRNA_multiobjective(sim_randomer_list[i],mRNAs;return_indicies=false, kwargs...)
    end
    max_sim_score,max_index=findmax(mRNA_score);
    max_sim_randomer=sim_randomer_list[max_index];
    return mean(mRNA_score),max_sim_randomer,max_sim_score
end

#Weighted scoring policy. The score for hitting a given gene decreases according to the number of times it's been hit
function simulate_random_mRNA_weighted(prefix,bases,sites,mRNA; nsims=1000, horizon=length(bases))
    mRNA_score=zeros(nsims)
    true_horizon = min(horizon, length(bases))
    Threads.@threads for i = 1:nsims
        mRNA_score[i]= mRNA_weighted(find_valid_randomer(prefix,bases[1:true_horizon],sites),mRNA,weights)
    end
    return mean(mRNA_score)
end

# Unweighted scoring policy. Finds randomers that hit the most genes at least once
function simulate_random_mRNA(prefix,bases,sites,mRNA; nsims=1000, horizon=length(bases))
    mRNA_hits=zeros(nsims)
    true_horizon = min(horizon, length(bases))
    Threads.@threads for i = 1:nsims
        mRNA_hits[i]= mRNA_coverage(find_valid_randomer(prefix,bases[1:true_horizon],sites),mRNA)
    end
    return mean(mRNA_hits)
end

### MAIN FUNCTION
function rollout_rt(bases,sites, mRNA; simulate=simulate_random_mRNA_multiobjective, kwargs...)
    n=length(bases)
    randomer= dna"-" ^ n
    # Find the longest blocking site and use this as the horizon. (they should all be equal)
    max_len = map(length, sites) |> maximum
    horizon = max_len
    candidates=valid_candidates(sites);
    checkpoint=length(candidates)
    best_sim_randomer=dna"-"^n
    best_sim_score=0;
    #println("Valid Candidates: $checkpoint")
    for i= 1:n
        best_base= DNA_Gap
        best_score=0;
        start = max(i - max_len, 1) # only need to look back max_len when checking the oligos
        bases=define_action_space(candidates[findall(j -> iscompatible_sequence(j[start:i-1],randomer[start:i-1]),candidates)])
        #println("Bases $bases")
        if length(bases[i])==1
            best_base=bases[i][1];
        else
            for base in bases[i]
                if ~isvalid(randomer[start:i-1] * base, sites)
                    # adding this bases creates a restriction site; skip it
                    continue
                end
                score,max_sim_randomer,max_sim_score= simulate(randomer[start:i-1] * base, bases[i+1:end], sites, mRNA;kwargs...)
                # Update use this base if:
                #   1. the mean randomer mRNA coverage is higher than the previous best, OR
                #   2. the randomer mRNA coverage is tied with the previous best, but the new base has
                #      higher individual degeneracy
                if score > best_score || (score == best_score && degeneracy(base) > degeneracy(best_base))
                    best_score = score
                    best_base = base
                end
                ## Update the overall best simulation result if
                #   1. the best globally simulated randomer mRNA score is better than the previous best
                #   2. the best globally simulated randomer mRNA score is tied but the randomer has higher degeneracy
                if max_sim_score >best_sim_score || (max_sim_score==best_sim_score && degeneracy(max_sim_randomer)>degeneracy(best_sim_randomer))
                    best_sim_score=max_sim_score;
                    best_sim_randomer=max_sim_randomer
                end
            end
        end
        if best_base == DNA_Gap
            # The sequence has terminated; there are no bases that can be added without making
            # a restriction site.
            break
        else
            randomer[i] = best_base

        end
        #println("randomer: $randomer")
    end
    final_randomer_score,hitcounts,dist_score,uniformity_weight,total_hits_weight=mRNA_uniform_coverage(randomer,mRNA;kwargs...)
    if best_sim_score >final_randomer_score
        randomer=best_sim_randomer
        println("Triggered Best Simulated Randomer")
    end
    return randomer
end

## Read in the list of sequences you want to exclude in the designed primers.
# Function finds every unique hexamer in the list of sequences and stores it as an array
#. For example, use list of rRNA and tRNA sequences. This funciton takes care of orientation. We end up using the reverse complement since we are finding primers that won't hybridize
## Example
#blocking_sites=read_blocking_sites(CSV.read("./SMU_UA159_rRNA_tRNA.csv"))
function read_blocking_sites(data; randomerlen=6)
    blocking_sites=[];
    for i =1:nrow(data)
        seq=reverse_complement(LongDNASeq(string(data[i,:Sequence])));
        for j=1:length(seq)-(randomerlen-1)
            if isvalid(seq[j:j+(randomerlen-1)],blocking_sites)
                push!(blocking_sites,seq[j:j+(randomerlen-1)])
            end
        end
    end
    return blocking_sites
end
# RNAs=read_rRNA_tRNA_list(CSV.read("./SMU_UA159_rRNA_tRNA.csv"))
function read_rRNA_tRNA_list(data)
    RNAs=[]
    for i=1:nrow(data)
        seq=reverse_complement(LongDNASeq(string(data[i,:Sequence])));
        if isvalid(seq,RNAs)
            push!(RNAs,seq)
        end
    end
    return RNAs
end
## Read in the list of sequences you want the designed primers to hybridize to
# Function takes care of the orientation, we want to end up using the reverse complmement.
## Example
# mRNAs=read_mRNA_list(CSV.read("./SMU_UA159_Genes.csv"))
function read_mRNA_list(data)
    mRNAs=[]
    for i=1:nrow(data)
        seq=reverse_complement(LongDNASeq(string(data[i,:mRNA_seq])));
        if isvalid(seq,mRNAs)
            push!(mRNAs,seq)
        end
    end
    return mRNAs
end

#Finds the number of bases as well as the GC content of the given file
# Example
# bases,GC_content=calculate_transcriptome_stats(read_mRNA_list(CSV.read("./SMU_UA159_Genes.csv")))
function calculate_transcriptome_stats(sequences)
    bases=0;  #number of bases in the file
    GC_count=0 # Count of G's and C's in file
    for i=1:length(sequences)
        bases += length(sequences[i]);
        comp=composition(sequences[i]);
        GC_count += comp[DNA_G] +comp[DNA_C];
    end
    GC_content= GC_count/bases*100;
    return bases,GC_content
end

#### Extra Analysis Funcitons

## Finds the edit distance distribution for a given designed primer to all of the blocking sites
function base_error_dist(randomer,sites)
    error_dist=[]
    for i in 0:length(randomer)
        count=0
        found_sites=[]
        for j in 1:length(sites)
            valid=approxsearchindex(randomer,sites[j],i)
            if valid == 1
                count += valid
                push!(found_sites,j)
            end
        end
        deleteat!(sites,found_sites)
        push!(error_dist,count)
    end

    return error_dist
end
# genes_hit,genes_missing,total_hits,uniformity_score=hit_stats(indicies,mRNAs,6)
function mRNA_multiobjective_hit_stats(indicies,mRNAs,randomerlen)
    genes_hit=0;
    genes_missing=[]
    total_hits=0;
    stds=ones(length(indicies))
    for i=1:length(indicies);
        if length(indicies[i])>0
            genes_hit+=1;
            dists=calculate_gap_distances(indicies[i],length(mRNAs[i]),randomerlen)
            stds[i]=std(dists)/length(mRNAs[i]);
        else
            push!(genes_missing,i)
        end
        total_hits+=length(indicies[i]);
    end
    return genes_hit,genes_missing,total_hits,mean(1 .- stds)
end

function ID_genes_missing(data,genes_missing)
    missing_ID=[];
    for i in genes_missing
        push!(missing_ID,data[i,:New_ID])
    end
    return missing_ID
end

## Calculates the distribution number of randomers that hit each gene at least once. Not very useful.
function coverage_distribution(randomers, mRNA)
    coverage_dist=zeros(length(randomers)+1)
     for seq in mRNA
         count=0;
         for randomer in randomers
             if occursin(randomer, seq)
                 count+=1;
             end
         end
         coverage_dist[count+1]+=1;

     end
     return coverage_dist
end

# Calcualtes the number of times each gene is hit by all randomers in the pool. Useful for comparing the "between gene uniformity".
function gene_coverage_counts(randomers,mRNAfile)
    mRNAdata=CSV.read(mRNAfile,DataFrame);
    mRNAs=read_mRNA_list(mRNAdata)
    gene_hits=zeros(length(mRNAs))
    randomerlen=length(randomers[1]);
    for i in 1:length(mRNAs)
        counter=0;
        for j in 1:length(mRNAs[i])-randomerlen
            for k in 1:length(randomers)
                if occursin(randomers[k],mRNAs[i][j:j+randomerlen-1])
                    counter+=1
                end
            end
        end
        gene_hits[i]=counter
    end
    data=DataFrame(counts=gene_hits)
    return data

end
function dnaarray2stringarray(array)
    array2=[]
    for i=1:length(array)
        push!(array2,convert(String,array[i]))
    end
    return array2
end
function stringarray2dnaarray(array)
    array2=[]
    for i=1:length(array)
        push!(array2,LongDNASeq(array[i]))
    end
    return array2
end

function pooled_degeneracy(pool)
    deg=0;
    for randomer in pool
        deg +=degeneracy(randomer;uselog2=false);
    end
    return deg
end
 ########################################################################
 # #CODE FOR CREATING RANDOMER POOLS And Saving them to files
 ########################################################################

 #Quick Test#
 # data=run_NSR_RL(pool_size=3,nsims=50)
 #CSV.write("NSR_RL_S_mutans_4_12_21",data)
function run_NSR_RL(;species="S_mutans",randomerlen=6,pool_size=25,all_bases = dna"AGCTMRWSYKVHDBN",kwargs...)
    randomers=[]
    mRNAfile=string("./Genome Data/",species,"_Genes.csv")
    rRNAfile=string("./Genome Data/",species,"_rRNA_tRNA.csv")
    mRNA_DF=CSV.read(mRNAfile,DataFrame);
    rRNA_DF=CSV.read(rRNAfile,DataFrame);
    mRNAs=read_mRNA_list(mRNA_DF);
    rRNAs=read_rRNA_tRNA_list(rRNA_DF);
    numgenes=length(mRNAs)
    blocking_sites=read_blocking_sites(rRNA_DF,randomerlen=randomerlen);
    mRNAbp,mRNA_GC=calculate_transcriptome_stats(mRNAs);
    rRNAbp,rRNA_GC=calculate_transcriptome_stats(rRNAs);
    transcriptome_size=mRNAbp+rRNAbp;
    all_ns = [all_bases for _ in 1:randomerlen];
    hitcounts=zeros(numgenes)
    times=[]
    num_blocked_sites=zeros(pool_size)
    println("Initialization Successful")
    for i = 1:pool_size
        if i > 1
            push!(blocking_sites,LongDNASeq(randomers[i-1]));
            push!(blocking_sites,LongDNASeq(reverse_complement(randomers[i-1]))) ; ## avoid adding randomers that are reverse complements of each other
            blocking_sites=unique(decompress_pool(blocking_sites));
        end
        num_blocked_sites[i]=length(blocking_sites)
        randomerstats= @timed rollout_rt(all_ns,blocking_sites,mRNAs; hitcounts=hitcounts, simulate=simulate_random_mRNA_uniform_coverage,kwargs...); #create new randomer and add to pool
        push!(randomers,randomerstats.value)
        push!(times,randomerstats.time)
        final_score,hitcounts,dist_score,uniformity_weight,total_hits_weight=mRNA_uniform_coverage(randomers[i],mRNAs; hitcounts=hitcounts, overwrite_hitcounts=true,kwargs...); #officially add hit positions from the new randomer
        #println("Iteration $i")
    end
    final_score,hitcounts,dist_score,uniformity_weight,total_hits_weight =mRNA_uniform_coverage(dna"-"^randomerlen,mRNAs; hitcounts=hitcounts, overwrite_hitcounts=false,kwargs...);
    genes_hit=length(findall(x ->x>0,hitcounts))
    genes_missing=findall(x -> x==0,hitcounts)
    total_hits=sum(hitcounts);
    missing_IDs=join(ID_genes_missing(mRNA_DF,genes_missing),",")
    totaldeg=pooled_degeneracy(randomers)
    total_time=sum(times)
    string_time=join(times,",")
    string_num_blocked_sites=join(num_blocked_sites,",")
    string_randomers=join(dnaarray2stringarray(randomers),",")
    data=DataFrame(poolsize=pool_size, primer_length=randomerlen, uniformity_weight=uniformity_weight,hits_weight=total_hits_weight,species=species,num_genes=numgenes,
        transcriptome_size=transcriptome_size,mRNA_GC=mRNA_GC,blocked_GC=rRNA_GC,randomers=string_randomers,degeneracy=totaldeg,num_genes_hit=genes_hit,
        genes_missing=missing_IDs,total_hits=total_hits,coverage_uniformity_score=dist_score,individual_times=string_time,time=total_time,num_blocked_sites=string_num_blocked_sites)
    return data
end

function benchmark_NSR_RL()
    DF1=DataFrame()
    nexps=1;
    uniformity_weights=[0]
    total_hits_weights=[100]
    species="S_mutans"
    filename="./S_mutans_NSR_Alt_Valid_Randomer_Test_Timing_6_10_21.csv"
    for i = 1:nexps
        data=run_NSR_RL(;species=species,pool_size=3,nsims=100,uniformity_weight=uniformity_weights[i],total_hits_weight=total_hits_weights[i])
        DF1=vcat(DF1,data);
        CSV.write(filename,DF1)
    end
end

##Code for analyzing previously made primer pools. stores the same data that run_NSR_RL stores
function analyze_NSR_Pool(randomers;species="S_mutans",mRNAfile="./SMU_UA159_Genes.csv",rRNAfile="./SMU_UA159_rRNA_tRNA.csv",kwargs...)
    mRNAdata=CSV.read(mRNAfile,DataFrame)
    rRNAdata=CSV.read(rRNAfile,DataFrame)
    mRNAs=read_mRNA_list(mRNAdata);
    rRNAs=read_rRNA_tRNA_list(rRNAdata);
    numgenes=length(mRNAs)
    randomerlen=length(randomers[1]);
    blocking_sites=read_blocking_sites(rRNAdata,randomerlen=randomerlen);
    mRNAbp,mRNA_GC=calculate_transcriptome_stats(mRNAs);
    rRNAbp,rRNA_GC=calculate_transcriptome_stats(rRNAs);
    transcriptome_size=mRNAbp+rRNAbp;
    hitcounts=zeros(numgenes)
    dist_score=0
    for i=1:length(randomers)
        final_score,hitcounts,dist_score,uniformity_weight,total_hits_weight=mRNA_uniform_coverage(randomers[i],mRNAs; hitcounts=hitcounts, overwrite_hitcounts=true,kwargs...); #officially add hit positions from the new randomer
    end
    genes_hit=length(filter(x->x>0,hitcounts));
    genes_missing=findall(x->x==0,hitcounts);
    total_hits=sum(hitcounts);
    missing_IDs=join(ID_genes_missing(mRNAdata,genes_missing),",")
    totaldeg=pooled_degeneracy(randomers)
    total_time=0
    string_time="N/A"
    string_randomers=join(dnaarray2stringarray(randomers),",")
    uniformity_weight=0
    hits_weight=0
    pool_size=length(randomers);
    data=DataFrame(poolsize=pool_size, primer_length=randomerlen, uniformity_weight=uniformity_weight,hits_weight=hits_weight,species=species,num_genes=numgenes,
        transcriptome_size=transcriptome_size,mRNA_GC=mRNA_GC,blocked_GC=rRNA_GC,randomers=string_randomers,degeneracy=totaldeg,num_genes_hit=genes_hit,
        genes_missing=missing_IDs,total_hits=total_hits,uniformity_score=dist_score,individual_times=string_time,time=total_time)
    return data
end

function analyze_all_genomes_NSR_Pools(species_list)
    DF1=DataFrame()
    n=length(species_list)
    for i = 1:n
        randomers=stringarray2dnaarray(CSV.read(string("./Brute Force Pools V2 Compressed/",species_list[i],"_NSR_Brute_Force_Compressed.csv"),DataFrame)[:Sequence])
        mRNAfile=string("./Genome Data/",species_list[i],"_Genes.csv")
        rRNAfile=string("./Genome Data/",species_list[i],"_rRNA_tRNA.csv")
        data=analyze_NSR_Pool(randomers;species=species_list[i],mRNAfile=mRNAfile,rRNAfile=rRNAfile)
        DF1=vcat(DF1,data);
        pct_complete=i/n*100;
        println("$pct_complete % Complete")
        CSV.write("./Brute_Force_Pool_Compressed_Analysis_V2.csv",DF1)
    end

end
function analyze_NSR_Pool_gene_coverage(species_name)
    randomers=stringarray2dnaarray(CSV.read(string("./Brute Force Pools V2/",species_name,"_NSR_Brute_Force_all_randomers.csv"),DataFrame)[:Sequence])
    mRNAfile=string("./Genome Data/",species_name,"_Genes.csv")
    data=gene_coverage_counts(randomers,mRNAfile);
    outputfile=string("./Brute Force Pools V2 Gene Coverage/",species_name,"_Gene_Coverage.csv")
    CSV.write(outputfile,data)
end

function analyze_cumulative_NSR_Pool(randomers,species_name;outputfile="./S_mutans_Cumulative_NSR_Pool.csv" ,kwargs...)
    mRNAfile=string("./Genome Data/",species_name,"_Genes.csv")
    mRNAdata=CSV.read(mRNAfile,DataFrame);
    mRNAs=read_mRNA_list(mRNAdata);
    nrandomers=length(randomers);
    cumulative_hits=zeros(nrandomers)
    cumulative_dist_score=zeros(nrandomers)
    hitcounts=zeros(length(mRNAs))
    randomer_num=1:nrandomers;
    str_randomers=dnaarray2stringarray(randomers);
    cumulative_genes_hit=zeros(nrandomers);
    for i=1:nrandomers
        final_score,hitcounts,dist_score,uniformity_weight,total_hits_weight=mRNA_uniform_coverage(randomers[i],mRNAs; hitcounts=hitcounts, overwrite_hitcounts=true,kwargs...);
        cumulative_hits[i]=sum(hitcounts)
        cumulative_dist_score[i]=dist_score;
        cumulative_genes_hit[i]=sum(hitcounts.>0)
    end
    data=DataFrame(run=randomer_num,randomer=str_randomers,cumulative_genes_hit=cumulative_genes_hit,cumulative_hits=cumulative_hits,cumulative_dist_score=cumulative_dist_score)
    CSV.write(outputfile,data)
end

#data=run_NSR_RL(;nsims=100,poolsize=1)
#CSV.write("NSR_RL_Prelims_Genome_Test_4_18_21.csv",data)
#speciesdata=CSV.read("NSR_RL_Prelims_Genome_Test_4_18_21.csv",DataFrame)
#SMUrandomers=stringarray2dnaarray(split(speciesdata[3,10],","));
#countdata=gene_coverage_counts(SMUrandomers,"./E_coli_Genes_4_15_21.csv")
#CSV.write("Ecoli_Coverage_Counts_4_18_21.csv",countdata)

#species_list=CSV.read("./Genome Data/Species_List.csv")[:Species]
#analyze_NSR_Pool_gene_coverage.(species_list)

#species_list=CSV.read("./Genome Data/Species_List.csv")[:Species]
#analyze_all_genomes_NSR_Pools(species_list)
#randomers=stringarray2dnaarray(CSV.read("./Brute Force Pools V2 Compressed/S_mutans_NSR_Brute_Force_Compressed.csv",DataFrame)[:Sequence])
#countdata=gene_coverage_counts(randomers,"./Genome Data/S_mutans_Genes.csv")
#CSV.write("S_mutans_NSR_Comparison_2_gene_coverage_5-19-21.csv",countdata)
#data=CSV.read("S_mutans_Add_Best_Sim_Option_Test_2.csv",DataFrame)
#randomers=stringarray2dnaarray(split(data[2,10],","));
#analyze_cumulative_NSR_Pool(randomers,"S_mutans";outputfile="./S_mutans_Cumulative_NSR_Pool_Best_Sim_Option_6_8_21_Uniformity.csv")
#data=CSV.read("S_mutans_NSR_Num_Blocked_Sites_Test_6_8_21.csv",DataFrame)
#randomers=stringarray2dnaarray(split(data[1,10],","))
#randomers=filter(x -> degeneracy(x;uselog2=false)>0,randomers)
#randomers=decompress_pool(randomers)
#rcomps=reverse_complement.(randomers)
#blocking_sites=read_blocking_sites(CSV.read("./Genome Data/S_mutans_rRNA_tRNA.csv",DataFrame));
#sites=unique(vcat(blocking_sites,rcomps,randomers))
#benchmark_NSR_RL()


# ------------------------------------------ #
#  Import functions defined in Rhone2020.jl  #
# ------------------------------------------ #

using Dates, MonteCarloMeasurements;

# ------------------------------------------ #
#    Load the data files into dictionaries   #
# ------------------------------------------ #

ctd145, ctd309, ctd265 = R.load_DCX22_CTD();
e_cond = 200*0.025 # error of conductivity in μS/cm, 2.5% of range FS
e_p = rhow*g*0.0005 # error of pressure, 0.05% FS; has to be multiplied with max. water column, e.g. *100 for ctd309-100mH20

# ------------------------------------------ #
#       Pick start/end points of traces      #
# ------------------------------------------ #

# plot conductivity to pick start and end points of individual injection signals
#using PyPlot
#pygui(true) # interactive window for figure
#plot(ctd309["1008"][:cond])
#plot(ctd265["1008"][:cond])

# manually determined start and end points of individual injection signals
# one column per injection
# 1st row: first index of ctd309
# 2nd row: last index of ctd309
# 3rd row: first index of ctd265
# 4th row: last index of ctd265
# 5th row: first index of ctd145
# 6th row: last index of ctd145

indices = Dict("0808" => ([16882 18408 19638 21934],  # traces 1 and 6-9: supersaturation
                          [17071 19079 19741 22000],
                          [17088 19355 19726 21992],
                          [17339 19474 19869 22080]),
               "0908" => ([8313 10072 11810 13638 15494 17204 18709 21049 22606 24239 26059 27914], # injection 13 too late for ctd265
                          [8386 10120 11900 13710 15560 17270 18784 21130 22682 24300 26140 27990],
                          [8339 10097 11833 13657 15514 17224 18732 21073 22630 24263 26086 27944],
                          [8440 10180 11950 13750 15590 17310 18815 21160 22716 24350 26170 28037]),
               "1008" => ([11829 15747 19984],
                          [11930 15795 20010],
                          [12070 15800 20040],
                          [12320 15880 20090]),
               "1108" => ([5307 7100 8213 9386 10902 11964 13122 18468 19575 25237],
                          [5337 7124 8246 9446 10984 12016 13152 18498 19604 25261],
                          [5329 7139 8273 9459 10960 12006 13164 18519 19631 25290],
                          [5367 7184 8330 9532 11050 12085 13213 18568 19693 25339]),
               "1208" => ([4134 5529 7557 8772 10018 25506],
                          [4181 5574 7592 8811 10050 25562],
                          [4148 5556 7592 8804 10052 25544],
                          [4218 5622 7662 8866 10104 25641]),
               "1308" => ([5437 7555 14943],
                          [5533 7606 14991],
                          [5441 7566 15017],
                          [5549 7641 15110]),
               "1408" => ([4642 5841 7764 9688 19782],
                          [4692 5893 7817 9729 19827],
                          [4649 5857 7805 9725 19822],
                          [4733 5955 7894 9830 19906]),
               "1708" => ([10072 12345 23422],
                          [10149 12437 23563],
                          [10101 12369 23444],
                          [10220 12514 23661]),
               "1808" => ([2416 21321],
                          [2530 21407],
                          [2421 21359],
                          [2540 21526]),
               "1908" => ([1732 2288 27037 28252],
                          [1925 2424 27187 28377],
                          [1740 2301 27091 28286],
                          [1939 2529 27318 28527]),
               "2008" => ([9960],[25900],[15160],[25900],[6213],[10571]), # not usable for calculations
               "2108" => ([722 2554 4313 6102 7894 9744 11763 13359 15143 16950 18758 20472 22224 23230 24193 26416 27983], # 309 and 265: end point = start point of next trace
                          [2554 4313 6102 7894 9744 11763 13359 15143 16950 18758 20472 22224 23230 24193 26416 27983 length(ctd309["2108"][:cond])],
                          [841 2670 4422 6195 7995 9879 11932 13499 15296 17101 18908 20634 22378 23389 24336 26564 28118],
                          [2670 4422 6195 7995 9879 11932 13499 15296 17101 18908 20634 22378 23389 24336 26564 28118 length(ctd265["2108"][:cond])],
                          [622 2446 4223 6021 7821 9627 11656 13233 15031 16830 18629 20363 22104 23122 24080 26307 27893],
                          [688 2512 4310 6100 7893 9760 11770 13355 15150 16943 18737 20461 22208 23228 24158 26400 27980]));

#plot to check if indices are ok
#using PyPlot
#pygui(true)
#date = "2108"
#figure()
#for tr in 1:length(indices[date][1])
#    plot(ctd309[date][:cond][indices[date][1][tr]:indices[date][2][tr]])
#end
#figure()
#for tr in 1:length(indices[date][1])
#    plot(ctd265[date][:cond][indices[date][3][tr]:indices[date][4][tr]])
#end

# ------------------------------------------ #
#            Indices of injections           #
# ------------------------------------------ #

# time of salt injection (translated to indices of ctd arrays)
t0 = Dict("0808" => ([16806 18186 19606 21906]),
          "0908" => ([8256 10026 11766 13596 15456 17166 18666 21006 22566 24196 26016 27876]),
          "1008" => ([11586 15441 19701]),
          "1108" => ([5291 7081 8191 9301 10816 11896 13066 18436 19521 25216]),
          "1208" => ([4126 5521 7546 8761 10006 25501]),
          "1308" => ([5431 7546 14926]),
          "1408" => ([4631 5831 7756 9676 19771]),
          "1708" => ([10066 12346 23411]),
          "1808" => ([2411 21316]),
          "1908" => ([1726 2281 27031 28246]),
          "2008" => ([4441]),
          "2108" => ([601 2416 4201 6001 7801 9601 11626 13201 15001 16801 18606 20341 22081 23101 24061 26281 27871]));
e_dt = 3 # estimated error for time difference in s between watch and ctd clock; dt error between two ctds is 0.5 s

# ------------------------------------------ #
#                Tracer masses               #
# ------------------------------------------ #

# masses of injected salt in kg
mass = Dict("0808" => [0.1;0.1;0.1;0.1],
            "0908" => [0.05;0.05;0.05;0.05;0.05;0.05;0.05;0.05;0.05;0.05;0.05;0.05],
            "1008" => [0.05;0.05;0.05],
            "1108" => [0.05;0.05;0.07;0.07;0.1;0.1;0.1;0.1;0.1;0.1],
            "1208" => [0.05;0.1;0.1;0.1;0.1;0.06],
            "1308" => [0.1;0.1;0.1],
            "1408" => [0.1;0.1;0.1;0.1;0.1],
            "1708" => [0.1;0.1;0.1],
            "1808" => [0.1;0.1],
            "1908" => [0.1;0.1;0.1;0.1],
            "2008" => [0.02],
            "2108" => [0.025;0.025;0.025;0.025;0.05;0.05;0.025;0.025;0.025;0.025;0.025;0.025;0.025;0.05;0.025;0.025;0.025]
            );

# repeatedly measured 100g with analogue suspended scale (as in the field) and compared it to kitchen scale:
m = [99.7,100.3,100.1,99.5,99.9,99.7,101.1,99.7,99.1,98.7,99.8,99.3,100.3,99.8,98.6,100.1,99.9,99.5,100.1,99.2,98.7,100.6];
e_m = 0.002 # error of mass in kg (small influence)

# ------------------------------------------ #
#              Depths of CTDs                #
# ------------------------------------------ #

e_dz = 1.0 # estimated error of vertical distance in m

# depths of ctds in m for each day [ctd309, ctd265, ctd145]
# upper 100m: add 0.04*(depth-100m),; 104m measured under tension
# lower 100m: add 0.01*depth; 101m measured under tension

ctd309["0808"][:depth], ctd265["0808"][:depth] = Particles.(n_partcl, Normal.((62.4, 164.6), e_dz))
ctd309["0908"][:depth], ctd265["0908"][:depth] = Particles.(n_partcl, Normal.((121.1, 171.7), e_dz))
ctd309["1008"][:depth], ctd265["1008"][:depth] = Particles.(n_partcl, Normal.((120.9, 171.7), e_dz))
ctd309["1108"][:depth], ctd265["1108"][:depth] = Particles.(n_partcl, Normal.((121.0, 171.7), e_dz))
ctd309["1208"][:depth], ctd265["1208"][:depth] = Particles.(n_partcl, Normal.((121.0, 171.7), e_dz))
ctd309["1308"][:depth], ctd265["1308"][:depth] = Particles.(n_partcl, Normal.((131.6, 171.9), e_dz))
ctd309["1408"][:depth], ctd265["1408"][:depth] = Particles.(n_partcl, Normal.((131.3, 171.7), e_dz))
ctd309["1708"][:depth], ctd265["1708"][:depth] = Particles.(n_partcl, Normal.((131.3, 171.7), e_dz))
ctd309["1808"][:depth], ctd265["1808"][:depth] = Particles.(n_partcl, Normal.((131.3, 171.7), e_dz))
ctd309["1908"][:depth], ctd265["1908"][:depth] = Particles.(n_partcl, Normal.((141.4, 171.7), e_dz))
ctd145["2008"][:depth], ctd309["2008"][:depth], ctd265["2008"][:depth] = Particles.(n_partcl, Normal.((10.4, 88.4, 189.9), e_dz))
ctd145["2108"][:depth], ctd309["2108"][:depth], ctd265["2108"][:depth] = Particles.(n_partcl, Normal.((10.4, 88.4, 189.9), e_dz))

# ------------------------------------------ #
#                Calibrations                #
# ------------------------------------------ #

# get functions converting conductivities to concentrations as well as shift of temperature sensors from 0°C
include(joinpath(@__DIR__,"calibration.jl"))
e_T = 0.05 # °C, error of temperature measurements introduced by calibration (otherwise it would be 0.1°C for PT sensor)

# ------------------------------------------ #
#           Subtract air pressure            #
# ------------------------------------------ #

stage = R.load_stage_pressure() # pressure sensor installed upstream, contains air pressure; sampling interval 2min
keller_p = R.load_DCX22_pressure() # pressure sensor installed in moulins over night; not used
for key in [collect(keys(indices));"1608"]
    if haskey(ctd309,key)
        local idx_stage309 = indexin(DateTime.(ctd309[key][:t]),DateTime.(stage[:t]))
        idx_stage309 = idx_stage309[idx_stage309.!==nothing]
        local idx_ctd309 = indexin(DateTime.(stage[:t]),DateTime.(ctd309[key][:t]))
        idx_ctd309 = idx_ctd309[idx_ctd309.!==nothing]
        local idx_stage265 = indexin(DateTime.(ctd265[key][:t]),DateTime.(stage[:t]))
        idx_stage265 = idx_stage265[idx_stage265.!==nothing]
        local idx_ctd265 = indexin(DateTime.(stage[:t]),DateTime.(ctd265[key][:t]))
        idx_ctd265 = idx_ctd265[idx_ctd265.!==nothing]
        ctd309[key][:dpress] = ctd309[key][:press][idx_ctd309]-stage[:airpress][idx_stage309]
        ctd309[key][:t_dpress] = ctd309[key][:t][idx_ctd309]
        ctd265[key][:dpress] = ctd265[key][:press][idx_ctd265]-stage[:airpress][idx_stage265]
        ctd265[key][:t_dpress] = ctd265[key][:t][idx_ctd265]
        ctd265[key][:idx_stage] = idx_stage265
    end
    if haskey(ctd145,key)
        local idx_stage145 = indexin(DateTime.(ctd145[key][:t]),DateTime.(stage[:t]))
        idx_stage145 = idx_stage145[idx_stage145.!==nothing]
        local idx_ctd145 = indexin(DateTime.(stage[:t]),DateTime.(ctd145[key][:t]))
        idx_ctd145 = idx_ctd145[idx_ctd145.!==nothing]
        ctd145[key][:dpress] = ctd145[key][:press][idx_ctd145]-stage[:airpress][idx_stage145]
        ctd145[key][:t_dpress] = ctd145[key][:t][idx_ctd145]
    end
    if haskey(keller_p,key) || key == "1608"
        local dt = keller_p[key][:t][1]-stage[:t]
        local i1_stage = findmin(abs.(dt))
        local iend_stage = min(i1_stage[2]+length(keller_p[key][:t])-1, length(stage[:t]))
        local iend_kellerp = iend_stage-i1_stage[2]+1
        keller_p[key][:dpress] = keller_p[key][:press][1:iend_kellerp]-stage[:airpress][i1_stage[2]:iend_stage]
        keller_p[key][:t_dpress] = keller_p[key][:t][1:iend_kellerp]
    end
end
stage[:dpress] = stage[:press]-stage[:airpress];


# This file is a part of GERDADeepLearning.jl, licensed under the MIT License (MIT).

using MXNet

import StatsBase: predict

export _build_dnn_classifier
function _build_dnn_classifier(properties, input_size)
  batch_size = properties["batch_size"]
  act_type = properties["activation"]
  fc_sizes = properties["fc"]
  dropout = properties["dropout"]

  X = mx.Variable(:data)
  Y = mx.Variable(:label)

  for i in 1:length(fc_sizes)
    X = fc_layer("fc_$i", X, fc_sizes[i], act_type, dropout)
  end
  X = mx.FullyConnected(X, num_hidden=1, name=:out)

  # Available outputs: SoftmaxOutput, LinearRegressionOutput, LogisticRegressionOutput, MAERegressionOutput, SVMOutput
  loss = mx.LogisticRegressionOutput(X, Y, name=:loss)
  return loss, X
end

export dnn_classifier
function dnn_classifier(env::DLEnv, data;
  id="dnn-classifier", action::Symbol=:auto, label_key=:SSE,
  train_key="train", xval_key="xval", evaluate=["test"], label_function=label_surface_bulk!)

  if action == :auto
    action = decide_best_action(network(env,id))
    info(env, 2, "$id: auto-selected action is $action")
  end

  n = network(env, id)

  if action != :load

    training_data = flatten(data[:set=>train_key])

    xval_data = flatten(data[:set=>xval_key])

    # Preprocessing
    if !haskey(training_data, label_key)
      info(env, 2, "$id: No labels found on training data -> Using default energy labels from label_energy_peaks()")
#full of -1 when no counts at label energies
      label_function(training_data, label_key)
      label_function(xval_data, label_key)
    end

    if length(find(x->x==-1, training_data[label_key])) > 0
      info(env, 2, "$id: Removing unlabeled events and equalizing class counts.")
      training_data, indices = equalize_counts_by_label(training_data, label_function, label_key)
      xval_data, indices = equalize_counts_by_label(xval_data, label_function, label_key)

      println("training data size ",size(training_data.waveforms))
      println("validation data size ",size(xval_data.waveforms))
    end

    println("Batch size originally: ",n["batch_size"])
    if eventcount(xval_data) < n["batch_size"]
      n["batch_size"] = eventcount(xval_data)
      println("Batch size trimmed to size of xval data: ",n["batch_size"])
      info("Cross validation set only has $(eventcount(xval_data)) data points. Adjusting bach size accordingly.")
    end

    train_provider = mx.ArrayDataProvider(:data => waveforms(training_data),
        :label => collect(transpose(training_data[label_key])), batch_size=n["batch_size"])
    xval_provider = mx.ArrayDataProvider(:data => waveforms(xval_data),
        :label => collect(transpose(xval_data[label_key])), batch_size=n["batch_size"])
  else
    train_provider = nothing
    xval_provider = nothing
  end

    if length(evaluate) > 0
      min_eval_count = minimum([eventcount(data[:set=>e]) for e in evaluate])
      if min_eval_count < n["batch_size"]
        n["batch_size"] = min_eval_count
        info("A test set only has $min_eval_count data points. Adjusting bach size accordingly.")
      end
    end

  build(n, action, train_provider, xval_provider, _build_dnn_classifier, verbosity=get_verbosity(env))

  for eval_set_name in evaluate
    for lib in data[:set=>eval_set_name]
      predict(lib, n)
    end
  end

#net model and training data sizes
  return n
end

export predict
function predict(data::EventLibrary, n::NetworkInfo, psd_name=:psd)
  if eventcount(data) > 0

    provider = mx.ArrayDataProvider(:data => waveforms(data), batch_size=n["batch_size"])

    predictions = mx.predict(n.model, provider, verbosity=0)
    data.labels[psd_name] = predictions[1,:]
  else
    data.labels[psd_name] = zeros(Float32, 0)
  end
  push_classifier!(data, "Neural Network")
end

predict(data::DLData, n::NetworkInfo; psd_name=:psd) = mapvalues(data, predict, n, psd_name)


module DEBTrait

using Parameters, FieldMetadata
import FieldMetadata: @default, @description, @units, @bounds, @logscaled, @flattenable, @plottable, @selectable,
                      default, description, units, bounds, logscaled, flattenable, plottable, selectable
using Unitful
Unitful.register(@__MODULE__);
@unit bp "bp" Basepair 1u"m" true;
using Unitful: °C, K, Pa, kPa, MPa, J, kJ, W, L, g, kg, cm, m, s, hr, d, mol, mmol, μmol, σ, R

using Roots
using LinearAlgebra

include("metabolism.jl")
include("cell_composition.jl")
include("assimilation.jl")
include("supeca.jl")
include("soil_properties.jl")
include("ThermoStoichWizard.jl")
include("models.jl")

export AbstractMetabolism, PMetabolismC, PMetabolismCN, growth!, growth_production
export AbstractGenomicData, BaseGenome, PCellComposition
export AbstractMonomer, AbstractAssimilation, BaseMonomer, PAssimilation
export AbstractSoil, BaseSoil
export AbstractParameterSpace, SetupBatchC, ParamsBatchC, rhs_BatchC!

# Write your package code here.

end


"Special Additive Runge Kutta integrator."
immutable IntegratorSARK{T} <: Integrator{T}
    equation::Equation
    tableau::TableauSARK

end

"Integrate DAE with Special Additive Runge Kutta integrator."
function integrate!(int::IntegratorSARK, s::SolutionDAE)
    # TODO
end


using POMDPs
using ContinuousPOMDPTreeSearchExperiments
using POMDPToolbox
using BasicPOMCP
using POMCPOW
using ParticleFilters
using CPUTime
using VDPTag
using DataFrames
using ARDESPOT

N = 100

pomdp = VDPTagPOMDP()
@show mdp(pomdp).tag_radius
@show mdp(pomdp).tag_terminate

file_contents = readstring(@__FILE__())

solvers = Dict{String, Union{Solver,Policy}}(

    "pomcpow" => begin
        rng = MersenneTwister(13)
        ro = ToNextMLSolver(rng)
        solver = POMCPOWSolver(tree_queries=10_000_000,
                               criterion=MaxUCB(40.0),
                               final_criterion=MaxTries(),
                               max_depth=10,
                               max_time=0.1,
                               k_action=8.0,
                               alpha_action=1/20,
                               k_observation=4.0,
                               alpha_observation=1/20,
                               estimate_value=FORollout(ro),
                               check_repeat_act=true,
                               check_repeat_obs=true,
                               next_action=RootToNextMLFirst(rng),
                               default_action=ReportWhenUsed(1),
                               rng=rng
                              )
    end,

    "pomcp" => begin
        rng = MersenneTwister(13)
        ro = ToNextMLSolver(rng)
        POMCPSolver(max_depth=10,
                    max_time=0.1,
                    c=40.0,
                    tree_queries=typemax(Int),
                    default_action=ReportWhenUsed(1),
                    estimate_value=FORollout(ro),
                    rng=rng
                   )
    end,

    "despot" => begin
        rng = MersenneTwister(13)
        ro = ToNextMLSolver(rng)
        b = IndependentBounds(DefaultPolicyLB(ro), VDPUpper())
        DESPOTSolver(lambda=0.01,
                     K=500,
                     D=10,
                     max_trials=1_000_000,
                     T_max=0.1,
                     bounds=b,
                     random_source=MemorizingSource(500, 10, rng, min_reserve=8),
                     rng=rng)
    end
)

alldata = DataFrame()
for n_angles_float in logspace(0.5, 3, 6)
# for n_obs_angles_float in logspace(0.5, 3, 6)
# for n_angles_float in [10.0]
    n_angles = round(Int, n_angles_float)
    for (k, solver) in solvers
        println("$k ($n_angles)")
        dpomdp = ADiscreteVDPTagPOMDP(n_angles=n_angles)
        planner = solve(solver, dpomdp)
        sims = []
        for i in 1:N
            srand(planner, i+50_000)
            cplanner = translate_policy(planner, dpomdp, pomdp, dpomdp)

            filter = ObsAdaptiveParticleFilter(deepcopy(pomdp),
                                               LowVarianceResampler(10_000),
                                               0.05, MersenneTwister(i+90_000))

            sim = Sim(deepcopy(pomdp),
                      deepcopy(cplanner),
                      filter,
                      rng=MersenneTwister(i+70_000),
                      max_steps=100,
                      metadata=Dict(:solver=>k,
                                    :n_angles=>n_angles,
                                    :i=>i)
                     )

            push!(sims, sim)
        end

        data = run_parallel(sims)
        # data = run(sims)

        rs = data[:reward]
        println(@sprintf("reward: %6.3f ± %6.3f", mean(rs), std(rs)/sqrt(length(rs))))
        alldata = vcat(alldata, data)
    end
end
# end

filename = Pkg.dir("ContinuousPOMDPTreeSearchExperiments", "data", "vdp_a_discretization_$(Dates.format(now(), "E_d_u_HH_MM"))")
println("saving to $filename...")
writetable(filename*".csv", alldata)
write(filename*"_file.jl", file_contents)
println("done.")