@testset "query synthesis" begin
    @testset "initialization" begin
        epsilon = fill(0.1, 2)
        data = ones(2, 10)
        labels = fill(:U, 10)
        history = Array{Float64, 2}[]

        @testset "RandomQss" begin
            @test_throws ArgumentError RandomQss(epsilon=-0.1)
            @test_throws ArgumentError RandomQss(epsilon=-ones(2))
            @test RandomQss(epsilon=1.0) !== nothing
            @test RandomQss(epsilon=ones(2)) !== nothing

        end

        @testset "RandomOutlierQss" begin
            occ = SVDD.RandomOCClassifier(data)
            @test_throws ArgumentError RandomOutlierQss(occ, max_tries=0)
            @test_throws ArgumentError RandomOutlierQss(occ, epsilon=-0.1)
            @test_throws ArgumentError RandomOutlierQss(occ, epsilon=-ones(2))
            @test RandomOutlierQss(occ, epsilon=1.0) !== nothing
            @test RandomOutlierQss(occ, epsilon=ones(2)) !== nothing
        end
    end

    @testset "get query" begin
        data = rand(2, 10)
        labels = fill(:U, size(data, 2))
        history = Vector{Array{Float64, 2}}()
        optimizer = ParticleSwarmOptimization()

        occ = SVDD.VanillaSVDD(data)
        init_strategy = SVDD.SimpleCombinedStrategy(SVDD.FixedGammaStrategy(GaussianKernel(2.0)), SVDD.FixedCStrategy(1))
        SVDD.initialize!(occ, init_strategy)
        SVDD.fit!(occ, TEST_SOLVER)
        for qs_type in [RandomQss, RandomOutlierQss, TestQss, DecisionBoundaryQss, NaiveExplorativeMarginQss]
            @testset "$qs_type" begin
                qs = initialize_qs(qs_type, occ, data, Dict(:optimizer => optimizer))
                query = get_query_objects(qs, data, labels, history)
                @test size(query) == (size(data, 1), 1)
                if qs_type == NaiveExplorativeMarginQss
                    labels[end] = :Lout
                    query = get_query_objects(qs, data, labels, history)
                    @test size(query) == (size(data, 1), 1)
                end
            end
        end
        @testset "ExplorativeMarginQss" begin
            qs = initialize_qs(ExplorativeMarginQss, occ, data,
                               Dict(:solver => TEST_SOLVER,
                                    :optimizer => optimizer))
            query = get_query_objects(qs, data, labels, history)
            @test size(query) == (size(data, 1), 1)
            labels[end] = :Lout
            query = get_query_objects(qs, data, labels, history)
            @test size(query) == (size(data, 1), 1)
        end
    end
end


@testset "callbacks" begin
    unicodeplots()

    enc = ConditionalMvNormal(Dense(5,2))
    dec = ConditionalMvNormal(Dense(2,5))
    model = VAE(2, enc, dec)
    loss(x) = elbo(model, x)
    opt = ADAM()

    # setup progress printing
    niter = 10
    test_data = rand(Float32, 5, 10)
    prog = Progress(niter)
    curr_iter = zeros(Int)

    callbacks = [
      GMExtensions.progress_callback(prog, curr_iter, model, test_data, loss),
      () -> (sleep(0.2))
    ]

    for ii in 1:niter
        curr_iter .+= 1
        test_data .= rand(Float32, 5, 10)
        for cb in callbacks cb() end
    end

    @test curr_iter[1] == niter
end


module EOS

using AbInitioSoftwareBase: load
using Comonicon: @cast
using EquationOfStateRecipes
using EquationsOfStateOfSolids: Parameters, EquationOfStateOfSolids, EnergyEquation
using Express.EquationOfStateWorkflow: Scf, VcOptim, FitEos, buildjob
using Express.EquationOfStateWorkflow.Recipes: buildworkflow
using QuantumESPRESSOExpress
using Serialization: deserialize
using SimpleWorkflows: run!, getresult
import Plots
# using ..ExpressCommands: @load_plugin

"""
Fit an equation of state from `cfgfile` for calculation `calc`.

# Arguments
- `calc`: the calculation type. Acceptable options are "scf" for self-consistent calculation and "optim" for structure optimizations.
- `cfgfile`: the file to be printed. Available extensions are `.jls`, `.json`, `.yaml`, `.yml` or `.toml`.
"""
@cast function fit(calc, cfgfile)
    calc = lowercase(calc)
    if calc == "scf"
        T = Scf
    elseif calc == "optim"
        T = VcOptim
    else
        throw(ArgumentError("unrecognized calculation type `$calc`!"))
    end
    job = buildjob(FitEos{T}(), cfgfile)
    run!(job)
    wait(job)
    display(something(getresult(job)))
end

"""
Plot an equation of state from `file` to file "out".

# Arguments
- `file`: a `.jls` file that saves the equation of state.
"""
@cast function plot(file)
    data = deserialize(file)
    for (key, value) in data
        if value isa Parameters
            eos = EnergyEquation(value)
        elseif data isa EquationOfStateOfSolids
            eos = value
        else
            error("unsupported type $(typeof(value))!")
        end
        Plots.plot(eos)
        Plots.savefig("$key.pdf")
    end
end

"""
Run a `config` file, better with absolute path.

# Arguments
- `config`: the file to be run. Available extensions are `.json`, `.yaml`, `.yml` or `.toml`.
"""
@cast function run(config)
    # @load_plugin
    workflow = buildworkflow(config)
    dict = load(config)
    run!(workflow; saveas = dict["save"]["status"])
    return workflow
end

end


# Based off of code from julia SparseArrays.jl
struct GeneralSparseMatrixCSC{Tv, Ti<:Integer, VTv<:AbstractVector{Tv}, VTi<:AbstractVector{Ti}} <: SparseArrays.AbstractSparseMatrixCSC{Tv, Ti}
    m::Int       # Number of rows
    n::Int       # Number of columns
    colptr::VTi  # Column i is in colptr[i]:(colptr[i+1]-1)
    rowval::VTi  # Row indices of stored values
    nzval::VTv   # Stored values, typically nonzeros

    function GeneralSparseMatrixCSC{Tv, Ti, VTv, VTi}(m::Integer, n::Integer,
                                                      colptr::AbstractVector{Ti},
                                                      rowval::AbstractVector{Ti},
                                                      nzval::AbstractVector{Tv}) where {Tv, Ti<:Integer,
                                                                                VTv<:AbstractVector{Tv},
                                                                                VTi<:AbstractVector{Ti}}
        new(Int(m), Int(n), colptr, rowval, nzval)
    end
end

function GeneralSparseMatrixCSC(m::Integer, n::Integer, colptr::AbstractVector,
                                rowval::AbstractVector, nzval::AbstractVector)
    Tv = eltype(nzval)
    VTv = typeof(nzval)
    Ti = promote_type(eltype(colptr), eltype(rowval))
    VTi = promote_type(typeof(colptr), typeof(rowval))

    GeneralSparseMatrixCSC{Tv, Ti, VTv, VTi}(m, n, colptr, rowval, nzval)
end

SparseArrays.getcolptr(S::GeneralSparseMatrixCSC) = getfield(S, :colptr)
SparseArrays.rowvals(S::GeneralSparseMatrixCSC) = getfield(S, :rowval)
SparseArrays.nonzeros(S::GeneralSparseMatrixCSC) = getfield(S, :nzval)
Base.size(S::GeneralSparseMatrixCSC) = (S.m, S.n)

function GeneralSparseMatrixCSC(S::SparseArrays.AbstractSparseMatrixCSC)
    m, n = size(S)
    colptr = SparseArrays.getcolptr(S)
    rowval = rowvals(S)
    nzval = nonzeros(S)

    return GeneralSparseMatrixCSC(m, n, colptr, rowval, nzval)
end

function Adapt.adapt_structure(to, S::GeneralSparseMatrixCSC)
    m, n = size(S)
    colptr = adapt(to, SparseArrays.getcolptr(S))
    rowval = adapt(to, rowvals(S))
    nzval = adapt(to, nonzeros(S))

    return GeneralSparseMatrixCSC(m, n, colptr, rowval, nzval)
end


# Copyright (c) 2017: Miles Lubin and contributors
# Copyright (c) 2017: Google Inc.
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

module TestNonlinear

using Test
import MathOptInterface
import ForwardDiff
import LinearAlgebra

const MOI = MathOptInterface
const Nonlinear = MOI.Nonlinear

function runtests()
    for name in names(@__MODULE__; all = true)
        if startswith("$(name)", "test_")
            @testset "$(name)" begin
                getfield(@__MODULE__, name)()
            end
        end
    end
    return
end

_hessian(f, x) = LinearAlgebra.LowerTriangular(ForwardDiff.hessian(f, x))

function test_copy()
    model = Nonlinear.Model()
    @test_throws(
        ErrorException("Copying nonlinear problems not yet implemented"),
        copy(model),
    )
    return
end

function test_parse_unable()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    input = :(f($x))
    @test_throws(
        ErrorException("Unable to parse: $input"),
        Nonlinear.set_objective(model, input),
    )
    return
end

function test_parse_sin()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    Nonlinear.set_objective(model, :(sin($x)))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) == :(sin(x[$x]))
    return
end

function test_parse_sin_squared()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    Nonlinear.set_objective(model, :(sin($x)^2))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) == :(sin(x[$x])^2)
    return
end

function test_parse_ifelse()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    Nonlinear.set_objective(model, :(ifelse($x, 1, 2)))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) == :(ifelse(x[$x], 1, 2))
    return
end

function test_parse_ifelse_inequality_less()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    Nonlinear.set_objective(model, :(ifelse($x < 1, $x - 1, $x + 1)))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) ==
          :(ifelse(x[$x] < 1, x[$x] - 1, x[$x] + 1))
    return
end

function test_parse_ifelse_inequality_greater()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    Nonlinear.set_objective(model, :(ifelse($x > 1, $x - 1, $x + 1)))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) ==
          :(ifelse(x[$x] > 1, x[$x] - 1, x[$x] + 1))
    return
end

function test_parse_ifelse_comparison()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    Nonlinear.set_objective(model, :(ifelse(0 <= $x <= 1, $x - 1, $x + 1)))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) ==
          :(ifelse(0 <= x[$x] <= 1, x[$x] - 1, x[$x] + 1))
    return
end

function test_parse_ifelse_logic_inequality()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    expr = :(ifelse($x < 1.0 || $x > 2.0, $x - 1.0, $x + 1.0))
    Nonlinear.set_objective(model, expr)
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) ==
          :(ifelse(x[$x] < 1.0 || x[$x] > 2.0, x[$x] - 1.0, x[$x] + 1.0))
    return
end

function test_parse_splat_prod()
    model = Nonlinear.Model()
    x = MOI.VariableIndex.(1:3)
    Nonlinear.set_objective(model, :(*($x...)))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) ==
          :(x[$(x[1])] * x[$(x[2])] * x[$(x[3])])
    return
end

function test_parse_splat_top_level()
    model = Nonlinear.Model()
    x = MOI.VariableIndex.(1:3)
    @test_throws(
        ErrorException(
            "Unsupported use of the splatting operator. This is only " *
            "supported in the arguments of a function call.",
        ),
        Nonlinear.set_objective(model, :(x...)),
    )
    return
end

function test_parse_splat_expr()
    model = Nonlinear.Model()
    x = MOI.VariableIndex.(1:3)
    @test_throws(
        ErrorException(
            "Unsupported use of the splatting operator. JuMP supports " *
            "splatting only symbols. For example, `x...` is ok, but " *
            "`(x + 1)...`, `[x; y]...` and `g(f(y)...)` are not.",
        ),
        Nonlinear.set_objective(model, :(*((x ./ 2)...))),
    )
    return
end

function test_parse_univariate_prod()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    Nonlinear.set_objective(model, :(*($x)))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) == :(*(x[$x]))
    return
end

function test_parse_string()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    @test_throws(
        ErrorException(
            "Unexpected object abc of type String in nonlinear expression.",
        ),
        Nonlinear.set_objective(model, :($x + "abc")),
    )
    return
end

function test_parse_array()
    model = Nonlinear.Model()
    x = [MOI.VariableIndex(1)]
    c = [1.0]
    @test_throws(
        ErrorException(
            "Unexpected array $(c') in nonlinear expression. Nonlinear " *
            "expressions may contain only scalar expressions.",
        ),
        Nonlinear.set_objective(model, :($(c') * $x)),
    )
    return
end

function test_parse_unsupported_expression()
    model = Nonlinear.Model()
    x = (y = 1,)
    @test_throws(
        ErrorException("Unsupported expression: $(:(x.y))"),
        Nonlinear.set_objective(model, :(x.y)),
    )
    return
end

function test_moi_variable_parse()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    expr = Nonlinear.parse_expression(model, :($x))
    @test expr.nodes == [Nonlinear.Node(Nonlinear.NODE_MOI_VARIABLE, 1, -1)]
    @test isempty(expr.values)
    return
end

function test_expression_parse()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    ex = Nonlinear.add_expression(model, :(sin($x)^2))
    @test model[ex] isa Nonlinear.Expression
    return
end

function test_parameter_parse()
    model = Nonlinear.Model()
    p = Nonlinear.add_parameter(model, 1.2)
    expr = Nonlinear.parse_expression(model, :($p))
    @test expr.nodes == [Nonlinear.Node(Nonlinear.NODE_PARAMETER, 1, -1)]
    @test isempty(expr.values)
    @test model.parameters == [1.2]
    return
end

function test_parameter_set()
    model = Nonlinear.Model()
    p = Nonlinear.add_parameter(model, 1.2)
    @test model.parameters == [1.2]
    @test model[p] == 1.2
    model[p] = 2.1
    @test model.parameters == [2.1]
    @test model[p] == 2.1
    return
end

function test_set_objective()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    input = :($x^2 + 1)
    Nonlinear.set_objective(model, input)
    @test model.objective == Nonlinear.parse_expression(model, input)
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) == :(x[$x]^2 + 1)
    return
end

function test_set_objective_subexpression()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    input = :($x^2 + 1)
    expr = Nonlinear.add_expression(model, input)
    Nonlinear.set_objective(model, :($expr^2))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) == :((x[$x]^2 + 1)^2)
    return
end

function test_set_objective_nested_subexpression()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    input = :($x^2 + 1)
    expr = Nonlinear.add_expression(model, input)
    expr_2 = Nonlinear.add_expression(model, :($expr^2))
    Nonlinear.set_objective(model, :($expr_2^2))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) == :(((x[$x]^2 + 1)^2)^2)
    return
end

function test_set_objective_parameter()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    p = Nonlinear.add_parameter(model, 1.2)
    Nonlinear.set_objective(model, :($x^2 + $p))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.objective_expr(evaluator) == :(x[$x]^2 + 1.2)
    return
end

function test_add_constraint_less_than()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    func = :($x^2 + 1)
    set = MOI.LessThan(1.0)
    c = Nonlinear.add_constraint(model, func, set)
    @test model[c].set == set
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2 + 1 <= 1.0)
    return
end

function test_add_constraint_delete()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    c1 = Nonlinear.add_constraint(model, :($x^2 + 1), MOI.LessThan(1.0))
    _ = Nonlinear.add_constraint(model, :(sqrt($x)), MOI.LessThan(1.0))
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2 + 1 <= 1.0)
    @test MOI.constraint_expr(evaluator, 2) == :(sqrt(x[$x]) <= 1.0)
    Nonlinear.delete(model, c1)
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.constraint_expr(evaluator, 1) == :(sqrt(x[$x]) <= 1.0)
    @test_throws BoundsError MOI.constraint_expr(evaluator, 2)
    return
end

function test_add_constraint_greater_than()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    func = :($x^2 + 1)
    set = MOI.GreaterThan(1.0)
    c = Nonlinear.add_constraint(model, func, set)
    @test model[c].set == set
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2 + 1 >= 1.0)
    return
end

function test_add_constraint_equal_to()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    func, set = :($x^2 + 1), MOI.EqualTo(1.0)
    c = Nonlinear.add_constraint(model, func, set)
    @test model[c].set == set
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2 + 1 == 1.0)
    return
end

function test_add_constraint_interval()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    func, set = :($x^2 + 1), MOI.Interval(-1.0, 1.0)
    c = Nonlinear.add_constraint(model, func, set)
    @test model[c].set == set
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, [:ExprGraph])
    @test MOI.constraint_expr(evaluator, 1) == :(-1.0 <= x[$x]^2 + 1 <= 1.0)
    return
end

function test_eval_univariate_function()
    r = Nonlinear.OperatorRegistry()
    @test Nonlinear.eval_univariate_function(r, :+, 1.0) == 1.0
    @test Nonlinear.eval_univariate_function(r, :-, 1.0) == -1.0
    @test Nonlinear.eval_univariate_function(r, :abs, -1.1) == 1.1
    @test Nonlinear.eval_univariate_function(r, :abs, 1.1) == 1.1
    return
end

function test_eval_univariate_gradient()
    r = Nonlinear.OperatorRegistry()
    @test Nonlinear.eval_univariate_gradient(r, :+, 1.2) == 1.0
    @test Nonlinear.eval_univariate_gradient(r, :-, 1.2) == -1.0
    @test Nonlinear.eval_univariate_gradient(r, :abs, -1.1) == -1.0
    @test Nonlinear.eval_univariate_gradient(r, :abs, 1.1) == 1.0
    return
end

function test_eval_univariate_hessian()
    r = Nonlinear.OperatorRegistry()
    @test Nonlinear.eval_univariate_hessian(r, :+, 1.2) == 0.0
    @test Nonlinear.eval_univariate_hessian(r, :-, 1.2) == 0.0
    @test Nonlinear.eval_univariate_hessian(r, :abs, -1.1) == 0.0
    @test Nonlinear.eval_univariate_hessian(r, :abs, 1.1) == 0.0
    return
end

function test_eval_univariate_function_registered_method_error()
    r = Nonlinear.Model()
    f(x::Float64) = sin(x)^2
    @test_throws ErrorException Nonlinear.register_operator(r, :f, 1, f)
    return
end

function test_univariate_function_register_twice()
    r = Nonlinear.Model()
    f(x) = x
    Nonlinear.register_operator(r, :f, 1, f)
    @test_throws(
        ErrorException("Operator f is already registered."),
        Nonlinear.register_operator(r, :f, 1, f),
    )
    @test_throws(
        ErrorException("Operator f is already registered."),
        Nonlinear.register_operator(r, :f, 2, f),
    )
    return
end

function test_multivariate_function_register_twice()
    r = Nonlinear.Model()
    f(x, y) = x + y
    Nonlinear.register_operator(r, :f, 2, f)
    @test_throws(
        ErrorException("Operator f is already registered."),
        Nonlinear.register_operator(r, :f, 1, f),
    )
    @test_throws(
        ErrorException("Operator f is already registered."),
        Nonlinear.register_operator(r, :f, 2, f),
    )
    return
end

function test_auto_register()
    r = Nonlinear.OperatorRegistry()
    f(x, y) = x + y
    @test_throws ErrorException Nonlinear.assert_registered(r, :f, 2)
    @test_logs (:warn,) Nonlinear.register_operator_if_needed(r, :f, 2, f)
    Nonlinear.assert_registered(r, :f, 2)
    return
end

function test_register_univariate_function_return_type()
    r = Nonlinear.OperatorRegistry()
    f(x) = x < 1 ? "x" : x
    @test_throws(
        ErrorException(
            "Expected return type of `Float64` from the user-defined " *
            "function :f, but got `String`.",
        ),
        Nonlinear.register_operator(r, :f, 1, f),
    )
    return
end

function test_eval_univariate_function_return_type()
    r = Nonlinear.OperatorRegistry()
    f(x) = x < 1 ? x : "x"
    Nonlinear.register_operator(r, :f, 1, f)
    @test_throws(
        ErrorException(
            "Expected return type of Float64 from a user-defined function, " *
            "but got String.",
        ),
        Nonlinear.eval_univariate_function(r, :f, 1.2),
    )
    return
end

function test_eval_univariate_function_registered()
    r = Nonlinear.OperatorRegistry()
    f(x) = sin(x)^2
    grad_calls = 0
    f′(x) = (grad_calls += 1; 2 * sin(x) * cos(x))
    hess_calls = 0
    f′′(x) = (hess_calls += 1; 2 * (cos(x)^2 - sin(x)^2))
    Nonlinear.register_operator(r, :f, 1, f)
    x = 1.2
    @test Nonlinear.eval_univariate_function(r, :f, x) ≈ f(x)
    @test Nonlinear.eval_univariate_gradient(r, :f, x) ≈ f′(x)
    @test grad_calls == 1
    @test Nonlinear.eval_univariate_hessian(r, :f, x) ≈ f′′(x)
    @test hess_calls == 1
    return
end

function test_eval_univariate_function_registered_grad()
    r = Nonlinear.OperatorRegistry()
    f(x) = sin(x)^2
    grad_calls = 0
    f′(x) = (grad_calls += 1; 2 * sin(x) * cos(x))
    hess_calls = 0
    f′′(x) = (hess_calls += 1; 2 * (cos(x)^2 - sin(x)^2))
    Nonlinear.register_operator(r, :f, 1, f, f′)
    @test grad_calls == 2
    x = 1.2
    @test Nonlinear.eval_univariate_function(r, :f, x) ≈ f(x)
    @test Nonlinear.eval_univariate_gradient(r, :f, x) ≈ f′(x)
    @test grad_calls == 4
    @test Nonlinear.eval_univariate_hessian(r, :f, x) ≈ f′′(x)
    @test hess_calls == 1
    return
end

function test_eval_univariate_function_registered_grad_hess()
    r = Nonlinear.OperatorRegistry()
    f(x) = sin(x)^2
    grad_calls = 0
    f′(x) = (grad_calls += 1; 2 * sin(x) * cos(x))
    hess_calls = 0
    f′′(x) = (hess_calls += 1; 2 * (cos(x)^2 - sin(x)^2))
    Nonlinear.register_operator(r, :f, 1, f, f′, f′′)
    x = 1.2
    @test Nonlinear.eval_univariate_function(r, :f, x) ≈ f(x)
    @test Nonlinear.eval_univariate_gradient(r, :f, x) ≈ f′(x)
    @test grad_calls == 2
    @test Nonlinear.eval_univariate_hessian(r, :f, x) ≈ f′′(x)
    @test hess_calls == 2
    return
end

function test_eval_multivariate_function()
    r = Nonlinear.OperatorRegistry()
    x = [1.1, 2.2]
    @test Nonlinear.eval_multivariate_function(r, :+, x) ≈ 3.3
    @test Nonlinear.eval_multivariate_function(r, :-, x) ≈ -1.1
    @test Nonlinear.eval_multivariate_function(r, :*, x) ≈ 1.1 * 2.2
    @test Nonlinear.eval_multivariate_function(r, :^, x) ≈ 1.1^2.2
    @test Nonlinear.eval_multivariate_function(r, :/, x) ≈ 1.1 / 2.2
    @test Nonlinear.eval_multivariate_function(r, :ifelse, [1; x]) == 1.1
    @test Nonlinear.eval_multivariate_function(r, :ifelse, [0; x]) == 2.2
    return
end

function test_eval_multivariate_gradient()
    r = Nonlinear.OperatorRegistry()
    x = [1.1, 2.2]
    g = zeros(2)
    Nonlinear.eval_multivariate_gradient(r, :+, g, x)
    @test g == [1.0, 1.0]
    Nonlinear.eval_multivariate_gradient(r, :-, g, x)
    @test g == [1.0, -1.0]
    Nonlinear.eval_multivariate_gradient(r, :*, g, x)
    @test g ≈ [2.2, 1.1]
    Nonlinear.eval_multivariate_gradient(r, :^, g, x)
    @test g ≈ [2.2 * 1.1^1.2, 1.1^2.2 * log(1.1)]
    Nonlinear.eval_multivariate_gradient(r, :^, g, [1.1, 1.0])
    @test g ≈ [1.0, 1.1 * log(1.1)]
    Nonlinear.eval_multivariate_gradient(r, :^, g, [1.1, 2.0])
    @test g ≈ [2.0 * 1.1, 1.1^2.0 * log(1.1)]
    Nonlinear.eval_multivariate_gradient(r, :^, g, [-1.1, 2.0])
    @test g[1] ≈ 2.0 * -1.1
    @test isnan(g[2])
    Nonlinear.eval_multivariate_gradient(r, :/, g, x)
    @test g ≈ [1 / 2.2, -1.1 / 2.2^2]
    g = zeros(3)
    Nonlinear.eval_multivariate_gradient(r, :ifelse, g, [1; x])
    @test g ≈ [0.0, 1.0, 0.0]
    Nonlinear.eval_multivariate_gradient(r, :ifelse, g, [0; x])
    @test g ≈ [0.0, 0.0, 1.0]
    return
end

function test_eval_multivariate_gradient_mult()
    r = Nonlinear.OperatorRegistry()
    x = [1.1, 0.0, 2.2]
    g = zeros(3)
    Nonlinear.eval_multivariate_gradient(r, :*, g, x)
    @test g == [0.0, 1.1 * 2.2, 0.0]
    return
end

function test_eval_multivariate_hessian_prod()
    r = Nonlinear.OperatorRegistry()
    # 1-arg *
    x = [1.1]
    H = LinearAlgebra.LowerTriangular(zeros(1, 1))
    @test Nonlinear.eval_multivariate_hessian(r, :*, H, x)
    @test H ≈ _hessian(x -> *(x[1]), x)
    # 2-arg *
    x = [1.1, 2.2]
    H = LinearAlgebra.LowerTriangular(zeros(2, 2))
    @test (@allocated Nonlinear.eval_multivariate_hessian(r, :*, H, x)) == 0
    @test Nonlinear.eval_multivariate_hessian(r, :*, H, x)
    @test H ≈ _hessian(x -> x[1] * x[2], x)
    # 3-arg *
    x = [1.1, 2.2, 3.3]
    H = LinearAlgebra.LowerTriangular(zeros(3, 3))
    @test Nonlinear.eval_multivariate_hessian(r, :*, H, x)
    @test H ≈ _hessian(x -> x[1] * x[2] * x[3], x)
    return
end

function test_eval_multivariate_hessian_exponentiation()
    r = Nonlinear.OperatorRegistry()
    # ^1.0
    x = [1.1, 1.0]
    H = LinearAlgebra.LowerTriangular(zeros(2, 2))
    @test (@allocated Nonlinear.eval_multivariate_hessian(r, :^, H, x)) == 0
    @test Nonlinear.eval_multivariate_hessian(r, :^, H, x)
    @test H ≈ _hessian(x -> x[1]^x[2], x)
    # ^2.0
    x = [1.1, 2.0]
    H = LinearAlgebra.LowerTriangular(zeros(2, 2))
    @test (@allocated Nonlinear.eval_multivariate_hessian(r, :^, H, x)) == 0
    @test Nonlinear.eval_multivariate_hessian(r, :^, H, x)
    @test H ≈ _hessian(x -> x[1]^x[2], x)
    # 2-arg ^
    x = [1.1, 2.2]
    H = LinearAlgebra.LowerTriangular(zeros(2, 2))
    @test (@allocated Nonlinear.eval_multivariate_hessian(r, :^, H, x)) == 0
    @test Nonlinear.eval_multivariate_hessian(r, :^, H, x)
    @test H ≈ _hessian(x -> x[1]^x[2], x)
    return
end

function test_eval_multivariate_hessian_division()
    r = Nonlinear.OperatorRegistry()
    # 2-arg /
    x = [1.1, 2.2]
    H = LinearAlgebra.LowerTriangular(zeros(2, 2))
    @test (@allocated Nonlinear.eval_multivariate_hessian(r, :/, H, x)) == 0
    @test Nonlinear.eval_multivariate_hessian(r, :/, H, x)
    @test H ≈ _hessian(x -> x[1] / x[2], x)
    return
end

function test_eval_multivariate_function_registered()
    r = Nonlinear.OperatorRegistry()
    f(x...) = x[1]^2 + x[1] * x[2] + x[2]^2
    Nonlinear.register_operator(r, :f, 2, f)
    x = [1.1, 2.2]
    @test Nonlinear.eval_multivariate_function(r, :f, x) ≈ f(x...)
    g = zeros(2)
    Nonlinear.eval_multivariate_gradient(r, :f, g, x)
    @test g ≈ [2 * x[1] + x[2], x[1] + 2 * x[2]]
    H = LinearAlgebra.LowerTriangular(zeros(2, 2))
    @test_throws(
        ErrorException,
        Nonlinear.eval_multivariate_hessian(r, :f, H, x),
    )
    return
end

function test_eval_multivariate_function_method_error()
    r = Nonlinear.OperatorRegistry()
    function f(x...)
        if x[1] > 1
            y = zeros(Float64, 1)
            y[1] = x[1]
            return y[1]^2
        end
        return x[1]^2 + x[1] * x[2] + x[2]^2
    end
    Nonlinear.register_operator(r, :f, 2, f)
    x = [1.1, 2.2]
    g = [0.0, 0.0]
    @test_throws(
        ErrorException,
        Nonlinear.eval_multivariate_gradient(r, :f, g, x),
    )
    return
end

function test_eval_multivariate_function_registered_grad()
    r = Nonlinear.OperatorRegistry()
    f(x...) = x[1]^2 + x[1] * x[2] + x[2]^2
    grad_calls = 0
    function ∇f(g, x...)
        grad_calls += 1
        g[1] = 2 * x[1] + x[2]
        g[2] = x[1] + 2 * x[2]
        return
    end
    Nonlinear.register_operator(r, :f, 2, f, ∇f)
    x = [1.1, 2.2]
    @test Nonlinear.eval_multivariate_function(r, :f, x) ≈ f(x...)
    g = zeros(2)
    Nonlinear.eval_multivariate_gradient(r, :f, g, x)
    @test g ≈ [2 * x[1] + x[2], x[1] + 2 * x[2]]
    @test grad_calls == 1
    H = LinearAlgebra.LowerTriangular(zeros(2, 2))
    @test_throws(
        ErrorException("Hessian is not defined for operator f"),
        Nonlinear.eval_multivariate_hessian(r, :f, H, x),
    )
    return
end

function test_eval_multivariate_function_registered_hessian()
    r = Nonlinear.OperatorRegistry()
    f(x...) = x[1]^2 + x[1] * x[2] + x[2]^2
    grad_calls = 0
    function ∇f(g, x...)
        grad_calls += 1
        g[1] = 2 * x[1] + x[2]
        g[2] = x[1] + 2 * x[2]
        return
    end
    hess_calls = 0
    function ∇²f(H, x...)
        hess_calls += 1
        H[1, 1] = 2.0
        H[2, 1] = 1.0
        H[2, 2] = 2.0
        return
    end
    Nonlinear.register_operator(r, :f, 2, f, ∇f, ∇²f)
    x = [1.1, 2.2]
    @test Nonlinear.eval_multivariate_function(r, :f, x) ≈ f(x...)
    g = zeros(2)
    Nonlinear.eval_multivariate_gradient(r, :f, g, x)
    @test g ≈ [2 * x[1] + x[2], x[1] + 2 * x[2]]
    @test grad_calls == 1
    H = LinearAlgebra.LowerTriangular(zeros(2, 2))
    @test Nonlinear.eval_multivariate_hessian(r, :f, H, x) == true
    @test H == [2.0 0.0; 1.0 2.0]
    @test hess_calls == 1
    return
end

function test_eval_logic_function()
    r = Nonlinear.OperatorRegistry()
    for lhs in (true, false), rhs in (true, false)
        @test Nonlinear.eval_logic_function(r, :&&, lhs, rhs) == (lhs && rhs)
        @test Nonlinear.eval_logic_function(r, :||, lhs, rhs) == (lhs || rhs)
        @test_throws(
            AssertionError,
            Nonlinear.eval_logic_function(r, :⊻, lhs, rhs),
        )
    end
    return
end

function test_eval_comprison_function()
    r = Nonlinear.OperatorRegistry()
    for lhs in (true, false), rhs in (true, false)
        @test Nonlinear.eval_comparison_function(r, :<=, lhs, rhs) ==
              (lhs <= rhs)
        @test Nonlinear.eval_comparison_function(r, :>=, lhs, rhs) ==
              (lhs >= rhs)
        @test Nonlinear.eval_comparison_function(r, :(==), lhs, rhs) ==
              (lhs == rhs)
        @test Nonlinear.eval_comparison_function(r, :<, lhs, rhs) == (lhs < rhs)
        @test Nonlinear.eval_comparison_function(r, :>, lhs, rhs) == (lhs > rhs)
        @test_throws(
            AssertionError,
            Nonlinear.eval_comparison_function(r, :⊻, lhs, rhs),
        )
    end
    return
end

function test_features_available()
    model = Nonlinear.Model()
    evaluator = Nonlinear.Evaluator(model)
    @test MOI.features_available(evaluator) == [:ExprGraph]
    return
end

function test_features_available_Default()
    model = Nonlinear.Model()
    evaluator = Nonlinear.Evaluator(
        model,
        Nonlinear.ExprGraphOnly(),
        MOI.VariableIndex[],
    )
    @test MOI.features_available(evaluator) == [:ExprGraph]
    return
end

function test_add_constraint_ordinal_index()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    constraints = [
        Nonlinear.add_constraint(model, :($x), MOI.LessThan(1.0 * i)) for
        i in 1:4
    ]
    for i in 1:4
        @test MOI.is_valid(model, constraints[i])
    end
    evaluator = Nonlinear.Evaluator(model)
    MOI.initialize(evaluator, Symbol[])
    for i in 1:4
        @test Nonlinear.ordinal_index(evaluator, constraints[i]) == i
    end
    Nonlinear.delete(model, constraints[1])
    Nonlinear.delete(model, constraints[3])
    @test !MOI.is_valid(model, constraints[1])
    @test MOI.is_valid(model, constraints[2])
    @test !MOI.is_valid(model, constraints[3])
    @test MOI.is_valid(model, constraints[4])
    MOI.initialize(evaluator, Symbol[])
    @test Nonlinear.ordinal_index(evaluator, constraints[2]) == 1
    @test_throws(
        ErrorException("Invalid constraint index $(constraints[3])"),
        Nonlinear.ordinal_index(evaluator, constraints[3]),
    )
    @test Nonlinear.ordinal_index(evaluator, constraints[4]) == 2
    return
end

function test_show()
    model = Nonlinear.Model()
    evaluator = Nonlinear.Evaluator(model)
    @test occursin(":ExprGraph", sprint(show, evaluator))
    return
end

function test_evaluate_comparison()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    ex = Nonlinear.add_expression(model, :(ifelse($x < 1, -1.0, 1.0)))
    @test Nonlinear.evaluate(Dict(x => 1.1), model, ex) == 1.0
    @test Nonlinear.evaluate(Dict(x => 0.9), model, ex) == -1.0
    return
end

function test_evaluate_logic()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    ex = Nonlinear.add_expression(model, :(ifelse($x < 0 || $x > 1, -1.0, 1.0)))
    @test Nonlinear.evaluate(Dict(x => 1.1), model, ex) == -1.0
    @test Nonlinear.evaluate(Dict(x => 0.9), model, ex) == 1.0
    @test Nonlinear.evaluate(Dict(x => -0.9), model, ex) == -1.0
    return
end

function test_evaluate_subexpressions()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    p = Nonlinear.add_parameter(model, 1.23)
    ex = Nonlinear.add_expression(model, :(*($p, $x, $x)))
    ex = Nonlinear.add_expression(model, :($ex + sqrt($ex)))
    ex_v = 1.23 * 1.1 * 1.1
    @test Nonlinear.evaluate(Dict(x => 1.1), model, ex) ≈ ex_v + sqrt(ex_v)
    model[p] = 3.21
    ex_v = 3.21 * 1.2 * 1.2
    @test Nonlinear.evaluate(Dict(x => 1.2), model, ex) ≈ ex_v + sqrt(ex_v)
    return
end

function test_NLPBlockData()
    model = Nonlinear.Model()
    x = MOI.VariableIndex(1)
    evaluator = Nonlinear.Evaluator(model, Nonlinear.ExprGraphOnly(), [x])
    block = MOI.NLPBlockData(evaluator)
    @test block.has_objective == false
    @test length(block.constraint_bounds) == 0
    return
end

end

TestNonlinear.runtests()

include("ReverseAD.jl")


## Umbrella functions to read and write graph file formats ##

"""
    readgraph(filename[; format, gzip, ...])

Load a network from file.

Currently supported formats are:

* `:auto` — try to gess the format from the file's extenstion,
* `:delim` — delimited plain-text file containing one edge per line
  (typically, `*.csv`, `*.tsv`, and `*.txt` extension),
* `:gml` — Graph Modeling Language file (`*.gml`),
* `:pajek` — Pajek file (`*.paj`, `*.net`),
* `:ngraph` — ngraph binary format (`*.bin` + `*.json`).

In the works:

* `:ucinet`  — UCINet files (`*.ucn`),
* `:graphml` — GraphML files (`*.graphml`),
* `:gdf` — GUESS file (https://gephi.org/users/supported-graph-formats/gdf-format/)

See the excellent [Gephi manual](https://gephi.org/users/supported-graph-formats/)
for the overview of different formats.
"""
function readgraph(filename; format=:auto, gzip=:auto, kvargs...)
    extensions = split(lowercase(basename(filename)), '.')[2:end]
    matchext(exts...) = any(i -> i in extensions, exts)
    if format == :auto
        if matchext("txt", "tsv", "csv", "edgelist")
            format = :delim
        elseif matchext("net", "paj")
            format = :pajek
        elseif matchext("gml")
            format = :gml
        elseif matchext("dl")
            format = :ucinet
        else
            error("could not determine file format automatically")
        end
    end
    if gzip == :auto
        gzip = matchext("gz")
    end
    f = gzip ? GZip.open(filename) : open(filename)
    if format == :delim
        g = read_edgelist(f; kvargs...)
    elseif format == :pajek
        g = read_pajek(f; kvargs...)
    elseif format == :gml
        g = read_gml(f; kvargs...)
    elseif format == :ucinet
        g = read_dl(f)
    end
    close(f)
    return g
end


# This file is a part of BinaryCommitteeMachineFBP.jl. License is MIT: http://github.com/carlobaldassi/BinaryCommitteeMachineFBP.jl/LICENCE.md

module Util

export exclusive, checkdims, chgeltype,
       @readmagvec, @dumpmagvecs,
       IVec, Vec, Vec2, MagVec, MagVec2, MagVec3

using ..Magnetizations: Mag64, showinner, parseinner

if success(`which lockfile`)
    function exclusive(f::Function, fn::AbstractString = "lock.tmp")
        run(`lockfile -1 $fn`)
        try
            f()
        finally
            run(`rm -f $fn`)
        end
    end
else
    @warn("could not find the `lockfile` program. Try installing it (it's included with `procmail` – www.procmail.org).")
    exclusive(f::Function, fn::AbstractString) = nothing
end

annotate_new!(T, ex) = ex
function annotate_new!(T, ex::Expr)
    if ex.head == :call && ex.args[1] == :new
        ex.args[1] = Expr(:curly, :new, T)
    end
    map!(x->annotate_new!(T, x), ex.args, ex.args)
    return ex
end

const IVec = Vector{Int}
const Vec = Vector{Float64}
const Vec2 = Vector{Vec}
const MagVec{M<:Mag64} = Vector{M}
const MagVec2{M<:Mag64} = Vector{MagVec{M}}
const MagVec3{M<:Mag64} = Vector{MagVec2{M}}

function checkdims(x::AbstractArray{<:AbstractArray}, N::Integer, r::Integer...)
    @assert length(x) == N
    @assert all(v->checkdims(v, r...), x)
    return true
end
function checkdims(x::AbstractArray, N::Integer)
    @assert length(x) == N
    return true
end
checkdims(x::AbstractArray{<:AbstractArray}, N::Integer) = error("too few dimensions")
checkdims(x::AbstractArray, N::Integer, r::Integer...) = error("not enough dimensions")

changedeltype(::Type{Vector{V}}, ::Type{T}) where {V<:Vector,T} = Vector{changedeltype(V, T)}
changedeltype(::Type{Vector{X}}, ::Type{T}) where {X,T} = Vector{T}

function chgeltype(x::Vector{<:Vector}, ::Type{T}) where {T}
    eltype(changedeltype(typeof(x), T))[chgeltype(y, T) for y in x]
end
chgeltype(x::Vector, ::Type{T}) where {T} = convert(Vector{T}, x)

macro readmagvec(l, fmt, vs...)
    ex = :()
    for v in vs
        vn = Regex(string(v, "(\\[|\\s)"))
        ex = quote
            $ex
            if occursin($vn, $(esc(l)))
                _readmagvec($(esc(l)), $(esc(fmt)), $(esc(v)))
                @goto found
            end
        end
    end
    ex = quote
        $ex
        error("unrecognized line: ", $(esc(l)))
        @label found
    end
    ex
end

function _readmagvec(l::AbstractString, fmt::Type{Val{F}}, v::Array) where {F}
    sl = split(l)
    pre = sl[1]
    occursin(r"^[^[]+(?:\[\d+\])*$", pre) || error("invalid messages file")
    inds = map(x->parse(Int,x), split(replace(pre, r"[][]" => " "))[2:end])
    for i in inds
        v = v[i]
    end
    length(v) == length(sl) - 1 || error("invalid messages file")
    for i = 1:length(v)
        v[i] = parseinner(fmt, sl[i+1])
    end
end

macro dumpmagvecs(io, vs...)
    ex = :()
    for v in vs
        vn = string(v)
        ex = quote
            $ex
            _dumpmagvec($(esc(io)), $(esc(v)), $vn)
        end
    end
    ex
end

function _dumpmagvec(io::IO, a::Array{<:Array}, s::AbstractString)
    for (i,v) in enumerate(a)
        _dumpmagvec(io, v, "$s[$i]")
    end
end
function _dumpmagvec(io::IO, a::Array, s::AbstractString)
    print(io, s)
    for x in a
        print(io, ' ')
        showinner(io, x)
    end
    println(io)
end

end


using Test
using CUDAnative, CuArrays
using CUDAatomics

if CUDAnative.functional()
    function kern_atomicadd( a, b )
        i = (blockIdx().x-1) * blockDim().x + threadIdx().x
        atomicadd(a,b[i], 2)
        return nothing
    end
    d_a = CuArray(zeros(Float32, 2))
    d_b = CuArray(Float32.(collect(1:1024)))
    @cuda threads=32 blocks=32 kern_atomicadd(d_a, d_b)
    @test abs(Array(d_a)[2] - sum(Array(d_b))) < 1.0f-7

    function kern_atomicsub( a, b )
        i = (blockIdx().x-1) * blockDim().x + threadIdx().x
        atomicsub(a,b[i])
        return nothing
    end
    d_a = CuArray(zeros(Int32, 1))
    d_b = CuArray(Int32.(collect(1:1024)))
    @cuda threads=32 blocks=32 kern_atomicsub(d_a, d_b)
    @test Array(d_a)[1] == -524800

    function kern_atomicmin( a, b )
        i = (blockIdx().x-1) * blockDim().x + threadIdx().x
        atomicmin(a,b[i])
        return nothing
    end
    d_a = CuArray(Array{Int32}([1025]))
    d_b = CuArray(Int32.(collect(1:1024)))
    @cuda threads=32 blocks=32 kern_atomicmin(d_a, d_b)
    @test Array(d_a)[1] == 1

    function kern_atomicmax( a, b )
        i = (blockIdx().x-1) * blockDim().x + threadIdx().x
        atomicmax(a,b[i])
        return nothing
    end
    d_a = CuArray(Array{Int32}([1]))
    d_b = CuArray(Int32.(collect(1:1024)))
    @cuda threads=32 blocks=32 kern_atomicmax(d_a, d_b)
    @test Array(d_a)[1] == 1024

    function kern_atomicinc( a, b )
        i = (blockIdx().x-1) * blockDim().x + threadIdx().x
        atomicinc(a,b[i])
        return nothing
    end
    d_a = CuArray(Array{UInt32}([UInt32(0)]))
    d_b = CuArray(repeat(Array{UInt32}([UInt32(1024)]), 1024))
    @cuda threads=32 blocks=32 kern_atomicinc(d_a, d_b)
    @test Array(d_a)[1] == 0x00000400

    function kern_atomicdec( a, b )
        i = (blockIdx().x-1) * blockDim().x + threadIdx().x
        atomicdec(a,b[i])
        return nothing
    end
    d_a = CuArray(Array{UInt32}([UInt32(1025)]))
    d_b = CuArray(repeat(Array{UInt32}([UInt32(1025)]), 1024))
    @cuda threads=32 blocks=32 kern_atomicdec(d_a, d_b)
    @test Array(d_a)[1] == 0x00000001

    function kern_atomicand( a, b )
        i = (blockIdx().x-1) * blockDim().x + threadIdx().x
        atomicand(a,b[i])
        return nothing
    end
    d_a = CuArray(Array{UInt32}([UInt32(1389)]))
    d_b = CuArray(repeat(Array{UInt32}([UInt32(1023)]), 1024))
    @cuda threads=32 blocks=32 kern_atomicand(d_a, d_b)
    @test Array(d_a)[1] == 0x0000016d

    function kern_atomicexch( a, b )
        i = (blockIdx().x-1) * blockDim().x + threadIdx().x
        atomicexch(a,b[i])
        return nothing
    end
    d_a = CuArray(zeros(Float32, 1))
    d_b = CuArray(Float32.(collect(1:1024)))
    @cuda threads=32 blocks=32 kern_atomicexch(d_a, d_b)
    @test findfirst( Array(d_b).==Array(d_a) ) < 1025

    function kern_atomiccas( a, b )
        i = (blockIdx().x-1) * blockDim().x + threadIdx().x
        atomiccas(a, i, b[i])
        return nothing
    end
    d_a = CuArray(Array{Int32}([17]))
    d_b = CuArray(Array{Int32}(collect(1025:2048)))
    @cuda threads=32 blocks=32 kern_atomiccas(d_a, d_b)
    @test findfirst(Array(d_b).==Array(d_a)) == 17
end


# Modified from Graphs.jl.
# Workaround for edge_index not being O(1) on SimpleGraph.
# edge_index was only needed to test for cycles, so
# this implementation skips that check.

# Graphs.jl is licensed under the MIT License:
#
# Copyright (c) 2012: John Myles White and other contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

mutable struct TopologicalSortVisitor
    vertices::Vector{Int}
    parents::Vector{Int}

    function TopologicalSortVisitor(n::Int)
        vs = Int[]
        sizehint!(vs, n)
        new(vs, zeros(Int,n))
    end
end

function depth_first_visit_impl!(
    adjlist,
    offsets,
    vertex_stack,                   # an (initialized) stack of vertex
    index_stack,                    # stack with out edge indices
    vertexcolormap::Vector{Int},    # an (initialized) color-map to indicate status of vertices
    visitor::TopologicalSortVisitor)  # the visitor

    while !isempty(vertex_stack)
        u = pop!(vertex_stack)
        out_idx = pop!(index_stack)
        #uegs = out_edges(u,graph)
        len_uegs = offsets[u+1]-offsets[u]
        found_new_vertex = false

        while out_idx <= len_uegs && !found_new_vertex
            v = adjlist[offsets[u]+out_idx-1]
            out_idx += 1
            v_color = vertexcolormap[v]

            if v_color == 0
                found_new_vertex = true
                vertexcolormap[v] = 1
                push!(vertex_stack, u)
                push!(index_stack, out_idx)
                visitor.parents[v] = u

                push!(vertex_stack, v)
                push!(index_stack, 1)
            end
        end

        if !found_new_vertex
            close_vertex!(visitor, u)
            vertexcolormap[u] = 2
        end
    end
end

function traverse_graph(
    adjlist,
    offsets,
    s,
    visitor::TopologicalSortVisitor,
    vertexcolormap,
    vertex_stack,
    index_stack)

    vertexcolormap[s] = 1

    resize!(vertex_stack,1)
    vertex_stack[1] = s
    resize!(index_stack,1)
    index_stack[1] = 1

    depth_first_visit_impl!(adjlist, offsets, vertex_stack, index_stack, vertexcolormap, visitor)
end

function close_vertex!(visitor::TopologicalSortVisitor, v::Int)
    push!(visitor.vertices, v)
end

function reverse_topological_sort_by_dfs(adjlist,offsets,num_vertices, cmap::Vector{Int}, vertex_stack::Vector{Int}=Int[], index_stack::Vector{Int}=Int[])

    @assert length(cmap) == num_vertices
    fill!(cmap,0)
    visitor = TopologicalSortVisitor(num_vertices)

    for s in 1:num_vertices
        if cmap[s] == 0
            traverse_graph(adjlist, offsets, s, visitor, cmap, vertex_stack, index_stack)
        end
    end

    visitor.vertices,visitor.parents
end


module GraphMakie

using NetworkLayout
using LightGraphs
using Makie
using LinearAlgebra

import Makie: DocThemer, ATTRIBUTES, project, automatic

include("recipes.jl")
include("interaction.jl")

end


using ModelicaScriptingTools
using Test
using OMJulia: sendExpression

refdir = "subprojects/shm-ref"
# only simulate full models for two seconds in order to reduce execution time
# of tests and size of reference data files
twosec = Dict("stopTime" => 2, "numberOfIntervals" => 2000)

withOMC("test-output", ".") do omc
    sendExpression(omc, "setCommandLineOptions(\"-d=newInst,nfAPI\")")
    installAndLoad(omc, "Modelica"; version="3.2.3")
    @testset "Simulations" begin
        @testset "Kotani2005" begin
            testmodel(omc, "SHM.Kotani2005.Examples.FullModel.KotaniFullExample"; refdir=refdir, override=twosec)
        end
        @testset "SeidelThesis" begin
            testmodel(omc, "SHM.SeidelThesis.Examples.FullModel.SeidelThesisFullExample"; refdir=refdir, override=twosec)
        end
        @testset "SchoelzelThesis" begin
            @testset "AVDelayExample" begin
                installAndLoad(omc, "SHMConduction") # SHMConduction is required for AVDelayExample
                testmodel(omc, "SHM.SchoelzelThesis.Examples.AVDelayExample"; refdir=refdir)
            end
            @testset "MultiDelayExample" begin
                testmodel(omc, "SHM.SchoelzelThesis.Examples.MultiDelayExample"; refdir=refdir)
            end
            @testset "PacemakerExample" begin
                testmodel(omc, "SHM.SchoelzelThesis.Examples.PacemakerExample"; refdir=refdir)
            end
            @testset "RefractoryDelayExample" begin
                testmodel(omc, "SHM.SchoelzelThesis.Examples.RefractoryDelayExample"; refdir=refdir)
            end
            @testset "RefractoryExample" begin
                testmodel(omc, "SHM.SchoelzelThesis.Examples.RefractoryExample"; refdir=refdir)
            end
            @testset "UnidirectionalModularExample" begin
                testmodel(omc, "SHM.SchoelzelThesis.Examples.UnidirectionalModularExample"; refdir=refdir)
            end
            @testset "BidirectionalContractionExample" begin
                testmodel(omc, "SHM.SchoelzelThesis.Examples.BidirectionalContractionExample"; refdir=refdir)
            end
            @testset "BidirectionalContractionMixinExample" begin
                testmodel(omc, "SHM.SchoelzelThesis.Examples.BidirectionalContractionMixinExample"; refdir=refdir)
            end
        end
        @testset "shm-conduction" begin
            testmodel(omc, "SHMConduction.Examples.ModularExample"; refdir=refdir)
        end
    end
end


import Pkg; Pkg.activate(".")
include("../required.jl")

## Test landcover variables
# Define coordinates range
coords = (left = -145.0, right = -50.0, bottom = 20.0, top = 75.0)

# Test loading variables
lc_vars = SimpleSDMPredictor(Copernicus, LandCover, 1:10; resolution = 10.0, coords...)

# Load worldclim variables to compare
wc_vars = SimpleSDMPredictor(WorldClim, BioClim, 1:19; resolution = 10.0, coords...)

## Load with geotiff instead

# Check which layer is crops
glossary = CSV.read(joinpath("data", "proc", "glossary.csv"), DataFrame) # lc2

# Load crops with geotiff
lc2_geo_full = geotiff(SimpleSDMPredictor, joinpath("assets", "landcover", "lc_crops_10m.tif"))
lc2_geo = geotiff(SimpleSDMPredictor, joinpath("assets", "landcover", "lc_crops_10m.tif"); coords...)
lc2_geo = convert(Float32, lc2_geo)
# Get corresponding layer
lc2_csv = lc_vars[2]

# Tests
isequal(lc2_geo, lc2_csv)
isequal(lc2_geo.grid, lc2_csv.grid)
isapprox(lc2_geo.grid, lc2_csv.grid)
isequal(collect(lc2_geo), collect(lc2_csv))
isequal(length(lc2_geo), length(lc2_csv)) # not same number of elements...

testdf = DataFrame([lc2_geo, lc2_csv]) # not same top coordinates

lc2_geo
lc2_csv
isapprox(stride(lc2_geo, 1), stride(lc2_csv, 1)) # not same stride...

size(lc2_geo_full) # 840 x 2159, but should be 840 x 2160...

## Check details from landcover function

# Arguments
layers = collect(1:10)
resolution = 10.0
path = joinpath("assets", "landcover/")

# Function content
# function landcover(layers::Vector{Int64}; resolution::AbstractFloat=10.0, path::AbstractString=joinpath("assets", "landcover/"))
    ## Get file paths
    # List files in path
    lc_files = readdir("$(path)")
    # Filter for selected resolution
    filter!(x -> occursin.("$(Int(resolution))m.tif", x), lc_files)
    # Create paths for selected layers only
    paths = joinpath.(path, lc_files)[layers]

    ## Load data
    # Read raster dataset
    datasets = [ArchGDAL.read(p) for p in paths]
    datasets[2] # 2160 x 240
    # Read raster values from band 1 (only band in this case)
    values = [ArchGDAL.read(d, 1) for d in datasets]
    values[2] # 2160 x 840
    # HOW???
    # Convert from UInt8 to Float64
    values_int = [convert.(Float64, v) for v in values]

    ## Preliminary manipulations
    # Reverse rows and permute dimensions
    landcover_mat = [permutedims(v[:,end:-1:1]) for v in values_int]
    # Replace 255 (default no data values) by nothing
    landcover_mat = convert.(Array{Union{Float32, Nothing}}, landcover_mat)
    [replace!(l, 255 => nothing) for l in landcover_mat]

    # Fill missing latitudes with nothing (latitude extent is only (-60,80) for landcover data)
    nlat, nlon = size(landcover_mat[1])
    slim, nlim = abs(-60), 80
    res = Int64(nlat/(nlim+slim))
    south_nothings = fill(nothing, ((90-slim)*res, nlon))
    north_nothings = fill(nothing, ((90-nlim)*res, nlon))
    landcover_grids = [vcat(south_nothings, l, north_nothings) for l in landcover_mat]

    # Convert to SimpleSDMLayers
    landcover_layers = SimpleSDMPredictor.(landcover_grids, -180.0, 180.0, -90.0, 90.0)

    # return landcover_layers
# end

[geotiff(SimpleSDMPredictor, p) for p in paths] # always returning 840 x 2159...

coords_landcover = (left = -180.0, right = 180.0, bottom = -60.0, top = 80.0)
geotiff(SimpleSDMPredictor, paths[2]; coords_landcover...) # still the same

## Check details from geotiff
file = paths[2]
bandnumber = 1
left = -180.0
right = 180.0
bottom = -90.0
top = 90.0

# function geotiff(
#     ::Type{LT},
#     file::AbstractString,
#     bandnumber::Integer=1;
#     left = -180.0,
#     right = 180.0,
#     bottom = -90.0,
#     top = 90.0
# ) where {LT<:SimpleSDMLayer}

    # This next block is reading the geotiff file, but also making sure that we
    # clip the file correctly to avoid reading more than we need.
    # This next block is reading the geotiff file, but also making sure that we
    # clip the file correctly to avoid reading more than we need.
    # layer = ArchGDAL.read(file) do dataset
    dataset = ArchGDAL.read(file)

        transform = ArchGDAL.getgeotransform(dataset)
        wkt = ArchGDAL.getproj(dataset)

        # The data we need is pretty much always going to be stored in the first
        # band, but this is not the case for the future WorldClim data.
        band = ArchGDAL.getband(dataset, bandnumber)
        T = ArchGDAL.pixeltype(band)

        # The nodata is not always correclty identified, so if it is not found, we assumed it is the smallest value in the band
        nodata = isnothing(ArchGDAL.getnodatavalue(band)) ? convert(T, ArchGDAL.minimum(band)) : convert(T, ArchGDAL.getnodatavalue(band))

        # Get the correct latitudes
        minlon = transform[1]
        maxlat = transform[4]
        maxlon = minlon + size(band,1)*transform[2]
        minlat = maxlat - abs(size(band,2)*transform[6])

        left = isnothing(left) ? minlon : max(left, minlon)
        right = isnothing(right) ? maxlon : min(right, maxlon)
        bottom = isnothing(bottom) ? minlat : max(bottom, minlat)
        top = isnothing(top) ? maxlat : min(top, maxlat)

        lon_stride, lat_stride = transform[2], transform[6]

        width = ArchGDAL.width(dataset)
        height = ArchGDAL.height(dataset)

        #global lon_stride, lat_stride
        #global left_pos, right_pos
        #global bottom_pos, top_pos

        lon_stride, left_pos, min_width = SimpleSDMLayers._find_span(width, minlon, maxlon, left)
        _, right_pos, max_width = SimpleSDMLayers._find_span(width, minlon, maxlon, right) # here's the problem!!
        lat_stride, top_pos, max_height = SimpleSDMLayers._find_span(height, minlat, maxlat, top)
        _, bottom_pos, min_height = SimpleSDMLayers._find_span(height, minlat, maxlat, bottom)

        max_height, min_height = height .- (min_height, max_height) .+ 1

        # We are now ready to initialize a matrix of the correct type.
        buffer = Matrix{T}(undef, length(min_width:max_width), length(min_height:max_height))
        ArchGDAL.read!(dataset, buffer, bandnumber, min_height:max_height, min_width:max_width)
        buffer = convert(Matrix{Union{Nothing,eltype(buffer)}}, rotl90(buffer))
        replace!(buffer, nodata => nothing)
        # LT(buffer, left_pos-0.5lon_stride, right_pos+0.5lon_stride, bottom_pos-0.5lat_stride, top_pos+0.5lat_stride)
        SimpleSDMPredictor(buffer, left_pos-0.5lon_stride, right_pos+0.5lon_stride, bottom_pos-0.5lat_stride, top_pos+0.5lat_stride)
    end

    # return layer

# end

geotiff(SimpleSDMPredictor, paths[2]; left=nothing, right=nothing, bottom=nothing, top=nothing)
lc2_geo_full

# Extract problem
_, right_pos, max_width = SimpleSDMLayers._find_span(width, minlon, maxlon, right) # here's the problem!!

# Check details of _find_span
n = width
m = minlon
M = maxlon
pos = right
# function _find_span(n, m, M, pos)
    pos > M && return nothing
    pos < m && return nothing
    stride2 = (M - m) / n
    centers = (m + 0.5stride2):stride2:(M-0.5stride2)
    span_pos = last(findmin(abs.(pos .- centers)))
    return (stride, centers[span_pos], span_pos)
# end

lc2_geo_fix = similar(lc2_csv)
lc2_geo_fix.grid = copy(lc2_geo.grid)
lc2_geo_fix = convert(SimpleSDMPredictor, lc2_geo_fix)

compdf = DataFrame([lc2_geo_fix, lc2_csv])
rename!(compdf, :x1 => :geo_fix, :x2 => :csv)
allowmissing!(compdf)
for col in [:geo_fix, :csv]
    replace!(compdf[!, col], nothing => missing)
end
compdf
dropmissing(compdf, :geo_fix)

plot(lc2_geo_fix, c = :viridis)
plot(lc2_csv, c = :viridis) # one cell misplacement

## Attempt to load full 100m resolution layer
megafilepath = "./assets/landcover/test/landcover_copernicus_global_100m_v2.0.2_moss.tif"
megafilepath = "./assets/landcover/landcover_copernicus_global_100m_v2.0.2_urban.tif"
isfile(megafilepath)
test = geotiff(SimpleSDMPredictor, megafilepath; coords...)
ArchGDAL.read(megafilepath)
dataset = ArchGDAL.read(megafilepath)
ArchGDAL.getgeotransform(dataset)
ArchGDAL.read("./assets/landcover/lc_bare_10m.tif")
ArchGDAL.getgeotransform(ArchGDAL.read("./assets/landcover/lc_bare_10m.tif"))

wc1_path = "/home/gdansereau/.data/SimpleSDMLayers.jl/assets/WorldClim/BioClim/10/wc2.1_10m_bio_1.tif"
geotiff(SimpleSDMPredictor, wc1_path)
wc1_dataset = ArchGDAL.read(wc1_path)
ArchGDAL.getgeotransform(wc1_dataset)

## In bash
# -tap option
gdalwarp -tr 0.166667 0.166667 -tap -r average --config GDAL_CACHEMAX 500 -wm 500 -multi assets/landcover/lc_moss_10m.tif -overwrite assets/landcover/landcover_test.tif

# -te option
gdalwarp -tr 0.166667 0.166667 -te -180.0 -60.0 180.0 80.0 -r average --config GDAL_CACHEMAX 500 -wm 500 -multi assets/landcover/lc_moss_10m.tif -overwrite assets/landcover/landcover_test.tif

# -te option and same stride as WorldClim
gdalwarp -tr 0.1666666666666666574 0.1666666666666666574 -te -180.0 -60.0 180.0 80.0 -r average --config GDAL_CACHEMAX 500 -wm 500 -multi assets/landcover/lc_moss_10m.tif -overwrite assets/landcover/landcover_test.tif

# -te option and same stride as WorldClim on full layer
gdalwarp -tr 0.1666666666666666574 0.1666666666666666574 -te -180.0 -60.0 180.0 80.0 -r average --config GDAL_CACHEMAX 500 -wm 500 -multi assets/landcover/landcover_copernicus_global_100m_v2.0.2_moss.tif -overwrite assets/landcover/landcover_fulltest.tif

# full spatial extent
gdalwarp -tr 0.1666666666666666574 0.1666666666666666574 -te -180.0 -90.0 180.0 90.0 -r average --config GDAL_CACHEMAX 500 -wm 500 -multi assets/landcover/landcover_copernicus_global_100m_v2.0.2_moss.tif -overwrite assets/landcover/landcover_fulltest.tif

# Back in Julia
test_path = "./assets/landcover/landcover_test.tif"
test_dataset = ArchGDAL.read(test_path)
ArchGDAL.getgeotransform(test_dataset)

moss_path = "./assets/landcover/lc_moss_10m.tif"
moss_dataset = ArchGDAL.read(moss_path)
ArchGDAL.getgeotransform(moss_dataset)

fulltest_path = "./assets/landcover/landcover_fulltest.tif"
fulltest_dataset = ArchGDAL.read(fulltest_path)
ArchGDAL.getgeotransform(fulltest_dataset)

geotiff(SimpleSDMPredictor, test_path)
geotiff(SimpleSDMPredictor, fulltest_path)
test_layer = convert(Float32, geotiff(SimpleSDMPredictor, test_path; coords...))
fulltest_layer = convert(Float32, geotiff(SimpleSDMPredictor, fulltest_path; coords...))
wctest_layer = SimpleSDMPredictor(WorldClim, BioClim, 1; coords...)
moss_indx = readdir("assets/landcover/") |> x ->
    filter(f -> occursin.("10m.tif", f), x) |> x ->
    findfirst(occursin.("moss", x))
lc_moss = lc_vars[moss_indx]

isequal(latitudes(fulltest_layer), latitudes(wctest_layer))
isequal(longitudes(fulltest_layer), longitudes(wctest_layer))
SimpleSDMLayers._layers_are_compatible(fulltest_layer, wctest_layer)

plot(test_layer)
plot(lc_moss)
plot(fulltest_layer)

isequal(lc_moss, fulltest_layer)
isequal(lc_moss.grid, fulltest_layer.grid)
testdf = DataFrame([lc_moss, fulltest_layer]) # not exactly same coordinates

isapprox(lc_moss.bottom, fulltest_layer.bottom)

moss_new = similar(lc_moss)
moss_new.grid = copy(fulltest_layer.grid)
moss_new = convert(SimpleSDMPredictor, moss_new)

testdf = DataFrame([lc_moss, moss_new])
isequal(testdf.x1, testdf.x2)
isapprox(testdf.x1, testdf.x2)
isequal(collect(lc_moss), collect(moss_new))
isapprox(collect(lc_moss), collect(moss_new))

writedlm("test.csv", collect(lc_moss))
writedlm("test.csv", collect(moss_new))

allowmissing!(testdf)
for col in [:x1, :x2]
    replace!(testdf[!, col], nothing => missing)
end
testdf
filter(x -> ismissing(x.x1) && !ismissing(x.x2), testdf)
filter(x -> !ismissing(x.x1) && ismissing(x.x2), testdf)
isequal(nrow(dropmissing(testdf)), length(lc_moss))

dropmissing!(testdf)
testdiff = filter(x -> x.x1 != x.x2, testdf) # 800 sites with difference
insertcols!(testdiff, :diff => testdiff.x2 .- testdiff.x1)
unique(testdiff.diff)
sort(countmap(testdiff.diff))


using Pkg
Pkg.add(url="https://github.com/LilithHafner/FBD")

using FBDCompare
using Test

ci = ("CI"=>"true") ∈ ENV

@testset "HyperPA" begin
    HyperPA.jl_benchmark()
    HyperPA.jl_profile()
    HyperPA.jl_with_io()
    if ci
        @test_broken false #=
https://github.com/LilithHafner/FBDCompare.jl/runs/4221035425?check_suite_focus=true
Traceback (most recent call last):
  File "hyper_preferential_attachment.py", line 8, in <module>
    import numpy as np
ModuleNotFoundError: No module named 'numpy'
=#  else
        HyperPA.external()
    end
    @test true
end

@test FBDCompare.hypergraphsize((Dict{Int64, Dict}(4 => Dict{Vector{Int64}, Int64}(), 2 => Dict([2, 4] => 1, [2, 6] => 1, [7, 7] => 1), 3 => Dict([5, 6, 6] => 1, [6, 6, 6] => 1), 1 => Dict([3] => 1, [7] => 1)), 1:7)) == 14

FBDCompare.save_figures()


mutable struct Simulation{S <: DiseaseStateSequence, M <: ILM}
  time::Float64
  iterations::Int64
  population::Population
  risk_functions::RiskFunctions{S}
  risk_parameters::RiskParameters{S}
  substitution_model::Union{Nothing, NucleicAcidSubstitutionModel}
  disease_states::DiseaseStates
  transmission_rates::TransmissionRates
  event_rates::EventRates{S}
  events::Events{S}
  transmission_network::TransmissionNetwork

  function Simulation(
    pop::Population,
    states::DiseaseStates,
    rf::RiskFunctions{S},
    rp::RiskParameters{S};
    time::Float64 = 0.0,
    skip_checks::Bool=false) where {
      S <: DiseaseStateSequence}
    @debug "Initializing $S TNILM Simulation with the following starting states:" states = convert(DiseaseStates, S) counts = [sum(states .== Ref(s)) for s in convert(DiseaseStates, S)]
    if !skip_checks
      if length(states) != individuals(pop)
        error("Length of initial disease state vector must match number of individuals")
      elseif !all(in.(states, Ref(convert(DiseaseStates, S))))
        error("All states in initial disease state vector must be valid within specified epidemic model")
      end
    end
    tr = initialize(TransmissionRates, states, pop, rf, rp)
    rates = initialize(EventRates, tr, states, pop, rf, rp)
    events = Events{S}(states)
    net = TransmissionNetwork(states)
    return new{S, TNILM}(time, 0, pop, rf, rp, nothing, copy(states), tr, rates, events, net)
  end

  function Simulation(
    pop::Population,
    states::DiseaseStates,
    rf::RiskFunctions{S},
    rp::RiskParameters{S},
    sm::NASM;
    time::Float64 = 0.0,
    skip_checks::Bool=false) where {
      S <: DiseaseStateSequence}
    @debug "Initializing $S PhyloILM Simulation with the following starting states:" states
    if !skip_checks
      if length(states) != individuals(pop)
        error("Length of initial disease state vector must match number of individuals")
      elseif !all(in.(states, Ref(convert(DiseaseStates, S))))
        error("All states in initial disease state vector must be valid within specified epidemic model")
      end
    end
    tr = initialize(TransmissionRates, states, pop, rf, rp)
    rates = initialize(EventRates, tr, states, pop, rf, rp)
    events = Events{S}(states)
    net = TransmissionNetwork(states)
    return new{S, PhyloILM}(time, 0, pop, rf, rp, sm, copy(states), tr, rates, events, net)
  end
end

function Simulation(
  pop::Population,
  rf::RiskFunctions{S},
  rp::RiskParameters{S},
  sm::NASM;
  time::Float64 = 0.0) where {
    S <: DiseaseStateSequence}
  return Simulation(pop, fill(State_S, individuals(pop)), rf, rp, sm, time=time, skip_checks=true)
end

function Simulation(
  pop::Population,
  rf::RiskFunctions{S},
  rp::RiskParameters{S};
  time::Float64 = 0.0) where {
    S <: DiseaseStateSequence}
  return Simulation(pop, fill(State_S, individuals(pop)), rf, rp, time=time, skip_checks=true)
end

function Base.show(
  io::IO,
  x::Simulation{S, M}) where {
    S <: DiseaseStateSequence,
    M <: ILM}
  y = "$S $M epidemic simulation @ time = $(round(x.time, digits=2))\n"
  for s in convert(DiseaseStates, S)
    y *= "\n$s = $(sum(x.disease_states .== Ref(s)))"
  end
  return print(io, y)
end


__precompile__()

module PDE

using DataStructures: OrderedDict

# types
export Model, ModelState
export DiagnosticEquation, PrognosticEquation

# model functions
export iterate, iterate_heaps

# time stepping routines
export euler_forward

# operator

include("equations.jl")
include("model.jl")
include("integrator.jl")
include("operator.jl")

end


using LaTeXStrings

export tex

@api tex => (TeX <: Tile) begin
    doc("Create TeX/LaTeX tiles from `LaTeXString` object.")
    arg(source::AbstractString, doc="The source TeX object.")
    kwarg(
        block::Bool=false,
        doc="""If set to true, the resulting tile will be a block. It is inline
             by default"""
    )
end

render(l::TeX, state) = Elem("ka-tex", attributes=@d(:source=>replace(l.source, r"(^\$)|(\$$)", ""), :block=>boolattr(l.block)))


using BenchmarkTools, StatsBase

function summarize(results, benchmark_set)

  for m in benchmark_set
    file = open("./stochkit/$(m).txt", "a")

    result = results[m]
    adjustment = adjust[m]

    for i in eachindex(result.times)
      # convert to ms and correct for overhead
      result.times[i] = result.times[i] * 1e-6 - adjustment
    end

    println(file, "model: $(m)")
    println(file)
    println(file, "n = $(length(result.times))")
    println(file, "mean:               $(mean(result.times)) ms")
    println(file, "std:                $(std(result.times)) ms")
    println(file, "median:             $(median(result.times)) ms")
    println(file, "mad (normal=true):  $(mad(result.times, normalize=true)) ms")
    println(file, "mad (normal=false): $(mad(result.times, normalize=false)) ms")
    println(file)

    close(file)
  end

  return nothing
end

# initialize test suite
suite = BenchmarkGroup()

# load the following variables:
# - parameters
# - MODELS
# - SEED
include("benchmark_parameters.jl")

# build handy collections
const benchmark_set = intersect(ARGS, MODELS)
const adjust = Dict(
  "kendall"     => 80.0,
  "mmek"        => 100.0,
  "autoreg"     => 115.0,
  "dimer-decay" => 100.0,
  "yeast"       => 130.0
)

# build up the benchmark suite
for m in benchmark_set
  # unpack parameters
  t_final, n_saves, n_trial, n_sample, t_limit = parameters[m]

  # build filename
  fname = "./$(m)/stochkit2.xml"

  # build command
  cmd = `ssa -m $(fname) -t $(t_final) -i $(n_saves) -r $(n_trial) -f --seed $(SEED) '&>' /dev/null`

  suite[m] = @benchmarkable run($cmd) samples=n_sample seconds=t_limit
end

results = run(suite, verbose = true)

# perform adjustments
for b in leaves(results)
  b_info, trial = b

  m = b_info[1]

  adjustment = adjust[m]

  for i in eachindex(trial.times)
    trial.times[i] -= adjustment
  end
end

# summarize(results, benchmark_set)
BenchmarkTools.save("stochkit.json", results)


function xyz_from_zmat(zmat_string::String)
    # Match an atom id (String for atomic symbol or integer for atomic number) with one or more blank spaces on the left of it
    re_id = r"\s*(\d+|\w{1,2})"

    # Match an integer (mapping to an atom) followed by a symbol or value (bond length, angle or dihedral). e.g. "1 R" or "2 106.5"
    re_map = r"\s*(\d+)\s+?([^\s]+)"

    # Match a definition e.g. "R = 1.024"
    re_def = r"(\w+)\s*[=]\s*(\d+)"

    line_num = 0
    entries = []
    definitions = []
    for line in split(strip(zmat_string), "\n")

        line_num += 1

        if isempty(strip(line, [' ']))
            continue
        end

        # Required format for first line:  "ATOM_ID"
        if line_num == 1
            m = occursin(re_id, line) ? match(re_id, line) : throw(ArgumentError("Failed to process first ZMAT entry:\n $(line)"))
            push!(entries, m.captures...)

        # Required format for second line:  "ATOM_ID INT VALUE"
        # e.g. "H 1 R" "1 1 1.025"
        elseif line_num == 2
            re_2line = re_id*re_map
            m = occursin(re_2line, line) ? match(re_2line, line) : throw(ArgumentError("Failed to process second ZMAT entry:\n $(line)"))
            println(m)
            push!(entries, m.captures...)

        # Required format for third line:  "ATOM_ID INT VALUE INT VALUE"
        # e.g. "H 1 R 2 A" "1 1 1.025 2 120"
        elseif line_num == 3
            re_3line = re_id*re_map*re_map
            m = occursin(re_3line, line) ? match(re_3line, line) : throw(ArgumentError("Failed to process third ZMAT entry:\n $(line)"))
            push!(entries, m.captures...)

        # Remaining lines are expected to be "ATOM_ID INT VALUE INT VALUE INT VALUE" or "VAR = VALUE"
        else
            re_line = re_id*re_map*re_map*re_map
            if occursin(re_def, line)
                m = match(re_def, line)
                push!(definitions, m.captures...)
            elseif occursin(re_line, line)
                m = match(re_line, line)
                push!(entries, m.captures...)
            else
                throw(ArgumentError("Failed to process line $line_num on ZMAT:\n $(line)"))
            end
        end
    end

    line_format = FormatExpr("{:<3}   {}")
    out = ""
    return entries, definitions
end


using Makie: ScenePlot
import Makie.MakieLayout: registration_setup!, process_interaction

export NodeHoverHandler, EdgeHoverHandler
export NodeHoverHighlight, EdgeHoverHighlight
export NodeClickHandler, EdgeClickHandler
export NodeDragHandler, EdgeDragHandler
export NodeDrag, EdgeDrag

"""
    convert_selection(element, idx)

`mouse_selection` returns the basic plot type. In case of `Lines` check if it is
part of a `BezierSegments` and convert selection accordingly. In case of `LineSegments`
check if it is part of a `EdgePlot` and convert idx.
"""
convert_selection(element, idx) = (element, idx)
function convert_selection(element::LineSegments, idx)
    if element.parent isa EdgePlot
        return (element.parent, Int(idx / 2))
    end
    return (element, idx)
end
function convert_selection(element::Lines, idx)
    if element.parent isa BezierSegments && element.parent.parent isa EdgePlot
        bezier = element.parent
        idx = findfirst(isequal(element), bezier.plots)
        return (bezier.parent, idx)
    end
    return (element, idx)
end

"""
    abstract type GraphInteraction

This abstract type supertypes other `GraphInteractions`. Some `GraphInteractions` need
references to the scatter and linesegments plot of the `GraphPlot`. By overloding
`set_nodeplot!(T<:GraphInteraction)` and `set_edgeplot!` those references will be set
during `registration_setup!`.
"""
abstract type GraphInteraction end;
set_nodeplot!(::GraphInteraction, plot) = nothing
set_edgeplot!(::GraphInteraction, plot) = nothing

function registration_setup!(parent, inter::GraphInteraction)
    # in case of multiple graph plots in one axis this won't work
    gplots = filter(p -> p isa GraphPlot, parent.scene.plots)
    if length(gplots) !== 1
        @warn "There are multiple GraphPlots, register interaction to first!"
    end
    gplot = gplots[1]
    set_nodeplot!(inter, get_node_plot(gplot))
    set_edgeplot!(inter, get_edge_plot(gplot))
end

####
#### Hover Interaction
####

"""
    mutable struct HoverHandler{P<:ScenePlot, F} <: GraphInteraction

Object to handle hovers on `plot::P`. Calls `fun` on hover.
"""
mutable struct HoverHandler{P<:ScenePlot,F} <: GraphInteraction
    idx::Union{Nothing,Int}
    plot::Union{Nothing,P}
    fun::F
end
set_nodeplot!(h::HoverHandler{Scatter}, plot) = h.plot = plot
set_edgeplot!(h::HoverHandler{EdgePlot}, plot) = h.plot = plot

function process_interaction(handler::HoverHandler, event::MouseEvent, axis)
    if event.type === MouseEventTypes.over
        (element, idx) = convert_selection(mouse_selection(axis.scene)...)
        if element == handler.plot
            if handler.idx === nothing
                handler.idx = idx
                ret = handler.fun(true, handler.idx, event, axis)
                return ret isa Bool ? ret : false
            end
        else
            if handler.idx !== nothing
                ret = handler.fun(false, handler.idx, event, axis)
                handler.idx = nothing
                return ret isa Bool ? ret : false
            end
        end
    end
    return false
end

"""
    NodeHoverHandler(fun)

Initializes `HoverHandler` for nodes. Calls function

    fun(hoverstate, idx, event, axis)

with `hoverstate=true` on hover and `false` at the end of hover. `idx` is the node index.

# Example
```
julia> g = wheel_digraph(10)
julia> f, ax, p = graphplot(g, node_size = [20 for i in 1:nv(g)])
julia> function action(state, idx, event, axis)
           p.node_size[][idx] = state ? 40 : 20
           p.node_size[] = p.node_size[] #trigger observable
       end
julia> register_interaction!(ax, :nodehover, NodeHoverHandler(action))
```
"""
NodeHoverHandler(fun::F) where {F} = HoverHandler{Scatter,F}(nothing, nothing, fun)

"""
    NodeHoverHeighlight(p::GraphPlot, factor=2)

Magnifies the `node_size` of node under cursor by `factor`.

# Example
```
julia> g = wheel_graph(10)
julia> f, ax, p = graphplot(g, node_size = [20 for i in 1:nv(g)])
julia> register_interaction!(ax, :nodehover, NodeHoverHighlight(p))
```
"""
function NodeHoverHighlight(p::GraphPlot, factor=2)
    @assert p.node_size[] isa Vector{<:Real} "`node_size` object needs to be an Vector{<:Real} for this interaction to work!"
    action = (state, idx, _, _) -> begin
        old = p.node_size[][idx]
        p.node_size[][idx] = state ? old * factor : old / factor
        p.node_size[] = p.node_size[] #trigger observable
    end
    return NodeHoverHandler(action)
end

"""
    EdgeHoverHandler(fun)

Initializes `HoverHandler` for edges. Calls function

    fun(hoverstate, idx, event, axis)

with `hoverstate=true` on hover and `false` at the end of hover. `idx` is the edge index.

# Example
```
julia> g = wheel_digraph(10)
julia> f, ax, p = graphplot(g, edge_width = [3.0 for i in 1:ne(g)])
julia> function action(state, idx, event, axis)
           p.edge_width[][idx] = state ? 6.0 : 3.0
           p.edge_width[] = p.edge_width[] #trigger observable
       end
julia> register_interaction!(ax, :edgehover, EdgeHoverHandler(action))
```
"""
EdgeHoverHandler(fun::F) where {F} = HoverHandler{EdgePlot,F}(nothing, nothing, fun)

"""
    EdgeHoverHeighlight(p::GraphPlot, factor=2)

Magnifies the `edge_width` of edge under cursor by `factor`.
If `arrow_size isa Vector{<:Real}` it also magnefies the arrow scatter.

# Example
```
julia> g = wheel_digraph(10)
julia> f, ax, p = graphplot(g, edge_width = [3 for i in 1:ne(g)],
                               arrow_size=[10 for i in 1:ne(g)])
julia> register_interaction!(ax, :nodehover, EdgeHoverHighlight(p))
```
"""
function EdgeHoverHighlight(p::GraphPlot, factor=2)
    @assert p.edge_width[] isa Vector{<:Real} "`edge_width` object needs to be an Vector{<:Real} for this interaction to work!"
    scale_arrows = p.arrow_size[] isa Vector{<:Real}

    if length(p.edge_width[]) == ne(p[:graph][])
        action = (state, idx, _, _) -> begin
            old = p.edge_width[][idx]
            p.edge_width[][idx] = state ? old * factor : old / factor
            p.edge_width[] = p.edge_width[] #trigger observable
            if scale_arrows
                old = p.arrow_size[][idx]
                p.arrow_size[][idx] = state ? old * factor : old / factor
                p.arrow_size[] = p.arrow_size[] #trigger observable
            end
        end
    elseif length(p.edge_width[]) == 2 * ne(p[:graph][])
        action = (state, idx, _, _) -> begin
            oldA = p.edge_width[][2 * idx - 1]
            oldB = p.edge_width[][2 * idx]
            p.edge_width[][2 * idx - 1] = state ? oldA * factor : oldA / factor
            p.edge_width[][2 * idx] = state ? oldB * factor : oldB / factor
            p.edge_width[] = p.edge_width[] #trigger observable
            if scale_arrows
                old = p.arrow_size[][idx]
                p.arrow_size[][idx] = state ? old * factor : old / factor
                p.arrow_size[] = p.arrow_size[] #trigger observable
            end
        end
    else
        error("Can not make sense of `length(p.edge_width)`")
    end

    return EdgeHoverHandler(action)
end

####
#### Drag Interaction
####

"""
    mutable struct DragHandler{P<:ScenePlot, F} <: GraphInteraction

Object to handle left mous drags on `plot::P`.
"""
mutable struct DragHandler{P<:ScenePlot,F} <: GraphInteraction
    dragstate::Bool
    idx::Int
    plot::Union{Nothing,P}
    fun::F
end
set_nodeplot!(h::DragHandler{Scatter}, plot) = h.plot = plot
set_edgeplot!(h::DragHandler{EdgePlot}, plot) = h.plot = plot

function process_interaction(handler::DragHandler, event::MouseEvent, axis)
    if handler.dragstate == false # not in drag state
        if event.type === MouseEventTypes.leftdown
            # on leftdown save idx if happens to be on right element/plot
            (element, idx) = convert_selection(mouse_selection(axis.scene)...)
            handler.idx = element === handler.plot ? idx : 0
        elseif event.type === MouseEventTypes.leftdragstart && handler.idx != 0
            # if idx!=0 the last leftdown was on right element!
            handler.dragstate = true
        end
    elseif handler.dragstate == true # drag state
        if event.type === MouseEventTypes.leftdrag
            ret = handler.fun(true, handler.idx, event, axis)
            return ret isa Bool ? ret : false
        elseif event.type === MouseEventTypes.leftdragstop
            ret = handler.fun(false, handler.idx, event, axis)
            handler.idx = 0
            handler.dragstate = false
            return ret isa Bool ? ret : false
        end
    end
    return false
end

"""
    NodeDragHandler(fun)

Initializes `DragHandler` for Nodes. Calls function

    fun(dragstate, idx, event, axis)

where `dragstate=true` during the drag and `false` at the end of the drag,
the last time `fun` is triggered. `idx` is the node index.

# Example
```
julia> g = wheel_digraph(10)
julia> f, ax, p = graphplot(g, node_size=20)
julia> deregister_interaction!(ax, :rectanglezoom)
julia> function action(state, idx, event, axis)
           p[:node_pos][][idx] = event.data
           p[:node_pos][] = p[:node_pos][]
       end
julia> register_interaction!(ax, :nodedrag, NodeDragHandler(action))
```
"""
NodeDragHandler(fun::F) where {F} = DragHandler{Scatter,F}(false, 0, nothing, fun)

"""
    NodeDrag(p::GraphPlot)

Allows drag and drop of Nodes. Please deregister the `:rectanglezoom` interaction.

# Example
```
julia> g = wheel_graph(10)
julia> f, ax, p = graphplot(g, node_size = [10 for i in 1:nv(g)])
julia> deregister_interaction!(ax, :rectanglezoom)
julia> register_interaction!(ax, :nodehover, NodeHoverHighlight(p))
julia> register_interaction!(ax, :nodedrag, NodeDrag(p))
```
"""
function NodeDrag(p)
    action = (state, idx, event, _) -> begin
        p[:node_pos][][idx] = event.data
        p[:node_pos][] = p[:node_pos][]
    end
    return NodeDragHandler(action)
end

"""
    EdgeDragHandler(fun)

Initializes `DragHandler` for Edges. Calls function

    fun(dragstate, idx, event, axis)

where `dragstate=true` during the drag and `false` at the end of the drag,
the last time `fun` is triggered. `idx` is the edge index.

See [`EdgeDrag`](@ref) for a concrete implementation.
```
"""
EdgeDragHandler(fun::F) where {F} = DragHandler{EdgePlot,F}(false, 0, nothing, fun)

"""
    EdgeDrag(p::GraphPlot)

Allows drag and drop of Edges. Please deregister the `:rectanglezoom` interaction.

# Example
```
julia> g = wheel_graph(10)
julia> f, ax, p = graphplot(g, edge_width = [3 for i in 1:ne(g)])
julia> deregister_interaction!(ax, :rectanglezoom)
julia> register_interaction!(ax, :edgehover, EdgeHoverHighlight(p))
julia> register_interaction!(ax, :edgedrag, EdgeDrag(p))
```
"""
function EdgeDrag(p)
    action = EdgeDragAction(p)
    return EdgeDragHandler(action)
end

mutable struct EdgeDragAction{PT<:GraphPlot}
    p::PT
    init::Union{Nothing,Point2f} # save click position
    src::Union{Nothing,Point2f}  # save src vertex position
    dst::Union{Nothing,Point2f}  # save dst vertex position
    EdgeDragAction(p::T) where {T} = new{T}(p, nothing, nothing, nothing)
end

function (action::EdgeDragAction)(state, idx, event, _)
    edge = collect(edges(action.p[:graph][]))[idx]
    if state == true
        if action.src === action.dst === action.init === nothing
            action.init = event.data
            action.src = action.p[:node_pos][][edge.src]
            action.dst = action.p[:node_pos][][edge.dst]
        end
        offset = event.data - action.init
        action.p[:node_pos][][edge.src] = action.src + offset
        action.p[:node_pos][][edge.dst] = action.dst + offset
        action.p[:node_pos][] = action.p[:node_pos][] # trigger change
    elseif state == false
        action.src = action.dst = action.init = nothing
    end
end

####
#### Click Interaction
####

"""
    mutable struct ClickHandler{P<:ScenePlot, F} <: GraphInteraction

Object to handle left mouse clicks on `plot::P`.
"""
mutable struct ClickHandler{P<:ScenePlot,F} <: GraphInteraction
    plot::Union{Nothing,P}
    fun::F
end
set_nodeplot!(h::ClickHandler{Scatter}, plot) = h.plot = plot
set_edgeplot!(h::ClickHandler{EdgePlot}, plot) = h.plot = plot

function process_interaction(handler::ClickHandler, event::MouseEvent, axis)
    if event.type === MouseEventTypes.leftclick
        (element, idx) = convert_selection(mouse_selection(axis.scene)...)
        if element == handler.plot
            ret = handler.fun(idx, event, axis)
            return ret isa Bool ? ret : false
        end
    end
    return false
end

"""
    NodeClickHandler(fun)

Initializes `ClickHandler` for nodes. Calls function

    fun(idx, event, axis)

on left-click events where `idx` is the node index.

# Example
```
julia> using Makie.Colors
julia> g = wheel_digraph(10)
julia> f, ax, p = graphplot(g, node_size=30, node_color=[colorant"red" for i in 1:nv(g)])
julia> function action(idx, event, axis)
           p.node_color[][idx] = rand(RGB)
           p.node_color[] = p.node_color[]
       end
julia> register_interaction!(ax, :nodeclick, NodeClickHandler(action))
```
"""
NodeClickHandler(fun::F) where {F} = ClickHandler{Scatter,F}(nothing, fun)

"""
    EdgeClickHandler(fun)

Initializes `ClickHandler` for edges. Calls function

    fun(idx, event, axis)

on left-click events where `idx` is the edge index.

# Example
```
julia> using Makie.Colors
julia> g = wheel_digraph(10)
julia> f, ax, p = graphplot(g, edge_width=4, edge_color=[colorant"black" for i in 1:ne(g)])
julia> function action(idx, event, axis)
           p.edge_color[][idx] = rand(RGB)
           p.edge_color[] = p.edge_color[]
       end
julia> register_interaction!(ax, :edgeclick, EdgeClickHandler(action))
```
"""
EdgeClickHandler(fun::F) where {F} = ClickHandler{EdgePlot,F}(nothing, fun)


@testset "ProductArray" begin
    a = ProductArray(tuple, [1,2], [3.0,4.0])

    @test a isa ProductArray{Tuple{Int,Float64}, 2, typeof(tuple), Vector{Int}, Vector{Float64}}
    @test size(a) == (2,2)
    @test axes(a) === (Base.OneTo(2), Base.OneTo(2))
    @test a == [(1,3.0) (1,4.0); (2,3.0) (2,4.0)]
end