mutable struct Model
    env::Env # Cplex environment
    lp::Ptr{Cvoid} # Cplex problem (lp)
    has_int::Bool # problem has integer variables?
    has_qc::Bool # problem has quadratic constraints?
    has_sos::Bool # problem has Special Ordered Sets?
    callback::Any
    terminator::Vector{Cint}
end
function Model(env::Env, lp::Ptr{Cvoid})
    notify_new_model(env)
    model = Model(env, lp, false, false, false, nothing, Cint[0])
    function model_finalizer(model)
        if model.lp != C_NULL
            free_problem(model)
        else
            # User must have called `free_problem` directly.
        end
        notify_freed_model(env)
    end
    finalizer(model_finalizer, model)
    set_terminate(model)
    return model
end

function Model(env::Env, name::String="CPLEX.jl")
    @assert is_valid(env)
    stat = Vector{Cint}(undef, 1)
    tmp = @cpx_ccall(createprob, Ptr{Cvoid}, (Ptr{Cvoid}, Ptr{Cint}, Ptr{Cchar}), env.ptr, stat, name)
    if tmp == C_NULL
        throw(CplexError(env, stat))
    end
    return Model(env, tmp)
end

function read_model(model::Model, filename::String)
    stat = @cpx_ccall(readcopyprob, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cchar}, Ptr{Cchar}), model.env.ptr, model.lp, filename, C_NULL)
    if stat != 0
        throw(CplexError(model.env, stat))
    end
    prob_type = get_prob_type(model)
    if prob_type in [:MILP,:MIQP, :MIQCP]
        model.has_int = true
    end
    if prob_type in [:QP, :MIQP, :QCP, :MIQCP]
        model.has_qc = true
    end
end

function write_model(model::Model, filename::String)
    if endswith(filename,".mps")
        filetype = "MPS"
    elseif endswith(filename,".lp")
        filetype = "LP"
    else
        error("Unrecognized file extension: $filename (Only .mps and .lp are supported)")
    end
    stat = @cpx_ccall(writeprob, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cchar}, Ptr{Cchar}), model.env.ptr, model.lp, filename, filetype)
    if stat != 0
        throw(CplexError(model.env, stat))
    end
end

## TODO: deep copy model, reset model

function c_api_getobjsen(model::Model)
    sense_int = @cpx_ccall(getobjsen, Cint, (
                           Ptr{Cvoid},
                           Ptr{Cvoid},
                           ),
                           model.env.ptr, model.lp)

    return sense_int
end
function get_sense(model::Model)
    sense_int = c_api_getobjsen(model)
    if sense_int == 1
        return :Min
    elseif sense_int == -1
        return :Max
    else
        error("CPLEX: problem object or environment does not exist")
    end
end

function set_sense!(model::Model, sense)
    if sense == :Min
        @cpx_ccall(chgobjsen, Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Cint), model.env.ptr, model.lp, 1)
    elseif sense == :Max
        @cpx_ccall(chgobjsen, Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Cint), model.env.ptr, model.lp, -1)
    else
        error("Unrecognized objective sense $sense")
    end
end

function c_api_chgobjsen(model::Model, sense_int::Cint)
    @cpx_ccall(chgobjsen, Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Cint),
               model.env.ptr, model.lp, sense_int)
end

function c_api_getobj(model::Model, sized_obj::FVec,
                      col_start::Cint, col_end::Cint)

    nvars = num_var(model)
    stat = @cpx_ccall(getobj, Cint, (
                      Ptr{Cvoid},
                      Ptr{Cvoid},
                      Ptr{Cdouble},
                      Cint,
                      Cint
                      ),
                      model.env.ptr, model.lp, sized_obj,
                      col_start - Cint(1), col_end - Cint(1))
    if stat != 0
        throw(CplexError(model.env, stat))
    end
end

function get_obj(model::Model)
    nvars = num_var(model)
    obj = Vector{Cdouble}(undef, nvars)
    stat = @cpx_ccall(getobj, Cint, (
                      Ptr{Cvoid},
                      Ptr{Cvoid},
                      Ptr{Cdouble},
                      Cint,
                      Cint
                      ),
                      model.env.ptr, model.lp, obj, 0, nvars-1)
    if stat != 0
        throw(CplexError(model.env, stat))
    end
    return obj
end

const type_map = Dict(
     0 => :LP,
     1 => :MILP,
     3 => :FIXEDMILP, # actually fixed milp
     5 => :QP,
     7 => :MIQP,
     8 => :FIXEDMIQP,
    10 => :QCP,
    11 => :MIQCP
)

const rev_prob_type_map = Dict(
    :LP    => 0,
    :MILP  => 1,
    :FIXEDMILP  => 3,
    :QP    => 5,
    :MIQP  => 7,
    :FIXEDMIQP  => 8,
    :QCP   => 10,
    :MIQCP => 11
)

function get_prob_type(model::Model)
  ret = @cpx_ccall(getprobtype, Cint, (
                   Ptr{Cvoid},
                   Ptr{Cvoid}),
                   model.env.ptr, model.lp)
  ret == -1 && error("No problem of environment")
  return type_map[Int(ret)]
end

function set_prob_type!(model::Model, tyint::Int)
    stat = @cpx_ccall(chgprobtype, Cint, (
                     Ptr{Cvoid},
                     Ptr{Cvoid},
                     Cint),
                     model.env.ptr, model.lp, tyint)
     if stat != 0
         throw(CplexError(model.env, stat))
     end
end
set_prob_type!(model::Model, ty::Symbol) = set_prob_type!(model, rev_prob_type_map[ty])

function set_obj!(model::Model, c::Vector)
    nvars = num_var(model)
    stat = @cpx_ccall(chgobj, Cint, (
                        Ptr{Cvoid},
                        Ptr{Cvoid},
                        Cint,
                        Ptr{Cint},
                        Ptr{Cdouble}
                        ),
                        model.env.ptr, model.lp, nvars, Cint[0:nvars-1;], float(c))
    if stat != 0
        throw(CplexError(model.env, stat))
    end
end

function c_api_chgobj(model::Model, indices::IVec, values::FVec)
    nvars = length(indices)
    stat = @cpx_ccall(chgobj, Cint, (
                        Ptr{Cvoid},
                        Ptr{Cvoid},
                        Cint,
                        Ptr{Cint},
                        Ptr{Cdouble}
                        ),
                        model.env.ptr, model.lp, nvars,
                        indices .- Cint(1), values)
    if stat != 0
        throw(CplexError(model.env, stat))
    end
end

function c_api_chgobjoffset(model::Model, offset::Float64)
    stat = @cpx_ccall(
        chgobjoffset,
        Cint,
        (Ptr{Cvoid}, Ptr{Cvoid}, Cdouble),
        model.env.ptr, model.lp, offset
    )
    if stat != 0
        throw(CplexError(model.env, stat))
    end
    return
end

function c_api_getobjoffset(model::Model)
    objoffset_p = Ref{Float64}()
    stat = @cpx_ccall(
        getobjoffset,
        Cint,
        (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cdouble}),
        model.env.ptr, model.lp, objoffset_p
    )
    if stat != 0
        throw(CplexError(model.env, stat))
    end
    return objoffset_p[]
end

set_warm_start!(model::Model, x::Vector{Float64}, effortlevel::Integer = CPX_MIPSTART_AUTO) = set_warm_start!(model, Cint[1:length(x);], x, effortlevel)

function set_warm_start!(model::Model, indx::IVec, val::FVec, effortlevel::Integer)
    stat = @cpx_ccall(addmipstarts, Cint, (
                      Ptr{Cvoid},
                      Ptr{Cvoid},
                      Cint,
                      Cint,
                      Ptr{Cint},
                      Ptr{Cint},
                      Ptr{Cdouble},
                      Ptr{Cint},
                      Ptr{Ptr{Cchar}}
                      ),
                      model.env.ptr, model.lp, 1, length(indx), Cint[0], indx .- Cint(1), val, Cint[effortlevel], C_NULL)
    if stat != 0
        throw(CplexError(model.env, stat))
    end
end

function free_problem(model::Model)
    stat = @cpx_ccall(freeprob, Cint, (Ptr{Cvoid}, Ptr{Cvoid}), model.env.ptr, model.lp)
    model.lp = C_NULL
    if stat != 0
        throw(CplexError(model.env, stat))
    end
end

function set_terminate(model::Model)
    stat = @cpx_ccall(setterminate, Cint, (Ptr{Cvoid},Ptr{Cint}), model.env.ptr, model.terminator)
    if stat != 0
        throw(CplexError(env, stat))
    end
end

terminate(model::Model) = (model.terminator[1] = 1)

mutable struct ConflictRefinerData
    stat::Int
    nrows::Int # number of rows participating in the conflict
    rowind::Vector{Cint} # index of the rows that participate
    rowstat::Vector{Cint} # state of the rows that participate
    ncols::Int # number of columns participating in the conflict
    colind::Vector{Cint} # index of the columns that participate
    colstat::Vector{Cint} # state of the columns that participates
end

function c_api_getconflict(model::Model)
    # This function always calls refineconflict first, which starts the conflict refiner.
    # In other words, any call to this function is expensive.

    # First, compute the conflict.
    confnumrows_p = Ref{Cint}()
    confnumcols_p = Ref{Cint}()
    stat = @cpx_ccall(
        refineconflict,
        Cint,
        (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cint}, Ptr{Cint}),
        model.env.ptr, model.lp, confnumrows_p, confnumcols_p)
    if stat != 0
        throw(CplexError(model.env, stat))
    end

    # Then, retrieve it.
    confstat_p = Ref{Cint}()
    rowind = Vector{Cint}(undef, confnumrows_p[])
    rowbdstat = Vector{Cint}(undef, confnumrows_p[])
    confnumrows_p = Ref{Cint}()
    colind = Vector{Cint}(undef, confnumcols_p[])
    colbdstat = Vector{Cint}(undef, confnumcols_p[])
    confnumcols_p  = Ref{Cint}()
    stat = @cpx_ccall(
        getconflict,
        Cint,
        (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cint}, Ptr{Cint}, Ptr{Cint}, Ptr{Cint}, Ptr{Cint}, Ptr{Cint}, Ptr{Cint}),
        model.env.ptr, model.lp, confstat_p, rowind, rowbdstat, confnumrows_p, colind, colbdstat, confnumcols_p)
    if stat != 0
        throw(CplexError(model.env, stat))
    end

    return ConflictRefinerData(confstat_p[], confnumrows_p[], rowind, rowbdstat, confnumcols_p[], colind, colbdstat)
end

function c_api_chgname(model::Model, key::Cchar, ij::Cint, name::String)
    stat = @cpx_ccall(
        chgname,
        Cint,
        (Ptr{Cvoid}, Ptr{Cvoid}, Cint, Cint, Ptr{Cchar}),
        model.env.ptr, model.lp, key, ij, name
    )
    if stat != 0
        throw(CplexError(model.env, stat))
    end
    return
end


module BooleanNetworks
export zerocfg, load_bnet, fasync_simulations, outputs_ratios, resolve

struct BooleanNetwork
    nodes::Vector{String}
    index::Dict{String,Int64}
    f::Vector{Function}
    out_influences::Vector{Vector{Int64}} # out-going influences
end

resolve(bn, indexes) = [bn.nodes[i] for i in indexes]

zerocfg(bn) = zeros(Bool, length(bn.f))

BNET_SYMBOL_RE = r"[\w\.:]+"

function load_bnet(filename, sep=",", impl_binary=true)
    data = [split(line, sep) for line in eachline(filename)]
    n = length(data)
    nodes = [strip(d[1]) for d in data]
    index = Dict(((n,i) for (i,n) in enumerate(nodes)))

    impl_expr_bin(expr) = "x -> " * replace(replace(expr,
            "!" => "~"),
             BNET_SYMBOL_RE => x -> "x[$(index[x])]")
    impl_expr_bool(expr) = "x -> " * replace(replace(replace(expr,
            "|" => "||"),
            "&" => "&&"),
             BNET_SYMBOL_RE => x -> "x[$(index[x])]")
    impl_expr = impl_binary ? impl_expr_bin : impl_expr_bool
    make_expr = eval ∘ Meta.parse ∘ impl_expr
    f = [make_expr(d[2]) for d in data]

    make_influences(expr) = [index[m.match] for m in eachmatch(BNET_SYMBOL_RE, expr)]
    influences = [make_influences(d[2]) for d in data]

    BooleanNetwork(nodes, index, f, influences)
end

struct FAsyncSimulationContext
    x::Vector{Bool}
    c::Vector{Int64}
end
new_fasync_simulation(n) = FAsyncSimulationContext(Vector{Bool}(undef, n), Vector{Int64}(undef, n))

function _fasync_step!(x, c, f, n)
    nc = 0
    for i in 1:n
        if f[i](x) ⊻ x[i]
            nc += 1
            c[nc] = i
        end
    end
    if nc == 0
        0
    else
        if nc == 1
            i = c[1]
        else
            i = c[rand(1:nc)]
        end
        x[i] = !x[i]
        i
    end
end

_fasync_unroll!(ctx, f, n, maxsteps) =
    for _ in 1:maxsteps
        if _fasync_step!(ctx.x, ctx.c, f, n) == 0
            break
        end
    end

function _fasync_simulation!(ctx, f, n, x, maxsteps, outputs)
    ctx.x[1:n] = x
    _fasync_unroll!(ctx, f, n, maxsteps)
    @view ctx.x[outputs]
end

function pack_output(binarray)
    x::Int64 = 0
    for i in 1:length(binarray)
        x |= binarray[i] << (i-1)
    end
    x
end
unpack_output(x, letters) = [a for (i,a) in enumerate(letters) if x & (1 << (i-1)) > 0]

function fasync_simulations(bn, outputs, nb_sims, maxsteps, x)
    f = bn.f
    n = length(f)
    res = Array{Int64}(undef, nb_sims)
    ctx = [new_fasync_simulation(n) for _ in 1:Threads.nthreads()]
    Threads.@threads :static for i in 1:nb_sims
        res[i] = pack_output(_fasync_simulation!(ctx[Threads.threadid()], f, n, x, maxsteps, outputs))
    end
    res
end

function outputs_ratios(result, outputs, bn)
    cv = zeros(Int64, 2^length(outputs))
    for k in result
        cv[k] += 1
    end
    make_key(k) = resolve(bn, unpack_output(k, outputs))
    Dict(make_key(k) => c/length(result) for (k,c) in enumerate(cv) if c > 0)
end

end


############################################################################################
#Import
include("phase.jl")
include("proposal.jl")
include("stepsize.jl")
include("diagnostics.jl")
include("tune.jl")

############################################################################################
# Export


module Adapt

using LinearAlgebra

adapt_(T, x) = x

adapt(T, x) = adapt_(T, x)

# Base integrations

adapt(T, x::Adjoint) = Adjoint(adapt(T, parent(x)))

end # module


# MIT license
# Copyright (c) Microsoft Corporation. All rights reserved.
# See LICENSE in the project root for full license information.

using Test
using OpticSim

using Unitful
using Base.Filesystem
using DataFrames
using StaticArrays

@testset "GlassCat" begin
    # ensure there aren't any ambiguities
    @test isempty(detect_ambiguities(GlassCat))
    @test isempty(detect_unbound_args(GlassCat))

    @testset "Build Tests" begin
        # check that all automatic downloads are working
        for catname in split("HOYA NIKON OHARA SCHOTT Sumita")
            agffile = joinpath(OpticSim.GlassCat.AGF_DIR, catname * ".agf")
            @test isfile(agffile)
        end

        # check that particularly problematic glasses are parsing correctly
        @test !isnan(OpticSim.GlassCat.NIKON.LLF6.C10)
    end

    @testset "generate.jl" begin
        CATALOG_NAME = "TEST_CAT"
        SOURCE_DIR = joinpath(@__DIR__, "..", "..", "test")
        SOURCE_FILE = joinpath(SOURCE_DIR, "$(CATALOG_NAME).agf")

        TMP_DIR = mktempdir()
        MAIN_FILE = joinpath(TMP_DIR, "AGF_TEST_CAT.jl")

        TEST_CAT_VALUES = DataFrame(
            name = split("MG463_ MG523 _5MG448 MG452_STAR"),
            raw_name = split("MG463& MG523 5MG448 MG452*"),
            dispform = repeat([2], 4),
            Nd = repeat([1.0], 4),
            Vd = repeat([0.0], 4),
            exclude_sub = [5, 0, 0, 0],
            status = [6, 0, 0, 0],
            meltfreq = repeat([-1], 4),
            TCE = [19.8, 18.3, 30.2, 25.7],
            p = [4.63, 5.23, 4.48, 4.52],
            ΔPgF = repeat([0.0], 4),
            ignore_thermal_exp = [3, 0, 0, 0],
            C1 = [6.781409960E+00, 4.401393677E+00, 8.427948454E+00, 4.656568861E+00],
            C2 = [9.868538020E-02, 2.234722960E-02, 6.325268390E-02, 1.170534894E-01],
            C3 = [3.469529960E-03, 4.562935070E+00, -2.441576539E+00, 1.409322578E+00],
            C4 = [2.277823700E+00, 3.239318260E-01, 1.997453950E-03, 2.445302672E-04],
            C5 = [2.126055280E+00, 6.863473749E-01, 4.975938104E-01, 6.352738622E-01],
            C6 = [3.227490100E+03, 1.625888344E+03, 1.443307391E+03, 1.397305161E+03],
            C7 = repeat([0.0], 4),
            C8 = repeat([0.0], 4),
            C9 = repeat([0.0], 4),
            C10 = repeat([0.0], 4),
            D₀ = [2.226000000E-05, 1.122401430E-04, -1.718689957E-05, -2.545621409E-07],
            D₁ = [5.150100000E-08, 2.868995096E-13, -6.499841076E-13, 1.258087492E-10],
            D₂ = [1.309700000E-10, -4.157332734E-16, -4.153311088E-16, 1.119703940E-11],
            E₀ = [4.382400000E-05, 9.532969159E-05, 8.220254442E-06, 2.184273853E-05],
            E₁ = [4.966000000E-07, -1.045247704E-11, 6.086168578E-12, -2.788674395E-09],
            λₜₖ = [3.728000000E-01, 6.338475915E-01, 3.610058073E-01, -1.948154058E+00],
            temp = [2.002000000E+01, 2.000000000E+01, 2.000000000E+01, 2.000000000E+01],
            relcost = [1.1, -1.0, -1.0, -1.0],
            CR = [1.2, -1.0, -1.0, -1.0],
            FR = [1.3, -1.0, -1.0, -1.0],
            SR = [1.4, -1.0, -1.0, -1.0],
            AR = [1.5, -1.0, -1.0, -1.0],
            PR = [1.6, -1.0, -1.0, -1.0],
            λmin = repeat([2.0], 4),
            λmax = repeat([14.0], 4),
            transmission = [
                [[2.0, 1.0, 3.0], [14.0, 4.0, 5.0]],
                [[2.0, 1.0, 1.0], [14.0, 1.0, 1.0]],
                [[2.0, 1.0, 1.0], [14.0, 1.0, 1.0]],
                [[2.0, 1.0, 1.0], [14.0, 1.0, 1.0]],
            ],
        )
        FIELDS = names(TEST_CAT_VALUES)[2:end]

        @testset "Parsing Tests" begin
            cat = OpticSim.GlassCat.sourcefile_to_catalog(SOURCE_FILE)

            for glass in eachrow(TEST_CAT_VALUES)
                name = glass["name"]
                @test name ∈ keys(cat)
                for field in FIELDS
                    @test glass[field] == cat[name][field]
                end
            end
        end

        @testset "Module Gen Tests" begin
            OpticSim.GlassCat.generate_jls([CATALOG_NAME], MAIN_FILE, TMP_DIR, SOURCE_DIR, test=true)
            include(MAIN_FILE)

            for row in eachrow(TEST_CAT_VALUES)
                name = row["name"]
                @test "$CATALOG_NAME.$name" ∈ TEST_GLASS_NAMES

                glass = getfield(getfield(Main, Symbol(CATALOG_NAME)), Symbol(name))
                @test glass ∈ TEST_GLASSES

                for field in FIELDS
                    if field === "raw_name"
                    elseif field === "transmission"
                        @test row[field] == getfield(glass, Symbol(field))[1:length(row[field])]
                        @test zero(SVector{100-length(row[field]),SVector{3,Float64}}) == getfield(glass, Symbol(field))[length(row[field])+1:end]
                    else
                        @test row[field] == getfield(glass, Symbol(field))
                    end
                end
            end

            # these used to be in the "Glass Tests" testset, but they rely on the generated AGF_TEST_CAT.jl file
            g = TEST_CAT.MG523
            @test OpticSim.GlassCat.index(g, ((g.λmin + g.λmax) / 2)u"μm") ≈ 3.1560980389455593 atol=1e-14
            @test_throws ErrorException GlassCat.index(g, (g.λmin - 1)u"μm")
            @test_throws ErrorException GlassCat.index(g, (g.λmax + 1)u"μm")
        end
    end

    @testset "Glass Tests" begin
        @test isapprox(OpticSim.GlassCat.absairindex(500 * u"nm"), 1.0002741948670688, atol = 1e-14)
        @test isapprox(OpticSim.GlassCat.absairindex(600 * u"nm", temperature = 35 * u"°C", pressure = 2.0), 1.0005179096900811, atol = 1e-14)
        @test OpticSim.GlassCat.index(OpticSim.GlassCat.Air, 500 * u"nm") == 1.0
        @test OpticSim.GlassCat.index(OpticSim.GlassCat.Air, 600 * u"nm") == 1.0
        # @test isapprox(index(OpticSim.GlassCat.Vacuum, 500 * u"nm"), 0.9997258841958212, atol = 1e-14)

        # test against true values
        g = OpticSim.GlassCat.SCHOTT.N_BK7
        @test isapprox(OpticSim.GlassCat.index(g, 533 * u"nm"), 1.519417351519283, atol = 1e-14)
        @test isapprox(OpticSim.GlassCat.index(g, 533 * u"nm", temperature = 35 * u"°C"), 1.519462486258311, atol = 1e-14)
        @test isapprox(OpticSim.GlassCat.index(g, 533 * u"nm", pressure = 2.0), 1.518994119690216, atol = 1e-14)
        @test isapprox(OpticSim.GlassCat.index(g, 533 * u"nm", temperature = 35 * u"°C", pressure = 2.0), 1.519059871499476, atol = 1e-14)

        # test transmission
        @test OpticSim.GlassCat.absorption(OpticSim.GlassCat.Air, 500 * u"nm") == 0.0
        @test OpticSim.GlassCat.absorption(OpticSim.GlassCat.Air, 600 * u"nm") == 0.0
        # TODO these are currently taken from this package (i.e. regression tests), ideally we would get true values somehow
        @test OpticSim.GlassCat.absorption(g, 500 * u"nm") == 0.0002407228930225164
        @test OpticSim.GlassCat.absorption(g, 600 * u"nm") == 0.00022060722752440445
        @test OpticSim.GlassCat.absorption(g, 3000 * u"nm") == 0.04086604990127926
        @test OpticSim.GlassCat.absorption(g, 600 * u"nm", temperature = 35 * u"°C", pressure = 2.0) == 0.00022075540719494738

        # test that everything is alloc-less
        @test (@allocated OpticSim.GlassCat.absorption(g, 600 * u"nm")) == 0
        @test (@allocated OpticSim.GlassCat.index(g, 533 * u"nm")) == 0
        @test (@allocated OpticSim.GlassCat.SCHOTT.N_BK7) == 0

        @test OpticSim.GlassCat.glassforid(OpticSim.GlassCat.glassid(OpticSim.GlassCat.SCHOTT.N_BK7)) == OpticSim.GlassCat.SCHOTT.N_BK7

        @test OpticSim.GlassCat.isair(OpticSim.GlassCat.Air) == true
        @test OpticSim.GlassCat.isair(OpticSim.GlassCat.SCHOTT.N_BK7) == false

        # test MIL and model glasses
        fit_acc = 0.001
        bk7 = OpticSim.GlassCat.glassfromMIL(517642)
        @test OpticSim.GlassCat.glassforid(OpticSim.GlassCat.glassid(bk7)) == bk7
        @test OpticSim.GlassCat.isapprox(OpticSim.GlassCat.index(bk7, 0.5875618), 1.517, atol = fit_acc)
        @test OpticSim.GlassCat.isapprox(OpticSim.GlassCat.index(bk7, 0.533), 1.519417351519283, atol = fit_acc)
        @test OpticSim.GlassCat.isapprox(OpticSim.GlassCat.index(bk7, 0.743), 1.511997032563557, atol = fit_acc)
        @test OpticSim.GlassCat.isapprox(OpticSim.GlassCat.index(bk7, 533 * u"nm", temperature = 35 * u"°C", pressure = 2.0), 1.519059871499476, atol = fit_acc)

        t2 = OpticSim.GlassCat.glassfromMIL(1.135635)
        @test OpticSim.GlassCat.isapprox(OpticSim.GlassCat.index(t2, 0.5875618), 2.135, atol = fit_acc)

        fit_acc = 0.0001
        bk7 = OpticSim.GlassCat.modelglass(1.5168, 64.167336, -0.0009)
        @test OpticSim.GlassCat.glassforid(OpticSim.GlassCat.glassid(bk7)) == bk7
        @test isapprox(OpticSim.GlassCat.index(bk7, 0.5875618), 1.5168, atol = fit_acc)
        @test isapprox(OpticSim.GlassCat.index(bk7, 0.533), 1.519417351519283, atol = fit_acc)
        @test isapprox(OpticSim.GlassCat.index(bk7, 0.743), 1.511997032563557, atol = fit_acc)
        @test isapprox(OpticSim.GlassCat.index(bk7, 533 * u"nm", temperature = 35 * u"°C", pressure = 2.0), 1.519059871499476, atol = fit_acc)

        # test other glass
        @test OpticSim.GlassCat.index(OpticSim.GlassCat.CARGILLE.OG0608, 0.578) == 1.4596475735607324

        # make sure that the other functions work
        OpticSim.GlassCat.plot_indices(OpticSim.GlassCat.SCHOTT.N_BK7; polyfit = true, fiterror = true)
    end
end # testset GlassCat


# This example shows how to convert a file with a list of images
# to HDF5 format that Mocha can process
#
# Note this is a simple demo to show how to convert a typical
# dataset into HDF5 file that Mocha could handle. We do not
# perform any pre-processing on the images and simply assume
# all the images are of the same size and with the same
# number of channels.
#
# The format of the file is:
#
# ImageName,Label,TrainTest
# image1.jpg,009,Train
# image2.jpg,018,Train
# ....
# imageK.jpg,053,Test
# ...
# imageN.jpg,009,Test

using DataFrames
using Images
using Color
using HDF5
using Compat

############################################################
# Configuration
############################################################
image_list_file = "example.csv"
data_type       = Float32

############################################################
# Converting dataset
############################################################
datasets = Dict{AbstractString, HDF5File}()
dsets_idx = Dict{AbstractString, Int}()

df = readtable(image_list_file)
for i = 1:size(df,1)
  image_fn = df[i, :ImageName]
  label    = df[i, :Label]
  dset_key = df[i, :TrainTest]

  println(image_fn)

  image = separate(convert(Image{RGB}, imread(image_fn)))
  data  = convert(Array, image)

  if !haskey(datasets, dset_key)
    #---------------
    # create dataset
    #---------------

    # count how many elements in this dataset
    dataset_count = sum(df[:TrainTest] .== dset_key)

    h5 = h5open("$dset_key.hdf5", "w")
    dset_data = d_create(h5, "data", datatype(data_type),
        dataspace(size(data)..., dataset_count))
    dset_label = d_create(h5, "label", datatype(data_type),
        dataspace(1, dataset_count))
    datasets[dset_key] = h5
    dsets_idx[dset_key] = 0
  end

  h5 = datasets[dset_key]
  idx = dsets_idx[dset_key]
  idx += 1
  h5["label"][1,idx] = label
  h5["data"][map(x -> 1:x, size(data))..., idx] = data
end

for h5 in values(datasets)
  close(h5)
end


"""
    change_sense(objective_sense)

Change the objective sense of optimization.
Possible arguments are `MOI.MAX_SENSE` and `MOI.MIN_SENSE`.

If you want to change the objective and sense at the same time, use
[`change_objective`](@ref) instead to do both at once.
"""
function change_sense(objective_sense)
    (model, opt_model) -> COBREXA.JuMP.set_objective_sense(opt_model, objective_sense)
end

"""
    change_optimizer(optimizer)

Change the JuMP optimizer used to run the optimization.

This may be used to try different approaches for reaching the optimum, and in
problems that may require different optimizers for different parts, such as the
[`parsimonious_flux_balance_analysis`](@ref).
"""
function change_optimizer(optimizer)
    (model, opt_model) -> COBREXA.JuMP.set_optimizer(opt_model, optimizer)
end

"""
    change_optimizer_attribute(attribute_key, value)

Change a JuMP optimizer attribute. The attributes are optimizer-specific, refer
to the JuMP documentation and the documentation of the specific optimizer for
usable keys and values.
"""
function change_optimizer_attribute(attribute_key, value)
    (model, opt_model) ->
        COBREXA.JuMP.set_optimizer_attribute(opt_model, attribute_key, value)
end

"""
    constrain_objective_value(tolerance)

Limit the objective value to `tolerance`-times the current objective value, as
with [`objective_bounds`](@ref).
"""
function constrain_objective_value(tolerance)
    return (model, opt_model) -> begin
        λmin, λmax = objective_bounds(tolerance)(objective_value(opt_model))
        old_objective = objective_function(opt_model)
        @constraint(opt_model, λmin <= sum(old_objective) <= λmax)
    end
end

"""
    change_constraint(id::String, lb, ub)

Change the lower and upper bounds (`lb` and `ub` respectively) of reaction `id`.
"""
function change_constraint(id::String, lb, ub)
    (model, opt_model) -> begin
        ind = first(indexin([id], reactions(model)))
        set_bound(ind, opt_model, lb = lb, ub = ub)
    end
end

"""
    change_objective(new_objective::Union{String,Vector{String}}; weights=[], sense=MOI.MAX_SENSE)

Modification that changes the objective function used in a constraint based
analysis function.  `new_objective` can be a single reaction identifier, or an
array of reactions identifiers.

Optionally, the objective can be weighted by a vector of `weights`, and a
optimization `sense` can be set.
"""
function change_objective(
    new_objective::Union{String,Vector{String}};
    weights = [],
    sense = MOI.MAX_SENSE,
)
    (model, opt_model) -> begin

        # Construct objective_indices array
        if typeof(new_objective) == String
            objective_indices = [first(indexin([new_objective], reactions(model)))]
        else
            objective_indices =
                [first(indexin([rxnid], reactions(model))) for rxnid in new_objective]
        end

        # Initialize weights
        opt_weights = spzeros(n_reactions(model))

        isempty(weights) && (weights = ones(length(objective_indices))) # equal weights

        for (j, i) in enumerate(objective_indices)
            opt_weights[i] = weights[j]
        end

        v = opt_model[:x]
        @objective(opt_model, sense, sum(opt_weights[i] * v[i] for i in objective_indices))
    end
end


######################## Mesh Generation function with GMSH ####################
function MeshGenerator(L,h1,h2,h3,hd,dpml,l0,ld,lpml)
    gmsh.initialize()
    gmsh.option.setNumber("General.Terminal", 1)
    gmsh.option.setNumber("Mesh.Algorithm", 6)
    gmsh.clear()
    gmsh.model.add("geometry") # name it whatever you want

    # Add points
    gmsh.model.geo.addPoint(-L/2-dpml, -hd-h2-h3-dpml, 0, lpml,  1)
    gmsh.model.geo.addPoint(-L/2     , -hd-h2-h3-dpml, 0, lpml,  2)
    gmsh.model.geo.addPoint(-L/2-dpml, -hd-h2-h3     , 0, lpml,  3)
    gmsh.model.geo.addPoint(-L/2     , -hd-h2-h3     , 0,   l0,  4)
    gmsh.model.geo.addPoint( L/2     , -hd-h2-h3-dpml, 0, lpml,  5)
    gmsh.model.geo.addPoint( L/2     , -hd-h2-h3     , 0,   l0,  6)
    gmsh.model.geo.addPoint( L/2+dpml, -hd-h2-h3-dpml, 0, lpml,  7)
    gmsh.model.geo.addPoint( L/2+dpml, -hd-h2-h3     , 0, lpml,  8)
    gmsh.model.geo.addPoint(-L/2-dpml, -hd-h2        , 0,   l0,  9)
    gmsh.model.geo.addPoint(-L/2     , -hd-h2        , 0,   l0, 10)
    gmsh.model.geo.addPoint( L/2     , -hd-h2        , 0,   l0, 11)
    gmsh.model.geo.addPoint( L/2+dpml, -hd-h2        , 0,   l0, 12)
    gmsh.model.geo.addPoint(-L/2-dpml, -hd           , 0,   ld, 13)
    gmsh.model.geo.addPoint(-L/2     , -hd           , 0,   ld, 14)
    gmsh.model.geo.addPoint( L/2     , -hd           , 0,   ld, 15)
    gmsh.model.geo.addPoint( L/2+dpml, -hd           , 0,   ld, 16)
    gmsh.model.geo.addPoint(-L/2-dpml, 0.            , 0,   ld, 17)
    gmsh.model.geo.addPoint(-L/2     , 0.            , 0,   ld, 18)
    gmsh.model.geo.addPoint( L/2     , 0.            , 0,   ld, 19)
    gmsh.model.geo.addPoint( L/2+dpml, 0.            , 0,   ld, 20)
    gmsh.model.geo.addPoint(-L/2-dpml, h1            , 0, lpml, 21)
    gmsh.model.geo.addPoint(-L/2     , h1            , 0,   l0, 22)
    gmsh.model.geo.addPoint( L/2     , h1            , 0,   l0, 23)
    gmsh.model.geo.addPoint( L/2+dpml, h1            , 0, lpml, 24)
    gmsh.model.geo.addPoint(-L/2-dpml, h1+dpml       , 0, lpml, 25)
    gmsh.model.geo.addPoint(-L/2     , h1+dpml       , 0, lpml, 26)
    gmsh.model.geo.addPoint( L/2     , h1+dpml       , 0, lpml, 27)
    gmsh.model.geo.addPoint( L/2+dpml, h1+dpml       , 0, lpml, 28)
    #gmsh.model.geo.addPoint(-Lt/2, h1/2-ht/2, 0, l0, 29)
    #gmsh.model.geo.addPoint( Lt/2, h1/2-ht/2, 0, l0, 30)
    #gmsh.model.geo.addPoint( Lt/2, h1/2+ht/2, 0, l0, 31)
    #gmsh.model.geo.addPoint(-Lt/2, h1/2+ht/2, 0, l0, 32)
    #gmsh.model.geo.addPoint(x_s,y_s, 0, l_s, 17)
    # Add lines
    gmsh.model.geo.addLine(  1,  2,  1)
    gmsh.model.geo.addLine(  2,  4,  2)
    gmsh.model.geo.addLine(  4,  3,  3)
    gmsh.model.geo.addLine(  3,  1,  4)
    gmsh.model.geo.addLine(  2,  5,  5)
    gmsh.model.geo.addLine(  5,  6,  6)
    gmsh.model.geo.addLine(  6,  4,  7)
    gmsh.model.geo.addLine(  5,  7,  8)
    gmsh.model.geo.addLine(  7,  8,  9)
    gmsh.model.geo.addLine(  8,  6, 10)
    gmsh.model.geo.addLine(  4, 10, 11)
    gmsh.model.geo.addLine( 10,  9, 12)
    gmsh.model.geo.addLine(  9,  3, 13)
    gmsh.model.geo.addLine(  6, 11, 14)
    gmsh.model.geo.addLine( 11, 10, 15)
    gmsh.model.geo.addLine(  8, 12, 16)
    gmsh.model.geo.addLine( 12, 11, 17)
    gmsh.model.geo.addLine( 10, 14, 18)
    gmsh.model.geo.addLine( 14, 13, 19)
    gmsh.model.geo.addLine( 13,  9, 20)
    gmsh.model.geo.addLine( 11, 15, 21)
    gmsh.model.geo.addLine( 15, 14, 22)
    gmsh.model.geo.addLine( 12, 16, 23)
    gmsh.model.geo.addLine( 16, 15, 24)
    gmsh.model.geo.addLine( 14, 18, 25)
    gmsh.model.geo.addLine( 18, 17, 26)
    gmsh.model.geo.addLine( 17, 13, 27)
    gmsh.model.geo.addLine( 15, 19, 28)
    gmsh.model.geo.addLine( 19, 18, 29)
    gmsh.model.geo.addLine( 16, 20, 30)
    gmsh.model.geo.addLine( 20, 19, 31)
    gmsh.model.geo.addLine( 18, 22, 32)
    gmsh.model.geo.addLine( 22, 21, 33)
    gmsh.model.geo.addLine( 21, 17, 34)
    gmsh.model.geo.addLine( 19, 23, 35)
    gmsh.model.geo.addLine( 23, 22, 36)
    gmsh.model.geo.addLine( 20, 24, 37)
    gmsh.model.geo.addLine( 24, 23, 38)
    gmsh.model.geo.addLine( 22, 26, 39)
    gmsh.model.geo.addLine( 26, 25, 40)
    gmsh.model.geo.addLine( 25, 21, 41)
    gmsh.model.geo.addLine( 23, 27, 42)
    gmsh.model.geo.addLine( 27, 26, 43)
    gmsh.model.geo.addLine( 24, 28, 44)
    gmsh.model.geo.addLine( 28, 27, 45)
    #gmsh.model.geo.addLine( 29, 30, 46)
    #gmsh.model.geo.addLine( 30, 31, 47)
    #gmsh.model.geo.addLine( 31, 32, 48)
    #gmsh.model.geo.addLine( 32, 29, 49)
    # Construct curve loops and surfaces
    gmsh.model.geo.addCurveLoop([  1,  2,  3,  4], 1)
    gmsh.model.geo.addCurveLoop([  5,  6,  7, -2], 2)
    gmsh.model.geo.addCurveLoop([  8,  9, 10, -6], 3)
    gmsh.model.geo.addCurveLoop([ 11, 12, 13, -3], 4)
    gmsh.model.geo.addCurveLoop([ -7, 14, 15,-11], 5)
    gmsh.model.geo.addCurveLoop([ 16, 17,-14,-10], 6)
    gmsh.model.geo.addCurveLoop([ 18, 19, 20,-12], 7)
    gmsh.model.geo.addCurveLoop([-15, 21, 22,-18], 8)
    gmsh.model.geo.addCurveLoop([-17, 23, 24,-21], 9)
    gmsh.model.geo.addCurveLoop([-19, 25, 26, 27],10)
    gmsh.model.geo.addCurveLoop([-22, 28, 29,-25],11)
    gmsh.model.geo.addCurveLoop([-24, 30, 31,-28],12)
    gmsh.model.geo.addCurveLoop([-26, 32, 33, 34],13)
    gmsh.model.geo.addCurveLoop([-29, 35, 36,-32],14)
    gmsh.model.geo.addCurveLoop([-31, 37, 38,-35],15)
    gmsh.model.geo.addCurveLoop([-33, 39, 40, 41],16)
    gmsh.model.geo.addCurveLoop([-36, 42, 43,-39],17)
    gmsh.model.geo.addCurveLoop([-38, 44, 45,-42],18)
    #gmsh.model.geo.addCurveLoop([ 46, 47, 48, 49],19)
    #gmsh.model.geo.addCurveLoop([29, 30], 11)
    gmsh.model.geo.addPlaneSurface([ 1],  1)
    gmsh.model.geo.addPlaneSurface([ 2],  2)
    gmsh.model.geo.addPlaneSurface([ 3],  3)
    gmsh.model.geo.addPlaneSurface([ 4],  4)
    gmsh.model.geo.addPlaneSurface([ 5],  5)
    gmsh.model.geo.addPlaneSurface([ 6],  6)
    gmsh.model.geo.addPlaneSurface([ 7],  7)
    gmsh.model.geo.addPlaneSurface([ 8],  8)
    gmsh.model.geo.addPlaneSurface([ 9],  9)
    gmsh.model.geo.addPlaneSurface([10], 10)
    gmsh.model.geo.addPlaneSurface([11], 11)
    gmsh.model.geo.addPlaneSurface([12], 12)
    gmsh.model.geo.addPlaneSurface([13], 13)
    gmsh.model.geo.addPlaneSurface([14], 14)
    gmsh.model.geo.addPlaneSurface([15], 15)
    gmsh.model.geo.addPlaneSurface([16], 16)
    gmsh.model.geo.addPlaneSurface([17], 17)
    gmsh.model.geo.addPlaneSurface([18], 18)
    #gmsh.model.geo.addPlaneSurface([19], 19)
    # Physical groups
    gmsh.model.addPhysicalGroup(0, [1,3,21,25], 1)
    gmsh.model.setPhysicalName(0, 1, "LeftNodes")
    gmsh.model.addPhysicalGroup(0, [7,8,24,28], 2)
    gmsh.model.setPhysicalName(0, 2, "RightNodes")
    gmsh.model.addPhysicalGroup(0, [2,5], 3)
    gmsh.model.setPhysicalName(0, 3, "BottomNodes")
    gmsh.model.addPhysicalGroup(0, [26,27], 4)
    gmsh.model.setPhysicalName(0, 4, "TopNodes")
    gmsh.model.addPhysicalGroup(0, [13,17], 5)
    gmsh.model.setPhysicalName(0, 5, "LeftDNodes")
    gmsh.model.addPhysicalGroup(0, [16,20], 6)
    gmsh.model.setPhysicalName(0, 6, "RightDNodes")
    gmsh.model.addPhysicalGroup(0, [14,15,18,19], 7)
    gmsh.model.setPhysicalName(0, 7, "TBDNodes")
    gmsh.model.addPhysicalGroup(0, [4,6,22,23], 8)
    gmsh.model.setPhysicalName(0, 8, "InnerNodes")
    gmsh.model.addPhysicalGroup(0, [9,10,11,12], 9)
    gmsh.model.setPhysicalName(0, 9, "SourceNodes")

    gmsh.model.addPhysicalGroup(1, [4,13,20,34,41], 10)
    gmsh.model.setPhysicalName(1, 10, "LeftEdge")
    gmsh.model.addPhysicalGroup(1, [9,16,23,37,44], 11)
    gmsh.model.setPhysicalName(1, 11, "RightEdge")
    gmsh.model.addPhysicalGroup(1, [1,5,8], 12)
    gmsh.model.setPhysicalName(1, 12, "BottomEdge")
    gmsh.model.addPhysicalGroup(1, [40,43,45], 13)
    gmsh.model.setPhysicalName(1, 13, "TopEdge")
    gmsh.model.addPhysicalGroup(1, [27], 14)
    gmsh.model.setPhysicalName(1, 14, "LeftDEdge")
    gmsh.model.addPhysicalGroup(1, [30], 15)
    gmsh.model.setPhysicalName(1, 15, "RightDEdge")
    gmsh.model.addPhysicalGroup(1, [19,22,24,26,29,31], 16)
    gmsh.model.setPhysicalName(1, 16, "TBDEdge")
    gmsh.model.addPhysicalGroup(1, [2,3,6,7,10,11,14,18,21,25,28,32,33,35,36,38,39,42], 17)
    gmsh.model.setPhysicalName(1, 17, "InnerEdge")
    gmsh.model.addPhysicalGroup(1, [46,47,48,49], 18)
    gmsh.model.setPhysicalName(1, 18, "TargetEdge")
    gmsh.model.addPhysicalGroup(1, [12,15,17], 19)
    gmsh.model.setPhysicalName(1, 19, "SourceEdge")

    gmsh.model.addPhysicalGroup(2, [10,11,12], 20)
    gmsh.model.setPhysicalName(2, 20, "Design")
    gmsh.model.addPhysicalGroup(2, [16,17,18], 21)
    gmsh.model.setPhysicalName(2, 21, "TopPML")
    gmsh.model.addPhysicalGroup(2, [1,2,3], 22)
    gmsh.model.setPhysicalName(2, 22, "BottomPML")
    gmsh.model.addPhysicalGroup(2, [14], 23)
    gmsh.model.setPhysicalName(2, 23, "Target")
    gmsh.model.addPhysicalGroup(2, [4,5,6,7,8,9], 24)
    gmsh.model.setPhysicalName(2, 24, "Incident")
    gmsh.model.addPhysicalGroup(2, [13,15], 25)
    gmsh.model.setPhysicalName(2, 25, "LRTarget")
    gmsh.model.geo.synchronize()
    gmsh.model.mesh.generate(2)

    # ... and save it to disk
    gmsh.write("geometry.msh")
    gmsh.finalize()
end


using PowerSystems
cost = VariableCost([(1.0, 1.0), (2.0, 1.1), (3.0, 1.2)])
slopes = get_slopes(cost)
res = [1.0, 10.0, 10.0]
for (ix, v) in enumerate(slopes)
    @test isapprox(v, res[ix])
end

bps = get_breakpoint_upperbounds(cost)
res = [1.0, 0.1, 0.1]
for (ix, v) in enumerate(bps)
    @test isapprox(v, res[ix])
end

@testset "Test MarketBidCost with Polynomial Cost Timeseries with Service Forecast " begin
    initial_time = Dates.DateTime("2020-01-01")
    resolution = Dates.Hour(1)
    other_time = initial_time + resolution
    name = "test"
    horizon = 24
    service_data = Dict(initial_time => ones(horizon), other_time => ones(horizon))
    polynomial_cost = repeat([(999.0, 1.0)], 24)
    data_polynomial =
        SortedDict(initial_time => polynomial_cost, other_time => polynomial_cost)
    sys = create_rts_system()
    generators = collect(get_components(ThermalStandard, sys))
    generator = get_component(ThermalStandard, sys, get_name(generators[1]))
    market_bid = MarketBidCost(nothing)
    set_operation_cost!(generator, market_bid)
    forecast = IS.Deterministic("variable_cost", data_polynomial, resolution)
    set_variable_cost!(sys, generator, forecast)
    for s in generator.services
        forecast = IS.Deterministic(get_name(s), service_data, resolution)
        set_service_bid!(sys, generator, s, forecast)
    end

    cost_forecast = get_variable_cost(generator, market_bid, start_time = initial_time)
    @test first(TimeSeries.values(cost_forecast)).cost ==
          first(data_polynomial[initial_time])

    for s in generator.services
        service_cost = get_services_bid(generator, market_bid, s, start_time = initial_time)
        @test first(TimeSeries.values(service_cost)).cost ==
              first(service_data[initial_time])
    end
end

@testset "Test MarketBidCost with PWL Cost Timeseries" begin
    initial_time = Dates.DateTime("2020-01-01")
    resolution = Dates.Hour(1)
    other_time = initial_time + resolution
    name = "test"
    horizon = 24
    pwl_cost = repeat([repeat([(999.0, 1.0)], 5)], 24)
    data_pwl = SortedDict(initial_time => pwl_cost, other_time => pwl_cost)
    sys = create_rts_system()
    generators = collect(get_components(ThermalStandard, sys))
    generator = get_component(ThermalStandard, sys, get_name(generators[1]))
    market_bid = MarketBidCost(nothing)
    set_operation_cost!(generator, market_bid)
    forecast = IS.Deterministic("variable_cost", data_pwl, resolution)
    set_variable_cost!(sys, generator, forecast)

    cost_forecast = get_variable_cost(generator, market_bid, start_time = initial_time)
    @test first(TimeSeries.values(cost_forecast)).cost == first(data_pwl[initial_time])
end

@testset "Test ReserveDemandCurve with Cost Timeseries" begin
    initial_time = Dates.DateTime("2020-01-01")
    resolution = Dates.Hour(1)
    other_time = initial_time + resolution
    name = "test"
    horizon = 24
    polynomial_cost = repeat([(999.0, 1.0)], 24)
    data_polynomial =
        SortedDict(initial_time => polynomial_cost, other_time => polynomial_cost)
    pwl_cost = repeat([repeat([(999.0, 1.0)], 5)], 24)
    data_pwl = SortedDict(initial_time => pwl_cost, other_time => pwl_cost)
    for d in [data_polynomial, data_pwl]
        @testset "Add deterministic from $(typeof(d)) to ReserveDemandCurve variable cost" begin
            sys = System(100)
            reserve = ReserveDemandCurve{ReserveUp}(nothing)
            add_component!(sys, reserve)
            forecast = IS.Deterministic("variable_cost", d, resolution)
            set_variable_cost!(sys, reserve, forecast)
            cost_forecast = get_variable_cost(reserve, start_time = initial_time)
            @test first(TimeSeries.values(cost_forecast)).cost == first(d[initial_time])
        end
    end
end


using ...Model
using ..Gui

# Data is a list of floats. This graph connects them end to end forming
# a curve.
# The data is loaded ???
mutable struct SomaAPFastGraph <: AbstractGraph
    show_vertical_t_bar_markers::Bool
    time_pos::Int64
    value::Float64

    function SomaAPFastGraph()
        o = new()
        o.show_vertical_t_bar_markers = false
        o
    end
end

function draw_header(graph::SomaAPFastGraph, gui_data::Gui.GuiData, model::Model.ModelData)
    if CImGui.TreeNode("Controls##2")
        CImGui.PushItemWidth(1000)

        duration = Cint(Model.duration(model))
        begin_v = Cint(Model.range_start(model))
        end_v = Cint(Model.range_end(model))
        @c CImGui.DragIntRange2("Range##2", &begin_v, &end_v, 1, 1, duration, "Start: %d", "End: %d")
        if Int64(begin_v) > 0 && Int64(end_v) <= duration
            if Int64(begin_v) < Int64(end_v)
                Model.set_range_start!(model, Int64(begin_v))
                Model.set_range_end!(model, Int64(end_v))
            end
        end

        pos = Cfloat(0.0)
        @c CImGui.SliderFloat("Scroll velocity", &pos, -5.0, 5.0, "%.2f")
        Model.set_scroll!(model, Float64(pos))

        range_start = Model.range_start(model)
        range_end = Model.range_end(model) # duration
        range = range_end - range_start

        if range < MAX_VERTICAL_BARS # Limit bars to less than 500 because Drawlist is limited to 2^16 items.
            @c CImGui.Checkbox("Vertical Time Bars", &graph.show_vertical_t_bar_markers)
        else
            graph.show_vertical_t_bar_markers = false
        end

        CImGui.PopItemWidth()

        CImGui.TreePop()
    end
end

function draw_data(graph::SomaAPFastGraph, gui_data::Gui.GuiData,
    draw_list::Ptr{CImGui.LibCImGui.ImDrawList},
    canvas_pos::CImGui.LibCImGui.ImVec2, canvas_size::CImGui.LibCImGui.ImVec2,
    model::Model.ModelData, samples::Model.Samples)
    CImGui.PushClipRect(draw_list, canvas_pos,
        ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y), true) # clip lines within the canvas (if we resize it, etc.)

    # We can't render anything until the model has been loaded
    # ALONG with the samples.
    if !Model.is_loaded(model)
        return
    end

    io = CImGui.GetIO()

    synapses = Model.synapses(model)
    span_time = Model.span_time(model)
    duration = Model.duration(model)
    canvas_width = Float64(canvas_size.x)
    canvas_height = Float64(canvas_size.y)

    # ------------------------------------------------------------------------
    # Adjust range_start and range_end
    # ------------------------------------------------------------------------
    range_start = Model.range_start(model)
    range_end = Model.range_end(model)
    range = range_end - range_start
    # Use window_position (0.0->1.0) to Lerp Range-start and Range-end.
    scroll = Model.scroll(model)

    velocity = scroll_velocity(scroll)

    if scroll < 0
        range_start += velocity
        # Left
        if range_start > 0
            range_end = range_start + range
        else
            range_start = 1
            range_end = range_start + range
        end
    elseif scroll > 0
        range_end += velocity
        if range_end < duration
            range_start = range_end - range
        else
            range_end = duration
            range_start = range_end - range
        end
    end
    range_start = Int64(round(range_start))
    if range_start < 1
        range_start = 1
        range_end = range_start + range
    end

    range_end = Int64(round(range_end))

    # Reflect values back to model
    Model.set_range_start!(model, Int64(round(range_start)))
    Model.set_range_end!(model, Int64(round(range_end)))
    # ------------------------------------------------------------------------

    # ------------------------------------------------------------------------
    # Mouse vertical tracking and markers
    # ------------------------------------------------------------------------
    mouse_pos = CImGui.GetMousePos()
    mpx = mouse_pos.x

    data_samples = samples.soma_apFast_samples

    # Mapped data coords
    u_x = 0.0
    w_x = 0.0
    pl_vx = 0.0
    # time_pos tracks the actual time regardless of scrolling so it always
    # starts at the current range start value.
    time_pos = range_start

    # "t" is a counter over the range "size". time_pos is the actual
    # time value capture for the tooltip
    for t in 1:range
        # We want the markers to track with time as well, so we map "t".
        u_x = map_sample_to_unit(Float64(t), 0.0, Float64(range))
        w_x = map_unit_to_window(u_x, 0.0, canvas_width)
        (l_x, l_y) = map_window_to_local(w_x, 0.0, canvas_pos)

        if mpx > pl_vx && mpx < l_x
            CImGui.AddLine(draw_list,
                ImVec2(l_x, l_y),
                ImVec2(l_x, l_y + canvas_size.y),
                LIGHT_GREY, LINE_THICKNESS)
            graph.time_pos = time_pos + 1
            graph.value = data_samples.samples[graph.time_pos]
        else
            # Show vertical bars for visual references.
            if graph.show_vertical_t_bar_markers
                CImGui.AddLine(draw_list,
                    ImVec2(l_x, l_y),
                    ImVec2(l_x, l_y + canvas_size.y),
                    GREY, LINE_THICKNESS)
            end
        end

        pl_vx = l_x # Capture previous value for interval testing
        time_pos += 1
    end
    # ------------------------------------------------------------------------
    s_y = 0.0
    pl_y = 0.0 # previously mapped y value
    pl_x = 0.0 # previously mapped x value

    # ------------------------------------------------------------------------
    # Render segmented curve
    # ------------------------------------------------------------------------
    # A span is a collection of rows

    # Iterate samples with the defined range. "t" is mapped as "x" coord
    for t in range_start:range_end
        s_y = data_samples.samples[t]

        # The sample value needs to be mapped
        u_x = map_sample_to_unit(Float64(t), Float64(range_start), Float64(range_end))
        u_y = map_sample_to_unit(s_y, data_samples.min, data_samples.max)

        w_x = map_unit_to_window(u_x, 0.0, canvas_width)
        # graph space has +Y downward, but the data is oriented as +Y upward
        # so we flip in unit-space.
        u_y = 1.0 - u_y
        w_y = map_unit_to_window(u_y, 0.0, canvas_height)
        (l_x, l_y) = map_window_to_local(w_x, w_y, canvas_pos)

        CImGui.AddLine(draw_list,
            ImVec2(pl_x, pl_y), ImVec2(l_x, l_y),
            ORANGE, LINE_THICKNESS)

        # if model.bug println("vt: ", vt) end
        pl_x = l_x
        pl_y = l_y
    end

    # if model.bug print(l_x, ",") end
    # model.bug = false
    # Show the min/max values of the data.
    CImGui.AddText(draw_list, ImVec2(5 + canvas_pos.x, 3 + canvas_pos.y), WHITE_TRAN, @sprintf("%3.3f", data_samples.max))
    CImGui.AddText(draw_list, ImVec2(5 + canvas_pos.x, canvas_pos.y + canvas_size.y - 20), WHITE_TRAN, @sprintf("%3.3f", data_samples.min))

    CImGui.PopClipRect(draw_list)

end
# if model.bug print("") end

function draw_graph(graph::SomaAPFastGraph, gui_data::Gui.GuiData, model::Model.ModelData, samples::Model.Samples)
    draw_list = CImGui.GetWindowDrawList()
    canvas_pos = CImGui.GetCursorScreenPos()            # ImDrawList API uses screen coordinates!
    canvas_size = CImGui.GetContentRegionAvail()        # resize canvas to what's available

    cx, cy = canvas_size.x, canvas_size.y
    if cx < 50.0
        cx = 50.0
    end
    if cy < 50.0
        cy = 50.0
    end
    canvas_size = ImVec2(cx, cy)
    # A visible button scaled to the size of the canvas is used for hover checking
    CImGui.InvisibleButton("canvas", canvas_size)

    CImGui.AddRectFilledMultiColor(draw_list, canvas_pos,
        ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y),
        IM_COL32(GRAY, GRAY, GRAY, 255), IM_COL32(GRAY, GRAY, GRAY, 255),
        IM_COL32(GRAY, GRAY, GRAY, 255), IM_COL32(GRAY, GRAY, GRAY, 255))

    # CImGui.Text(@sprintf("hello"))

    if CImGui.IsItemHovered()
        CImGui.BeginTooltip()
        CImGui.Text(@sprintf("%d (%4.3f)", graph.time_pos, graph.value))
        CImGui.EndTooltip()
    end

    CImGui.AddRect(draw_list, canvas_pos,
        ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y),
        IM_COL32(128, 128, 128, 255))

    draw_data(graph, gui_data, draw_list, canvas_pos, canvas_size, model, samples)
end

function draw(graph::SomaAPFastGraph, gui_data::Gui.GuiData, model::Model.ModelData, samples::Model.Samples, vert_pos::Int64)
    # CImGui.SetNextWindowPos((0, 25 + WINDOW_HEIGHT), CImGui.ImGuiCond_Once)
    CImGui.SetNextWindowPos((0, vert_pos), CImGui.ImGuiCond_Once)
    CImGui.SetNextWindowSize((GRAPH_WINDOW_WIDTH, GRAPH_WINDOW_HEIGHT), CImGui.ImGuiCond_Always)

    CImGui.Begin("Soma apFast Graph")

    draw_header(graph, gui_data, model)

    CImGui.Separator()

    draw_graph(graph, gui_data, model, samples)

    CImGui.End()
end


type wfinfo
    mediumtype::String
    component :: Array{String} # provide information of output data component
    mediumsize :: Array{Int64}
    ext :: Int64
    gridsize :: Array{Float64}
    timesamp :: Int64 # Total time sampling nT
    timepoint :: Array{Float64}
end

function readwfinfo(DataPath::String)
    fid = open(DataPath,"r")
    iflag = read(fid,Int64,1)
    if iflag[1] == 1
        mediumtype = "free surface"
    elseif iflag[1] == 2
        mediumtype = "unlimited"
    else error("Check wavefield file")
    end

    CpntDict = Dict(1=>"P",2=>"Vx",3=>"Vz",
                  4=>"Txx",5=>"Tzz",6=>"Txz")
    nCpnt = read(fid,Int64,1)
    NumCpnt = read(fid,Int64,nCpnt[1])
    component = []
    for i in NumCpnt
      if i == 23
          push!(component,CpntDict[2])
          push!(component,CpntDict[3])
      elseif i == 45
          push!(component,CpntDict[4])
          push!(component,CpntDict[5])
      else push!(component,CpntDict[i])
      end
    end
    mediumsize = read(fid,Int64,2)
    ext = read(fid,Int64,1)
    gridsize = read(fid,Float64,2)
    timesamp = read(fid,Int64,1)
    timepoint = read(fid,Float64,timesamp[1])

    return wfinfo(mediumtype, component, mediumsize,
    ext[1], gridsize, timesamp[1], timepoint)

end
function readwfdata(fid::IOStream,compnumber::Int64,timesamp::Int64,
    startpoint::Int64,length::Int64)
    seek(fid,8*(8+compnumber+timesamp+startpoint))
    requireddata = read(fid,Float64,length)
    return requireddata
end

function readwfdata(fid::IOStream,startpoint::Int64,length::Int64)
    seek(fid,8)
    compnumber = read(fid,Int64,1)
    seek(fid,8*(7+compnumber[1]))
    timesamp = read(fid,Int64,1)
    seek(fid,8*(8+compnumber[1]+timesamp[1]+startpoint))
    requireddata = read(fid,Float64,length)
    return(requireddata)
end

function readwfdata(fid::IOStream,length::Int64)
    requireddata = read(fid,Float64,length)
    return requireddata
end

function readwfdata(DataPath::String)
    fid = open(DataPath,"r")
    iflag = read(fid,Int64,1)
    nCpnt = read(fid,Int64,1)
    NumCpnt = read(fid,Int64,nCpnt[1])
    mediumsize = read(fid,Int64,2)
    ext = read(fid,Int64,1)
    gridsize = read(fid,Float64,2)
    timesamp = read(fid,Int64,1)
    timepoint = read(fid,Float64,timesamp[1])
    CpntDict = Dict(1=>mediumsize[1]*mediumsize[2],
                    2=>mediumsize[1]*(mediumsize[2]-1),
                    3=>(mediumsize[1]-1)*mediumsize[2],
                    4=>mediumsize[1]*mediumsize[2],
                    5=>mediumsize[1]*mediumsize[2],
                    6=>(mediumsize[1]-1)*(mediumsize[2]-1))
    totalCpnt = 0
    for Cpnt in NumCpnt
        if Cpnt == 23
            totalCpnt = totalCpnt + CpntDict[2] + CpntDict[3]
        elseif Cpnt == 45
            totalCpnt = totalCpnt + CpntDict[4] + CpntDict[5]
        else totalCpnt = totalCpnt + CpntDict[Cpnt]
        end
    end
    requireddata = read(fid,Float64,totalCpnt*timesamp[1])
    requireddata = reshape(requireddata, totalCpnt, timesamp[1])
    return requireddata
end


function prepare_metnet(proj, base_model; inf_medium = false)

    base_model = deepcopy(base_model)

    ## ---------------------------------------------------------------------
    println("FBOut: ", MetLP.fba!(base_model, HG.HUMAN_BIOMASS_IDER).obj_val)

    ## ---------------------------------------------------------------------
    ## Description

    # This script prepare the original HumanGEM model for modeling. It include some modifications to it (see comment in code) extracted from several data sources.

    ## ---------------------------------------------------------------------
    make_supp_files = (proj == HG)
    make_supp_files && @info("Creating support files")

    ## ---------------------------------------------------------------------
    M, N = size(base_model)
    println("Base Model: ", (M, N))

    ## ---------------------------------------------------------------------
    # mets_readable_ids
    # we build a more human readable met ids
    if make_supp_files
        println("\nMet readable ids")
        mat_model = HG.load_humangem_mat_model()
        met_readable_ids = Dict()
        for i in 1:M
            readable_id = mat_model[:metNames][i] * "[" * mat_model[:comps][mat_model[:metComps][i]] * "]";
            met_readable_ids[base_model.mets[i]] = readable_id
            met_readable_ids[readable_id] = base_model.mets[i]
        end

        # Saving
        sdat(HG, met_readable_ids,
            "met_readable_ids", ".jls";
            verbose = true
        )
    else
        met_readable_ids = HG.load_met_readable_ids()
    end

    ## ---------------------------------------------------------------------
    # Exchanges
    # bkwd and fwd splitted reactions are troublemakers for EP, but they are necessary to model enzymatic costs.
    # So, we leave as least as possible. We unified the exchanges (make them a unique rxn),
    # and let them in a semi-open state (intake bloked, outtake open)

    # I will delete all the Boundary (x) (see comps in the matmodel) metabilites,
    # leaving only the Extracellular (s) metabolites in the exchange reactions.
    # Why? they are not required
    println("\nDeleting Boundary (x) metabolites ")
    to_del = [met for met in base_model.mets if endswith(met, "x")];
    println("todel: ", length(to_del))
    println("before: ", size(base_model))
    MetNets.del_met!(base_model, to_del);
    MetNets.del_void_rxns_mets!(base_model)
    base_model = MetNets.compacted_model(base_model)
    println("after: ", size(base_model))

    # redefining the Chemostat exchange criteria
    exch_subsys_hint = "Exchange/demand"
    function is_exchange(model::MetNets.MetNet, ider::MetNets.IDER_TYPE)
        idx = MetNets.rxnindex(model, ider)
        subsys = model.subSystems[idx]
        return occursin(exch_subsys_hint, string(subsys))
    end

    ## ---------------------------------------------------------------------
    println("FBOut: ", MetLP.fba!(base_model, HG.HUMAN_BIOMASS_IDER).obj_val)

    ## ---------------------------------------------------------------------
    # Exchanges
    exchs = []
    for exch in filter((ider) -> is_exchange(base_model, ider), base_model.rxns)
        exch_i = MetNets.rxnindex(base_model, exch)

        # First close it, later if it is what I want, open the outtake
        MetNets.lb!(base_model, exch_i, 0.0)
        MetNets.ub!(base_model, exch_i, 0.0)

        mets = MetNets.rxn_mets(base_model, exch_i)
        reacts = MetNets.rxn_reacts(base_model, exch_i)

        # I want only the forward monomoleculars
        !(length(mets) == length(reacts) == 1) && continue
        # I want only the exchanges 's'
        isempty(reacts) && !endswith(base_model.mets[first(reacts)], "s") && continue

        # Because, this reactions are forward unbalanced (A <-> nothing)
        # positibe (+) bounds limit the outtake of the cell and
        # negative (-) bounds limit the intake.
        # Because in the Chemostat the intakes is
        # controlled by the medium, we'll close all intakes to handle with them later
        # We'll open all outtakes
        MetNets.lb!(base_model, exch_i, 0.0)
        MetNets.ub!(base_model, exch_i, HG.ABS_MAX_BOUND)

        push!(exchs, exch_i)

    end
    println("\nExchanges: ", exchs |> length)

    ## ---------------------------------------------------------------------
    println("FBOut: ", MetLP.fba!(base_model, HG.HUMAN_BIOMASS_IDER).obj_val)

    ## ---------------------------------------------------------------------
    # Exch Met map
    # A fast way to get the exch reaction from the metabolite and viceversa
    if make_supp_files
        exch_met_map = Dict()
        for rxn in exchs
            mets = MetNets.rxn_mets(base_model, rxn)
            if length(mets) == 1 # only the monomoleculars
                met = base_model.mets[mets[1]]
                rxn = base_model.rxns[rxn]

                exch_met_map[met] = rxn
                exch_met_map[rxn] = met
            end
        end
        println("\nExchanges met map: ", exch_met_map |> length)

        # Saving
        sdat(HG, exch_met_map,
            "exch_met_map", ".jls";
            verbose = true
        )
    else
        exch_met_map = HG.load_exch_met_map()
    end

    ## ---------------------------------------------------------------------
    println("FBOut: ", MetLP.fba!(base_model, HG.HUMAN_BIOMASS_IDER).obj_val)

    ## ---------------------------------------------------------------------
    # Base intake info
    # The Base model will have a medium (expressed as open intake fluxes) that resamble the cultivation
    # at xi = 1 using the set up in Rath 2017 exp A. So the lb of the intakes will be directly the (negative)
    # concentration in 42_MAX_UB standard medium (see Cossio's paper). Also, a few intakes, not justified in
    # the standard medium will be add based in the intakes of the original model FBA analysis.
    base_intake_info = HG.load_base_intake_info(; inf_medium)

    ## ---------------------------------------------------------------------
    println("FBOut: ", MetLP.fba!(base_model, HG.HUMAN_BIOMASS_IDER).obj_val)

    ## ---------------------------------------------------------------------
    # Apply medium
    # The steady state assumption in the context of the Chemostat culture impose a constraint over the intakes dependent of xi and c
    ξ = 1
    println("\nApplaying chemostat bound, xi: ", ξ)
    Chemostat.apply_bound!(base_model, ξ, base_intake_info;
        emptyfirst = true, ignore_miss = true
    );

    ## ---------------------------------------------------------------------
    println("FBOut: ", MetLP.fba!(base_model, HG.HUMAN_BIOMASS_IDER).obj_val)

    ## ---------------------------------------------------------------------
    # Niklas Biomasss

    # I will modified the biomass equation of MODEL1105100000 model with data
    # derived from Niklas (2013): https://doi.org/10.1016/j.ymben.2013.01.002. Table1. (see README)
    # I do not touch the energetic part of the equation, atp + h20 -> adp + h2 + pi
    println("\nApplying Niklas Biomass")

    biomass_idx = MetNets.rxnindex(base_model, HG.HUMAN_BIOMASS_IDER)
    base_model.S[:, biomass_idx] .= zeros(size(base_model, 1))
    for (met, y) in HG.load_Niklas_biomass()
        MetNets.S!(base_model, met, biomass_idx, y)
    end

    ## ---------------------------------------------------------------------
    println("FBOut: ", MetLP.fba!(base_model, HG.HUMAN_BIOMASS_IDER).obj_val)

    ## ---------------------------------------------------------------------
    # ATPM demand

    # +
    # Cell density ρ = 0.25 pgDW/μm³ from Niklas(2011) https://doi.org/10.1007/s00449-010-0502-y.
    # pgDW/μm³ * 1e9 = pgDW/μL
    # pgDW/μL * 1e6 = pgDW/L
    # pgDW/L * 1e-12 = gDW/L
    # atpm_flux = 0.3 mol ATP/ L hr from Fernandez-de-Cossio-Diaz,
    # Jorge, and Alexei Vazquez. https://doi.org/10.1038/s41598-018-26724-7.
    atpm_flux = 0.3 / (0.25 * 1e9 * 1e6 * 1e-12) * 1e3  # mmol/gWD hr

    # This reaction is foward defined with respect to atp
    # atp + more_reacts <-> adp + more_products
    # so we limit the lower bounds as the minimum atp demand
    # that the cell metabolism must fullfill
    if HG.HUMAN_ATPM_IDER in base_model.rxns
        MetNets.lb!(base_model, HG.HUMAN_ATPM_IDER, atpm_flux)

        println("\nATP demand")
        println(MetNets.rxn_str(base_model, HG.HUMAN_ATPM_IDER), " ", MetNets.bounds(base_model, HG.HUMAN_ATPM_IDER))
    end

    ## ---------------------------------------------------------------------
    println("FBOut: ", MetLP.fba!(base_model, HG.HUMAN_BIOMASS_IDER).obj_val)

    ## ---------------------------------------------------------------------
    # dimentions
    base_model = MetNets.force_dims(base_model)

    ## ---------------------------------------------------------------------
    println("FBOut: ", MetLP.fba!(base_model, HG.HUMAN_BIOMASS_IDER).obj_val)

    ## ---------------------------------------------------------------------
    # summary
    println("\nBase Model summary")
    MetNets.summary(base_model)

    ## ---------------------------------------------------------------------
    #  FBA Test
    fbaout = MetLP.fba!(base_model, HG.HUMAN_BIOMASS_IDER)
    println("\nFBAout summary")
    MetNets.summary(base_model, fbaout)

    ## ---------------------------------------------------------------------
    println("\nComparing with experiments")
    compare_with_experimets(base_model)

    return base_model
end


# ************************************************************************
# FAUST Architecture File
# Copyright (C) 2021 GRAME, Centre National de Creation Musicale
# ---------------------------------------------------------------------

# This is sample code. This file is provided as an example of minimal
# FAUST architecture file. Redistribution and use in source and binary
# forms, with or without modification, in part or in full are permitted.
# In particular you can create a derived work of this FAUST architecture
# and distribute that work under terms of your choice.

# This sample code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# ************************************************************************

# Architectures files
#include("audio.jl")

# Using PortAudio for audio rendering
using PortAudio

mutable struct portaudio <: audio
    portaudio(buffer_size::Int, sample_rate::Int) = begin
        driver = new()
        driver.buffer_size = buffer_size
        driver.sample_rate = sample_rate
        driver
    end
    dsp::dsp
    buffer_size::Int
    sample_rate::Int
    name::String
end

function init!(driver::portaudio, name::String, dsp::dsp)
    driver.name = name
    driver.dsp = dsp
    init!(dsp, Int32(driver.sample_rate))
end

function run(driver::portaudio)
    PortAudioStream(1, 2) do stream
        outputs = zeros(FAUSTFLOAT, driver.buffer_size, getNumOutputs(driver.dsp))
        while true
            inputs = convert(Matrix{FAUSTFLOAT}, read(stream, driver.buffer_size))
            compute!(driver.dsp, Int32(driver.buffer_size), inputs, outputs)
            write(stream, outputs)
        end
    end
end