"""
    solve(problem::WaveguideProblem; [ρ₀, Nouts], kwargs...)

Solves the `WaveguideProblem`, using `QuantumOptics.timeevolution.master_nh[_dynamic]`.

# Arguments
- `problem`: the waveguide problem
- `Nouts::Array{Int}` (optional): specifies the dimension of the output Fock spaces
- `ρ₀` (optional): Alternative initial state for the simulation.
	Overrides the initial state provided by the `WavePacket` and the quantum system.
- Additional keyword arguments are passed to `QuantumOptics.jl`'s master equation solver,
  see [it's documentation](https://docs.qojulia.org/api/#QuantumOptics.timeevolution.master)

# Returns
- ts:    The times on which the output is determined.
- ρ(t):  The density matrix at each point in time. The Hilbert space depends on the specific
         problem; if unsure check out `basis(ρ[1])`.
"""
function solve(problem::_DrivenProblem{O, Coherent, ContinuousWave};
		ρ₀ = nothing, kwargs...) where {O}
	ψ₀, Ls, H_nh = _generateLindbladian(problem)
	ρ₀ = isnothing(ρ₀) ? ψ₀ : ρ₀

	return timeevolution.master_nh(problem.ts, ρ₀, H_nh, Ls; kwargs...)
end

function solve(problem::WaveguideProblem; Nouts::Union{Nothing,Array} = nothing,
		ρ₀ = nothing,  kwargs...)
	if problem isa _ScatterProblem
		if !isnothing(ρ₀)
			!isnothing(Nouts) && @warn "`Nouts` and `ρ₀` simultaneously set. Ignoring `Nouts`"
			Nouts = [basis.N for basis ∈ basis(ρ₀).bases[3:end]]
		end
		isnothing(Nouts) && ( Nouts = _expectedOutputPhotons(problem) )
		@assert length(Nouts) == length(problem.ψₒ)
	end

	ψ₀, Ls, H_nh = _generateLindbladian(problem, Nouts)
	ρ₀ = isnothing(ρ₀) ? ψ₀ : ρ₀

	function Operators(t, ψ)
		Ls_t = [Ls[1](t), Ls[2:end]...]
		return H_nh(t), H_nh(t)', Ls_t, dagger.(Ls_t)
	end

	return timeevolution.master_nh_dynamic(problem.ts, ρ₀, Operators; kwargs...)
end

function _expectedOutputPhotons(problem::_ScatterProblem{O, Coherent, ContinuousWave}) where {O}
	if problem.displace_output
		return fill(ceil(Int, 3 * sqrt(problem.ts[end] - problem.ts[1]) ), length(problem.ψₒ) )
	end
	fill(coherent_cutoff( sqrt( (problem.ts[end] - problem.ts[1]) * abs2(problem.ψᵢ.α) ) ),
		length(problem.ψₒ)
	)
end

function _expectedOutputPhotons(problem::_ScatterProblem{O, Coherent, WavePacket{Coherent}}) where {O}
	if problem.displace_output
		return fill(ceil(Int, 3 * sqrt(problem.ts[end] - problem.ts[1]) ), length(problem.ψₒ) )
	end
	N = problem.ψᵢ.state.N_cutoff
	fill(N, length(problem.ψₒ))
end

function _expectedOutputPhotons(problem::_ScatterProblem{O, S, WavePacket{S}}) where {O, S <: Displaced}
	if problem.displace_output
		return fill(
			roblem.ψᵢ.state.N_cutoff + ceil(Int, 3 * sqrt(problem.ts[end] - problem.ts[1]) ),
			length(problem.ψₒ)
		)
	end
	N = problem.ψᵢ.state.N_cutoff + coherent_cutoff(problem.ψᵢ.state.α)
	fill(N, length(problem.ψₒ))
end

function _expectedOutputPhotons(problem::_ScatterProblem{O, S, WavePacket{S}}) where {O, S}
	N = problem.ψᵢ.state.N_cutoff
	fill(N, length(problem.ψₒ))
end

# ------------------------------------------------------------
# _DrivenProblems

function _generateLindbladian(problem::_DrivenProblem{O, Coherent, ContinuousWave}) where {O}
	# ----------------------------------------
	# Scatter System
	H_sys, σ⁻, σ⁺ = problem.H, problem.σ, problem.σ'
	σ⁺σ⁻ = σ⁺ * σ⁻

	# ----------------------------------------
	# Constructing Disspator and Hamiltonian
	α  = problem.ψᵢ.α
	atomic_dissipation = isempty(problem.Ls) ?
		-0.5im * σ⁺σ⁻ :
		-0.5im * σ⁺σ⁻ - 0.5im * sum([L'*L for L ∈ problem.Ls])

	H_nh = atomic_dissipation - 1.0im * (σ⁺ * α - σ⁻ * α) + H_sys

	ψ₀ = problem.system_state
	return ψ₀, [σ⁻, problem.Ls...], H_nh
end

function _generateLindbladian(problem::_DrivenProblem{O, Coherent, WavePacket{Coherent}}, _) where {O}
	# ----------------------------------------
	# Scatter System
	H_sys, σ⁻, σ⁺ = problem.H, problem.σ, problem.σ'
	σ⁺σ⁻ = σ⁺ * σ⁻

	# ----------------------------------------
	# Constructing Disspator and Hamiltonian
	α  = problem.ψᵢ.state.α
	mf = problem.ψᵢ.mode.modeFunction
	atomic_dissipation = isempty(problem.Ls) ?
		-0.5im * σ⁺σ⁻ :
		-0.5im * σ⁺σ⁻ - 0.5im * sum([L'*L for L ∈ problem.Ls])

	H_nh(t) = atomic_dissipation - 1.0im * (σ⁺ * α * mf(t) - σ⁻ * α * mf(t)) + H_sys

	ψ₀ = problem.system_state
	return ψ₀, [t -> σ⁻, problem.Ls...], H_nh
end

function _generateLindbladian(problem::_DrivenProblem{O, T, WavePacket{T}}, _) where {O, T}
	# ----------------------------------------
	# Scatter System
	H_sys, σ⁻, σ⁺ = problem.H, problem.σ, problem.σ'
	σ⁺σ⁻ = σ⁺ * σ⁻
	idₛ = one(basis(H_sys))

	# ----------------------------------------
	# Input System
	inBasis = FockBasis(problem.ψᵢ.state.N_cutoff)
	gᵢ      = problem.ψᵢ.mode.gᵢ
	idᵢ     = identityoperator(inBasis)
	aᵢ, aᵢᵀ = destroy(inBasis) ⊗ idₛ, create(inBasis) ⊗ idₛ
	aᵢᵀaᵢ   = aᵢᵀ * aᵢ

	# ----------------------------------------
	# Embed Operators and abbreviations
	H_sys, σ⁻, σ⁺, σ⁺σ⁻ = idᵢ ⊗ H_sys, idᵢ ⊗ σ⁻, idᵢ ⊗ σ⁺, idᵢ ⊗ σ⁺σ⁻
	aᵢᵀσ⁻,  aᵢσ⁺ = aᵢᵀ * σ⁻, aᵢ * σ⁺

	# ----------------------------------------
	# Constructing Disspator and Hamiltonian
	L  = t -> σ⁻ + gᵢ(t)aᵢ
	Lᵀ = t -> σ⁺ + gᵢ(t)aᵢᵀ

	atomic_dissipation = isempty(problem.Ls) ?
		-0.5im * σ⁺σ⁻ :
		-0.5im * σ⁺σ⁻ - (0.5im * idᵢ) ⊗ sum([L'*L for L ∈ problem.Ls])

	H_nh = _createHamiltonian(problem, atomic_dissipation, aᵢᵀaᵢ, aᵢσ⁺, σ⁺, σ⁻, H_sys)

	ψ₀ = Ket(inBasis, createState(problem.ψᵢ.state)) ⊗ problem.system_state
	return ψ₀, [L, [idᵢ ⊗ L for L ∈ problem.Ls]...], H_nh
end

function _createHamiltonian(problem::_DrivenProblem{O, S, WavePacket{S}},
		atomic_dissipation, aᵢᵀaᵢ, aᵢσ⁺, σ⁺, σ⁻, H_sys) where {O, S <: Displaced}
	gᵢ, mf, α = problem.ψᵢ.mode.gᵢ, problem.ψᵢ.mode.modeFunction, problem.ψᵢ.state.α

	return t -> atomic_dissipation - 0.5im * gᵢ(t)^2 * aᵢᵀaᵢ -
		1.0im * (σ⁺ * α * mf(t) - σ⁻ * α' * mf(t)') - 1.0im * gᵢ(t)aᵢσ⁺ + H_sys
end

function _createHamiltonian(problem::_DrivenProblem{O, S, WavePacket{S}},
		atomic_dissipation, aᵢᵀaᵢ, aᵢσ⁺, σ⁺, σ⁻, H_sys) where {O, S <: NonDisplaced}
	gᵢ = problem.ψᵢ.mode.gᵢ

	return t -> atomic_dissipation - 0.5im * gᵢ(t)^2 * aᵢᵀaᵢ - 1.0im * gᵢ(t)aᵢσ⁺ + H_sys
end

# ------------------------------------------------------------
# _ScatterProblems

function _generateLindbladian(problem::_ScatterProblem{O, Coherent, ContinuousWave}, Nouts) where {O}
	_generateLindbladian(
		WaveguideProblem(
			[problem.H, problem.Ls, problem.σ, problem.system_state],
			ContinuousWave(t -> problem.ψᵢ.α),
			problem.ψₒ,
			problem.ts,
			problem.displace_output
		),
		Nouts
	)
end

function _generateLindbladian(problem::_ScatterProblem{O, Coherent, WavePacket{Coherent}}, Nouts) where {O}
	# ----------------------------------------
	# Output System
	outBasis, groundstateOutput, aₒs, aₒᵀs = _generateOutputCavities(Nouts)
	idₒ = one(outBasis)

	# ----------------------------------------
	# Scatter System
	H_sys, σ⁻, σ⁺ = problem.H ⊗ idₒ, problem.σ ⊗ idₒ, problem.σ' ⊗ idₒ
	σ⁺σ⁻ = σ⁺ * σ⁻
	idₛ = one(basis(problem.H))

	# ----------------------------------------
	# Embed Operators and abbreviations
	aₒs, aₒᵀs = [idₛ ⊗ a for a ∈ aₒs], [idₛ ⊗ aᵀ for aᵀ ∈ aₒᵀs]

	gₒs = [mode.gₒ for mode in problem.ψₒ]

	gₒaₒs(t)  = sum([gₒ(t) for gₒ ∈ gₒs] .* aₒs)
	gₒaₒᵀs(t) = sum([gₒ(t) for gₒ ∈ gₒs] .* aₒᵀs)

	aₒᵀσ⁻s = [aₒᵀ * σ⁻ for aₒᵀ in aₒᵀs]
	aₒᵀaₒs = aₒᵀs .* aₒs
	# TODO: maybe it is faster to do `gₒaₒs(t)  = idₛ ⊗ sum(gₒs(t) .* aₒs)` and leave aₒs unchanged
	# due to the sum

	# ----------------------------------------
	# Constructing Disspator and Hamiltonian
	L  = t -> (σ⁻ + gₒaₒs(t) )
	Lᵀ = t -> (σ⁺ + gₒaₒᵀs(t) )

	α  = problem.ψᵢ.state.α
	mf = problem.ψᵢ.mode.modeFunction
	atomic_dissipation = isempty(problem.Ls) ?
		-0.5im * σ⁺σ⁻ :
		-0.5im * σ⁺σ⁻ - 0.5im * sum([L'*L for L ∈ problem.Ls]) ⊗ idₒ

	if problem.displace_output
		H_nh = function(t)
			return atomic_dissipation - 0.5im * sum([gₒ(t)^2 for gₒ ∈ gₒs] .* aₒᵀaₒs) -
				1.0im * sum([gₒ(t) for gₒ ∈ gₒs] .* aₒᵀσ⁻s) -
				1.0im * (σ⁺ * α * mf(t) - σ⁻ * α' * mf(t)) +
				H_sys
		end
	else
		H_nh = function(t)
			return atomic_dissipation - 0.5im * sum([gₒ(t)^2 for gₒ ∈ gₒs] .* aₒᵀaₒs) -
				1.0im * sum([gₒ(t) for gₒ ∈ gₒs] .* aₒᵀσ⁻s) -
				1.0im * (Lᵀ(t) * α * mf(t) - L(t) * α' * mf(t)) +
				H_sys
		end
	end

	ψ₀ = problem.system_state ⊗ groundstateOutput
	return ψ₀, [t -> L(t), [L ⊗ idₒ for L ∈ problem.Ls]...], H_nh
end

function _generateLindbladian(problem::_ScatterProblem{O, T, WavePacket{T}}, Nouts) where {O, T}
	# ----------------------------------------
	# Output System
	outBasis, groundstateOutput, aₒs, aₒᵀs = _generateOutputCavities(Nouts)
	idₒ = one(outBasis)

	# ----------------------------------------
	# Scatter System
	H_sys, σ⁻, σ⁺  = problem.H, problem.σ, problem.σ'
	σ⁺σ⁻ = σ⁺ * σ⁻
	idₛ = one(basis(problem.H))

	# ----------------------------------------
	# Input System
	inBasis = FockBasis(problem.ψᵢ.state.N_cutoff)
	gᵢ      = problem.ψᵢ.mode.gᵢ
	idᵢ     = identityoperator(inBasis)
	aᵢ, aᵢᵀ = destroy(inBasis) ⊗ idₛ ⊗ idₒ, create(inBasis) ⊗ idₛ ⊗ idₒ
	aᵢᵀaᵢ = aᵢᵀ * aᵢ

	# ----------------------------------------
	# Embed Operators and abbreviations
	H_sys, σ⁻, σ⁺, σ⁺σ⁻ = idᵢ ⊗ H_sys ⊗ idₒ, idᵢ ⊗ σ⁻ ⊗ idₒ, idᵢ ⊗ σ⁺ ⊗ idₒ, idᵢ ⊗ σ⁺σ⁻ ⊗ idₒ
	aₒs, aₒᵀs = [idᵢ ⊗ idₛ ⊗ a for a ∈ aₒs], [idᵢ ⊗ idₛ ⊗ aᵀ for aᵀ ∈ aₒᵀs]

	gₒs = [mode.gₒ for mode in problem.ψₒ]

	gₒaₒs(t)  = sum([gₒ(t)' for gₒ ∈ gₒs] .* aₒs)
	gₒaₒᵀs(t) = sum([gₒ(t)  for gₒ ∈ gₒs] .* aₒᵀs)

	aᵢσ⁺   = aᵢ * σ⁺
	aₒᵀaᵢs = [aₒᵀ * aᵢ for aₒᵀ in aₒᵀs]
	aₒᵀσ⁻s = [aₒᵀ * σ⁻ for aₒᵀ in aₒᵀs]
	aₒᵀaₒs = aₒᵀs .* aₒs
	# TODO: maybe it is faster to do `gₒaₒs(t)  = idₛ ⊗ sum(gₒs(t) .* aₒs)` and leave aₒs unchanged
	# due to the sum

	# ----------------------------------------
	# Constructing Disspator and Hamiltonian
	L  = t -> σ⁻ + gᵢ(t)aᵢ  + gₒaₒs(t)

	atomic_dissipation = isempty(problem.Ls) ?
		-0.5im * σ⁺σ⁻ :
		-0.5im * σ⁺σ⁻ - (0.5im * idᵢ) ⊗ sum([L'*L for L ∈ problem.Ls]) ⊗ idₒ

	H_nh = _createHamiltonian(problem, atomic_dissipation,
		aₒᵀaₒs, aᵢᵀaᵢ, aᵢσ⁺, aₒᵀaᵢs, aₒᵀσ⁻s, σ⁺, σ⁻, gₒaₒᵀs, gₒaₒs, H_sys
	)

	ψ₀ = Ket(inBasis, createState(problem.ψᵢ.state)) ⊗ problem.system_state ⊗ groundstateOutput
	return ψ₀, [t -> L(t), [idᵢ ⊗ L ⊗ idₒ for L ∈ problem.Ls]...], H_nh
end

function _generateOutputCavities(Nouts)
	# Preconverting into `CompositeBasis` allows us to `embed` into it, even for a single system
	outBasis  = reduce(⊗, FockBasis.(Nouts) .|> CompositeBasis)

	groundstateOutput = reduce(⊗, [fockstate(FockBasis(N), 0) for N in Nouts])

	aₒs  = [embed(outBasis, i, destroy(FockBasis(N))) for (i, N) in enumerate(Nouts)]
	aₒᵀs = [embed(outBasis, i, create(FockBasis(N)))  for (i, N) in enumerate(Nouts)]

	return outBasis, groundstateOutput, aₒs, aₒᵀs
end

function _createHamiltonian(problem::_ScatterProblem{O, S, WavePacket{S}},
		atomic_dissipation, aₒᵀaₒs, aᵢᵀaᵢ, aᵢσ⁺, aₒᵀaᵢs, aₒᵀσ⁻s, σ⁺, σ⁻, gₒaₒᵀs, gₒaₒs,
		H_sys) where {O, S<: Displaced}
	gᵢ, mf, α = problem.ψᵢ.mode.gᵢ, problem.ψᵢ.mode.modeFunction, problem.ψᵢ.state.α
	gₒs   = [mode.gₒ for mode in problem.ψₒ]

	return t -> atomic_dissipation -
		0.5im * (gᵢ(t)^2 * aᵢᵀaᵢ + sum([gₒ(t)^2 for gₒ ∈ gₒs] .* aₒᵀaₒs) ) -
		1.0im * (gᵢ(t)aᵢσ⁺ + sum([gₒ(t) for gₒ ∈ gₒs] .* aₒᵀσ⁻s) +
			sum(gᵢ(t) .* [gₒ(t) for gₒ ∈ gₒs] .* aₒᵀaᵢs) ) -
		1.0im * (( σ⁺ + gₒaₒᵀs(t) ) * α * mf(t) - ( σ⁻ + gₒaₒs(t) )  * α' * mf(t)') + H_sys
end

function _createHamiltonian(problem::_ScatterProblem{O, S, WavePacket{S}},
		atomic_dissipation, aₒᵀaₒs, aᵢᵀaᵢ, aᵢσ⁺, aₒᵀaᵢs, aₒᵀσ⁻s, σ⁺, σ⁻, gₒaₒᵀs, gₒaₒs,
		H_sys) where {O, S<: NonDisplaced}
	gᵢ    = problem.ψᵢ.mode.gᵢ
	gₒs   = [mode.gₒ for mode in problem.ψₒ]

	return t -> atomic_dissipation -
		0.5im * (gᵢ(t)^2 * aᵢᵀaᵢ + sum([gₒ(t)^2 for gₒ ∈ gₒs] .* aₒᵀaₒs) ) -
		1.0im * (gᵢ(t)aᵢσ⁺ + sum([gₒ(t) for gₒ ∈ gₒs] .* aₒᵀσ⁻s) +
			sum(gᵢ(t) .* [gₒ(t) for gₒ ∈ gₒs] .* aₒᵀaᵢs) ) +
		H_sys
end


abstract type AbstractPAR end

struct PAR <: AbstractPAR
    value::Rational{UInt32}
end

struct PAR1 <: AbstractPAR
end

AbstractPAR(x) = isone(x) ? PAR1() : PAR(x)

par(x::PAR) = x.value
par(_::PAR1) = 1//1

const Image = Matrix{RGB{N0f8}}#PermutedDimsArray{RGB{Normed{UInt8,8}},2,(2, 1),(2, 1),Array{RGB{Normed{UInt8,8}},2}} #{RGB{Normed{UInt8,8}}}

abstract type AbstractPhotage end

struct Snapshot{P <: AbstractPAR} <: AbstractPhotage
    img::Image
    par::P
end

struct TimeLapse{P <: AbstractPAR} <: AbstractPhotage
    imgs::Vector{Image}
    par::P
end

par(x::AbstractPhotage) = par(x.par)

const STEP = Millisecond(60)

timestamps(t::Temporal{WholeVideo, Instantaneous}) = (t.video.file.name, start(t.time))

function timestamps(t::Temporal{FragmentedVideo, Instantaneous})
    t = start(t.time)
    for vf in files(t.video)
        d = duration(vf)
        if t ≤ d
            return (vf.name, t)
        end
        t -= d
    end
end

timestamps(t::Temporal{WholeVideo, Prolonged}) = (t.video.file.name, start(t.time):STEP:stop(t.time))

function timestamps(t::Temporal{FragmentedVideo, Prolonged})
    ranges = Dict{String, StepRange{Millisecond,Millisecond}}()
    t1 = start(t.time)
    t2 = stop(t.time)
    files = Iterators.Stateful(t.video.files)
    for vf in files
        if t1 ≤ vf.duration
            if t2 ≤ vf.duration
                ranges[vf.name] = t1:STEP:t2
                return ranges
            else
                ranges[vf.name] = t1:STEP:vf.duration
                t2 -= vf.duration
                for _vf in files
                    if t2 ≤ _vf.duration
                        ranges[_vf.name] = Millisecond(0):STEP:t2
                        return ranges
                    end
                    ranges[_vf.name] = Millisecond(0):STEP:_vf.duration
                    t2 -= _vf.duration
                end
            end
        end
        t1 -= vf.duration
        t2 -= vf.duration
    end
end

ms2s(t::Millisecond) = t/Millisecond(1000)

function seekread(f, s::Float64)
    seek(f, s)
    full = read(f)
    full[1:2:end, :]
end

fetchimages(_::Missing) = missing

function fetchimages(t::Temporal{V, Instantaneous}) where V
    file, time = timestamps(t)
    f = VideoIO.openvideo(joinpath(Databula.coffeesource, file))
    img = seekread(f, ms2s(time))
    sar = 2VideoIO.aspect_ratio(f)
    Snapshot(img, AbstractPAR(sar))
end

function fetchimages(t::Temporal{WholeVideo, Prolonged})
    file, times = timestamps(t)
    f = VideoIO.openvideo(joinpath(Databula.coffeesource, file))
    imgs = [seekread(f, ms2s(t)) for t in times]
    sar = 2VideoIO.aspect_ratio(f)
    TimeLapse(imgs, AbstractPAR(sar))
end

function fetchimages(t::Temporal{FragmentedVideo, Prolonged})
    ft = timestamps(t)
    imgs = Vector{TimeLapse}(undef, length(ft))
    for (i, (file, times)) in enumerate(ft)
        f = VideoIO.openvideo(joinpath(Databula.coffeesource, file))
        _imgs = [seekread(f, ms2s(t)) for t in times]
        sar = 2VideoIO.aspect_ratio(f)
        imgs[i] = TimeLapse(_imgs, AbstractPAR(sar))
    end
    imgs
end


function init_from_experiment(experiment, data, labels, res)
    debug(LOGGER, "[INIT] Start initialization of experiment.")
    pools = copy(experiment[:param][:initial_pools])
    split_strategy = experiment[:split_strategy]

    # model
    train_data, train_pools, _ = get_train(split_strategy, data, pools)
    debug(LOGGER, "[INIT] Initializing model '$(experiment[:model])' with $(format_observations(train_data)) observations.")
    model = instantiate(eval(experiment[:model][:type]), train_data, train_pools, experiment[:model][:param])
    initialize!(model, eval(experiment[:model][:init_strategy]))
    set_model_fitted!(res, model)
    set_adjust_K!(model, experiment[:param][:adjust_K])
    solver = experiment[:param][:solver]
    debug(LOGGER, "[INIT] Model solver for this experiment is '$(typeof(solver))'.")

    # query strategy
    debug(LOGGER, "[INIT] Initializing QS '$(experiment[:query_strategy][:type])'.")
    query_data, _, _ = get_query(split_strategy, data, pools)
    qs = QueryStrategies.initialize_qs(eval(experiment[:query_strategy][:type]), model, query_data, experiment[:query_strategy][:param])

    debug(LOGGER, "[INIT] Initializing oracle.")
    oracle = initialize_oracle(eval(experiment[:oracle]), labels)

    info(LOGGER, "[INIT] Initialization done.")
    return (model, pools, solver, qs, split_strategy, oracle)
end

function check_active_learn_args(data, labels)
    MLLabelUtils.islabelenc(labels, OneClassActiveLearning.LABEL_ENCODING) || throw(ArgumentError("Argument labels is in the wrong encoding."))
    return size(data, 2) == length(labels) || throw(ArgumentError("Number of observations ($(size(data,2))) does not equal number of labels $(length(labels))."))
end

function active_learn(experiment::Dict{Symbol, Any})
    data, labels = OneClassActiveLearning.load_data(experiment[:data_file])
    return active_learn(experiment, data, labels)
end

function active_learn(experiment::Dict{Symbol, Any}, data::Array{T, 2}, labels::Vector{Symbol}) where T <: Real
    check_active_learn_args(data, labels)
    log_experiment_info(experiment)

    res = Result(experiment)
    set_worker_info!(res)
    set_data_stats!(res, data, experiment[:split_strategy])

    model, pools, solver, qs, split_strategy, oracle = try
        init_from_experiment(experiment, data, labels, res)
    catch e
        warn(LOGGER, e)
        if isa(e, KDEException)
            res.status[:exit_code] = :kde_error
        elseif isa(e, MissingLabelTypeException)
            res.status[:exit_code] = :missing_label_type
        else
            throw(e)
        end
        return res
    end

    debug(LOGGER, "Start active learning cycle with $(experiment[:param][:num_al_iterations]) queries.")
    for i in 0:experiment[:param][:num_al_iterations]
        info(LOGGER, "Iteration $(i)")
        debug(LOGGER, "Memory consumption $(round(Int, Sys.free_memory() / 2^20)) MB / $(round(Int, Sys.total_memory() / 2^20)) MB")

        train_data, train_pools, _ = get_train(split_strategy, data, pools)
        time_set_data = @elapsed set_data!(model, train_data)
        set_pools!(model, labelmap(train_pools))

        debug(LOGGER, "[FIT] Start fitting model on $(format_observations(train_data)) observations.")
        # Workaround: redirect solver output
        stdout_orig, stderr_orig = stdout, stderr
        redirect_stdout(open("/dev/null", "w")); redirect_stderr(open("/dev/null", "w"))
        status, time_fit, mem_fit = @timed fit!(model, solver)
        redirect_stdout(stdout_orig); redirect_stderr(stderr_orig)
        debug(LOGGER, "[FIT] Fitting done ($(time_fit) s, $(format_bytes(mem_fit))).")

        @trace res.al_history i time_fit mem_fit time_set_data
        if status !== JuMP.MathOptInterface.Success && status != :Optimal
            warn(LOGGER, "Not solved to optimality. Solver status: $status.")
            res.status[:exit_code] = :solver_error
            return res
        end

        test_data, _, test_indices = get_test(split_strategy, data, pools)
        debug(LOGGER, "[TEST] Testing by predicting $(format_observations(test_data)) observations.")
        predictions = SVDD.predict(model, test_data)
        push_evaluation!(res.al_history, i, predictions, labels[test_indices])
        debug(LOGGER, "[TEST] Testing done.")

        if i < experiment[:param][:num_al_iterations]
            query_data, query_pools, query_indices = try
                get_query(split_strategy, data, pools)
            catch e
                warn(LOGGER, e)
                if isa(e, KDEException)
                     res.status[:exit_code] = :kde_error
                elseif isa(e, MissingLabelTypeException)
                    res.status[:exit_code] = :missing_label_type
                else
                    res.status[:exit_code] = :unknown_qs_error
                end
                return res
            end
            if :U ∉ query_pools
                info(LOGGER, "Aborting '$(experiment[:hash])' after $(i) iterations because no more unlabeled observations are left.")
                al_summarize!(res)
                res.status[:exit_code] = :early_abort
                return res
            end
            debug(LOGGER, "[QS] Starting query strategy on $(format_observations(query_data)) observations.")
            if i == 0
                query_id, time_qs, mem_qs = @timed get_query_object(qs, query_data, query_pools, query_indices, Int[])
            else
                query_id, time_qs, mem_qs = @timed get_query_object(qs, query_data, query_pools, query_indices, values(res.al_history, :query_history))
            end
            debug(LOGGER, "[QS] Query strategy scoring done ($(time_qs) s, $(format_bytes(mem_qs))).")
            query_label = ask_oracle(oracle, query_id)
            update_pools!(pools, query_id, query_label)
            push_query!(res.al_history, i, query_id, query_label, time_qs, mem_qs)
            debug(LOGGER, "[QS] Query(id = $(query_id), label = $(labels[query_id]))")
            debug(LOGGER, "[QS] Query strategy done.")

        end
        debug(LOGGER, "Finished iteration $(i).")
    end
    debug(LOGGER, "Finished active learning cycle.")

    al_summarize!(res)
    debug(LOGGER, "Summary done.")
    info(LOGGER, "Finished experiment '$(experiment[:hash])'.")
    res.status[:exit_code] = :success
    return res
end

function update_pools!(pools, query_id, query_label)
    pools[query_id] = query_label == :inlier ? :Lin : :Lout
    return nothing
end

"""
get_query_object(qs::QueryStrategy, data::Array{T, 2}, pools::Vector{Symbol}, global_indices::Vector{Int}, history::Vector{Int})

# Arguments
- `query_data`: Subset of the full data set
- `pools`: Labels for `query_data`
- `global_indices`: Indices of the observations in `query_data` relative to the full data set.
"""
function get_query_object(qs::QueryStrategy, query_data::Array{T, 2}, pools::Vector{Symbol}, global_indices::Vector{Int}, history::Vector{Int})::Int where T <: Real
    pool_map = labelmap(pools)
    haskey(pool_map, :U) || throw(ArgumentError("No more points that are unlabeled."))
    scores = qs_score(qs, query_data, pool_map)
    @assert length(scores) == size(query_data, 2)
    candidates = [i for i in pool_map[:U] if global_indices[i] ∉ history]
    debug(LOGGER, "[QS] Selecting from $(format_number(length(candidates))) candidates.")
    local_query_index = candidates[argmax(scores[candidates])]
    return global_indices[local_query_index]
end

function push_query!(al_history::MVHistory, i, query_id, query_label, time_qs, mem_qs)
    push!(al_history, :query_history, i, query_id)
    @trace al_history i query_label time_qs mem_qs
    return nothing
end

function push_evaluation!(al_history::MVHistory, i, predictions, labels)
    cm = ConfusionMatrix(SVDD.classify.(predictions), labels)
    push!(al_history, :cm, i, cm)
    for e in [:cohens_kappa, :matthews_corr, :f1_score, :tpr, :fpr]
        push!(al_history, e, i, eval(e)(cm))
    end
    push!(al_history, :auc, i, roc_auc(predictions, labels))
    for k in [0.01, 0.02, 0.05, 0.1, 0.2]
        auc_fpr = OneClassActiveLearning.roc_auc(predictions, labels, fpr = k)
        auc_fpr_normalized = OneClassActiveLearning.roc_auc(predictions, labels, fpr = k, normalize = true)
        push!(al_history, Symbol("auc_fpr_$(k)"), i, auc_fpr)
        push!(al_history, Symbol("auc_fpr_normalized_$(k)"), i, auc_fpr_normalized)
    end
    return nothing
end


"""
    SumAudioGenerator

An audio generator used to combine (sum) multiple other sources.
"""
mutable struct SumAudioGenerator <: AbstractAudioGenerator
    generators::Vector{AbstractAudioGenerator}
end

function reset!(gen::SumAudioGenerator)
    for g in gen.generators
        reset!(g)
    end
    nothing
end
function has_samples(gen::SumAudioGenerator, sample_rate::Float64)
    any(
        has_samples(g, sample_rate)
        for g in gen.generators
    )
end
function next_samples!(gen::SumAudioGenerator, max_n::Int,
                       sample_rate::Float64)
    samples = zeros(Float64, max_n, 2)
    max_len = 0
    for g in gen.generators
        s = next_samples!(g, max_n, sample_rate)
        len = size(s)[1]
        samples[1:len, :] .+= s
        max_len = max(max_len, len)
    end
    samples[1:max_len]
end


function setCollisionSmoothingRadius(shape, collisionSmoothingRadius) end

# function setCollisionSmoothingRadius(shape::Sphere) end # not defined

# function setCollisionSmoothingRadius(shape::Ellipsoid) end # not defined

function setCollisionSmoothingRadius(shape::Box, collisionSmoothingRadius)
    @assert(collisionSmoothingRadius >= 0.0)
    collisionSmoothingRadius2 = min(collisionSmoothingRadius, 0.1*min(shape.lengthX, shape.lengthY, shape.lengthZ)) # at most 10% of the smallest edge length
    if collisionSmoothingRadius2 < collisionSmoothingRadius
      println("collisionSmoothingRadius in Box changed from $collisionSmoothingRadius to $collisionSmoothingRadius2")
    end
    @assert(shape.lengthX >= 2*collisionSmoothingRadius2)
    @assert(shape.lengthY >= 2*collisionSmoothingRadius2)
    @assert(shape.lengthZ >= 2*collisionSmoothingRadius2)
    return collisionSmoothingRadius2
end

function setCollisionSmoothingRadius(shape::Cylinder, collisionSmoothingRadius)
    @assert(collisionSmoothingRadius >= 0.0)
    collisionSmoothingRadius = min(collisionSmoothingRadius, 0.1*min(shape.diameter, shape.length))  # at most 10% of the smallest edge length
    return collisionSmoothingRadius
end

function setCollisionSmoothingRadius(shape::Cone, collisionSmoothingRadius)
    @assert(collisionSmoothingRadius >= 0.0)
    collisionSmoothingRadius2 = min(collisionSmoothingRadius, 0.1*min(shape.diameter, shape.length)) # at most 10% of the smallest edge length
    return collisionSmoothingRadius2
end

function setCollisionSmoothingRadius(shape::Capsule, collisionSmoothingRadius)
    @assert(collisionSmoothingRadius >= 0.0)
    collisionSmoothingRadius2 = min(collisionSmoothingRadius, 0.1*min(shape.diameter, shape.length)) # at most 10% of the smallest edge length
    return collisionSmoothingRadius2
end

function setCollisionSmoothingRadius(shape::Beam, collisionSmoothingRadius)
    @assert(collisionSmoothingRadius >= 0.0)
    collisionSmoothingRadius2 = min(collisionSmoothingRadius, 0.1*min(shape.length, shape.width, shape.thickness)) # at most 10% of the smallest edge length
    return collisionSmoothingRadius2
end


const DimOrDimTuple = Union{Dimension,Tuple{Vararg{<:Dimension}}}
const IntOrIntTuple = Union{Int,Tuple{Vararg{<:Int}}}

"""
    aggregate(method, object, scale)

Aggregate array, or all arrays in a stack or series, by some scale.

# Arguments

- `method`: a function such as `mean` or `sum` that can combine the
  value of multiple cells to generate the aggregated cell, or a [`Locus`]($DDlocusdocs)
  like `Start()` or `Center()` that species where to sample from in the interval.
- `object`: Object to aggregate, like `AbstractGeoSeries`, `AbstractStack`,
  `AbstractGeoArray`, `Dimension`
- `scale`: the aggregation factor, which can be an integer, a tuple of integers
  for each dimension, or any `Dimension`, `Selector` or `Int` combination you can
  usually use in `getindex`. Using a `Selector` will determine the scale by the
  distance from the start of the index.
"""
function aggregate end

"""
    aggregate(method, series::AbstractGeoSeries, scale)

Aggregate an [`AbstractGeoSeries`](@ref) by `scale` using `method`.

Returns a [`GeoSeries`](ref).
"""
function aggregate(
    method, series::AbstractGeoSeries, scale, args...; progress=true, kwargs...
)
    f = i -> aggregate(method, series[i], scale, args...; progress=false, kwargs...)
    data = if progress
        @showprogress "Aggregating series..." map(f, 1:length(series))
    else
        map(f, 1:length(series))
    end
    return rebuild(series, data)
end
"""
    aggregate(method, stack::AbstractGeoStack, scale)

Aggregate an [`AbstractGeoStack`](@ref) by `scale` using `method`.

Returns a [`GeoStack`](ref).
"""
function aggregate(
    method, stack::AbstractGeoStack, scale; keys=keys(stack), progress=true
)
    f = key -> aggregate(method, stack[key], scale)
    keys_nt = NamedTuple{keys}(keys)
    data = if progress
        @showprogress "Aggregating stack..." map(f, keys_nt)
    else
        map(f, keys_nt)
    end
    return GeoStack(stack; data=data)
end

# DimensionalData methods

"""
    aggregate(method, src::AbstractDimArray, scale)

Aggregate an `AbstractDimArray` by `scale` using `method`.

[`DiskGeoArray`](@ref) will be converted to [`GeoArray`](@ref).
"""
aggregate(method, src::AbstractDimArray, scale) =
    aggregate!(method, alloc_ag(method, src, scale), src, scale)
aggregate(method, src::DiskGeoArray, scale) =
    aggregate(method, GeoArray(src), scale)

"""
    aggregate(method, dim::Dimension, scale)

Aggregate a `Dimension` by `scale` using `method`.
"""
function aggregate(method, dim::Dimension, scale)
    intscale = scale2int(dim, scale)
    intscale == 1 && return dim
    start, stop = endpoints(dim, method, intscale)
    return rebuild(dim, val(dim)[start:scale:stop], aggregate(method, mode(dim), intscale))
end

function endpoints(dim, method, scale)
    start = firstindex(dim) + agoffset(method, dim, scale)
    stop = (length(dim) ÷ scale) * scale
    return start, stop
end

"""
    aggregate(method, dim::IndexMode, scale)

Aggregate an `IndexMode` by `scale` using `method`.
"""
aggregate(method, mode::IndexMode, scale) = mode
aggregate(method, mode::AbstractSampled, scale) =
    rebuild(mode; span=aggregate(method, span(mode), scale))

"""
    aggregate(method, dim::Span, scale)

Aggregate a `Span` by `scale` using `method`.
"""
aggregate(method, span::Span, scale) = span
aggregate(method, span::Regular, scale) = Regular(val(span) * scale)

"""
    aggregate!(method, dst::AbstractDimArray, src::AbstractDimArray, scale)

Aggregate array `src` to array `dst` by `scale`, using `method`.

# Arguments

- `method`: a function such as `mean` or `sum` that can combine the
  value of multiple cells to generate the aggregated cell, or a [`Locus`]($DDlocusdocs)
  like `Start()` or `Center()` that species where to sample from in the interval.
- `scale`: the aggregation factor, which can be an integer, a tuple of integers
  for each dimension, or any `Dimension`, `Selector` or `Int` combination you can
  usually use in `getindex`. Using a `Selector` will determine the scale by the
  distance from the start of the index in the `src` array.
"""
aggregate!(locus::Locus, dst::AbstractDimArray, src, scale) =
    aggregate!((locus,), dst, src, scale)
function aggregate!(loci::Tuple{Locus,Vararg}, dst::AbstractDimArray, src, scale)
    intscale = scale2int(dims(src), scale)
    offsets = agoffset.(loci, intscale)
    for I in CartesianIndices(dst)
        dst[I] = src[(upsample.(Tuple(I), intscale) .+ offsets)...]
    end
    return dst
end
# Function/functor methods
function aggregate!(f, dst::AbstractDimArray, src, scale)
    intscale = scale2int(dims(src), scale)
    for I in CartesianIndices(dst)
        topleft = upsample.(Tuple(I), intscale)
        bottomright = topleft .+ intscale .- 1
        dst[I] = f(view(src, map(:, topleft, bottomright)...))
    end
    return dst
end

"""
    alloc_ag(method, A::AbstractDimArray, scale)

Allocate an array of the correct size to aggregate `A` by `scale`
"""
alloc_ag(method, A::AbstractDimArray, scale) = alloc_ag((method,), A, scale)
function alloc_ag(method::Tuple, A::AbstractDimArray, scale)
    intscale = scale2int(dims(A), scale)
    # Aggregate the dimensions
    dims_ = aggregate.(method, dims(A), intscale)
    # Dim aggregation determines the array size
    data_ = similar(data(A), map(length, dims_)...)
    return rebuild(A; data=data_, dims=dims_)
end

# Convert scale or tuple of scale to integer using dims2indices
scale2int(dims, scale::Tuple) = dims2indices(dims, scale)
scale2int(dims, scale::Int) = scale

"""
    disaggregate(method, object, scale)

Disaggregate array, or all arrays in a stack or series, by some scale.

# Arguments

- `method`: a function such as `mean` or `sum` that can combine the
  value of multiple cells to generate the aggregated cell, or a [`Locus`]($DDlocusdocs)
  like `Start()` or `Center()` that species where to sample from in the interval.
- `object`: Object to aggregate, like `AbstractGeoSeries`, `AbstractStack`,
  `AbstractGeoArray`, `Dimension`
- `scale`: the aggregation factor, which can be an integer, a tuple of integers
  for each dimension, or any `Dimension`, `Selector` or `Int` combination you can
  usually use in `getindex`. Using a `Selector` will determine the scale by the
  distance from the start of the index.
"""
function disaggregate end

"""
    disaggregate(method, series::AbstractGeoSeries, scale)

Disaggregate an [`AbstractGeoSeries`](@ref) by `scale` using `method`.
"""
function disaggregate(method, series::AbstractGeoSeries, scale; progress=true, kwargs...)
    f = i -> disaggregate(method, series[i], scale; progress=false, kwargs...)
    return if progress
        @showprogress "Disaggregating series..." map(f, 1:length(series))
    else
        map(f, 1:length(series))
    end
end
"""
    disaggregate(method, stack::AbstractGeoStack, scale)

Disaggregate an [`AbstractGeoStack`](@ref) by `scale` using `method`.
"""
function disaggregate(method, stack::AbstractGeoStack, scale; keys=keys(stack), progress=true)
    f = key -> disaggregate(method, stack[key], scale)
    keys_nt = NamedTuple{keys}(keys)
    data = if progress
        @showprogress "Disaggregating stack..." map(f, keys_nt)
    else
        map(f, keys_nt)
    end
    return GeoStack(stack; data=data)
end

# DimensionalData methods
"""
    disaggregate(method, src::AbstractDimArray, scale)

Disaggregate an `AbstractDimArray` by `scale` using `method`.

[`DiskGeoArray`](@ref) will be converted to [`GeoArray`](@ref).
"""
disaggregate(method, src::AbstractDimArray, scale) =
    disaggregate!(method, alloc_disag(method, src, scale), src, scale)
disaggregate(method, src::DiskGeoArray, scale) = disaggregate(method, GeoArray(src), scale)

"""
    disaggregate(method, dim::Dimension, scale)

Disaggregate a `Dimension` by `scale` using `method`.
"""
function disaggregate(locus::Locus, dim::Dimension, scale)
    intscale = scale2int(dim, scale)
    intscale == 1 && return dim
    len = length(dim) * intscale
    step_ = step(mode(dim)) / intscale
    start = dim[1] - agoffset(locus, intscale) * step_
    stop = start + (len - 1)  * step_
    index = LinRange(start, stop, len)
    return rebuild(dim, index, disaggregate(locus, mode(dim), intscale))
end
disag_index(locus::Start, dim, scale) =
    LinRange(dim[1], dim[end] + (scale - 1) * step(dim), length(dim) * scale)
disag_index(locus::End, dim, scale) =
    LinRange(dim[1], dim[end] + (scale - agoffset(locus)) * step(dim), length(dim) * scale)

"""
    disaggregate(method, dim::IndexMode, scale)

Disaggregate an `IndexMode` by `scale` using `method`.
"""
disaggregate(method, mode::IndexMode, scale) = mode
disaggregate(method, mode::AbstractSampled, scale) =
    rebuild(mode; span=disaggregate(method, span(mode), scale))

"""
    disaggregate(method, dim::Span, scale)

Disaggregate a `Span` by `scale` using `method`.
"""
disaggregate(method, span::Span, scale) = span
disaggregate(method, span::Regular, scale) = Regular(val(span) / scale)

"""
    disaggregate!(method, dst::AbstractDimArray, src::AbstractDimArray, scale)

Disaggregate array `src` to array `dst` by some scale, using `method`.

- `method`: a function such as `mean` or `sum` that can combine the
  value of multiple cells to generate the aggregated cell, or a [`Locus`]($DDlocusdocs)
  like `Start()` or `Center()` that species where to sample from in the interval.
- `scale`: the aggregation factor, which can be an integer, a tuple of integers
  for each dimension, or any `Dimension`, `Selector` or `Int` combination you can
  usually use in `getindex`. Using a `Selector` will determine the scale by the
  distance from the start of the index in the `src` array.
"""
disaggregate!(locus::Locus, dst::AbstractDimArray, src, scale) =
    disaggregate!((locus,), dst, src, scale)
function disaggregate!(loci::Tuple{Locus,Vararg}, dst::AbstractDimArray, src, scale)
    intscale = scale2int(dims(src), scale)
    for I in CartesianIndices(dst)
        dst[I] = src[(downsample.(Tuple(I), intscale))...]
    end
    return dst
end

"""
    alloc_ag(method, A::AbstractDimArray, scale)

Allocate an array of the correct size to disaggregate `A` by `scale`
"""
alloc_disag(method, A::AbstractDimArray, scale) = alloc_disag((method,), A, scale)
function alloc_disag(method::Tuple, A::AbstractDimArray, scale)
    intscale = scale2int(dims(A), scale)
    dims_ = disaggregate.(method, dims(A), intscale)
    # Dim aggregation determines the array size
    data_ = similar(data(A), map(length, dims_)...)
    return rebuild(A; data=data_, dims=dims_)
end

"""
    upsample(index::Int, scale::Int)

Convert indicies from the aggregated array to the larger original array.
"""
upsample(index::Int, scale::Int) = (index - 1) * scale + 1

"""
    downsample(index::Int, scale::Int)

Convert indicies from the original array to the aggregated array.
"""
downsample(index::Int, scale::Int) = (index - 1) ÷ scale + 1

agoffset(locus::Locus, dim::Dimension, scale) = agoffset(locus, scale)
agoffset(method, dim::Dimension, scale) = agoffset(locus(dim), scale)
agoffset(locus::Start, scale) = 0
agoffset(locus::End, scale) = scale - 1
agoffset(locus::Center, scale) = scale ÷ 2


# Indexing and dimensions
export workitemIdx, workgroupIdx, workgroupDim, gridDim, gridDimWG
export threadIdx, blockIdx, blockDim

@generated function _index(::Val{fname}, ::Val{name}, ::Val{range}) where {fname, name, range}
    Context() do ctx
        T_int32 = LLVM.Int32Type(ctx)

        # create function
        llvm_f, _ = create_function(T_int32)
        mod = LLVM.parent(llvm_f)

        # generate IR
        Builder(ctx) do builder
            entry = BasicBlock(llvm_f, "entry"; ctx)
            position!(builder, entry)

            # call the indexing intrinsic
            intr_typ = LLVM.FunctionType(T_int32)
            intr = LLVM.Function(mod, "llvm.amdgcn.$fname.id.$name", intr_typ)
            idx = call!(builder, intr)

            # attach range metadata
            range_metadata = MDNode([ConstantInt(UInt32(range.start); ctx),
                                     ConstantInt(UInt32(range.stop); ctx)];
                                    ctx)
            metadata(idx)[LLVM.MD_range] = range_metadata
            ret!(builder, idx)
        end

        call_function(llvm_f, UInt32)
    end
end

@generated function _dim(::Val{base}, ::Val{off}, ::Val{range}, ::Type{T}) where {base, off, range, T}
    Context() do ctx
        T_int8 = LLVM.Int8Type(ctx)
        T_int32 = LLVM.Int32Type(ctx)
        _as = convert(Int, AS.Constant)
        T_ptr_i8 = LLVM.PointerType(T_int8, _as)
        T_ptr_i32 = LLVM.PointerType(T_int32, _as)
        T_ptr_T = LLVM.PointerType(convert(LLVMType, T; ctx), _as)

        # create function
        llvm_f, _ = create_function(T_int32)
        mod = LLVM.parent(llvm_f)

        # generate IR
        Builder(ctx) do builder
            entry = BasicBlock(llvm_f, "entry"; ctx)
            position!(builder, entry)

            # get the kernel dispatch pointer
            intr_typ = LLVM.FunctionType(T_ptr_i8)
            intr = LLVM.Function(mod, "llvm.amdgcn.dispatch.ptr", intr_typ)
            ptr = call!(builder, intr)

            # load the index
            offset = base+((off-1)*sizeof(T))
            idx_ptr_i8 = inbounds_gep!(builder, ptr, [ConstantInt(offset; ctx)])
            idx_ptr_T = bitcast!(builder, idx_ptr_i8, T_ptr_T)
            idx_T = load!(builder, idx_ptr_T)
            idx = zext!(builder, idx_T, T_int32)

            # attach range metadata
            range_metadata = MDNode([ConstantInt(T(range.start); ctx),
                                     ConstantInt(T(range.stop); ctx)];
                                    ctx)
            metadata(idx_T)[LLVM.MD_range] = range_metadata
            ret!(builder, idx)
        end

        call_function(llvm_f, UInt32)
    end
end

# TODO: look these up for the current agent/queue
# TODO: grids can be up to typemax(UInt64)
const _max_group_size = (x=1023, y=1023, z=1023)
const _max_groups     = (x=typemax(UInt32)-1, y=typemax(UInt32)-1, z=typemax(UInt32)-1)
const _max_grid_size  = (x=typemax(UInt32)-1, y=typemax(UInt32)-1, z=typemax(UInt32)-1)

for dim in (:x, :y, :z)
    # Workitem index
    fname = Symbol("workitem")
    fn = Symbol("workitemIdx_$dim")
    intr = Symbol("$dim")
    @eval @inline $fn() = Int(_index($(Val(fname)), $(Val(intr)), $(Val(0:_max_group_size[dim])))) + 1
    cufn = Symbol("threadIdx_$dim")
    @eval @inline $cufn() = $fn()

    # Workgroup index
    fname = Symbol("workgroup")
    fn = Symbol("workgroupIdx_$dim")
    intr = Symbol("$dim")
    @eval @inline $fn() = Int(_index($(Val(fname)), $(Val(intr)), $(Val(0:_max_groups[dim])))) + 1
    cufn = Symbol("blockIdx_$dim")
    @eval @inline $cufn() = $fn()
end
for (dim,off) in ((:x,1), (:y,2), (:z,3))
    # Workgroup dimension (in workitems)
    fn = Symbol("workgroupDim_$dim")
    base = _packet_offsets[findfirst(x->x==:workgroup_size_x,_packet_names)]
    @eval @inline $fn() = Int(_dim($(Val(base)), $(Val(off)), $(Val(0:_max_group_size[dim])), UInt16))
    cufn = Symbol("blockDim_$dim")
    @eval @inline $cufn() = $fn()

    # Grid dimension (in workitems)
    fn = Symbol("gridDim_$dim")
    base = _packet_offsets[findfirst(x->x==:grid_size_x,_packet_names)]
    @eval @inline $fn() = Int(_dim($(Val(base)), $(Val(off)), $(Val(0:_max_grid_size[dim])), UInt32))
    # Grid dimension (in workgroups)
    fn_wg = Symbol("gridDimWG_$dim")
    fn_wg_dim = Symbol("workgroupDim_$dim")
    @eval @inline $fn_wg() = div($fn(), $fn_wg_dim())
end

"""
    workitemIdx()::ROCDim3

Returns the work item index within the work group.
See also: [`threadIdx`](@ref)
"""
@inline workitemIdx() = (x=workitemIdx_x(), y=workitemIdx_y(), z=workitemIdx_z())

"""
    workgroupIdx()::ROCDim3

Returns the work group index.
See also: [`blockIdx`](@ref)
"""
@inline workgroupIdx() = (x=workgroupIdx_x(), y=workgroupIdx_y(), z=workgroupIdx_z())

"""
    workgroupDim()::ROCDim3

Returns the size of each workgroup in workitems.
See also: [`blockDim`](@ref)
"""
@inline workgroupDim() = (x=workgroupDim_x(), y=workgroupDim_y(), z=workgroupDim_z())

"""
    gridDim()::ROCDim3

Returns the size of the grid in workitems.
This behaviour is different from CUDA where `gridDim` gives the size of the grid in blocks.
"""
@inline gridDim() = (x=gridDim_x(), y=gridDim_y(), z=gridDim_z())

"""
    gridDimWG()::ROCDim3

Returns the size of the grid in workgroups.
This is equivalent to CUDA's `gridDim`.
"""
@inline gridDimWG() = (x=gridDimWG_x(), y=gridDimWG_y(), z=gridDimWG_z())

# For compat with CUDAnative et. al

"""
    threadIdx()::ROCDim3

Returns the thread index within the block.
See also: [`workitemIdx`](@ref)
"""
@inline threadIdx() = (x=threadIdx_x(), y=threadIdx_y(), z=threadIdx_z())

"""
    blockIdx()::ROCDim3

Returns the block index within the grid.
See also: [`workgroupIdx`](@ref)
"""
@inline blockIdx() = (x=blockIdx_x(), y=blockIdx_y(), z=blockIdx_z())

"""
    blockDim()::ROCDim3

Returns the dimensions of the block.
See also: [`workgroupDim`](@ref)
"""
@inline blockDim() = (x=blockDim_x(), y=blockDim_y(), z=blockDim_z())


# Julia version of cmu_arctic_demo.sh.

JULIABIN_PATH=get(ENV, "JULIABIN_PATH", "julia")

BIN_DIR = joinpath(Pkg.dir("VoiceConversion"), "bin")
JULIABIN = [JULIABIN_PATH, "--depwarn=no", "--color=yes"]

## Experimental conditions

src_id = "clb" # Source speaker identifier
tgt_id = "slt" # Target speaker identifier

diff = true # enable direct waveform modification based on spectrum differencial
max = 100   # maximum number of training data
order = 40  # order of mel-cepstrum (expect for 0th)
mix = 8     # number of mixtures of GMMs
power_threshold = -14.0 # power threshold to select frames used in training
niter = 30  # number of iteration in EM algorithm

skip_feature_extraction = false
skip_feature_alignment = false
skip_model_training = false
skip_voice_conversion = false

## Directory settings

cmu_arctic_root = joinpath(expanduser("~"), "data", "cmu_arctic")

feature_save_dir = "./features"
model_save_dir = "./models"
vc_save_dir_top = "./converted_wav"

if diff
    vc_save_dir = joinpath(vc_save_dir_top,
                           "$(src_id)_to_$(tgt_id)_order$(order)_gmm$(mix)_diff")
else
    vc_save_dir = joinpath(vc_save_dir_top,
                           "$(src_id)_to_$(tgt_id)_order$(order)_gmm$(mix)")
end

run(`mkdir -p $(feature_save_dir)`)
run(`mkdir -p $(model_save_dir)`)
run(`mkdir -p $(vc_save_dir)`)

## Feature Extraction

if !skip_feature_extraction
    for s in [src_id, tgt_id]
        tgt_wav_path = joinpath(cmu_arctic_root, "cmu_us_$(s)_arctic/wav")
        options = ["--max", max, "--order", order]
        cmd = `$(JULIABIN) $(BIN_DIR)/mcep.jl
        $(tgt_wav_path) $(feature_save_dir)/speakers/$(s) $options`
        run(cmd)
    end
end

## Alignment

if !skip_feature_alignment
    options = ["--max", max, "--threshold", power_threshold]
    run(`$(JULIABIN) $(BIN_DIR)/align.jl
        $(feature_save_dir)/speakers/$(src_id)
        $(feature_save_dir)/speakers/$(tgt_id)
        $(feature_save_dir)/parallel/$(src_id)_and_$(tgt_id) $options`)
end

if diff
    model_path = joinpath(model_save_dir,
                          "$(src_id)_to_$(tgt_id)_gmm$(mix)_order$(order)_diff.jld")
else
    model_path = joinpath(model_save_dir,
                          "$(src_id)_to_$(tgt_id)_gmm$(mix)_order$(order).jld")
end

if !skip_model_training
    options = ["--max", max, "--n_components", mix, "--n_iter", niter,
               "--n_init", 1]
    diff && push!(options, "--diff")
    cmd = `$(JULIABIN) $(BIN_DIR)/train_gmm.jl
    $(feature_save_dir)/parallel/$(src_id)_and_$(tgt_id)
    $(model_path) $options`
    run(cmd)
end

## Voice Conversion

if !skip_voice_conversion
    synthesis_script = diff ? ["diffvc.jl"] : ["vc.jl"]
    for n=100:105
        tgt_wav_path = joinpath(cmu_arctic_root,
                                "cmu_us_$(src_id)_arctic/wav/arctic_a0$(n).wav")
        cmd = `$(JULIABIN) $(BIN_DIR)/$(synthesis_script)
        $(tgt_wav_path)
        $model_path
        $(vc_save_dir)/arctic_a0$(n)_$(src_id)_to_$(tgt_id).wav --order $order`
        run(cmd)
    end
end


#! format: off

abstract type AbstractLoadFormulation <: AbstractDeviceFormulation end
abstract type AbstractControllablePowerLoadFormulation <: AbstractLoadFormulation end
struct StaticPowerLoad <: AbstractLoadFormulation end
struct InterruptiblePowerLoad <: AbstractControllablePowerLoadFormulation end
struct DispatchablePowerLoad <: AbstractControllablePowerLoadFormulation end

########################### ElectricLoad ####################################

get_variable_sign(_, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = -1.0

########################### ActivePowerVariable, ElectricLoad ####################################

get_variable_binary(::ActivePowerVariable, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = false

get_variable_expression_name(::ActivePowerVariable, ::Type{<:PSY.ElectricLoad}) = :nodal_balance_active

get_variable_lower_bound(::ActivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = 0.0
get_variable_upper_bound(::ActivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = PSY.get_active_power(d)

########################### ReactivePowerVariable, ElectricLoad ####################################

get_variable_binary(::ReactivePowerVariable, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = false

get_variable_expression_name(::ReactivePowerVariable, ::Type{<:PSY.ElectricLoad}) = :nodal_balance_reactive

get_variable_lower_bound(::ReactivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = 0.0
get_variable_upper_bound(::ReactivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = PSY.get_reactive_power(d)

########################### ReactivePowerVariable, ElectricLoad ####################################

get_variable_binary(::OnVariable, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = true

#! format: on

####################################### Reactive Power Constraints #########################
"""
Reactive Power Constraints on Controllable Loads Assume Constant power_factor
"""
function DeviceRangeConstraintSpec(
    ::Type{<:RangeConstraint},
    ::Type{ReactivePowerVariable},
    ::Type{<:PSY.ElectricLoad},
    ::Type{<:AbstractControllablePowerLoadFormulation},
    ::Type{<:PM.AbstractPowerModel},
    feedforward::Union{Nothing, AbstractAffectFeedForward},
    use_parameters::Bool,
    use_forecasts::Bool,
)
    return DeviceRangeConstraintSpec(;
        custom_optimization_container_func = custom_reactive_power_constraints!,
    )
end

function custom_reactive_power_constraints!(
    optimization_container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    ::Type{<:AbstractControllablePowerLoadFormulation},
) where {T <: PSY.ElectricLoad}
    time_steps = model_time_steps(optimization_container)
    constraint = JuMPConstraintArray(undef, [PSY.get_name(d) for d in devices], time_steps)
    assign_constraint!(optimization_container, REACTIVE, T, constraint)

    for t in time_steps, d in devices
        name = PSY.get_name(d)
        pf = sin(atan((PSY.get_max_reactive_power(d) / PSY.get_max_active_power(d))))
        reactive = get_variable(optimization_container, REACTIVE_POWER, T)[name, t]
        real = get_variable(optimization_container, ACTIVE_POWER, T)[name, t] * pf
        constraint[name, t] =
            JuMP.@constraint(optimization_container.JuMPmodel, reactive == real)
    end
end

function DeviceRangeConstraintSpec(
    ::Type{<:RangeConstraint},
    ::Type{ActivePowerVariable},
    ::Type{T},
    ::Type{<:DispatchablePowerLoad},
    ::Type{<:PM.AbstractPowerModel},
    feedforward::Union{Nothing, AbstractAffectFeedForward},
    use_parameters::Bool,
    use_forecasts::Bool,
) where {T <: PSY.ElectricLoad}
    if (!use_parameters && !use_forecasts)
        return DeviceRangeConstraintSpec(;
            range_constraint_spec = RangeConstraintSpec(;
                constraint_name = make_constraint_name(
                    RangeConstraint,
                    ActivePowerVariable,
                    T,
                ),
                variable_name = make_variable_name(ActivePowerVariable, T),
                limits_func = x -> (min = 0.0, max = PSY.get_active_power(x)),
                constraint_func = device_range!,
                constraint_struct = DeviceRangeConstraintInfo,
            ),
        )
    end

    return DeviceRangeConstraintSpec(;
        timeseries_range_constraint_spec = TimeSeriesConstraintSpec(
            constraint_name = make_constraint_name(RangeConstraint, ActivePowerVariable, T),
            variable_name = make_variable_name(ActivePowerVariable, T),
            parameter_name = use_parameters ? ACTIVE_POWER : nothing,
            forecast_label = "max_active_power",
            multiplier_func = x -> PSY.get_max_active_power(x),
            constraint_func = use_parameters ? device_timeseries_param_ub! :
                              device_timeseries_ub!,
        ),
    )
end

function DeviceRangeConstraintSpec(
    ::Type{<:RangeConstraint},
    ::Type{ActivePowerVariable},
    ::Type{T},
    ::Type{<:InterruptiblePowerLoad},
    ::Type{<:PM.AbstractPowerModel},
    feedforward::Union{Nothing, AbstractAffectFeedForward},
    use_parameters::Bool,
    use_forecasts::Bool,
) where {T <: PSY.ElectricLoad}
    if (!use_parameters && !use_forecasts)
        return DeviceRangeConstraintSpec(;
            range_constraint_spec = RangeConstraintSpec(;
                constraint_name = make_constraint_name(
                    RangeConstraint,
                    ActivePowerVariable,
                    T,
                ),
                variable_name = make_variable_name(ActivePowerVariable, T),
                bin_variable_names = [make_variable_name(OnVariable, T)],
                limits_func = x -> (min = 0.0, max = PSY.get_active_power(x)),
                constraint_func = device_semicontinuousrange!,
                constraint_struct = DeviceRangeConstraintInfo,
            ),
        )
    end

    return DeviceRangeConstraintSpec(;
        timeseries_range_constraint_spec = TimeSeriesConstraintSpec(
            constraint_name = make_constraint_name(RangeConstraint, ActivePowerVariable, T),
            variable_name = make_variable_name(ActivePowerVariable, T),
            bin_variable_name = make_variable_name(OnVariable, T),
            parameter_name = use_parameters ? ON : nothing,
            forecast_label = "max_active_power",
            multiplier_func = x -> PSY.get_max_active_power(x),
            constraint_func = use_parameters ? device_timeseries_ub_bigM! :
                              device_timeseries_ub_bin!,
        ),
    )
end

########################## Addition to the nodal balances ##################################

function NodalExpressionSpec(
    ::Type{T},
    ::Type{<:PM.AbstractPowerModel},
    use_forecasts::Bool,
) where {T <: PSY.ElectricLoad}
    return NodalExpressionSpec(
        "max_active_power",
        REACTIVE_POWER,
        use_forecasts ? x -> PSY.get_max_reactive_power(x) : x -> PSY.get_reactive_power(x),
        -1.0,
        T,
    )
end

function NodalExpressionSpec(
    ::Type{T},
    ::Type{<:PM.AbstractActivePowerModel},
    use_forecasts::Bool,
) where {T <: PSY.ElectricLoad}
    return NodalExpressionSpec(
        "max_active_power",
        ACTIVE_POWER,
        use_forecasts ? x -> PSY.get_max_active_power(x) : x -> PSY.get_active_power(x),
        -1.0,
        T,
    )
end

############################## FormulationControllable Load Cost ###########################
function AddCostSpec(
    ::Type{T},
    ::Type{DispatchablePowerLoad},
    ::OptimizationContainer,
) where {T <: PSY.ControllableLoad}
    cost_function = x -> (x === nothing ? 1.0 : PSY.get_variable(x))
    return AddCostSpec(;
        variable_type = ActivePowerVariable,
        component_type = T,
        variable_cost = cost_function,
        multiplier = OBJECTIVE_FUNCTION_NEGATIVE,
    )
end

function AddCostSpec(
    ::Type{T},
    ::Type{InterruptiblePowerLoad},
    ::OptimizationContainer,
) where {T <: PSY.ControllableLoad}
    cost_function = x -> (x === nothing ? 1.0 : PSY.get_fixed(x))
    return AddCostSpec(;
        variable_type = OnVariable,
        component_type = T,
        fixed_cost = cost_function,
        multiplier = OBJECTIVE_FUNCTION_NEGATIVE,
    )
end


using Parameters
using DifferentialEquations
using ForwardDiff

abstract type AbstractReactor end
export AbstractReactor

struct Reactor{D<:AbstractDomain} <: AbstractReactor
    domain::D
    ode::ODEProblem
end

function Reactor(domain::T,y0::Array{W,1},tspan::Tuple) where {T<:AbstractDomain,W<:Real}
    dydt(y::Array{T,1},p::V,t::Q) where {T<:Real,Q<:Real,V} = dydtreactor!(y,t,domain)
    ode = ODEProblem(dydt,y0,tspan)
    return Reactor(domain,ode)
end
export Reactor

@inline function getrate(rxn::T,cs::Array{W,1},kfs::Array{Q,1},krevs::Array{Q,1}) where {T<:AbstractReaction,Q,W<:Real}
    Nreact = length(rxn.reactantinds)
    Nprod = length(rxn.productinds)
    R = 0.0
    if Nreact == 1
        @fastmath @inbounds R += kfs[rxn.index]*cs[rxn.reactantinds[1]]
    elseif Nreact == 2
        @fastmath @inbounds R += kfs[rxn.index]*cs[rxn.reactantinds[1]]*cs[rxn.reactantinds[2]]
    elseif Nreact == 3
        @fastmath @inbounds R += kfs[rxn.index]*cs[rxn.reactantinds[1]]*cs[rxn.reactantinds[2]]*cs[rxn.reactantinds[3]]
    end

    if Nprod == 1
        @fastmath @inbounds R -= krevs[rxn.index]*cs[rxn.productinds[1]]
    elseif Nprod == 2
        @fastmath @inbounds R -= krevs[rxn.index]*cs[rxn.productinds[1]]*cs[rxn.productinds[2]]
    elseif Nprod == 3
        @fastmath @inbounds R -= krevs[rxn.index]*cs[rxn.productinds[1]]*cs[rxn.productinds[2]]*cs[rxn.productinds[3]]
    end

    return R
end
export getrate

@inline function addreactionratecontributions!(dydt::Array{Q,1},rarray::Array{UInt16,2},cs::Array{W,1},kfs::Array{Z,1},krevs::Array{Z,1}) where {Q<:Real,Z<:Real,T<:Integer,W<:Real}
    @inbounds @simd for i = 1:size(rarray)[2]
        if @inbounds rarray[2,i] == 0
            @inbounds @fastmath fR = kfs[i]*cs[rarray[1,i]]
        elseif @inbounds rarray[3,i] == 0
            @inbounds @fastmath fR = kfs[i]*cs[rarray[1,i]]*cs[rarray[2,i]]
        else
            @inbounds @fastmath fR = kfs[i]*cs[rarray[1,i]]*cs[rarray[2,i]]*cs[rarray[3,i]]
        end
        if @inbounds rarray[5,i] == 0
            @inbounds @fastmath rR = krevs[i]*cs[rarray[4,i]]
        elseif @inbounds rarray[6,i] == 0
            @inbounds @fastmath rR = krevs[i]*cs[rarray[4,i]]*cs[rarray[5,i]]
        else
            @inbounds @fastmath rR = krevs[i]*cs[rarray[4,i]]*cs[rarray[5,i]]*cs[rarray[6,i]]
        end
        @fastmath R = fR - rR
        @inbounds @fastmath dydt[rarray[1,i]] -= R
        if @inbounds rarray[2,i] != 0
            @inbounds @fastmath dydt[rarray[2,i]] -= R
            if @inbounds rarray[3,i] != 0
                @inbounds @fastmath dydt[rarray[3,i]] -= R
            end
        end
        @inbounds @fastmath dydt[rarray[4,i]] += R
        if @inbounds rarray[5,i] != 0
            @inbounds @fastmath dydt[rarray[5,i]] += R
            if @inbounds rarray[6,i] != 0
                @inbounds @fastmath dydt[rarray[6,i]] += R
            end
        end
    end
end
export addreactionratecontributions!

@inline function dydtreactor!(y::Array{U,1},t::Z,domain::Q;sensitivity::Bool=true) where {Z<:Real,U<:Real,J<:Integer,Q<:AbstractDomain}
    dydt = zeros(U,length(y))
    if sensitivity #if sensitivity isn't explicitly set to false set it to domain.sensitivity
        sensitivity = domain.sensitivity
    end
    ns,cs,T,P,V,C,N,mu,kfs,krevs,Hs,Us,Gs,diffs,Cvave = calcthermo(domain,y,t)
    addreactionratecontributions!(dydt,domain.rxnarray,cs,kfs,krevs)
    dydt *= V
    if sensitivity && isa(domain,ConstantVDomain)
        wV = copy(dydt[domain.indexes[1]:domain.indexes[2]])
    else
        wV = Array{U,1}()
    end
    calcdomainderivatives!(domain,dydt;t=t,T=T,P=P,Us=Us,V=V,C=C,ns=ns,N=N,Cvave=Cvave)
    if sensitivity
        Nspcs = length(cs)
        Nrxns = length(domain.phase.reactions)
        jacobian!(domain.jacobian,y,nothing,t,domain)
        dgdk = ratederivative(domain;cs=cs,V=V,T=T,kfs=kfs,Us=Us,Cvave=Cvave,N=N,krevs=krevs,wV=wV,sparse=false)
        for j  = 1:Nspcs+Nrxns #kfs and Gfs
            for i = 1:(domain.indexes[end]-domain.indexes[1]+1) #species
                for z in 1:(domain.indexes[end]-domain.indexes[1]+1)
                    dydt[j*Nspcs+i] += domain.jacobian[i,z]*y[j*Nspcs+z]
                end
                dydt[j*Nspcs+i] += dgdk[i,j]
            end
        end
    end
    return dydt
end
export dydtreactor!

function jacobian!(J::Q,y::U,p::W,t::Z,domain::V) where {Q<:AbstractArray,U<:AbstractArray,W,Z<:Real,V<:AbstractDomain}
    if domain.t[1] == t && domain.jacuptodate == true
        return
    else
        f(y::Array{T,1}) where {T<:Real} = dydtreactor!(y,domain.t[1],domain;sensitivity=false)
        ForwardDiff.jacobian!(domain.jacobian,f,y[1:domain.indexes[end]-domain.indexes[1]+1])
        domain.jacuptodate[1] = true
        return
    end
end
export jacobian!

function ratederivative(d::W; cs::Q,V::Y,T::Y2,Us::Z3,Cvave::Z4,N::Z5,kfs::Z,krevs::X,wV::Q2,sparse::Bool=false) where {W<:Union{ConstantTPDomain,ConstantTVDomain},Z4<:Real,Z5<:Real,Z3<:AbstractArray,Q2<:AbstractArray,Q<:AbstractArray,Y2<:Real,Y<:Real,Z<:AbstractArray,X<:AbstractArray}
    Nspcs = length(cs)
    rxns = d.phase.reactions
    Nrxns = length(rxns)

    if sparse
        ratederiv = spzeros(Nspcs,Nspcs+Nrxns)
    else
        ratederiv = zeros(Nspcs,Nspcs+Nrxns)
    end
    RTinv = 1.0/(R*T)

    for (j,rxn) in enumerate(rxns)
        Nreact = length(rxn.reactantinds)
        Nprod = length(rxn.productinds)

        if Nreact == 1
            rind1 = rxn.reactantinds[1]
            fderiv = cs[rind1]
        elseif Nreact == 2
            rind1,rind2 = rxn.reactantinds
            fderiv = cs[rind1]*cs[rind2]
        else
            rind1,rind2,rind3 = rxn.reactantinds
            fderiv = cs[rind1]*cs[rind2]*cs[rind3]
        end

        if Nprod == 1
            pind1 = rxn.productinds[1]
            rderiv = krevs[j]/kfs[j]*cs[pind1]
        elseif Nprod == 2
            pind1,pind2 = rxn.productinds
            rderiv = krevs[j]/kfs[j]*cs[pind1]*cs[pind2]
        else
            pind1,pind2,pind3 = rxn.productinds
            rderiv = krevs[j]/kfs[j]*cs[pind1]*cs[pind2]*cs[pind3]
        end

        flux = fderiv-rderiv
        gderiv = rderiv*kfs[j]*RTinv

        deriv = zeros(Nspcs)

        deriv[rind1] += gderiv
        if Nreact > 1
            deriv[rind2] += gderiv
            if Nreact > 2
                deriv[rind3] == gderiv
            end
        end

        deriv[pind1] -= gderiv
        if Nprod > 1
            deriv[pind2] -= gderiv
            if Nprod > 2
                deriv[pind3] -= gderiv
            end
        end

        ratederiv[rind1,j] -= flux
        ratederiv[rind1,Nrxns+1:Nrxns+Nspcs] .-= deriv
        if Nreact > 1
            ratederiv[rind2,j] -= flux
            ratederiv[rind2,Nrxns+1:Nrxns+Nspcs] .-= deriv
            if Nreact > 2
                ratederiv[rind3,j] -= flux
                ratederiv[rind3,Nrxns+1:Nrxns+Nspcs] .-= deriv
            end
        end

        ratederiv[pind1,j] += flux
        ratederiv[pind1,Nrxns+1:Nrxns+Nspcs] .+= deriv
        if Nprod > 1
            ratederiv[pind2,j] += flux
            ratederiv[pind2,Nrxns+1:Nrxns+Nspcs] .+= deriv
            if Nprod > 2
                ratederiv[pind3,j] += flux
                ratederiv[pind3,Nrxns+1:Nrxns+Nspcs] .+= deriv
            end
        end
    end
    return V*ratederiv
end

function ratederivative(d::ConstantVDomain; cs::Q,V::Y,T::Y2,Us::Z3,Cvave::Y3,N::Y2,kfs::Z,krevs::X,wV::Q2,sparse::Bool=false) where {Z3<:AbstractArray,Q<:AbstractArray,Q2<:AbstractArray,Y3<:Real,Y2<:Real,Y<:Real,Z<:AbstractArray,X<:AbstractArray}
    Nspcs = length(cs)
    rxns = d.phase.reactions
    Nrxns = length(rxns)

    if sparse
        ratederiv = spzeros(Nspcs+1,Nspcs+Nrxns)
    else
        ratederiv = zeros(Nspcs+1,Nspcs+Nrxns)
    end
    RTinv = 1.0/(R*T)

    for (j,rxn) in enumerate(rxns)
        Nreact = length(rxn.reactantinds)
        Nprod = length(rxn.productinds)

        if Nreact == 1
            rind1 = rxn.reactantinds[1]
            fderiv = cs[rind1]
        elseif Nreact == 2
            rind1,rind2 = rxn.reactantinds
            fderiv = cs[rind1]*cs[rind2]
        else
            rind1,rind2,rind3 = rxn.reactantinds
            fderiv = cs[rind1]*cs[rind2]*cs[rind3]
        end

        if Nprod == 1
            pind1 = rxn.productinds[1]
            rderiv = krevs[j]/kfs[j]*cs[pind1]
        elseif Nprod == 2
            pind1,pind2 = rxn.productinds
            rderiv = krevs[j]/kfs[j]*cs[pind1]*cs[pind2]
        else
            pind1,pind2,pind3 = rxn.productinds
            rderiv = krevs[j]/kfs[j]*cs[pind1]*cs[pind2]*cs[pind3]
        end

        flux = fderiv-rderiv
        gderiv = rderiv*kfs[j]*RTinv

        deriv = zeros(Nspcs)

        deriv[rind1] += gderiv
        if Nreact > 1
            deriv[rind2] += gderiv
            if Nreact > 2
                deriv[rind3] == gderiv
            end
        end

        deriv[pind1] -= gderiv
        if Nprod > 1
            deriv[pind2] -= gderiv
            if Nprod > 2
                deriv[pind3] -= gderiv
            end
        end

        ratederiv[rind1,j] -= flux
        ratederiv[rind1,Nrxns+1:Nrxns+Nspcs] .-= deriv
        if Nreact > 1
            ratederiv[rind2,j] -= flux
            ratederiv[rind2,Nrxns+1:Nrxns+Nspcs] .-= deriv
            if Nreact > 2
                ratederiv[rind3,j] -= flux
                ratederiv[rind3,Nrxns+1:Nrxns+Nspcs] .-= deriv
            end
        end

        ratederiv[pind1,j] += flux
        ratederiv[pind1,Nrxns+1:Nrxns+Nspcs] .+= deriv
        if Nprod > 1
            ratederiv[pind2,j] += flux
            ratederiv[pind2,Nrxns+1:Nrxns+Nspcs] .+= deriv
            if Nprod > 2
                ratederiv[pind3,j] += flux
                ratederiv[pind3,Nrxns+1:Nrxns+Nspcs] .+= deriv
            end
        end
    end
    ratederiv *= V
    #Temperature stuff
    @views ratederiv[end,:] += (Us'*ratederiv[1:end-1,:])[1,:]
    ratederiv[end,1:Nspcs] += wV
    ratederiv[end,:] /= (N*Cvave)
    return ratederiv
end
export ratederivative


module HistokatControllerImageLoader
export getImageObject, getTile
using PyCall

using DeformableRegistration.ImageProcessing: createImage
@pyimport histokat
# if HistokatController does not load:
# cp -r HistokatController /usr/local/anaconda3/lib/python3.6/site-packages/histokat/backend/lib
# cd /usr/local/anaconda3/lib/python3.6/site-packages/histokat/backend/lib
# install_name_tool -add_rpath "@loader_path/HistokatController/lib" libHistokatController.dylib

    tile_size = [1000, 1000]

    function getImageObject(filename::String)
        return histokat.controller[:open_session](filename)[:get_image]()
    end

    function getVoxelSize(image; level = 0)
        x = image[:get_voxel_size](level)[:x]/1000000
        y = image[:get_voxel_size](level)[:y]/1000000
        return (x,y)
    end
    function getExtent(image; level = 0)
        x = image[:get_extent](level)[:x]/1000000
        y = image[:get_extent](level)[:y]/1000000
        return (x,y)
    end

    function getTile(image, level, x, y, z)
        # get correct level and position
        max_level = image[:get_num_levels]() - 1
        level = max_level - level
        x = tile_size[1] * x
        y = tile_size[2] * y
        region = [[x, y, z], [x + tile_size[2] - 1, y + tile_size[2] - 1, z]]
        # load tile as numpy array
        tile = image[:open_region](level, region)
        # normalize and convert to uint8 image
        tile = tile .* (255.0/image[:get_value_range]()[:max])
        #tile = Image.fromarray(tile.astype(np.uint8))
        tile = sum(Array(tile)[1,:,:,:],3)[:,:,1]/3
        vx = getVoxelSize(image,level=level)
        tileIm = createImage(tile, voxelsize = [vx[1],vx[2]], shift = [tile_size[1] * x * vx[1], tile_size[2] * y * vx[2]])
        return tileIm
    end

end


using AdaGram

type Context
    position::Int
    words::Array{String}
end

type Sense
    sense::String
    weight::Int
    has_utf::Bool
    contexts::Array{Context}
end

type Word
    plain_word::String
    word::String
    weight::Int
    number_of_senses::Int
    senses::Array{Sense}
end

const MIN_WORD_WEIGHT = 500
const MAX_NUMBER_OF_CONTEXTS = 500

function read_and_process_dataset(dataset_number, vm, dict, stopwords, CONTEXT_SEMIWIDTH, result_filename)
    aris = Float64[]
    f = open("datasets/dataset-$dataset_number", "r")
    while !eof(f)
        tokens = split(strip(readline(f)), '\t')
        plain_word = tokens[1]
        word = lowercase(tokens[2])
        word_weight = int32(tokens[3])
        number_of_senses = int32(tokens[4])
        senses = Array(Sense, number_of_senses)
        for i in 1:number_of_senses
            if eof(f) break end

            tokens = split(strip(readline(f)), '\t')
            has_utf = endof(tokens[1]) != length(tokens[1])
            sense = remove_utf_characters(lowercase(tokens[1]))
            sense_weight = int32(tokens[2])
            number_of_contexts = int32(tokens[3])
            contexts = Array(Context, number_of_contexts)

            for j in 1:number_of_contexts
                position = int32(readline(f)) + 1
                words = split(lowercase(strip(readline(f))), " ")
                if length(words) == 0
                    contexts[j] = Context(position, words)
                end
                left_context = reverse(words[1:min(position,length(words))])
                filter!(w -> !isempty(w) && w in keys(dict.word2id) && !(w in stopwords), left_context)
                left_bound = min(length(left_context), CONTEXT_SEMIWIDTH)
                if position < length(words)
                    right_context = words[(position+1):end]
                    filter!(w -> !isempty(w) && w in keys(dict.word2id) && !(w in stopwords), right_context)
                    right_bound = min(length(right_context), CONTEXT_SEMIWIDTH)
                    words = vcat(left_context[1:left_bound], right_context[1:right_bound])
                else
                    words = left_context[1:left_bound]
                end
                contexts[j] = Context(position, words)
            end

            senses[i] = Sense(sense, sense_weight, has_utf, contexts)
        end
        if endof(word) != length(word) || !(word in keys(dict.word2id))
            # skip non-ASCII words
            continue
        end
        a = process_one_word(Word(plain_word, word, word_weight, number_of_senses, senses), vm, dict)
        if a > -100
            push!(aris, a)
        end
    end
    @printf("%f\n", mean(aris))
    close(f)
end

function remove_utf_characters(s::String)
    if endof(s) == length(s) return s end
    r = Char[]
    for i in 1:endof(s)
        if nextind(s, i) == i + 1 && prevind(s, i + 1) == i
            push!(r, s[i])
        end
    end
    return join(r, "")
end

function get_number_of_senses(vm, dict, word)
    z = Array(Float64,T(vm))
    N = expected_pi!(z, vm, dict.word2id[word])
    return N
end

function get_senses_mask(vm, dict, word)
    z = Array(Float64,T(vm))
    expected_pi!(z, vm, dict.word2id[word])
    return z .> 0
end

function disambiguate_senses(vm, dict, word::Word, N)
    clustering_matrix = zeros(word.number_of_senses, T(vm))
    mask = get_senses_mask(vm, dict, word.word)
    for i in 1:word.number_of_senses
        for j in 1:min(length(word.senses[i].contexts), MAX_NUMBER_OF_CONTEXTS)
            p = disambiguate(vm, dict, word.word, word.senses[i].contexts[j].words)
            clustering_matrix[i, indmax(p)] += 1
        end
    end
    return clustering_matrix[:,mask]
end

function compute_ARI(x)
    a = sum(x, 2)
    b = sum(x, 1)
    index = sum(x.*(x-1)*0.5)
    a1 = sum(a.*(a-1)*0.5)
    b1 = sum(b.*(b-1)*0.5)
    n = sum(x)
    expected_index = a1*b1/(n*(n-1)*0.5)
    max_index = (a1+b1)/2
    return (index - expected_index) / (max_index - expected_index)
end

function process_one_word(word, vm, dict)
    if word.weight >= MIN_WORD_WEIGHT
        N = get_number_of_senses(vm, dict, word.word)
        if N > 1
            clustering_matrix = disambiguate_senses(vm, dict, word, N)
            number_of_all_contexts = sum(clustering_matrix)
            ARI = compute_ARI(clustering_matrix)

#            @printf(f_verbose, "%s\t%d\t%d\n", word.word, word.number_of_senses, N)
#            for i in 1:word.number_of_senses
#                @printf(f_verbose, "%s\n", word.senses[i].sense)
#            end
#            for i in 1:word.number_of_senses
#                for j in 1:N
#                    @printf(f_verbose, "%d ", clustering_matrix[i, j])
#                end
#                @printf(f_verbose, "\n")
#            end
        else
            # hack for faster processing
            ARI = 0
            number_of_all_contexts = 0
            for i in 1:word.number_of_senses
                number_of_all_contexts += length(word.senses[i].contexts)
            end
        end
#        @printf(f, "%s\t%d\t%d\t%d\t%f\n", word.word, N, word.number_of_senses, number_of_all_contexts, ARI)
#        flush(f)
#        flush(f_verbose)
        return ARI
    end
    return -100
end

using ArgParse

s = ArgParseSettings()

@add_arg_table s begin
  "model"
    help = "path to serialized model"
    arg_type = String
    required = true
  "result-filename"
    help = "where to save result"
    arg_type = String
    required = true
  "--window"
    help = "window size"
    arg_type = Int
    default = 5
end

args = parse_args(ARGS, s)

vm, dict = load_model(args["model"])
#println("read stopwords")
stopwords = readdlm("benchmark/english.txt")
#println("process dataset")
read_and_process_dataset(14, vm, dict, stopwords, args["window"], args["result-filename"])


export LSForcingModel, NoLSForcing, HadGEMVertical

abstract type LSForcingModel end

vars_state(::LSForcingModel, ::AbstractStateType, FT) = @vars()

function compute_gradient_argument!(
    lsforcing::LSForcingModel,
    transform::Vars,
    state::Vars,
    aux::Vars,
    t::Real,
)
    nothing
end

function compute_gradient_flux!(
    lsforcing::LSForcingModel,
    diffusive::Vars,
    ∇transform::Grad,
    state::Vars,
    aux::Vars,
    t::Real,
)
    nothing
end

"""
    NoLSForcing <: LSForcingModel
No large-scale forcing
"""
struct NoLSForcing <: LSForcingModel end

"""
    Container for GCM variables from HadGEM2-A forcing,
used in the AMIP experiments
"""
struct HadGEMVertical <: LSForcingModel end

vars_state(m::HadGEMVertical, ::Auxiliary, FT) = @vars(
    ta::FT,
    hus::FT,
    ua::FT,
    va::FT,
    tntΣhava::FT,
    Σtemp_tendency::FT,
    Σqt_tendency::FT,
    w_s::FT,
)

vars_state(::HadGEMVertical, ::Gradient, FT) = @vars(ta::FT, hus::FT)
vars_state(::HadGEMVertical, ::GradientFlux, FT) = @vars(∇ᵥta::FT, ∇ᵥhus::FT)

function compute_gradient_argument!(
    lsforcing::HadGEMVertical,
    transform::Vars,
    state::Vars,
    aux::Vars,
    t::Real,
)
    transform.lsforcing.ta = aux.lsforcing.ta
    transform.lsforcing.hus = aux.lsforcing.hus
end

function compute_gradient_flux!(
    lsforcing::HadGEMVertical,
    diffusive::Vars,
    ∇transform::Grad,
    state::Vars,
    aux::Vars,
    t::Real,
)
    diffusive.lsforcing.∇ᵥta = ∇transform.lsforcing.ta[3]
    diffusive.lsforcing.∇ᵥhus = ∇transform.lsforcing.hus[3]
end


#####
##### AlphaZero.jl
##### Jonathan Laurent, Carnegie Mellon University (2019-2020)
#####

module AlphaZero

# Submodules
export MCTS, MinMax, GameInterface, GI, Report, Benchmark
export Network, KnetLib, FluxLib, NetLib
# AlphaZero parameters
export Params, SelfPlayParams, LearningParams, ArenaParams
export MctsParams, MemAnalysisParams
export SamplesWeighingPolicy, CONSTANT_WEIGHT, LOG_WEIGHT, LINEAR_WEIGHT
export AbstractSchedule, ConstSchedule, PLSchedule, StepSchedule
# Players and games
export AbstractGame, AbstractPlayer, TwoPlayers, Trace
export think, select_move, reset_player!, player_temperature, apply_temperature
export play_game, interactive!, total_reward
export MctsPlayer, RandomPlayer, EpsilonGreedyPlayer, NetworkPlayer, Human
export ColorPolicy, ALTERNATE_COLORS, BASELINE_WHITE, CONTENDER_WHITE
# Networks
export AbstractNetwork, OptimiserSpec, Nesterov, CyclicNesterov, Adam
# Training environments
export Env, train!, get_experience
# User interface
export UserInterface
export Session, resume!, save, play_interactive_game
export Explorer, start_explorer

include("util.jl")
import .Util
using .Util: Option, apply_temperature

include("game.jl")
using .GameInterface
const GI = GameInterface

include("mcts.jl")
import .MCTS

include("networks/network.jl")
using .Network

include("batchifier.jl")
import .Batchifier

using Formatting
using Base: @kwdef
using DataStructures: CircularBuffer
using Distributions: Categorical, Dirichlet
using Statistics: mean

import Distributed

include("schedule.jl")
include("params.jl")
include("report.jl")
include("trace.jl")
include("memory.jl")
include("learning.jl")
include("play.jl")
include("training.jl")

include("minmax.jl")
import .MinMax

include("benchmark.jl")
using .Benchmark

include("networks/knet.jl")
# import .KnetLib

include("networks/flux.jl")
# import .FluxLib

# The default user interface is included here for convenience but it could be
# replaced or separated from the main AlphaZero.jl package (which would also
# enable dropping some dependencies such as Crayons or JSON3).

include("ui/ui.jl")
using .UserInterface

# Choose the default DL framework based on an environment variable
function __init__()
  @eval begin
    const DEFAULT_DL_FRAMEWORK = get(ENV, "ALPHAZERO_DEFAULT_DL_FRAMEWORK", "FLUX")
    const NetLib =
      if DEFAULT_DL_FRAMEWORK == "FLUX"
        @info "Using Flux."
        FluxLib
      elseif DEFAULT_DL_FRAMEWORK == "KNET"
        @info "Using Knet."
        KnetLib
      else
        error("Unknown DL framework: $(DEFAULT_DL_FRAMEWORK)")
      end
  end
end

end


using BenchmarkTools: leaves
using PkgBenchmark: benchmarkgroup
using TransducersBenchmarksReports: loadresult
using DataFrames
using Statistics
using VegaLite

fanout(; kwargs...) =
    x -> NamedTuple{keys(kwargs)}(Ref(x) .|> Tuple(values(kwargs)))

macro fanout(names...)
    kwargs = [:($n=$n) for n in names]
    esc(:($fanout(; $(kwargs...))))
end

function trialasrow((key, trial),)
    # stats = @fanout(mean, median, var, minimum, maximum)(trial.times)
    stats = @fanout(mean, median, minimum)(trial.times)
    if length(key) == 3
        impl, simd, n = key
    else
        impl, n = key
        simd = "none"
    end
    return (impl=impl, simd=simd, n=n, stats...)
end

group = benchmarkgroup(loadresult())["gemm"]
data = trialasrow.(leaves(group["mul"]))

df = melt(DataFrame(data), [:impl, :simd, :n])

dfnorm = join(
    df[df[:impl] .!= "linalg", :],
    deletecols!(df[df[:impl] .== "linalg", :], [:impl, :simd]),
    on = [:n, :variable],
    kind = :left,
    makeunique = true,
)
dfnorm[:normalized] = dfnorm[:value] ./ dfnorm[:value_1]
dfnorm[:fullname] = string.(dfnorm[:impl], ", ", dfnorm[:simd])

plt = dfnorm |> @vlplot(
    :text,
    x = {
        field = :n,
        typ = :quantitative,
        scale = {typ = :log},
    },
    y = {
        field = :normalized,
        typ = :quantitative,
        scale = {typ = :log},
    },
    column = :variable,
    # color = :fullname,
    color = :simd,
    text = :impl,
    resolve = {
        scale = {y = :independent},
    }
)


function read_gms_file(filename::AbstractString)

    println("Reading $(filename) ...")

    filepath = joinpath(Pkg.dir("toJuMP"),".gms","")
    filepath = string(filepath,filename,".gms")

    if isfile(filepath)
        f = open(filepath, "r")
    elseif isfile(filename)
        f = open(filename, "r")
    else
        error("No gms file detected.")
    end

    #=
        ----------------------- Line Header Space ------------------
            Example :: 4stufen.gms
        Unkown ::
        $offlisting
        $if set nostart $goto modeldef
        $label modeldef
        $if not set MINLP $set MINLP MINLP  Model Type

        *                 -> comment
        Null              -> emptyline
        Variables         -> All Variable List, separated by ","
        Positive Variable -> Non-negative variables list, separated by "," (assert)
        Binary Variable   -> Binary variables list, separated by "," (assert)
        Equations         -> Equations symbols, separated by ","
        EquSymbols..      -> Equation content
        VarSymbols.lo     -> Lower Bound
        VarSymbols.l      -> Upper Bound ??
        Model m / all /   -> ??
        m.limrow=0
        m.
        ------------------------------------------------------------
    =#

    # Null Trackers
    skip = 0
    comment = 0

    # Main data structure initialization
    gms = oneProblem()

    # -------------------------- Start Reading -------------------------- #
    for l in eachline(f)
        sl = lstrip(l, ' ')
        if isempty(sl) || length(sl) == 1  # Skip empty line
            skip += 1
        elseif sl[1] == "*"
            comment += 1
        else
            slt = split(sl, r" |,|\.")
            slt = [slt[i] for i in 1:length(slt) if length(slt[i]) > 0]
            if slt[1] in GMS_BLOCK_HEADER
                read_block(f, gms, l)           # Reading a general block
            elseif slt[1] in gms.rows #ismatch(r"e\d", sl[1])
                read_equation(f, gms, l)        # Reading an equation block
            elseif slt[1] in gms.cols #ismatch(r"\w\.\w", sl[1])
                read_bounds(f, gms, l)          # Reading a bound block
            elseif slt[1] == "\$"
                read_command(f, gms, l)         # Reading a logical command line
            elseif slt[1] == "Model"
                read_model(f, gms, sl)           # Reading a model command
            elseif slt[1] == "Solve"
                read_solve(f, gms, sl)           # Reading a solve command line
            else
                # println("[Unkown]DUMPING >> ", l)
            end
        end

    end
    close(f)
    return gms
end

function read_block(file::IOStream, gms::oneProblem, lInit::AbstractString; kwargs...)

    sl = split(get_one_line(file, one_line=lInit), r" |,")
    sl = [sl[i] for i in 1:length(sl) if !isempty(sl[i])]  # Eliminate empty entries

    blockIdentifier = sl[1]  # This should always be true

    for i in 1:length(sl)
        if blockIdentifier == "Variables" && (i > 1)
            push!(gms.cols, sl[i])
        elseif blockIdentifier == "Positive" && (i > 2)
            gms.colsType[sl[i]] = "Positive"
        elseif blockIdentifier == "Negative" && (i > 2)
            gms.colsType[sl[i]] = "Negative"
        elseif blockIdentifier == "Binary" && (i > 2)
            gms.colsType[sl[i]] = "Binary"
        elseif blockIdentifier == "Semicont"
            error("Currently don't support semi continous variables parsing.")
        elseif blockIdentifier == "Integer" && (i > 2)
            gms.colsType[sl[i]] = "Integer"
        elseif blockIdentifier == "Equations" && (i > 1)
            push!(gms.rows, sl[i])
        end
    end

    return
end

function read_equation(file::IOStream, gms::oneProblem, lInit::AbstractString; kwargs...)

    #=
        Example Equation
        #Single Line
        e1..    objvar - x145 - x146 - x147 - x148 - x149 - x150 =E= 3271.22725820856;
        # Multi Line
        e38..    109.0495*x1 + 100.462*x2 + 115.2937*x3 + 2.860271*x4 + 15.1404*x5
               + x44 - x556 =E= 101.1304;
    =#

    lhs = ""
    rhs = ""
    sense = ""
	eN = string(strip(split(lInit, r" |,")[1], ['.', '\r', '\n']))
    eS = split(eN, '.')
    if eN in gms.rows
        one_l = get_one_line(file, one_line=lInit)
        one_l = replace(one_l, "$(eN)..", "")
        if contains(one_l, "=E=")
            gms.rowsSense[eN] = "E"
        elseif contains(one_l, "=L=")
            gms.rowsSense[eN] = "L"
        elseif contains(one_l, "=G=")
            gms.rowsSense[eN] = "G"
		elseif contains(one_l, "=N=")
			gms.rowsSense[eN] = "N"
        else
            error("NO sense detected in equation. $(one_l)")
        end
        sl = split(one_l, "=$(gms.rowsSense[eN])=", keep=false)
        gms.rowsLHS[eN] = replace(sl[1], " ", "")
        gms.rowsRHS[eN] = Float64(parse(sl[2]))
    elseif length(eS) == 2 && eS[2] == "m"
        @assert eS[1] in gms.rows
        one_l = get_one_line(file, one_line=lInit)
        mVal = parse(split(one_l, "=", keep=false)[2])
        gms.m[eS[1]] = mVal
    else
        error("Non-indexed constraint detected. \n$(lInit)")
    end

    return
end

function read_bounds(file::IOStream, gms::oneProblem, lInit::AbstractString; kwargs...)

    #=
        .lo lower bound
        .l level or primal value
        .up upper bound
        .m marginal or dual value
    =#
	@assert rstrip(strip(lInit, ['\n','\r']), ' ')[end] == ';' # Assumption :: alwasy one line
    all_segs = split(lInit, ';', keep=false)
    all_segs = [i for i in all_segs if !isempty(strip(i,' '))]
    for lseg in all_segs
        lseg = strip(lseg, [';', '\n', ' ', '\r'])
        sl = split(lseg, "=")
        sl = [sl[i] for i in 1:length(sl) if !isempty(sl[i])]
        # There shouldn't be any space to begin with
        sl[1] = strip(sl[1], [';', '\n', ' ', '\r'])
		sl[2] = strip(sl[2], [';', '\n', ' ', '\r'])
        @assert ismatch(r"\w.\w", sl[1])
        boundVal = Float64(parse(sl[end]))
        var = split(sl[1],".")[1]
        attr = split(sl[1],".")[2]

        if attr == "lo"
            gms.lb[var] = boundVal
        elseif attr == "l"
            gms.l[var] = boundVal
        elseif attr == "fx"
            gms.fx[var] = boundVal
        elseif attr == "up"
            gms.ub[var] = boundVal
        elseif attr == "m"
            gms.m[var] = boundVal
        elseif attr == "scale"
            gms.scale[var] = boundVal
        elseif attr == "prior"
            gms.prior[var] = boundVal
        elseif attr == "stage"
            gms.prior[var] = boundVal
        else
            error("Unknown variable attribute. Consult gams hand-book for definition and report this issue.")
        end
    end

    return
end

function read_model(file::IOStream, gms::oneProblem, lInit::AbstractString; kwargs...)

    # Assume this will always be one line
	@assert strip(lInit, ['\n','\r'])[end] == ';'
    sl = split(strip(lInit,[';','\n','\r']), r" |,")
    @assert sl[1] == "Model"  # Already did outside
    # Not sure if the printlnrmation stored here is useful or not.
    for i in 1:length(sl)
        if sl[i] == "Model"
            gms.modelSymbol = sl[i+1]
        end
    end

    return
end

function read_solve(file::IOStream, gms::oneProblem, lInit::AbstractString; kwargs...)

	@assert strip(lInit, ['\n','\r'])[end] == ';'

    sl = split(strip(lInit, [';','\n','\r']), r" |,")
    @assert sl[1] == "Solve"

    for i in 1:length(sl)
        if sl[i] in PROBTYPE
            gms.modelType = sl[i]
        elseif sl[i] == "maximizing"
            gms.objSense = sl[i]
        elseif sl[i] == "minimizing"
            gms.objSense = sl[i]
        elseif sl[i] in gms.cols
            gms.objective = sl[i]
        end
    end
end

function gms2jump(gmsPath::AbstractString;
                    mode::AbstractString="index",
                    ending="m=m",
                    quadNL::Bool=false,
                    loopifpossible::Bool=true,
                    outdir::AbstractString="")

    probName = replace(splitdir(gmsPath)[end],".gms", "")
    gms = read_gms_file(gmsPath)
    write_julia_script(probName, gms, mode, loopifpossible=loopifpossible, quadNL=quadNL, outdir=outdir)

    return
end


module TransportUndirectedModule

import ..GridWorlds as GW
import Random
import ReinforcementLearningBase as RLBase

#####
##### game logic
#####

const NUM_OBJECTS = 4
const AGENT = 1
const WALL = 2
const GEM = 3
const TARGET = 4
const NUM_ACTIONS = 6

mutable struct TransportUndirected{R, RNG} <: GW.AbstractGridWorld
    tile_map::BitArray{3}
    agent_position::CartesianIndex{2}
    reward::R
    rng::RNG
    done::Bool
    terminal_reward::R
    gem_position::CartesianIndex{2}
    target_position::CartesianIndex{2}
    has_gem::Bool
end

function TransportUndirected(; R = Float32, height = 8, width = 8, rng = Random.GLOBAL_RNG)
    tile_map = falses(NUM_OBJECTS, height, width)

    tile_map[WALL, 1, :] .= true
    tile_map[WALL, height, :] .= true
    tile_map[WALL, :, 1] .= true
    tile_map[WALL, :, width] .= true

    agent_position = GW.sample_empty_position(rng, tile_map)
    tile_map[AGENT, agent_position] = true

    gem_position = GW.sample_empty_position(rng, tile_map)
    tile_map[GEM, gem_position] = true

    target_position = GW.sample_empty_position(rng, tile_map)
    tile_map[TARGET, target_position] = true

    reward = zero(R)
    done = false
    terminal_reward = one(R)
    has_gem = false

    env = TransportUndirected(tile_map, agent_position, reward, rng, done, terminal_reward, gem_position, target_position, has_gem)

    GW.reset!(env)

    return env
end

function GW.reset!(env::TransportUndirected)
    tile_map = env.tile_map
    rng = env.rng

    tile_map[AGENT, env.agent_position] = false
    tile_map[GEM, env.gem_position] = false
    tile_map[TARGET, env.target_position] = false

    new_agent_position = GW.sample_empty_position(rng, tile_map)
    env.agent_position = new_agent_position
    tile_map[AGENT, new_agent_position] = true

    new_gem_position = GW.sample_empty_position(rng, tile_map)
    env.gem_position = new_gem_position
    tile_map[GEM, new_gem_position] = true

    new_target_position = GW.sample_empty_position(rng, tile_map)
    env.target_position = new_target_position
    tile_map[TARGET, new_target_position] = true

    env.reward = zero(env.reward)
    env.done = false
    env.has_gem = false

    return nothing
end

function GW.act!(env::TransportUndirected, action)
    @assert action in Base.OneTo(NUM_ACTIONS) "Invalid action $(action). Action must be in Base.OneTo($(NUM_ACTIONS))"

    tile_map = env.tile_map
    agent_position = env.agent_position

    if action in Base.OneTo(4)
        if action == 1
            new_agent_position = GW.move_up(agent_position)
        elseif action == 2
            new_agent_position = GW.move_down(agent_position)
        elseif action == 3
            new_agent_position = GW.move_left(agent_position)
        else
            new_agent_position = GW.move_right(agent_position)
        end

        if !tile_map[WALL, new_agent_position]
            tile_map[AGENT, agent_position] = false
            env.agent_position = new_agent_position
            tile_map[AGENT, new_agent_position] = true
        end
    elseif action == 5
        if tile_map[GEM, agent_position]
            tile_map[GEM, agent_position] = false
            env.has_gem = true
        end
    else
        if env.has_gem
            env.has_gem = false
            env.gem_position = agent_position
            tile_map[GEM, agent_position] = true
        end
    end

    if tile_map[GEM, env.target_position]
        env.reward = env.terminal_reward
        env.done = true
    else
        env.reward = zero(env.reward)
        env.done = false
    end

    return nothing
end

#####
##### miscellaneous
#####

GW.get_height(env::TransportUndirected) = size(env.tile_map, 2)
GW.get_width(env::TransportUndirected) = size(env.tile_map, 3)

GW.get_action_names(env::TransportUndirected) = (:MOVE_UP, :MOVE_DOWN, :MOVE_LEFT, :MOVE_RIGHT, :PICK_UP, :DROP)
GW.get_object_names(env::TransportUndirected) = (:AGENT, :WALL, :GEM, :TARGET)

function GW.get_pretty_tile_map(env::TransportUndirected, position::CartesianIndex{2})
    characters = ('☻', '█', '♦', '✖', '⋅')

    object = findfirst(@view env.tile_map[:, position])
    if isnothing(object)
        return characters[end]
    else
        return characters[object]
    end
end

function GW.get_pretty_sub_tile_map(env::TransportUndirected, window_size, position::CartesianIndex{2})
    tile_map = env.tile_map
    agent_position = env.agent_position

    characters = ('☻', '█', '♦', '✖', '⋅')

    sub_tile_map = GW.get_sub_tile_map(tile_map, agent_position, window_size)

    object = findfirst(@view sub_tile_map[:, position])
    if isnothing(object)
        return characters[end]
    else
        return characters[object]
    end
end

function Base.show(io::IO, ::MIME"text/plain", env::TransportUndirected)
    str = "tile_map:\n"
    str = str * GW.get_pretty_tile_map(env)
    str = str * "\nsub_tile_map:\n"
    str = str * GW.get_pretty_sub_tile_map(env, GW.get_window_size(env))
    str = str * "\nreward: $(env.reward)"
    str = str * "\ndone: $(env.done)"
    str = str * "\nhas_gem: $(env.has_gem)"
    str = str * "\naction_names: $(GW.get_action_names(env))"
    str = str * "\nobject_names: $(GW.get_object_names(env))"
    print(io, str)
    return nothing
end

GW.get_action_keys(env::TransportUndirected) = ('w', 's', 'a', 'd', 'p', 'l')

#####
##### RLBase API
#####

RLBase.StateStyle(env::GW.RLBaseEnv{E}) where {E <: TransportUndirected} = RLBase.InternalState{Any}()
RLBase.state_space(env::GW.RLBaseEnv{E}, ::RLBase.InternalState) where {E <: TransportUndirected} = nothing
RLBase.state(env::GW.RLBaseEnv{E}, ::RLBase.InternalState) where {E <: TransportUndirected} = env.env.tile_map

RLBase.reset!(env::GW.RLBaseEnv{E}) where {E <: TransportUndirected} = GW.reset!(env.env)

RLBase.action_space(env::GW.RLBaseEnv{E}) where {E <: TransportUndirected} = Base.OneTo(NUM_ACTIONS)
(env::GW.RLBaseEnv{E})(action) where {E <: TransportUndirected} = GW.act!(env.env, action)

RLBase.reward(env::GW.RLBaseEnv{E}) where {E <: TransportUndirected} = env.env.reward
RLBase.is_terminated(env::GW.RLBaseEnv{E}) where {E <: TransportUndirected} = env.env.done

end # module


mutable struct Agent
    opinion::Float64
    inclin_interact::Float64
    perceiv_publ_opinion::Float64
    check_regularity::Float64
    active::Bool
    inactive_ticks::Int16
    feed::Array{Tweet, 1}
    liked_Tweets::Array{Tweet, 1}
    retweeted_Tweets::Array{Tweet, 1}
    function Agent(opinion, inclin_interact, check_regularity)
        # check if opinion value is valid
        if opinion < -1 || opinion > 1
            error("invalid opinion value")
        end
        # check if value for inclination to interact is valid
        if inclin_interact < 0
            error("invalid value for inclination to interact")
        end
        new(
            opinion,
            inclin_interact,
            opinion,
            check_regularity,
            true,
            0,
            Array{Tweet, 1}(undef, 0),
            Array{Tweet, 1}(undef, 0),
            Array{Tweet, 1}(undef, 0)
        )
    end
end