#module TestResampling

using Distributed
import ComputationalResources: CPU1, CPUProcesses, CPUThreads
using .TestUtilities
using ProgressMeter
import Tables

@everywhere begin
    using .Models
    using StableRNGs
    rng = StableRNG(1513515)
    const verb = 0
end

using Test
using MLJBase
import Distributions
import StatsBase
@static if VERSION >= v"1.3.0-DEV.573"
    using .Threads
end

struct DummyInterval <: Interval end
dummy_interval=DummyInterval()

dummy_measure_det(yhat, y) = 42
MLJBase.target_scitype(::typeof(dummy_measure_det)) = Table(MLJBase.Textual)
MLJBase.prediction_type(::typeof(dummy_measure_det)) = :deterministic

dummy_measure_interval(yhat, y) = [123, 456]
MLJBase.target_scitype(::typeof(dummy_measure_interval)) =
    Table(MLJBase.Textual)
MLJBase.prediction_type(::typeof(dummy_measure_interval)) = :interval

@testset "_actual_operations" begin
    clf = ConstantClassifier()
    rgs = ConstantRegressor()
    clf_det = DeterministicConstantClassifier()
    rgs_det = DeterministicConstantRegressor()
    measures = [LogLoss(), Accuracy(), BrierScore()] # mixed prob/determ
    measures_det = [Accuracy(), FScore()]
    operations = [predict, predict_mode, predict]

    # single measure gets replicated to match length of `measures`:
    @test MLJBase._actual_operations(predict_mean,
                                   [Accuracy(), FScore()],
                                    clf,
                                    1) ==
                                   [predict_mean, predict_mean]

    # handling of a measure with `:unknown` `prediction_type` (eg,
    # custom measure):
    my_mae(yhat, y) = abs.(yhat - y)
    @test(
        MLJBase._actual_operations(nothing, [my_mae, LPLoss()], rgs_det , 1) ==
        [predict, predict])

    @test MLJBase._actual_operations(predict, [LogLoss(),], clf, 1) ==
        [predict,]
    @test MLJBase._actual_operations(operations, measures, clf, 1) == operations
    @test_throws MLJBase.ERR_OPERATION_MEASURE_MISMATCH _ =
        MLJBase._actual_operations([predict, predict_mode], measures, clf, 1)
    @test_throws MLJBase.ERR_OPERATION_MEASURE_MISMATCH junk =
        MLJBase._actual_operations([predict,], measures, clf, 1)
    @test_throws MLJBase.ERR_INVALID_OPERATION _ =
        MLJBase._actual_operations(transform, [LogLoss(),], clf, 1)
    @test MLJBase._actual_operations(nothing, measures, clf, 1) == operations
    @test(
       @test_logs MLJBase._actual_operations(nothing, [Accuracy(),], clf, 1) ==
           [predict_mode])
    @test MLJBase._actual_operations(nothing, [l2,], rgs, 1) ==
        [predict_mean, ]
    @test_throws(MLJBase.err_incompatible_prediction_types(clf_det, LogLoss()),
                 MLJBase._actual_operations(nothing, [LogLoss(),], clf_det, 1))
    @test MLJBase._actual_operations(nothing, measures_det, clf_det, 1) ==
        [predict, predict]

    # measure/model differ in prediction type but weird target_scitype:
    @test_throws(
        MLJBase.err_ambiguous_operation(clf, dummy_measure_det),
        MLJBase._actual_operations(nothing, [dummy_measure_det, ], clf, 1))

    # measure has :interval prediction type but model does not (2 cases):
    @test_throws(
        MLJBase.err_ambiguous_operation(clf, dummy_measure_interval),
        MLJBase._actual_operations(nothing,
                                   [dummy_measure_interval, ], clf, 1))
    @test_throws(
        MLJBase.err_ambiguous_operation(clf_det, dummy_measure_interval),
        MLJBase._actual_operations(nothing,
                                   [dummy_measure_interval, ], clf_det, 1))

    # both measure and model have :interval prediction type:
    @test MLJBase._actual_operations(nothing,
                               [dummy_measure_interval, ],
                               dummy_interval, 1) == [predict, ]

    # model has :interval prediction type but measure does not:
    @test_throws(
        MLJBase.err_ambiguous_operation(dummy_interval, LogLoss()),
        MLJBase._actual_operations(nothing,
                                   [LogLoss(), ], dummy_interval, 1))
end

@testset "_feature_dependencies_exist" begin
    measures = Any[rms, rsq, log_loss, brier_score]
    @test !MLJBase._feature_dependencies_exist(measures)
    my_feature_dependent_loss(ŷ, X, y) =
        sum(abs.(ŷ - y) .* X.penalty)/sum(X.penalty);
    MLJBase.is_feature_dependent(::typeof(my_feature_dependent_loss)) = true
    push!(measures, my_feature_dependent_loss)
    @test MLJBase._feature_dependencies_exist(measures)
end

@testset_accelerated "dispatch of resources and progress meter" accel begin

    @info "Checking progress bars:"

    X = (x = [1, ],)
    y = [2.0, ]

    @everywhere begin
        nfolds = 6
        nmeasures = 2
        func(mach, k) = (
            (sleep(MLJBase.PROG_METER_DT*rand(rng)); fill(1:k, nmeasures)),
            :fitted_params,
            :report,
        )
    end
    mach = machine(ConstantRegressor(), X, y)
    if accel isa CPUThreads
        result = MLJBase._evaluate!(
            func,
            mach,
            CPUThreads(Threads.nthreads()),
            nfolds,
            1
        )
    else
        result = MLJBase._evaluate!(func, mach, accel, nfolds, 1)
    end
    measurements = vcat(result[1]...)
    @test measurements ==
        [1:1, 1:1, 1:2, 1:2, 1:3, 1:3, 1:4, 1:4, 1:5, 1:5, 1:6, 1:6]
    @test collect(result[2]) == fill(:fitted_params, nfolds)
end

@test CV(nfolds=6) == CV(nfolds=6)
@test CV(nfolds=5) != CV(nfolds=6)
@test MLJBase.train_test_pairs(CV(), 1:10) !=
     MLJBase.train_test_pairs(CV(shuffle=true), 1:10)
@test MLJBase.train_test_pairs(Holdout(), 1:10) !=
     MLJBase.train_test_pairs(Holdout(shuffle=true), 1:10)

@testset "train test pairs" begin
    cv = CV(nfolds=5)

    pairs = MLJBase.train_test_pairs(cv, 1:24)
    @test pairs == [
        (6:24, 1:5),
        ([1:5..., 11:24...], 6:10),
        ([1:10..., 16:24...], 11:15),
        ([1:15..., 21:24...], 16:20),
        (1:20, 21:24)
    ]

    # Not enough data for the number of folds.
    @test_throws ArgumentError MLJBase.train_test_pairs(cv, 1:4)
end

@testset "checking measure/model compatibility" begin
    model = ConstantRegressor()
    y = rand(rng,4)

    # model prediction type is Probablistic but measure is Deterministic:
    @test_throws(ArgumentError,
                  MLJBase._check_measure(rms, predict, model, y))

    @test MLJBase._check_measure(rms, predict_mean, model, y)

    @test MLJBase._check_measure(rms, predict_median, model, y)

    # has `y`  `Finite` elscityp but measure `rms` is for `Continuous`:
    y=categorical(collect("abc"))
    @test_throws(ArgumentError,
                 MLJBase._check_measure(rms, predict_median, model, y))
    model = ConstantClassifier()
    # model prediction type is Probablistic but measure is Deterministic:
    @test_throws(ArgumentError,
                 MLJBase._check_measure(mcr, predict, model, y))

    @test MLJBase._check_measure(mcr, predict_mode, model, y)

    # `Determistic` model but `Probablistic` measure:
    model = DeterministicConstantClassifier()
    @test_throws(ArgumentError,
                 MLJBase._check_measure(cross_entropy, predict, model, y))

    # measure with wrong target_scitype:
    @test_throws(ArgumentError,
                 MLJBase._check_measures([brier_score, rms],
                                         [predict_mode, predict_mean],
                                         model, y))

    model = ConstantClassifier()
    @test MLJBase._check_measures([brier_score, cross_entropy, accuracy],
                                  [predict, predict, predict_mode],
                                  model, coerce(y, Multiclass))
end

@testset "check weights" begin
    @test_throws(MLJBase.ERR_WEIGHTS_REAL,
                 MLJBase._check_weights([:junk, :junk], 2))
    @test_throws(MLJBase.ERR_WEIGHTS_LENGTH,
                 MLJBase._check_weights([0.5, 0.5], 3))
    @test MLJBase._check_weights([0.5, 0.5], 2)
end

@testset "check class weights" begin
    w = Dict('a'=> 0.2, 'b'=>0.8)
    @test_throws(MLJBase.ERR_WEIGHTS_DICT,
                 MLJBase._check_class_weights([0.1, 0.4], ['a', 'b']))
    @test_throws(MLJBase.ERR_WEIGHTS_CLASSES,
                 MLJBase._check_class_weights(w, ['a', 'c']))
    @test MLJBase._check_class_weights(w, ['b', 'a'])
end

@testset_accelerated "folds specified" accel begin
    x1 = ones(10)
    x2 = ones(10)
    X  = (x1=x1, x2=x2)
    y  = [1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0, 1.0, 1.0]

    my_rms(yhat, y) = sqrt(mean((yhat -y).^2))
    my_mae(yhat, y) = abs.(yhat - y)
    MLJBase.reports_each_observation(::typeof(my_mae)) = true
    MLJBase.prediction_type(::typeof(my_rms)) = :deterministic
    MLJBase.prediction_type(::typeof(my_mae)) = :deterministic

    resampling = [(3:10, 1:2),
                  ([1, 2, 5, 6, 7, 8, 9, 10], 3:4),
                  ([1, 2, 3, 4, 7, 8, 9, 10], 5:6),
                  ([1, 2, 3, 4, 5, 6, 9, 10], 7:8),
                  (1:8, 9:10)]

    for cache in [true, false]

        model = DeterministicConstantRegressor()
        mach  = machine(model, X, y, cache=cache)

        # check detection of incompatible measure (cross_entropy):
        @test_throws ArgumentError evaluate!(mach, resampling=resampling,
                                             measure=[cross_entropy, rmslp1],
                                             verbosity=verb,
                                             acceleration=accel)
        result = evaluate!(mach, resampling=resampling, verbosity=verb,
                           measure=[my_rms, my_mae, rmslp1], acceleration=accel)

        v = [1/2, 3/4, 1/2, 3/4, 1/2]

        @test result.per_fold[1] ≈ v
        @test result.per_fold[2] ≈ v
        @test result.per_fold[3][1] ≈ abs(log(2) - log(2.5))
        @test ismissing(result.per_observation[1])
        @test result.per_observation[2][1] ≈ [1/2, 1/2]
        @test result.per_observation[2][2] ≈ [3/4, 3/4]
        @test result.measurement[1] ≈ mean(v)
        @test result.measurement[2] ≈ mean(v)

        # fitted_params and report per fold:
        @test map(fp->fp.fitresult, result.fitted_params_per_fold) ≈
            [1.5, 1.25, 1.5, 1.25, 1.5]
        @test all(==(NamedTuple()), result.report_per_fold)
    end
end

@testset "repeated resampling" begin
    x1 = ones(20)
    x2 = ones(20)
    X = (x1=x1, x2=x2)
    y = rand(rng, 20)

    holdout = Holdout(fraction_train=0.75, rng=rng)
    model = Models.DeterministicConstantRegressor()

    for cache in [true, false]
        mach = machine(model, X, y, cache=cache)
        result = evaluate!(mach, resampling=holdout, verbosity=verb,
                       measure=[rms, rmslp1], repeats=6)
        per_fold = result.per_fold[1]
        @test unique(per_fold) |> length == 6
        @test abs(mean(per_fold) - std(y)) < 0.06 # very rough check

        cv = CV(nfolds=3, rng=rng)
        result = evaluate!(mach, resampling=cv, verbosity=verb,
                       measure=[rms, rmslp1], repeats=6)
        per_fold = result.per_fold[1]
        @test unique(per_fold) |> length == 18
        @test abs(mean(per_fold) - std(y)) < 0.06 # very rough check
    end
end

@testset_accelerated "holdout" accel begin
    x1 = ones(4)
    x2 = ones(4)
    X = (x1=x1, x2=x2)
    y = [1.0, 1.0, 2.0, 2.0]

    @test MLJBase.show_as_constructed(Holdout)
    holdout = Holdout(fraction_train=0.75)
    model = Models.DeterministicConstantRegressor()
    for cache in [true, false]
        mach = machine(model, X, y, cache=cache)
        result = evaluate!(mach, resampling=holdout, verbosity=verb,
                           measure=[rms, rmslp1], acceleration=accel)
        result = evaluate!(mach, resampling=holdout, verbosity=verb,
                           acceleration=accel)
        @test result.measurement[1] ≈ 2/3

        # test direct evaluation of a model + data:
        result = evaluate(model, X, y, verbosity=0,
                          resampling=holdout, measure=rms, cache=cache)
        @test result.measurement[1] ≈ 2/3
    end

    X = (x=rand(rng,100),)
    y = rand(rng,100)

    for cache in [true, false]
        mach = machine(model, X, y, cache=cache)
        evaluate!(mach, verbosity=verb,
                  resampling=Holdout(shuffle=true, rng=rng), acceleration=accel)
        e1 = evaluate!(mach, verbosity=verb,
                       resampling=Holdout(shuffle=true),
                       acceleration=accel).measurement[1]
        @test e1 != evaluate!(mach, verbosity=verb,
                              resampling=Holdout(),
                              acceleration=accel).measurement[1]
    end
end

@testset_accelerated "Exception handling (see issue 235)" accel begin
    X, y = make_moons(50)
    model = ConstantClassifier()

    bad_loss(yhat, y) = throw(Exception())
    @test_throws Exception evaluate(model, X, y, measure=bad_loss, verbosity=0)
end

@testset_accelerated "cv" accel begin
    x1 = ones(10)
    x2 = ones(10)
    X = (x1=x1, x2=x2)
    y = [1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0, 1.0, 1.0]

    @test MLJBase.show_as_constructed(CV)
    cv=CV(nfolds=5)
    for cache in [true, false]
        model = Models.DeterministicConstantRegressor()
        mach = machine(model, X, y, cache=cache)
        result = evaluate!(mach, resampling=cv, measure=[rms, rsq, rmslp1],
                           acceleration=accel, verbosity=verb)

        @test result.per_fold[1] ≈ [1/2, 3/4, 1/2, 3/4, 1/2]

        shuffled = evaluate!(mach, resampling=CV(shuffle=true), verbosity=verb,
                             acceleration=accel) # using rms default
        @test shuffled.measurement[1] != result.measurement[1]
    end
end

@testset "TimeSeriesCV" begin
    tscv = TimeSeriesCV(; nfolds=3)

    pairs = MLJBase.train_test_pairs(tscv, 1:10)
    @test pairs == [
        (1:4, [5, 6]),
        (1:6, [7, 8]),
        (1:8, [9, 10])
    ]

    pairs = MLJBase.train_test_pairs(tscv, 1:2:15)
    @test pairs == [
        ([1, 3], [5, 7])
        ([1, 3, 5, 7], [9, 11])
        ([1, 3, 5, 7, 9, 11], [13, 15])
    ]

    @test_logs(
        (:warn, "TimeSeriesCV is being applied to `rows` not in sequence. "),
        MLJBase.train_test_pairs(tscv, reverse(1:10))
    )

    # Not enough data for the number of folds.
    @test_throws ArgumentError MLJBase.train_test_pairs(TimeSeriesCV(10), 1:8)
end

@testset "stratified_cv" begin

    # check in explicit example:
    y = categorical(
        ['b', 'c', 'a', 'b', 'b', 'b', 'c', 'c', 'c', 'a', 'a', 'a']
    )
    scv = StratifiedCV(nfolds=3)
    rows = 1:12
    pairs = MLJBase.train_test_pairs(scv, rows, y)
    expected_pairs = [
        ([2, 4, 9, 11, 3, 5, 7, 12], [1, 6, 8, 10]),
        ([1, 6, 8, 10, 3, 5, 7, 12], [2, 4, 9, 11]),
        ([1, 6, 8, 10, 2, 4, 9, 11], [3, 5, 7, 12])
    ]
    # Explanation of expected_pairs: The row indices are processed one at
    # a time. The test fold that a row index is placed in is determined
    # by this lookup:
    #
    # b b b b c c c c a a a a
    # 1 2 3 1 2 3 1 2 3 1 2 3
    #
    # For example, the first row such that y[row] == 'c' is placed in the
    # second fold, and the second row such that y[row] == 'c' is placed in
    # the third fold.
    @test pairs == expected_pairs

    # test invariance to label renaming:
    z = replace(y, 'a' => 'b', 'b' => 'c', 'c' => 'a')
    pairs = MLJBase.train_test_pairs(scv, rows, z)
    @test pairs == expected_pairs

    # test the case where rows is a shuffled subset of y:
    y = categorical(['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c'])
    rows = 8:-1:2
    pairs = MLJBase.train_test_pairs(scv, rows, y)
    @test pairs == [
        ([5, 4, 6, 2], [8, 7, 3]),
        ([8, 7, 3, 6, 2], [5, 4]),
        ([8, 7, 3, 5, 4], [6, 2])
    ]

    # test shuffle:
    scv_random = StratifiedCV(nfolds=3, shuffle=true, rng=1)
    pairs_random = MLJBase.train_test_pairs(scv_random, rows, y)
    @test pairs != pairs_random

    # wrong target type throws error:
    @test_throws(ArgumentError,
                 MLJBase.train_test_pairs(scv,
                                          rows,
                                          CategoricalArrays.unwrap.(y)))

    # check class distribution is preserved in a larger randomized example:
    N = 30
    y = shuffle(vcat(fill('a', N), fill('b', 2N),
                        fill('c', 3N), fill('d', 4N))) |> categorical;
    d = Distributions.fit(MLJBase.UnivariateFinite, y)
    pairs = MLJBase.train_test_pairs(scv, 1:10N, nothing, y)
    folds = vcat(first.(pairs), last.(pairs))
    @test all([Distributions.fit(MLJBase.UnivariateFinite, y[fold]) ≈
               d for fold in folds])
end

@testset_accelerated "sample weights in evaluation" accel begin
    # cv:
    x1 = ones(4)
    x2 = ones(4)
    X = (x1=x1, x2=x2)
    y = [1.0, 2.0, 3.0, 1.0]
    w = 1:4
    cv=CV(nfolds=2)
    model = Models.DeterministicConstantRegressor()
    mach = machine(model, X, y)
    e = evaluate!(mach, resampling=cv, measure=l1,
                  weights=w, verbosity=verb, acceleration=accel).measurement[1]
    efold1 = mean([1*1, 1*0])
    efold2 = mean([3*3/2, 4*1/2])
    @test e ≈ mean([efold1, efold2])

    # if I don't specify weights in `evaluate!`, then uniform should
    # be used:
    e = evaluate!(mach, resampling=cv, measure=l1,
                  verbosity=verb, acceleration=accel).measurement[1]
    efold1 = mean([1*1, 1*0])
    efold2 = mean([1*3/2, 1*1/2])
    @test e ≈ mean([efold1, efold2])
end

@testset_accelerated "class weights in evaluation" accel begin
    x = [1,2,3,4,5,6,7]
    X, y = Tables.table([x x x x x x]), coerce([1,2,1,3,1,2,2], Multiclass)
    model = Models.DeterministicConstantClassifier()
    mach = machine(model, X, y)
    cv=CV(nfolds = 2)
    class_w = Dict(1=>1, 2=>2, 3=>3)
    e = evaluate!(mach,
                  resampling=cv,
                  measure=MLJBase.MulticlassFScore(return_type=Vector),
                  class_weights=class_w,
                  verbosity=verb,
                  acceleration=accel).measurement[1]
    @test round(e, digits=3) ≈ 0.217

    # if class weights in `evaluate!` isn't specified:
    e = evaluate!(mach, resampling=cv, measure=multiclass_f1score,
                  verbosity=verb, acceleration=accel).measurement[1]
    @test e ≈ 0.15
end

@testset_accelerated "resampler as machine" accel begin
    N = 50
    X = (x1=rand(rng, N), x2=rand(rng, N), x3=rand(rng, N))
    y = X.x1 -2X.x2 + 0.05*rand(rng, N)
    ridge_model = FooBarRegressor(lambda=20.0)
    holdout = Holdout(fraction_train=0.75)
    resampler = Resampler(resampling=holdout, model=ridge_model, measure=mae,
                          acceleration=accel)
    resampling_machine = machine(resampler, X, y)
    @test_logs((:info, r"^Training"), fit!(resampling_machine))
    e1=evaluate(resampling_machine).measurement[1]
    mach = machine(ridge_model, X, y)
    @test e1 ≈  evaluate!(mach, resampling=holdout,
                          measure=mae, verbosity=verb,
                          acceleration=CPU1()).measurement[1]
    ridge_model.lambda=1.0
    fit!(resampling_machine, verbosity=verb)
    e2=evaluate(resampling_machine).measurement[1]
    @test e1 != e2
    resampler.weights = rand(rng,N)
    fit!(resampling_machine, verbosity=verb)
    e3=evaluate(resampling_machine).measurement[1]
    @test e3 != e2

    @test MLJBase.package_name(Resampler) == "MLJBase"
    @test MLJBase.is_wrapper(Resampler)

    # when only `model` changes, the folds shouldn't change, even in
    # shuffled case:
    cv = CV(rng=StableRNGs.StableRNG(123))
    resampler=Resampler(model=ridge_model,
                        resampling=cv,
                        repeats=3,
                        measure=mae, acceleration=accel)
    mach = machine(resampler, X, y)
    fit!(mach, verbosity=verb)
    ev1 = evaluate(mach)
    rows1 = ev1.train_test_rows
    resampler.model.lambda *= 0.5
    fit!(mach, verbosity=verb)
    ev2 = evaluate(mach)
    @test rows1 == ev2.train_test_rows
    resampler.model.lambda *= 2
    fit!(mach, verbosity=verb)
    @test ev1.measurement[1] ≈ evaluate(mach).measurement[1]

    # but if `resampling` or `repeats`, then new
    # folds should be generated:
    resampler.resampling = CV(rng=cv.rng, nfolds=2)
    fit!(mach, verbosity=verb)
    rows2 = evaluate(mach).train_test_rows
    @test length(rows2) == 2 * resampler.repeats
    @test rows2 != rows1
    resampler.repeats += 1
    fit!(mach, verbosity=verb)
    rows3 = evaluate(mach).train_test_rows
    @test length(rows3) == 2 * resampler.repeats
end

struct DummyResamplingStrategy <: MLJBase.ResamplingStrategy end

@testset_accelerated "custom strategy depending on X, y" accel begin
    function MLJBase.train_test_pairs(resampling::DummyResamplingStrategy,
                              rows, X, y)
        train = filter(rows) do j
            y[j] == y[1]
        end
        test = setdiff(rows, train)
        return [(train, test),]
    end

    X = (x = rand(rng,8), )
    y = categorical(string.([:x, :y, :x, :x, :y, :x, :x, :y]))
    @test MLJBase.train_test_pairs(DummyResamplingStrategy(), 2:6, X, y) ==
        [([3, 4, 6], [2, 5]),]

    e = evaluate(ConstantClassifier(), X, y,
                 measure=misclassification_rate,
                 resampling=DummyResamplingStrategy(),
                 operation=predict_mode,
                 acceleration=accel,
                 verbosity=verb)
    @test e.measurement[1] ≈ 1.0
end

@testset_accelerated "sample weights in training and evaluation" accel begin
    yraw = ["Perry", "Antonia", "Perry", "Antonia", "Skater"]
    X = (x=rand(rng,5),)
    y = categorical(yraw)
    w = [1, 10, 1, 10, 5]

    for cache in [true, false]
        # without weights:
        mach = machine(ConstantClassifier(), X, y, cache=cache)
        e = evaluate!(mach, resampling=Holdout(fraction_train=0.6),
                      operation=predict_mode, measure=misclassification_rate,
                      acceleration=accel, verbosity=verb)
        @test e.measurement[1] ≈ 1.0

        # with weights in training and evaluation:
        mach = machine(ConstantClassifier(), X, y, w, cache=cache)
        e = evaluate!(mach, resampling=Holdout(fraction_train=0.6),
                      operation=predict_mode, measure=misclassification_rate,
                      acceleration=accel, verbosity=verb, weights=w)
        @test e.measurement[1] ≈ mean([10*0, 5*1])

        # with different weights in training and evaluation:
        e = evaluate!(mach, resampling=Holdout(fraction_train=0.6),
                      operation=predict_mode, measure=misclassification_rate,
                      weights = fill(1, 5), acceleration=accel, verbosity=verb)
        @test e.measurement[1] ≈ 1/2

        @test_throws(DimensionMismatch,
                     evaluate!(mach, resampling=Holdout(fraction_train=0.6),
                               operation=predict_mode,
                               measure=misclassification_rate,
                               weights = fill(1, 100), acceleration=accel,
                               verbosity=verb))

        @test_throws(ArgumentError,
                     evaluate!(mach, resampling=Holdout(fraction_train=0.6),
                               operation=predict_mode,
                               measure=misclassification_rate,
                               weights = fill('a', 5), acceleration=accel,
                               verbosity=verb))
    end

    # resampling on a subset of all rows:
    model = KNNClassifier()

    N = 200
    X = (x = rand(rng,3N), );
    y = categorical(rand(rng,"abcd", 3N));
    w = rand(rng,3N);
    class_w = Dict(zip(levels(y), rand(length(levels(y)))));
    rows = StatsBase.sample(1:3N, 2N, replace=false);
    Xsmall = selectrows(X, rows);
    ysmall = selectrows(y, rows);
    wsmall = selectrows(w, rows);

    for cache in [true, false]
        mach1 = machine(model, Xsmall, ysmall, wsmall, cache=cache)
        e1 = evaluate!(mach1,
                       resampling=CV(),
                       measure=misclassification_rate,
                       weights=wsmall,
                       operation=predict_mode,
                       acceleration=accel,
                       verbosity=verb)

        mach2 = machine(model, X, y, w, cache=cache)
        e2 = evaluate!(mach2,
                       resampling=CV(),
                       measure=misclassification_rate,
                       weights=w,
                       operation=predict_mode,
                       rows=rows,
                       acceleration=accel,
                       verbosity=verb)

        @test e1.per_fold ≈ e2.per_fold
    end

    for cache in [true, false]

        # resampler as machine with evaluation weights not specified:
        resampler = Resampler(model=model, resampling=CV();
                              measure=misclassification_rate,
                              operation=predict_mode,
                              cache=cache)
        resampling_machine = machine(resampler, X, y, w, cache=false)
        fit!(resampling_machine, verbosity=verb)
        e1 = evaluate(resampling_machine).measurement[1]
        mach = machine(model, X, y, w, cache=cache)
        e2 = evaluate!(mach, resampling=CV();
                       measure=misclassification_rate,
                       operation=predict_mode,
                       acceleration=accel, verbosity=verb).measurement[1]

        @test e1 ≈ e2

        # resampler as machine with evaluation weights specified:
        weval = rand(rng,3N);
        resampler = Resampler(model=model, resampling=CV();
                              measure=misclassification_rate,
                              operation=predict_mode,
                              weights=weval, acceleration=accel,
                              cache=cache)
        resampling_machine = machine(resampler, X, y, w, cache=false)
        fit!(resampling_machine, verbosity=verb)
        e1   = evaluate(resampling_machine).measurement[1]
        mach = machine(model, X, y, w, cache=cache)
        e2   = evaluate!(mach, resampling=CV();
                         measure=misclassification_rate,
                         operation=predict_mode,
                         weights=weval,
                         acceleration=accel,
                         verbosity=verb).measurement[1]

        @test e1 ≈ e2

    end

    x = [1,2,3,4,5,6,7]
    X, y = Tables.table([x x x x x x]), coerce([1,2,1,3,1,2,2], Multiclass)
    model = Models.DeterministicConstantClassifier()
    class_w = Dict(zip(levels(y), rand(length(levels(y)))))

    for cache in [true, false]

        #resampler as a machine with class weights specified
        cweval = Dict(zip(levels(y), rand(length(levels(y)))));
        resampler = Resampler(model=model, resampling=CV(nfolds=2);
                              measure=MulticlassFScore(return_type=Vector),
                              class_weights=cweval, acceleration=accel)
        resampling_machine = machine(resampler, X, y, cache=false)
        fit!(resampling_machine, verbosity=verb)
        e1   = evaluate(resampling_machine).measurement[1]
        mach = machine(model, X, y, cache=cache)
        e2   = evaluate!(mach, resampling=CV(nfolds=2);
                         measure=MulticlassFScore(return_type=Vector),
                         class_weights=cweval,
                         acceleration=accel,
                         verbosity=verb).measurement[1]

        @test e1 ≈ e2

    end

    @testset "warnings about measures not supporting weights" begin
        model = ConstantClassifier()
        N = 100
        X, y = make_moons(N)
        class_weights = Dict(0=>0.4, 1=>0.6)
        @test_logs((:warn, r"Sample weights"),
                   evaluate(model, X, y,
                            resampling=Holdout(fraction_train=0.5),
                            measure=auc, weights=ones(N)))
        @test_logs((:warn, r"Class weights"),
                   evaluate(model, X, y,
                            resampling=Holdout(fraction_train=0.5),
                            measure=auc, class_weights=class_weights))
    end

end

@testset_accelerated "automatic operations - integration" accel begin
    clf = ConstantClassifier()
    X, y = make_moons(100)
    e1 = evaluate(clf, X, y, resampling=CV(),
                  measures=[LogLoss(), Accuracy()], verbosity=1)
    e2 = evaluate(clf, X, y, resampling=CV(),
                  operation=[predict, predict_mode],
                  measures=[LogLoss(), Accuracy()], verbosity=1)
    @test e1.measurement ≈ e2.measurement
    evaluate(clf, X, y, resampling=CV(),
             operation=predict,
             measures=[LogLoss(), BrierScore()], verbosity=0)
end

#end
true


# Profile data processing. Skip plotting for now.

using RosDataProcess
using ProfileView
Profile.init(100000000, 0.001)

function profile_fun()
  println("Entering profiling code")

  bag_directory = "$(homedir())/bagfiles/decentralized_exploration_journal/"
  bags = load_directory(bag_directory)

  # show topics
  println("Topic names:")
  foreach(println, get_topic_names(bags[1]))

  params = Dict(:num_robots => "num_robots",
                :num_decentralized =>
                "kinematic_exploration/num_decentralized_planning_rounds")

  ranges = Dict(key => get_range(value, bags) for (key, value) in params)
  for num_robots = ranges[:num_robots][1:1]
    for (ii, num_decentralized) = enumerate(ranges[:num_decentralized][1:1])
      fixed = Dict(params[:num_robots] => num_robots,
                   params[:num_decentralized] => num_decentralized)
      trials = filter_bags(fixed, bags)

      entropy = read_series("/kinematic_exploration/iteration",
                            "/kinematic_exploration/entropy_reduction", trials;
                            access_data=x->x.data,
                            access_time=x-> num_robots * x.data,
                            intersect=true)
    end
  end

  foreach(close, bags)

  nothing
end

profile_fun()

@profile foreach(1:3) do _
  @time profile_fun()
end

ProfileView.view()


function examine_order(closed_order, id_str, client, config, bot_path)
    tmp_order = deepcopy(closed_order)
    order_check = Dict()
    order_check["orders"] = tmp_order

    tmp_pro    = 0
    order_check      = Dict()
    order_check["buys_exec"]  = 0
    order_check["total_vol"]  = 0
    order_check["use_vol"]    = 0
    order_check["commission"] = 0
    order_check["commission_coin"] = 0
    order_check["buy_vol"] = 0
    order_check["sell_vol"] = 0
    for (k2, v2) in tmp_order
        buy_sign = 1
        if v2["side"] == "BUY"
            order_check["buys_exec"] += 1
            buy_sign = -1
        end
        sleep(0.3)
        if !haskey(v2, "trades")
            trades = try client.get_my_trades(symbol=v2["symbol"], orderId=v2["orderId"])
                catch
                @info("Getting trades failed.")
                return
            end
            v2["trades"] = trades
            v2 = try add_fees_to_closed_order(v2, config, bot_path, client)
                catch
                @info("Adding fees failed.")
                return
            end
            closed_orders = JSON.parsefile(joinpath(bot_path, "closed_orders.json"));
            closed_orders[id_str][k2] = v2
            open(joinpath(bot_path, "closed_orders.json"), "w") do f
                JSON.print(f, closed_orders, 4)
            end
        else
            for trade in v2["trades"]
            @info("Trades already exist")
                if !haskey(trade, "com")
                    @info("No comissions stored")
                    v2 = try add_fees_to_closed_order(v2, config, bot_path, client)
                        catch
                        @info("Adding fees failed.")
                        return
                    end
                    closed_orders = JSON.parsefile(joinpath(bot_path, "closed_orders.json"));
                    closed_orders[id_str][k2] = v2
                    open(joinpath(bot_path, "closed_orders.json"), "w") do f
                        JSON.print(f, closed_orders, 4)
                    end
                end
            end
        end

        for t in v2["trades"]
            tmp_pro += buy_sign * parse(Float64, t["quoteQty"])
            tmp_pro -= t["com"]
            if buy_sign > 0
                order_check["sell_vol"] += parse(Float64, t["qty"])
            else
                order_check["buy_vol"] += parse(Float64, t["qty"])
            end
            order_check["total_vol"]  += -1 * buy_sign * parse(Float64, t["qty"])
            order_check["use_vol"]    += -1 * buy_sign * parse(Float64, t["qty"])
            if t["commissionAsset"] == config["farm_coin"]
                order_check["use_vol"] -= t["com_coin"]
            end
            order_check["commission"] += t["com"]
            order_check["commission_coin"] += t["com_coin"]
        end
        closed_orders = JSON.parsefile(joinpath(bot_path, "closed_orders.json"));
        if tmp_order != closed_orders[id_str]
            closed_orders[id_str] = tmp_order
            open(joinpath(bot_path, "closed_orders.json"), "w") do f
                JSON.print(f, closed_orders, 4)
            end
        end
    end
    order_check["current_profit"] = tmp_pro
    order_check["price_correction"]  = order_check["total_vol"] / order_check["use_vol"]
    return order_check
end
export examine_order


"""
Code declaring ChunkList, ChunkListTimeseries, and their abstract versions.

Author: Eric Ford
Created: August 2020
"""

abstract type AbstractChunkList end

""" Struct containing an array of ChunkOfSpectrum """
struct ChunkList{CT<:AbstractChunkOfSpectrum, ACT<:AbstractArray{CT,1}, OT<:Integer, AOT<:AbstractArray{OT,1} } <: AbstractChunkList
      data::ACT
      order::AOT

      function ChunkList{CT,ACT,OT,AOT}(chunks::ACT, orders::AOT) where {CT<:AbstractChunkOfSpectrum, ACT<:AbstractArray{CT,1}, OT<:Integer, AOT<:AbstractArray{OT,1} }
          #@assert length(chunks)>=1
          @assert length(chunks) == length(orders)
          new(chunks,orders)
      end
end

function ChunkList(chunks::ACT, orders::AOT) where {CT<:AbstractChunkOfSpectrum, ACT<:AbstractArray{CT,1}, OT<:Integer, AOT<:AbstractArray{OT,1} }
        ChunkList{CT,ACT,OT,AOT}(chunks,orders)
end
import Base.getindex, Base.setindex!, Base.append!

""" Provide direct access to data, an AbstractArray of ChunkOfSpectrum's via [] operator """
function getindex(x::CLT,idx) where {CLT<:AbstractChunkList}
    x.data[idx]
end

""" Provide direct access to data, an AbstractArray of ChunkOfSpectrum's via [] operator """
function setindex!(x::CLT, idx, y::CLT) where {CLT<:AbstractChunkList}
    x.data[idx] .= y.data[idx]
end

""" Append second chunk list to the first.  """
function append!(x::CLT, y::CLT) where {CLT<:AbstractChunkList}
    append!(x.data,y.data)
end

import Base.length
""" Return number of chunks in ChunkList """
length(cl::CLT) where {CLT<:AbstractChunkList} = length(cl.data)

abstract type AbstractChunkListTimeseries end

""" Mtching lists of times and array of ChunkLists """
struct ChunkListTimeseries{ TT<:Real, AT<:AbstractArray{TT,1}, CLT<:AbstractChunkList, ACLT<:AbstractArray{CLT,1}, InstT<:AbstractInstrument, AVMT<:AbstractVector{MetadataT} } <: AbstractChunkListTimeseries
    times::AT
    chunk_list::ACLT
    inst::InstT
    metadata::AVMT
end

function ChunkListTimeseries(times::AT, clt::ACLT; inst::InstT = TheoreticalInstrument1D(),
            metadata::AbstractVector{MetadataT} = fill(MetadataT,length(times)) ) where {
                TT<:Real, AT<:AbstractArray{TT,1}, CLT<:AbstractChunkList, ACLT<:AbstractArray{CLT,1},  InstT<:AbstractInstrument, AVMT<:AbstractVector{MetadataT} }
    #ChunkListTimeseries{eltype(times),typeof(times),eltype(clt),typeof(clt),typeof(inst),typeof(metadata)}(times,clt,inst,metadata)
    ChunkListTimeseries(times,clt,inst,metadata)
end

#=
function ChunkListTimeseries(t::AT, cl::ACLT) where {CLT<:AbstractChunkList, ACLT<:AbstractArray{CLT,1}, TT<:Real, AT<:AbstractArray{TT,1} }
    ChunkListTimeseries{CLT,ACLT,TT,AT}(t,cl)
end
=#
import Base.getindex, Base.setindex!

""" Allow direct access to data, an AbstractArray of ChunkOfSpectrum's via [] operator """
function getindex(x::CLT,idx) where {CLT<:AbstractChunkListTimeseries}
    x.chunk_list[idx]
end

""" Allow direct access to data, an AbstractArray of ChunkOfSpectrum's via [] operator """
function setindex!(x::CLT, idx, y::CLT) where {CLT<:AbstractChunkListTimeseries}
    x.times[idx] .= y.times[idx]
    x.chunk_list[idx] .= y.chunk_list[idx]
    x.metadata[idx] .= y.metadata[idx]
end

""" Return number of times/ChunkLists in a ChunkListTimeseries """
length(clts::ACLT) where {ACLT<:AbstractChunkListTimeseries} = length(clts.chunk_list)
""" Return number of times/ChunkLists in a ChunkListTimeseries """
num_times(clts::ACLT) where {ACLT<:AbstractChunkListTimeseries} = length(clts.chunk_list)

""" Number of chunks in first chunk_list in chunk_list_timeseries. """
num_chunks(chunk_list_timeseries::ACLT) where {ACLT<:AbstractChunkListTimeseries} = length(first(chunk_list_timeseries.chunk_list))

""" Get time of ith observation in chunk_list_timeseries.  """
time(chunk_list_timeseries::ACLT, i::Integer) where {ACLT<:AbstractChunkListTimeseries} = chunk_list_timeseries.times[i]

""" Get metadata[:key] of ith observation in chunk_list_timeseries.  """
metadata(chunk_list_timeseries::ACLT, i::Integer, key::Symbol) where {ACLT<:AbstractChunkListTimeseries} = chunk_list_timeseries.metadata[i][key]

""" Get instrument used for chunk_list_timeseries.  """
instrument(chunk_list_timeseries::ACLT) where {ACLT<:AbstractChunkListTimeseries} = chunk_list_timeseries.inst

""" Make a ChunkListTimeseries containing a subset of observations from an existing  ChunkListTimeseries """
function extract_chunklist_timeseries_with_subset_obs( clt::AbstractChunkListTimeseries, ref_obs_idx::Union{Integer,AbstractVector{Integer}} ) where { CLT<:AbstractChunkListTimeseries }
  if typeof(ref_obs_idx) <: Integer
      @assert 1 <= ref_obs_idx <= num_times(clt)
  else
      @assert all(1 .<= ref_obs_idx .<= num_times(clt))
  end
  ChunkListTimeseries([clt.times[ref_obs_idx]], [clt.chunk_list[ref_obs_idx]], clt.inst, [clt.metadata[ref_obs_idx]]   )
end


#############################################################################
# Joulia
# A Large-Scale Spatial Power System Model for Julia
# See https://github.com/JuliaEnergy/Joulia.jl
#############################################################################
# This file is the main package file

module Joulia

using DataFrames
using JuMP
using ProgressMeter

# utility functions
include("util.jl")

# power plants
include("powerplants.jl")

# nodes
include("nodes.jl")

# lines
include("lines.jl")

# renewables
include("res.jl")

# storages
include("storages.jl")

# model
include("model.jl")

export
    PowerPlants,
    Lines,
    Nodes,
    RenewableEnergySource,
    Storages,
    week_slices,
    JouliaModel,
    run_model

end # module


# MIT License
#
# Copyright (c) 2018 Martin Biel
#
# 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.

"""
    SerialExecution

Functor object for using serial execution in a quasi-gradient algorithm. Create by supplying a [`Serial`](@ref) object through `execution` in `QuasiGradient.Optimizer` or by setting the [`Execution`](@ref) attribute.

"""
struct SerialExecution{T <: AbstractFloat, S <: AbstractSubProblem{T}} <: AbstractQuasiGradientExecution
    subproblems::Vector{S}
    decisions::DecisionMap

    function SerialExecution(structure::StageDecompositionStructure{2, 1, <:Tuple{ScenarioProblems}}, x::AbstractVector, subproblems::Unaltered, ::Type{T}) where T <: AbstractFloat
        execution = new{T,SubProblem{T}}(Vector{SubProblem{T}}(), structure.decisions[2])
        initialize_subproblems!(execution, scenarioproblems(structure, 2))
        return execution
    end

    function SerialExecution(structure::StageDecompositionStructure{2, 1, <:Tuple{ScenarioProblems}}, x::AbstractVector, subproblems::Smoothed, ::Type{T}) where T <: AbstractFloat
        execution = new{T,SmoothSubProblem{T}}(Vector{SmoothSubProblem{T}}(), structure.decisions[2])
        initialize_subproblems!(execution, scenarioproblems(structure, 2), x; type2dict(subproblems.parameters)...)
        return execution
    end
end

function initialize_subproblems!(execution::SerialExecution{T,SubProblem{T}}, scenarioproblems::ScenarioProblems) where T <: AbstractFloat
    for i in 1:num_subproblems(scenarioproblems)
        push!(execution.subproblems, SubProblem(
            subproblem(scenarioproblems, i),
            i,
            T(probability(scenario(scenarioproblems, i)))))
    end
    return nothing
end

function initialize_subproblems!(execution::SerialExecution{T,SmoothSubProblem{T}},
                                 scenarioproblems::ScenarioProblems,
                                 x::AbstractVector;
                                 kw...) where T <: AbstractFloat
    for vi in all_known_decisions(execution.decisions)
        # Unfix first-stage decisions
        execution.decisions[vi].state = NotTaken
    end
    # Load smooth subproblems (through Moreau envelope)
    for i in 1:num_subproblems(scenarioproblems)
        push!(execution.subproblems, SmoothSubProblem(
            subproblem(scenarioproblems, i),
            i,
            T(probability(scenario(scenarioproblems, i))),
            x; kw...))
    end
    return nothing
end

function resolve_subproblems!(quasigradient::AbstractQuasiGradient, execution::SerialExecution{T,SubProblem{T}}) where T <: AbstractFloat
    # Update subproblems
    update_known_decisions!(execution.decisions, quasigradient.x)
    # Initialize subgradient
    quasigradient.gradient .= quasigradient.c
    Q = zero(T)
    # Update and solve subproblems
    for subproblem in execution.subproblems
        update_subproblem!(subproblem)
        subgradient = subproblem(quasigradient.x)
        if isinf(subgradient.Q)
            return subgradient.Q
        end
        quasigradient.gradient .-= subgradient.δQ
        Q += subgradient.Q
    end
    # Return current objective value and subgradient
    return current_objective_value(quasigradient, Q)
end

function resolve_subproblems!(quasigradient::AbstractQuasiGradient, execution::SerialExecution{T,SmoothSubProblem{T}}) where T <: AbstractFloat
    # Update subproblems
    update_known_decisions!(execution.decisions, quasigradient.x)
    # Initialize subgradient
    quasigradient.gradient .= quasigradient.c
    Q = zero(T)
    # Update and solve subproblems
    for subproblem in execution.subproblems
        update_subproblem!(subproblem)
        gradient = subproblem(quasigradient.x)
        if isinf(gradient.Q)
            return gradient.Q
        end
        quasigradient.gradient .+= gradient.δQ
        Q += gradient.Q
    end
    # Return current objective value and subgradient
    return current_objective_value(quasigradient, Q)
end

function restore_subproblems!(::AbstractQuasiGradient, execution::SerialExecution{T,SubProblem{T}}) where T <: AbstractFloat
    return nothing
end

function restore_subproblems!(::AbstractQuasiGradient, execution::SerialExecution{T,SmoothSubProblem{T}}) where T <: AbstractFloat
    for subproblem in execution.subproblems
        restore_subproblem!(subproblem)
    end
    for vi in all_known_decisions(execution.decisions)
        # Remove common projection targets
        remove_decision!(execution.decisions, vi)
    end
    for vi in all_decisions(execution.decisions)
        # Re-fix first-stage decisions
        execution.decisions[vi].state = Known
    end
    return nothing
end

# API
# ------------------------------------------------------------
function (execution::Serial)(structure::StageDecompositionStructure{2, 1, <:Tuple{ScenarioProblems}},
                             x::AbstractVector,
                             subproblems::AbstractSubProblemState,
                             ::Type{T}) where T <: AbstractFloat
    return SerialExecution(structure, x, subproblems, T)
end

function str(::Serial)
    return ""
end


__precompile__()
module TWPBVP

export twpbvpc, FInt

FInt = Int32

type TWPBVPCProblem
	fsub :: Function
	dfsub :: Function
	gsub :: Function
	dgsub :: Function
end

function __init__()
	global const libtwpbvpc = joinpath(Pkg.dir("TWPBVP"), "deps", "libtwpbvpc.so")
	global const fsub_ptr = cfunction(unsafe_fsub, Void, (Ref{FInt}, Ref{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{Void}, Ptr{FInt}))

	global const dfsub_ptr = cfunction(unsafe_dfsub, Void, (Ref{FInt}, Ref{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{Void}, Ptr{FInt}))

	global const gsub_ptr = cfunction(unsafe_gsub, Void, (Ref{FInt}, Ref{FInt}, Ptr{Float64}, Ptr{Float64}, Ptr{Void}, Ptr{FInt}))

	global const dgsub_ptr = cfunction(unsafe_dgsub, Void, (Ref{FInt}, Ref{FInt}, Ptr{Float64}, Ptr{Float64}, Ptr{Void}, Ptr{FInt}))
end

"""
Provides the conversion betweeen Julia's function (including closures)
and Fortran's subroutine. The closure is passed in `rpar` parameter
that Fortran believes to be an array of Float64, it never looks into it
anyway and only passes it around.
"""
function unsafe_fsub(n :: FInt, x :: Float64, py :: Ptr{Float64}, pdy :: Ptr{Float64}, rpar :: Ptr{Void}, ipar :: Ptr{FInt}) :: Void
	y = unsafe_wrap(Array, py, n)
	dy = unsafe_wrap(Array, pdy, n)
	problem = unsafe_pointer_to_objref(rpar) :: TWPBVPCProblem
	problem.fsub(x, y, dy)
	return nothing
end

function unsafe_dfsub(n :: FInt, x :: Float64, py :: Ptr{Float64}, pjac :: Ptr{Float64}, rpar :: Ptr{Void}, ipar :: Ptr{FInt}) :: Void
	y = unsafe_wrap(Array, py, n)
	jac = unsafe_wrap(Array, pjac, (n,n))
	problem = unsafe_pointer_to_objref(rpar) :: TWPBVPCProblem
	problem.dfsub(x, y, jac)
	return nothing
end

function unsafe_gsub(i :: FInt, n :: FInt, py :: Ptr{Float64}, pg :: Ptr{Float64}, rpar :: Ptr{Void}, ipar :: Ptr{FInt}) :: Void
	y = unsafe_wrap(Array, py, n)
	g = unsafe_wrap(Array, pg, 1)
	problem = unsafe_pointer_to_objref(rpar) :: TWPBVPCProblem
	g[1] = problem.gsub(i, y)
	return nothing
end

function unsafe_dgsub(i :: FInt, n :: FInt, py :: Ptr{Float64}, pdg :: Ptr{Float64}, rpar :: Ptr{Void}, ipar :: Ptr{FInt}) :: Void
	y = unsafe_wrap(Array, py, n)
	dg = unsafe_wrap(Array, pdg, n)
	problem = unsafe_pointer_to_objref(rpar) :: TWPBVPCProblem
	problem.dgsub(i, y, dg)
	return nothing
end

"""
Thin layer of compatibility
"""
function twpbvpc(nlbc :: FInt,
		aleft :: Float64, aright :: Float64,
		fixpnt :: Nullable{Vector{Float64}},
		ltol :: Vector{FInt}, tol :: Vector{Float64},
		linear :: Bool, givmsh :: Bool, giveu :: Bool, nmsh :: Ref{FInt},
		xx :: Vector{Float64}, u :: Array{Float64, 2}, nmax :: Ref{FInt},
		wrk :: Vector{Float64}, iwrk :: Vector{FInt},
		fsub :: Function, dfsub :: Function,
		gsub :: Function, dgsub :: Function,
		ckappa1 :: Ref{Float64}, gamma1 :: Ref{Float64},
		ckappa :: Ref{Float64},
		# rpar :: Vector{Float64},
		# ipar :: Vector{FInt},
		iflbvp :: Ref{FInt})

	# Keep problem functions in rpar
	# HACK!
	rpar = TWPBVPCProblem(fsub, dfsub, gsub, dgsub)

	# Fake external parameters
	# Can't have it 0-length as it would be Any[0] and not Float64[0]
	# local rpar :: Vector{Float64} = [0.0]
	local ipar :: Vector{FInt} = [0]

	# No need to pass these parameters
	# u is a matrix for the solution only!
	ncomp, nucol = size(u)
	nudim = ncomp
	# Get the maximum of xx
	nxxdim = length(xx)
	# max for mesh points must be the same as the number of columns of u
	assert(nucol == nxxdim)

	# Sizes of work arrays
	lwrkfl = length(wrk)
	lwrkin = length(iwrk)

	# Number of fixed mesh points
	if isnull(fixpnt)
		nfxpnt = 0
		fixpnt_v = [0.0]
	else
		fixpnt_v = fixpnt[]
		nfxpnt = length(fixpnt_v)
	end

	# Size of tolerance vector ≤ ncomp
	ntol = length(ltol)

	println("tol[1] = $(tol[1])")
	println("xx[2] = $(xx[2])")
	println("aright = $aright")

	varight = [aright]

	ccall((:twpbvpc_, libtwpbvpc), Void,
		(Ref{FInt}, Ref{FInt},					# ncomp, nlbc,
		Ref{Float64}, Ptr{Float64},					# aleft, aright
		Ref{FInt}, Ptr{Float64}, 					# nfxpnt, fixpnt
		Ref{FInt}, Ptr{FInt}, Ptr{Float64},		# ntol, ltol, tol
		Ref{FInt}, Ref{FInt}, Ref{FInt},			# linear, givmsh, giveu
		Ref{FInt}, Ref{FInt},						# nmsh, nxxdim
		Ptr{Float64}, Ref{FInt},					# xx, nudim
		Ptr{Float64}, Ref{FInt},					# u, nmax
		Ref{FInt}, Ptr{Float64},					# lwrkfl, wrk
		Ref{FInt}, Ptr{FInt},						# lwrkin, iwrk
		Ptr{Void}, Ptr{Void}, Ptr{Void}, Ptr{Void},	# fsub, dfsub, gsub, dgsub
		Ref{Float64}, Ref{Float64},					# ckappa1, gamma1
		Ref{Float64}, Ptr{Void}, Ptr{FInt},		# ckappa, rpar, ipar
		Ref{FInt}),								# iflbvp
		ncomp, nlbc,
		aleft, & aright,
		nfxpnt, fixpnt_v,
		ntol, ltol, tol,
		linear, givmsh, giveu, nmsh,
		nxxdim, xx,
		nudim, u, nmax,					# nudim = ncomp
		lwrkfl, wrk,
		lwrkin, iwrk,
		fsub_ptr, dfsub_ptr, gsub_ptr, dgsub_ptr,
		ckappa1, gamma1, ckappa,
		pointer_from_objref(rpar), ipar,
		iflbvp)
end

end # module


# General visualization utils

"""
    $(SIGNATURES)
Initialize empty visualizer window with home axis.  New browser window will be opened based on `show=true`.
"""
function startDefaultVisualization(;show::Bool=true,
                                    draworigin::Bool=true,
                                    originscale::Float64=1.0  )
    #
    global drawtransform

    viz = MeshCat.Visualizer()
    if draworigin
      setobject!( viz[:origin], Triad(originscale) )
      settransform!( viz[:origin], drawtransform ∘ (Translation(0.0, 0.0, 0.0) ∘ LinearMap( CTs.Quat(1.0,0,0,0))) )
    end

    # open a new browser tab if required
    show && open(viz)

    return viz
end

# tagsOnPoses = Dict{Symbol, Vector{TagkTl}}()

#


export segyhdr
# ============================================================================
# Utility functions not for export
function ibmfloat(x::UInt32)
  local fra = ntoh(x)
  local sgn = UInt8((fra & 0x80000000)>>31)
  fra <<= 1
  local exp = Int16(fra >> 25) - Int16(64)
  fra <<= 7
  y = (sgn == 0x00 ? 1.0 : -1.0) * 16.0^exp * signed(fra >> 8)/16777216
  return y
end
#=
  This is actual IBM hexadecimal float, and correctly parses the examples from
  https://en.wikipedia.org/wiki/IBM_hexadecimal_floating_point ; see tests.

  The version in JuliaSeis is wrong because IBM float has a range too wide for
  IEEE single float in Julia.

  The description in the SEG Y manual does something strange with the last bit.

  My last line is computationally expensive; working out the radix shift and
  using >> would be much better.
=#

function trid(i::Int16, fs::Float64, fc::Float64)
  S = ["DH", "HZ", "H1", "H2", "HZ", "HT", "HR"]
  return string(getbandcode(fs, fc=fc), S[i])
end

function mk_lc(lv::Int32)
  L1 = UInt8(div(lv, Int32(36)))
  L2 = UInt8(rem(lv, Int32(36)))
  c1 = 0x30 + L1 + (L1 > 0x09 ? 0x07 : 0x00)
  c2 = 0x30 + L2 + (L2 > 0x09 ? 0x07 : 0x00)
  return String([c1, c2])
end

function auto_coords(xy::Array{Int32, 1}, sc::Array{Int16, 1})
  xy == Int32[0,0] && return (0.0, 0.0)
  lon = Float64(xy[1])
  lat = Float64(xy[2])
  coord_scale = sc[1]
  coord_units = sc[2]
  (coord_scale < 0) && (coord_scale = -1 / coord_scale)
  lat *= coord_scale
  lon *= coord_scale

  #=
  Conversion formula for coord_units == 1 "borrowed" from PASSSOFT segy2sac.
  Assumes (possibly wrong) x-y coordinate origin & spherical Earth.
  coord_units == 3 and coord_units == 4 aren't supported; never seen them.
  =#
  if coord_units == 1
      iflg = lon < 0 ? -1 : 1
      c = 111194.6976
      D = sqrt(lat^2 + lon^2) / c
      lat /= c
      d = cosd(D)
      lon = iflg*acosd(d / cosd(lat))
  else
    lat /= 3600.0
    lon /= 3600.0
  end
  return lat, lon
end

function do_trace(f::IO,
                  passcal::Bool,
                  full::Bool,
                  ll::UInt8,
                  swap::Bool,
                  fh::Array{Int16,1}
                  )

  # First part of trace header is quite standard
  buf = BUF.buf
  ints = BUF.int32_buf
  shorts = BUF.int16_buf
  lat = 0.0
  lon = 0.0
  checkbuf_8!(buf, max(180, length(buf)))
  fast_readbytes!(f, buf, 180)

  intbuf = reinterpret(Int32, buf)
  copyto!(ints, 1, intbuf, 1, 7)
  copyto!(ints, 8, intbuf, 10, 8)
  copyto!(ints, 16, intbuf, 19, 4)

  shortbuf = reinterpret(Int16, buf)
  copyto!(shorts, 1, shortbuf, passcal ? 1 : 15, 4) # shorts[1:4]
  copyto!(shorts, 5, shortbuf, 35, 2)               # shorts[5:6]
  copyto!(shorts, 7, shortbuf, 45, 46)              # shorts[7:52]

  if passcal
    fast_readbytes!(f, buf, 40)

    scale_fac     = fastread(f, Float32)
    inst_no       = fastread(f, UInt16)
    shorts[62]    = fastread(f, Int16)
    ints[21]      = fastread(f, Int32)
    ints[22]      = fastread(f, Int32)
    ints[23]      = fastread(f, Int32)

    setindex!(shorts, shortbuf[10], 53)
    copyto!(shorts, 54, shortbuf, 13, 8)
    setindex!(ints, intbuf[6], 20)

    if swap
      shorts     .= ntoh.(shorts)
      ints       .= ntoh.(ints)
      scale_fac   = bswap(scale_fac)
      inst_no     = bswap(inst_no)
    end

    chars = buf[1:18]
    dt    = getindex(ints,20)
    n     = getindex(shorts, 20)
    fmt   = getindex(shorts, 54)
    nx    = (n == typemax(Int16) ? getindex(ints,21) : Int32(n))
    T     = (fmt == one(Int16) ? Int32 : Int16)
    nb    = checkbuf!(buf, nx, T)

    fast_readbytes!(f, buf, nb)

    # trace processing
    y = reinterpret(T, buf)
    x = Array{Float32,1}(undef, nx)
    if swap
      for i = 1:nx
        x[i] = bswap(y[i])
      end
    else
      copyto!(x, 1, y, 1, nx)
      # faster than reprocessing with fillx_ for long files
    end

    z = getindex(shorts, 5)
    δ = getindex(shorts, 21)
    fs        = sμ / Float64(δ == one(Int16) ? dt : δ)
    gain      = 1.0 / (Float64(scale_fac)  * 10.0^(-1.0*Float64(shorts[24]) / 10.0) / Float64(shorts[23]))
    lat, lon  = auto_coords(ints[18:19], shorts[6:7])
    if abs(lat) < 0.1 && abs(lon) < 0.1
      lat *= 3600.0
      lon *= 3600.0
    end
    el        = Float64(ints[9]) * Float64(abs(z))^(z < zero(Int16) ? -1.0 : 1.0)

    # Create ID
    id_arr = zeros(UInt8,8)
    i = one(Int8)
    o = one(Int8)
    while i < Int8(18)
      if chars[i] == 0x00
        chars[i] = 0x20
      end
      i = i+o
    end
    fill_id!(id_arr, chars, one(Int16), Int16(6), Int16(2), Int16(6))
    id_arr[1] = 0x2e
    id_arr[8] = 0x2e
    deleteat!(id_arr, id_arr.==0x00)

    # Channel string is tedious; use of one-char channel names like "Z"
    inds = Int64[]
    for i in 15:18
      if chars[i] > 0x20
        push!(inds, i)
      end
    end
    ch_arr = isempty(inds) ? UInt8[] : chars[inds]
    nc = length(ch_arr)
    if nc == 1
      c = uppercase(Char(ch_arr[1]))
      if c in ('Z','N','E')
        cha = string(getbandcode(fs), 'H', c)
      else
        cha = "YYY"
      end
    elseif nc == 0
      cha = "YYY"
    else
      cha = String(ch_arr[1:min(3, nc)])
    end
    id = String(id_arr) * cha

  else
    (dt, nx, fmt) = fh
    fast_readbytes!(f, buf, 38)
    ints[26]      = fastread(f, Int32)
    shorts[60]    = fastread(f, Int16)
    ints[27]      = fastread(f, Int32)
    shorts[61]    = fastread(f, Int16)
    shorts[62]    = fastread(f, Int16)
    ints[28]      = fastread(f, Int32)
    ints[29]      = fastread(f, Int32)

    copyto!(ints, 20, intbuf, 1, 5)
    copyto!(shorts, 53, shortbuf, 11, 2)          # shorts[53:54]
    ints[25] = getindex(intbuf, 7)
    copyto!(shorts, 55, shortbuf, 15, 5)          # shorts[55:59]

    T = getindex(segy_ftypes, fmt)
    if (T == Any)
      close(f)
      error(string("Trace data code = ", fmt, "  unsupported!"))
    end
    nb = checkbuf!(buf, nx, T)
    fast_readbytes!(f, buf, nb)

    # trace processing
    x = Array{Float32,1}(undef, nx)
    if T == Int16
      fillx_i16_be!(x, buf, nx, 0)
    elseif T == Int32
      fillx_i32_be!(x, buf, nx, 0)
    elseif T == Int8
      fillx_i8!(x, buf, nx, 0)
    elseif T == Float32
      x .= bswap.(reinterpret(Float32, buf))[1:nx]
    elseif T == UInt32
      y = Array{UInt32,1}(undef, nx)
      fillx_u32_le!(y, buf, nx, 0) # _le because ibmfloat bswaps
      x = ibmfloat.(y)
    end

    if swap
    shorts  .= ntoh.(shorts)
    ints    .= ntoh.(ints)
    end
    z       = getindex(shorts, 5)
    δ       = getindex(shorts, 21)
    fs      = sμ / Float64(δ)

    # not sure about meaning of "dB" in gain constants
    gain    = Float64(ints[25]) * 10.0^(shorts[55] + (shorts[23] + shorts[24])/10.0)
    el      = Float64(ints[9]) * Float64(abs(z))^(z<Int16(0) ? -1.0 : 1.0)

    # Create ID
    sta = string(reinterpret(UInt16, shorts[57]))
    lc = ll > 0x00 ? mk_lc(ints[ll]) : ""
    cha = Int16(10) < shorts[1] < Int16(18) ? trid(shorts[1]-Int16(10), fs, 1.0) : "YYY"
    id = string(".", sta[1:min(lastindex(sta),5)], ".", lc, ".", cha)

  end
  if full == true
    shorts_k = String["trace_id_code", "n_summed_z", "n_summed_h", "data_use",
      "z_sc", "h_sc", "h_units_code", "v_weather",
      "v_subweather", "t_src_uphole", "t_rec_uphole", "src_static_cor",
      "rec_static_cor", "total_static", "t_lag_a", "t_lag_b",
      "t_delay", "t_mute_st", "t_mute_en", "nx",
      "delta", "gain_type", "gain_const", "init_gain",
      "correlated", "sweep_st", "sweep_en", "sweep_len",
      "sweep_type", "sweep_tap_st", "sweep_tap_en", "tap_type",
      "f_alias", "slope_alias", "f_notch", "slope_notch",
      "f_low_cut", "f_high_cut", "slope_low_cut", "slope_high_cut",
      "year", "day", "hour", "minute",
      "second", "time_code", "trace_wt_fac", "geophone_roll_p1",
      "geophone_first_tr", "geophone_last_tr", "gap_size", "overtravel"]
    append!(shorts_k, passcal ?
        String["total_static_hi", "data_form", "ms", "trigyear",
        "trigday", "trighour", "trigminute", "trigsecond",
        "trigms", "not_to_be_used"] :
        String["shot_scalar", "trace_units_code", "trans_exp", "trans_units_code",
        "device_id", "trace_time_sc", "src_type_code", "src_energy_dir",
        "src_exp", "src_units_code"])

    ints_k = String["trace_seq_line", "trace_seq_file", "rec_no", "channel_no",
      "energy_src_pt", "ensemble_no", "trace_in_ensemble", "src-rec_dist",
      "rec_ele", "src_ele", "src_dep", "rec_datum_ele",
      "src_datum_ele", "src_water_dep", "rec_water_dep", "src_x",
      "src_y", "rec_x", "rec_y"]
    append!(ints_k, passcal ? String["samp_rate", "num_samps", "max", "min"] :
      String["cdp_x", "cdp_y", "inline_3d", "crossline_3d", "shot_point",
      "trans_mant", "unassigned_1", "unassigned_2"] )
    misc = Dict{String,Any}()
    [misc[shorts_k[i]] = shorts[i] for i in 1:length(shorts_k)]
    [misc[ints_k[i]] = ints[i] for i in 1:length(ints_k)]
    misc["scale_fac"] = gain
    if passcal
      sta = String(chars[1:6])
      misc["inst_no"] = inst_no
      misc["sensor_serial"] = String(chars[7:14])
    end
    misc["station_name"] = sta
    misc["channel_name"] = cha
  end

  # Trace info
  ts = mktime(shorts[41], shorts[42], shorts[43], shorts[45], shorts[45], zero(Int16)) +
        shorts[53] + 1000*sum(shorts[15:17])
  loc = GeoLoc()
  loc.lat = lat
  loc.lon = lon
  loc.el = el

  C = SeisChannel()
  setfield!(C, :name, id)
  setfield!(C, :id, id)
  setfield!(C, :loc, loc)
  setfield!(C, :gain, gain)
  setfield!(C, :fs, fs)
  mk_t!(C, length(x), ts)
  setfield!(C, :x, x)
  if passcal == false
    setfield!(C, :units, get(segy_units, Int16(shorts[56]), ""))
  end
  if full == true
    setfield!(C, :misc, misc)
  end
  return C
end

function read_segy_file!( S::GphysData,
                          fname::String,
                          ll::UInt8,
                          passcal::Bool,
                          memmap::Bool,
                          full::Bool,
                          swap::Bool,
                          strict::Bool)

  f = memmap ? IOBuffer(Mmap.mmap(fname)) : open(fname, "r")
  trace_fh = Array{Int16, 1}(undef, 3)
  if passcal == true
    C = do_trace(f, true, full, ll, swap, trace_fh)
    add_chan!(S, C, strict)
    close(f)
  else
    shorts  = getfield(BUF, :int16_buf)

    # File headers
    filehdr       = fastread(f, 3200)
    jobid         = bswap(fastread(f, Int32))
    lineid        = bswap(fastread(f, Int32))
    reelid        = bswap(fastread(f, Int32))
    fast_readbytes!(f, BUF.buf, 48)
    fillx_i16_be!(shorts, BUF.buf, 24, 0)
    fastskip(f, 240)

    # Some early sample files had these in little-endian byte order
    for i = 25:27
      shorts[i] = bswap(fastread(f, Int16))
    end
    fastskip(f, 94)

    # Process file header
    nh = max(zero(Int16), getindex(shorts, 27))
    if full == false
      fastskip(f, 3200*nh)
    else
      exthdr = Array{String,1}(undef, nh)
      [exthdr[i] = fastread(f, 3200) for i in 1:nh]
      fhd = Dict{String,Any}(
              zip(String["ntr", "naux", "filedt", "origdt", "filenx",
                         "orignx", "fmt", "cdpfold", "trasort", "vsum",
                         "swst", "swen0", "swlen", "swtyp", "tapnum",
                         "swtapst", "swtapen", "taptyp", "corrtra", "bgainrec",
                         "amprec", "msys", "zupdn", "vibpol", "segyver",
                         "isfixed", "n_exthdr"], shorts[1:27])
                            )
      fhd["jobid"]  = jobid
      fhd["lineid"] = lineid
      fhd["reelid"] = reelid
      fhd["filehdr"] = filehdr
      fhd["exthdr"] = exthdr
    end

    trace_fh[1] = getindex(shorts,3)
    trace_fh[2] = getindex(shorts,5)
    trace_fh[3] = getindex(shorts,7)

    # Channel headers
    nt = shorts[1]
    for i = 1:nt
      # "swap" is always true for valid SEG Y data
      C = do_trace(f, false, full, ll, true, trace_fh)
      j = add_chan!(S, C, strict)
      if full == true
        merge!(S.misc[j], fhd)
      end
    end
    close(f)
  end
  resize!(BUF.buf, 65535)
  return S
end

# ============================================================================

"""
    segyhdr(f[; passcal=false, ll=LL, swap=false])

Print formatted, sorted SEG-Y headers of file `f` to stdout. Use keyword
`passcal=true` for PASSCAL/NMT modified SEG Y; use `swap=true` for big-endian
PASSCAL. See SeisIO `read_data` documentation for `ll` codes.
"""
function segyhdr(fname::String; ll::UInt8=0x00, passcal::Bool=false, swap::Bool=false)
  if passcal
    seis = read_data("passcal", fname::String, full=true, ll=ll, swap=swap)
  else
    seis = read_data("segy", fname::String, ll=ll, full=true)
  end
  if passcal
    printstyled(stdout, @sprintf("%20s: %s\n", "PASSCAL SEG-Y FILE", realpath(fname)), color=:green, bold=true)
    D = getindex(getfield(seis, :misc),1)
    for k in sort(collect(keys(D)))
      @printf(stdout, "%20s: %s\n", k, string(get(D, k, "")))
    end
  else
    p = 1; w = 32; W = displaysize(stdout)[2]-2
    S = fill("", length(seis.misc[1])+1)
    printstyled(stdout, @sprintf("%20s: %s\n", "SEG-Y FILE", realpath(fname)), color=:green, bold=true)
    for i = 1:seis.n
      if p > 1
        s = @sprintf("%20i/%i", i, seis.n)
      else
        s = @sprintf(" Trace # %11i/%i", i, seis.n)
      end
      S[1] *= s
      S[1] *= " "^(w-length(s))
      D = getindex(getfield(seis, :misc),1)
      for (j,k) in enumerate(sort(collect(keys(D))))
        if k == "exthdr" || k == "filehdr"
          val = get(seis.misc[i], k, "")
          if isempty(val)
            s = "(empty)"
          else
            s = length(val) > 8 ? String(val[1:8])*"…" : String(val)
          end
        else
          s = string(get(seis.misc[i], k, ""))
        end
        filler = " "^max(0, 10-length(s))
        S[j+1] *= @sprintf("%20s: %s%s", k, s, filler)
      end
      if (p+2*w > W) || (i == seis.n)
        printstyled(stdout, S[1]*"\n", color=:yellow, bold=true)
        [println(S[j]) for j=2:length(S)]
        println("")
        S = fill("", length(seis.misc[i])+1)
        p = 1
      else
        p += w
      end
    end
    printstyled(stdout, @sprintf("%20s\n","END OF RECORD"), color=208, bold=true)
  end
  return nothing
end


const __m128i = NTuple{2, VecElement{UInt64}}
@assert ccall(
    "llvm.x86.aesni.aeskeygenassist", llvmcall, __m128i, (__m128i, UInt8),
    __m128i((0x0123456789123450, 0x9876543210987654)), 0x1
) ≡ __m128i((0x857c266f7c266e85, 0x2346382146382023))


# 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.

"""
    _replace_moi_variables(
        nodes::Vector{Nonlinear.Node},
        moi_index_to_consecutive_index::Dict{MOI.VariableIndex,Int},
    )

Return a new `Vector{Nonlinear.Node}` where all occurances of
`NODE_MOI_VARIABLE` are replaced by `NODE_VARIABLE` that is 1-indexed and
ordered.
"""
function _replace_moi_variables(
    nodes::Vector{Nonlinear.Node},
    moi_index_to_consecutive_index::Dict{MOI.VariableIndex,Int},
)
    new_nodes = Vector{Nonlinear.Node}(undef, length(nodes))
    for (i, node) in enumerate(nodes)
        if node.type == Nonlinear.NODE_MOI_VARIABLE
            new_nodes[i] = Nonlinear.Node(
                Nonlinear.NODE_VARIABLE,
                moi_index_to_consecutive_index[MOI.VariableIndex(node.index)],
                node.parent,
            )
        else
            new_nodes[i] = node
        end
    end
    return new_nodes
end

@enum(Linearity, CONSTANT, LINEAR, PIECEWISE_LINEAR, NONLINEAR)

"""
    _classify_linearity(
        nodes::Vector{Nonlinear.Node},
        adj::SparseArrays.SparseMatrixCSC,
        subexpression_linearity::Vector{Linearity},
    )

Classify the nodes in a tree as constant, linear, or nonlinear with respect to
the input.
"""
function _classify_linearity(
    nodes::Vector{Nonlinear.Node},
    adj::SparseArrays.SparseMatrixCSC,
    subexpression_linearity::Vector{Linearity},
)
    linearity = Array{Linearity}(undef, length(nodes))
    children_arr = SparseArrays.rowvals(adj)
    for k in length(nodes):-1:1
        node = nodes[k]
        if node.type == Nonlinear.NODE_VARIABLE
            linearity[k] = LINEAR
            continue
        elseif node.type == Nonlinear.NODE_VALUE
            linearity[k] = CONSTANT
            continue
        elseif node.type == Nonlinear.NODE_PARAMETER
            linearity[k] = CONSTANT
            continue
        elseif node.type == Nonlinear.NODE_SUBEXPRESSION
            linearity[k] = subexpression_linearity[node.index]
            continue
        end
        children_idx = SparseArrays.nzrange(adj, k)
        num_constant_children, any_nonlinear = 0, false
        for r in children_idx
            if linearity[children_arr[r]] == NONLINEAR
                any_nonlinear = true
                break
            elseif linearity[children_arr[r]] == CONSTANT
                num_constant_children += 1
            end
        end
        if any_nonlinear
            # If any children are nonlinear, then we're nonlinear...
            linearity[k] = NONLINEAR
            # ...except in the case of ifelse. If the operands are linear then
            # we're piecewise linear.
            op = get(
                Nonlinear.DEFAULT_MULTIVARIATE_OPERATORS,
                node.index,
                nothing,
            )
            if (
                node.type == Nonlinear.NODE_CALL_MULTIVARIATE &&
                op == :ifelse &&
                linearity[children_arr[children_idx[2]]] == LINEAR &&
                linearity[children_arr[children_idx[3]]] == LINEAR
            )
                linearity[k] = PIECEWISE_LINEAR
            end
            continue
        elseif num_constant_children == length(children_idx)
            # If all children are constant, then we're constant.
            linearity[k] = CONSTANT
            continue
        end
        # By this point, some children are constant and some are linear, so if
        # the operator is nonlinear, then we're nonlinear.
        if node.type == Nonlinear.NODE_CALL_UNIVARIATE
            op =
                get(Nonlinear.DEFAULT_UNIVARIATE_OPERATORS, node.index, nothing)
            if op == :+ || op == :-
                linearity[k] = LINEAR
            else
                linearity[k] = NONLINEAR
            end
        elseif node.type == Nonlinear.NODE_CALL_MULTIVARIATE
            op = get(
                Nonlinear.DEFAULT_MULTIVARIATE_OPERATORS,
                node.index,
                nothing,
            )
            if op == :+
                linearity[k] = LINEAR
            elseif op == :-
                linearity[k] = LINEAR
            elseif op == :*
                # Multiplication is linear if there is one non-constant term.
                one_op = num_constant_children == length(children_idx) - 1
                linearity[k] = one_op ? LINEAR : NONLINEAR
            elseif op == :^
                linearity[k] = NONLINEAR
            elseif op == :/
                if linearity[children_arr[children_idx[2]]] == CONSTANT
                    # If the denominator is constant, we're linear.
                    linearity[k] = LINEAR
                else
                    linearity[k] = NONLINEAR
                end
            elseif op == :ifelse
                linearity[k] = NONLINEAR
            else  # User-defined functions
                linearity[k] = NONLINEAR
            end
        elseif node.type == Nonlinear.NODE_LOGIC
            linearity[k] = NONLINEAR
        else
            @assert node.type == Nonlinear.NODE_COMPARISON
            linearity[k] = NONLINEAR
        end
    end
    return linearity
end

"""
    _compute_gradient_sparsity!(
        indices::Coloring.IndexedSet,
        nodes::Vector{Nonlinear.Node},
    )

Compute the sparsity pattern of the gradient of an expression (i.e., a list of
which variable indices are present).
"""
function _compute_gradient_sparsity!(
    indices::Coloring.IndexedSet,
    nodes::Vector{Nonlinear.Node},
)
    for node in nodes
        if node.type == Nonlinear.NODE_VARIABLE
            push!(indices, node.index)
        elseif node.type == Nonlinear.NODE_MOI_VARIABLE
            error(
                "Internal error: Invalid to compute sparsity if Nonlinear.NODE_MOI_VARIABLE " *
                "nodes are present.",
            )
        end
    end
    return
end

"""
    _compute_hessian_sparsity(
        nodes::Vector{Nonlinear.Node},
        adj,
        input_linearity::Vector{Linearity},
        indexedset::Coloring.IndexedSet,
        subexpression_edgelist::Vector{Set{Tuple{Int,Int}}},
        subexpression_variables::Vector{Vector{Int}},
    )

Compute the sparsity pattern the Hessian of an expression.

 * `input_linearity` is the linearity with respect to the input, computed by
   `_classify_linearity`
 * `subexpression_edgelist` is the edge_list of each subexpression
 * `subexpression_variables` is the list of all variables which appear in a
   subexpression (including recursively).

Idea: consider the (non)linearity of a node *with respect to the output*. The
children of any node which is nonlinear with respect to the output should have
nonlinear interactions, hence nonzeros in the hessian. This is not true in
general, but holds for everything we consider.

A counter example is `f(x, y, z) = x + y * z`, but we don't have any functions
like that. By "nonlinear with respect to the output", we mean that the output
depends nonlinearly on the value of the node, regardless of how the node itself
depends on the input.
"""
function _compute_hessian_sparsity(
    nodes::Vector{Nonlinear.Node},
    adj,
    input_linearity::Vector{Linearity},
    indexedset::Coloring.IndexedSet,
    subexpression_edgelist::Vector{Set{Tuple{Int,Int}}},
    subexpression_variables::Vector{Vector{Int}},
)
    # So start at the root of the tree and classify the linearity wrt the output.
    # For each nonlinear node, do a mini DFS and collect the list of children.
    # Add a nonlinear interaction between all children of a nonlinear node.
    edge_list = Set{Tuple{Int,Int}}()
    nonlinear_wrt_output = fill(false, length(nodes))
    children_arr = SparseArrays.rowvals(adj)
    stack = Int[]
    stack_ignore = Bool[]
    nonlinear_group = indexedset
    if length(nodes) == 1 && nodes[1].type == Nonlinear.NODE_SUBEXPRESSION
        # Subexpression comes in linearly, so append edge_list
        for ij in subexpression_edgelist[nodes[1].index]
            push!(edge_list, ij)
        end
    end
    for k in 2:length(nodes)
        nod = nodes[k]
        if nod.type == Nonlinear.NODE_MOI_VARIABLE
            error(
                "Internal error: Invalid to compute sparsity if Nonlinear.NODE_MOI_VARIABLE " *
                "nodes are present.",
            )
        end
        if nonlinear_wrt_output[k]
            continue # already seen this node one way or another
        elseif input_linearity[k] == CONSTANT
            continue # definitely not nonlinear
        end
        @assert !nonlinear_wrt_output[nod.parent]
        # check if the parent depends nonlinearly on the value of this node
        par = nodes[nod.parent]
        if par.type == Nonlinear.NODE_CALL_UNIVARIATE
            op = get(Nonlinear.DEFAULT_UNIVARIATE_OPERATORS, par.index, nothing)
            if op === nothing || (op != :+ && op != :-)
                nonlinear_wrt_output[k] = true
            end
        elseif par.type == Nonlinear.NODE_CALL_MULTIVARIATE
            op = get(
                Nonlinear.DEFAULT_MULTIVARIATE_OPERATORS,
                par.index,
                nothing,
            )
            if op === nothing
                nonlinear_wrt_output[k] = true
            elseif op in (:+, :-, :ifelse)
                # pass
            elseif op == :*
                # check if all siblings are constant
                sibling_idx = SparseArrays.nzrange(adj, nod.parent)
                if !all(
                    i ->
                        input_linearity[children_arr[i]] == CONSTANT ||
                            children_arr[i] == k,
                    sibling_idx,
                )
                    # at least one sibling isn't constant
                    nonlinear_wrt_output[k] = true
                end
            elseif op == :/
                # check if denominator is nonconstant
                sibling_idx = SparseArrays.nzrange(adj, nod.parent)
                if input_linearity[children_arr[last(sibling_idx)]] != CONSTANT
                    nonlinear_wrt_output[k] = true
                end
            else
                nonlinear_wrt_output[k] = true
            end
        end
        if nod.type == Nonlinear.NODE_SUBEXPRESSION && !nonlinear_wrt_output[k]
            # subexpression comes in linearly, so append edge_list
            for ij in subexpression_edgelist[nod.index]
                push!(edge_list, ij)
            end
        end
        if !nonlinear_wrt_output[k]
            continue
        end
        # do a DFS from here, including all children
        @assert isempty(stack)
        @assert isempty(stack_ignore)
        sibling_idx = SparseArrays.nzrange(adj, nod.parent)
        for sidx in sibling_idx
            push!(stack, children_arr[sidx])
            push!(stack_ignore, false)
        end
        empty!(nonlinear_group)
        while length(stack) > 0
            r = pop!(stack)
            should_ignore = pop!(stack_ignore)
            nonlinear_wrt_output[r] = true
            if nodes[r].type == Nonlinear.NODE_LOGIC ||
               nodes[r].type == Nonlinear.NODE_COMPARISON
                # don't count the nonlinear interactions inside
                # logical conditions or comparisons
                should_ignore = true
            end
            children_idx = SparseArrays.nzrange(adj, r)
            for cidx in children_idx
                push!(stack, children_arr[cidx])
                push!(stack_ignore, should_ignore)
            end
            if should_ignore
                continue
            end
            if nodes[r].type == Nonlinear.NODE_VARIABLE
                push!(nonlinear_group, nodes[r].index)
            elseif nodes[r].type == Nonlinear.NODE_SUBEXPRESSION
                # append all variables in subexpression
                union!(nonlinear_group, subexpression_variables[nodes[r].index])
            end
        end
        for i_ in 1:nonlinear_group.nnz
            i = nonlinear_group.nzidx[i_]
            for j_ in 1:nonlinear_group.nnz
                j = nonlinear_group.nzidx[j_]
                if j > i
                    continue  # Only lower triangle.
                end
                push!(edge_list, (i, j))
            end
        end
    end
    return edge_list
end

"""
    _list_subexpressions(nodes::Vector{Nonlinear.Node})

Returns the list of subexpressions which a given tape depends on directly
"""
function _list_subexpressions(nodes::Vector{Nonlinear.Node})
    indices = Set{Int}(
        n.index for n in nodes if n.type == Nonlinear.NODE_SUBEXPRESSION
    )
    return sort(collect(indices))
end

"""
    _topological_sort(
        starts::Vector{Int},
        subexpressions::Vector{Vector{Nonlinear.Node}},
        subexpression_dependency_graph::Vector{Vector{Int}} =
            Vector{Vector{Int}}(undef, length(subexpressions)),
    )

Return a topologically sorted list of the integer subexpresssion indices that
need to be computed to evaluate `subexpressions[s]` for all `s in starts`.

`starts` should be ordered, and not contain duplicates.

`subexpression_dependency_graph[i]` is a lazily-computed list of "out" edges
from node `i`, in terms of the integer-valued subexpression index (i.e.,
`node.index`). This list should be unique and ordered.

If calling `_topological_sort` a single time, you may omit the
`subexpression_dependency_graph` argument.

However, if calling `_topological_sort` multiple times on the _same_ vector of
subexpresssions, you should create `subexpression_dependency_graph` once (either
as the uninitialized vector, or by explicitly computing the full
`subexpression_dependency_graph`), and pass it in.

## Notes

* It is important to not use recursion here, because expressions may have
  arbitrary levels of nesting!
* This function assumes `subexpressions` is acyclic.
"""
function _topological_sort(
    starts,
    subexpressions::Vector{Vector{Nonlinear.Node}},
    subexpression_dependency_graph::Vector{Vector{Int}} = Vector{Vector{Int}}(
        undef,
        length(subexpressions),
    ),
)
    ordered = Int[]
    in_order = fill(false, length(subexpressions))
    stack = Tuple{Int,Bool}[]
    for s in starts
        if in_order[s]
            continue  # s is already in `ordered`.
        end
        push!(stack, (s, true))
        while !isempty(stack)
            node, needs_checking = pop!(stack)
            if !needs_checking
                # We must be returning to this node for a second time, and we
                # have already checked all of the children. Therefore, we can
                # add it to the set of ordered nodes.
                push!(ordered, node)
                in_order[node] = true
                continue
            elseif in_order[node]
                continue  # This node has already been added to `ordered`.
            end
            # Re-add the node to the stack, but set the `false` flag this time
            # so next time we visit it, it will go on the `ordered` list
            # instead.
            push!(stack, (node, false))
            if !isassigned(subexpression_dependency_graph, node)
                subexpression_dependency_graph[node] =
                    _list_subexpressions(subexpressions[node])
            end
            for child in subexpression_dependency_graph[node]
                if !in_order[child]
                    push!(stack, (child, true))
                end
            end
        end
    end
    return ordered
end

"""
    _order_subexpressions(
        main_expressions::Vector{Vector{Nonlinear.Node}},
        subexpressions::Vector{Vector{Nonlinear.Node}};
    )

Topologically sort the subexpression needed to evaluate `main_expressions`.

Returns two things:

 * A `Vector{Int}` containing the ordered list of subexpression-indices that
   need to be evaluated to compute all `main_expressions`
 * A `Vector{Vector{Int}}`, containing a list of ordered lists of
   subexpression-indices that need to be evaluated to compute
   `main_expressions[i]`.

**Warning:** This doesn't handle cyclic expressions! But this should be fine
because we can't compute them in JuMP anyway.
"""
function _order_subexpressions(
    main_expressions::Vector{Vector{Nonlinear.Node}},
    subexpressions::Vector{Vector{Nonlinear.Node}},
)
    # The graph of node dependencies. Constructed lazily.
    subexpression_dependency_graph =
        Vector{Vector{Int}}(undef, length(subexpressions))
    # Node dependencies of the main expressions.
    starts = Set{Int}()
    individual_sorts = Vector{Int}[]
    for expression in main_expressions
        s = _list_subexpressions(expression)
        union!(starts, s)
        push!(
            individual_sorts,
            _topological_sort(
                s,
                subexpressions,
                subexpression_dependency_graph,
            ),
        )
    end
    full_sort = _topological_sort(
        starts,
        subexpressions,
        subexpression_dependency_graph,
    )
    return full_sort, individual_sorts
end

"""
    _order_subexpressions(
        main_expression::Vector{Nonlinear.Node},
        subexpressions::Vector{Vector{Nonlinear.Node}},
    )

Return a `Vector{Int}` containing the topologically ordered list of
subexpression-indices that need to be evaluated to compute `main_expression`.
"""
function _order_subexpressions(
    main_expression::Vector{Nonlinear.Node},
    subexpressions::Vector{Vector{Nonlinear.Node}},
)
    s = _list_subexpressions(main_expression)
    return _topological_sort(s, subexpressions)
end


function construct_services!(
    psi_container::PSIContainer,
    sys::PSY.System,
    services_template::Dict{Symbol, ServiceModel},
    devices_template::Dict{Symbol, DeviceModel},
)
    isempty(services_template) && return
    services_mapping = PSY.get_contributing_device_mapping(sys)
    for service_model in values(services_template)
        @debug "Building $(service_model.service_type) with $(service_model.formulation) formulation"
        services = PSY.get_components(service_model.service_type, sys)
        if validate_available_services(service_model.service_type, services)
            construct_service!(
                psi_container,
                services,
                services_mapping,
                service_model,
                devices_template,
            )
        end
    end
    return
end

function construct_service!(
    psi_container::PSIContainer,
    services::IS.FlattenIteratorWrapper{SR},
    services_mapping::PSY.ServiceContributingDevicesMapping,
    model::ServiceModel{SR, RangeReserve},
    devices_template::Dict{Symbol, DeviceModel},
) where {SR <: PSY.Reserve}
    time_steps = model_time_steps(psi_container)
    names = (PSY.get_name(s) for s in services)

    incompatible_device_types = Vector{DataType}()
    for model in values(devices_template)
        formulation = get_formulation(model)
        if formulation == FixedOutput
            @info "$(formulation) for $(get_device_type(model)) is not compatible with the provision of reserve services"
            push!(incompatible_device_types, get_device_type(model))
        end
    end

    if model_has_parameters(psi_container)
        container = add_param_container!(
            psi_container,
            UpdateRef{SR}("service_requirement", "get_requirement"),
            names,
            time_steps,
        )
        get_parameter_array(container)
    end

    add_cons_container!(psi_container, constraint_name(REQUIREMENT, SR), names, time_steps)

    for service in services
        contributing_devices =
            services_mapping[(
                type = typeof(service),
                name = PSY.get_name(service),
            )].contributing_devices
        if !isempty(incompatible_device_types)
            contributing_devices =
                [d for d in contributing_devices if typeof(d) ∉ incompatible_device_types]
        end

        #Variables
        activeservice_variables!(psi_container, service, contributing_devices)
        # Constraints
        service_requirement_constraint!(psi_container, service, model)
        modify_device_model!(devices_template, model, contributing_devices)
    end
    return
end


import MathProgBase
export can_solve_mip, can_solve_socp, can_solve_sdp, can_solve_exp
export set_default_solver, get_default_solver

function set_default_solver(solver::MathProgBase.AbstractMathProgSolver)
  global DEFAULT_SOLVER
  DEFAULT_SOLVER = solver
end

function get_default_solver()
  if DEFAULT_SOLVER == nothing
    error("The default solver is set to `nothing`
         You must have at least one solver installed to use Convex.
         You can install a solver such as SCS by running:
         Pkg.add(\"SCS\").
         You will have to restart Julia after that.")
  end
  return DEFAULT_SOLVER
end

solvers = [("SCS", "SCSSolver"), ("ECOS", "ECOSSolver"), ("Gurobi", "GurobiSolver"), ("Mosek", "MosekSolver"),
          ("GLPKMathProgInterface", "GLPKSolverMIP")]

for (dir, solver) in solvers
  if isdir(Pkg.dir(dir)) && DEFAULT_SOLVER == nothing
    eval(parse("using "*dir))
    eval(parse("set_default_solver("*solver*"())"))
  end
end

if get_default_solver() == nothing
  packages = ""
  for (dir, solver) in solvers
    packages = packages*dir*" | "
  end
  warn("***********************************************************************************************
       You don't have any of
       "*packages*" installed.
       You must have at least one of these solvers. You can install a solver such as SCS by running:
       Pkg.add(\"SCS\")
       You will have to restart Julia after that.
       ***********************************************************************************************")
end


# default options go here
# if user specifies option they will be overwritten
export CurrentOptions
export InvalidFermiOption
export @reset
export @set
export @get
export @molecule

const DefaultOptions = Dict{String,Union{Float64,Int,String,Bool,Nothing}}(
                                  "molstring" => """
                                  O        1.2091536548      1.7664118189     -0.0171613972
                                  H        2.1984800075      1.7977100627      0.0121161719
                                  H        0.9197881882      2.4580185570      0.6297938832
                                  """,
                                  "basis" => "sto-3g",
                                  "jkfit" => "auto",
                                  "rifit" => "auto",
                                  "charge" => 0,
                                  "multiplicity" => 1,
                                  "unit" => "angstrom",
                                  "reference" => "rhf",
                                  "scf_max_iter" => 50,
                                  "scf_max_rms" => 10^-10,
                                  "scf_alg" => "conventional",
                                  "oda" => true,
                                  "oda_cutoff" => 1E-1,
                                  "oda_shutoff" => 20,
                                  "scf_guess" => "gwh",
                                  "quiet" => true,
                                  "mp2_type" => "df",
                                  "precision" => "double",
                                  "cc_alg" => "CTF",
                                  "ci_alg" => "aci",
                                  "σ"      => 0.001,
                                  "γ"      => 1.0,
                                  "e_conv" => 10,
                                  "d_conv" => 8,
                                  "cc_max_iter" => 50,
                                  "cc_max_rms" => 10^-10,
                                  "cc_e_conv" => 10^-10,
                                  "bcc_max_t1" => 1^-7,
                                  "preconv_T1" => true,
                                  "drop_occ" => 0,
                                  "drop_vir" => 0,
                                  "diis" => true,
                                  "cc_diis" => true,
                                  "ndiis" => 8,
                                  "cc_ndiis" => 3,
                                  "bcc_tol" => 1.0e-6,
                                  "diis_prec" => "single",
                                  "diis_start" => 3,
                                  "cc_damp_ratio" => 0.0,
                                  "cc_diis_relax" => 3,
                                  "num_frozen" => 0,
                                  "aci_print_screen" => nothing,
                                  "cas_frozen" => 0,
                                  "cas_active" => -1,
                                  "cas_cutoff" => 10^-9,
                                  "cas_nroot" => 1,
                                  "min_matrix_elem" => 10^-9,
                                  "precision_override" => false
                                 )
"""
    Fermi.CurrentOptions

Dictionary containing options for Fermi. Any information not given
explicitly to Methods is obtained from here.
"""
CurrentOptions = deepcopy(DefaultOptions)

"""
    Fermi.@reset(key=nothing)

Restores `key` option to default setting. If `key == nothing`, restores all options to their default values.
"""
macro reset(key=nothing)
    if key == nothing
        return quote
            for key in keys(Fermi.CurrentOptions)
                Fermi.CurrentOptions[key] = Fermi.DefaultOptions[key]
            end
        end
    elseif String(key) in keys(Fermi.CurrentOptions)
        return quote
            Fermi.CurrentOptions[$(String(key))] = Fermi.DefaultOptions[$(String(key))]
        end
    else
        return quote
            error("Invalid option "*$(String(key)))
        end
    end
end

"""
    Fermi.@set

Set options for Fermi computations. It saves the options into Fermi.CurrentOptions.

*Usage:*  @set A B

A is set to B. By default A is taken as a string. B is evaluated at runtime. If the evaluation
is not possible, B is converted to a string.

# Examples

```
@set basis cc-pVDZ
@set cc_max_iter 100
@set e_conv 10^-9
```
One can also use the block syntax
```
@set {
    basis cc-pVDZ
    cc_max_iter 100
    e_conv 10^-9
}
```
Note that for the block syntax variables are not accepted because the evaluation of B is
done at parse time.
```
mybasis = 6-31g
@set {
    basis mybasis
}
```
Will set the basis to "mybasis" not "6-31g".
"""
macro set(opt,val)
    clean_up(s) = String(filter(c->!occursin(c," ():"),s))
    A = clean_up(repr(opt))
    B = clean_up(repr(val))
    quote
        try
            CurrentOptions[$A] = $val
        catch UndefVarError
            CurrentOptions[$A] = $B
        end
    end |> esc
end

macro set(block)
    lines = split(repr(block),";")
    quote
        clean_up(s) = String(strip(filter(c->!occursin(c," {}():"),s)))
        for l in $lines
            opt, val = split(strip(l), " ", limit=2)
            opt = clean_up(opt)
            val = clean_up(val)
            try
                CurrentOptions[opt] = eval(Meta.parse(val))
            catch UndefVarError
                CurrentOptions[opt] = val
            end
        end
    end
end

macro get(opt)
    clean_up(s) = String(filter(c->!occursin(c," ():"),s))
    A = clean_up(repr(opt))
    quote
        try
            CurrentOptions[$A]
        catch KeyError
            throw(InvalidFermiOption($A*" not a valid option."))
        end
    end |> esc
end
"""
    Fermi.@molecule

Set the molecule used in computations. String is save into Fermi.CurrentOptions

# Example
```
@molecule {
C                 -0.00000000     0.00000000    -0.00000000
O                  0.00000000     0.00000000    -2.19732722
O                 -0.00000000    -0.00000000     2.19732722
}
```
"""
macro molecule(block)
    clean_up(s) = strip(filter(c->!occursin(c,"{}():"),s))
    mol = repr(block)
    mol = replace(mol, ";"=>"\n")
    mol = clean_up(mol)
    CurrentOptions["molstring"] = String(mol)
end

struct InvalidFermiOption <: Exception
    msg::String
end
Base.showerror(io::IO, e::InvalidFermiOption) = print(io, "InvalidFermiOption: ", e.msg)

#function notimplemented()
#    @output "🚧 Not implemented yet! We're working on it 🔨 👷 \n"
#end


# MIT License
# Copyright (c) 2017: Xavier Gandibleux, Anthony Przybylski, Gauthier Soleilhac, and contributors.
module vOptSpecific

const libLAPpath = joinpath(@__DIR__, "..", "deps", "libLAP.so")
const libUMFLPpath = joinpath(@__DIR__, "..", "deps", "libUMFLP.so")
const libcomboPath = joinpath(@__DIR__,"..","deps","libcombo.so")

include("LAP.jl")
include("UMFLP.jl")
include("jMOScheduling.jl")
include("Bi01KP/Bi01KP.jl")
include("KP.jl")

@static if VERSION > v"0.7-"
	using DelimitedFiles
	using SparseArrays
	_unsafe_wrap(Atype, p, dims ; own=false) = unsafe_wrap(Atype, p, dims, own=own)
end

@static if VERSION < v"0.7-"
	_unsafe_wrap(Atype, p, dims ; own=false) = unsafe_wrap(Atype, p, dims, own)
end

export vSolve,
    load2LAP, set2LAP, LAP_Przybylski2008,
    load2OSP, set2OSP, OSP_VanWassenhove1980, generateHardInstance,
    load2UMFLP, set2UMFLP, UMFLP_Delmee2017,
    load2UKP, set2UKP, UKP_Jorge2010

__init__() = (!isfile(libLAPpath) || !isfile(libUMFLPpath)) || !isfile(libcomboPath) && warn("""Some libraries are missing, run Pkg.build("vOptSpecific")""")

end # module