"""
    initialize_sbm_model(config::Config)

Initial part of the SBM model concept. Reads the input settings and data as defined in the
Config object. Will return a Model that is ready to run.
"""
function initialize_sbm_model(config::Config)

    # unpack the paths to the NetCDF files
    tomldir = dirname(config)
    static_path = joinpath(tomldir, config.input.path_static)
    cyclic_path = joinpath(tomldir, config.input.path_static)
    dynamic_path = joinpath(tomldir, config.input.path_forcing)
    instate_path = joinpath(tomldir, config.state.path_input)
    output_path = joinpath(tomldir, config.output.path)

    Δt = Second(config.timestepsecs)
    sizeinmetres = get(config.model, "sizeinmetres", false)
    reinit = get(config.model, "reinit", true)
    do_snow = get(config.model, "snow", false)
    do_reservoirs = get(config.model, "reservoirs", false)
    do_lakes = get(config.model, "lakes", false)
    do_pits = get(config.model, "pits", false)

    kw_river_tstep = get(config.model, "kw_river_tstep", 0)
    kw_land_tstep = get(config.model, "kw_land_tstep", 0)

    nc = NCDataset(static_path)
    dims = dimnames(nc[param(config, "input.subcatchment")])

    # There is no need to permute the dimensions of the data, since the active indices are
    # correctly calculated in both ways.
    # The dimension order only needs to be known for interpreting the LDD directions
    # and creating the coordinate maps.
    dims_xy = dims[2] in ("y", "lat")

    subcatch_2d = ncread(nc, param(config, "input.subcatchment"); allow_missing = true)
    # indices based on catchment
    inds, rev_inds = active_indices(subcatch_2d, missing)
    n = length(inds)
    modelsize_2d = size(subcatch_2d)

    river_2d = ncread(nc, param(config, "input.river_location"); type = Bool, fill = false)
    river = river_2d[inds]
    riverwidth_2d =
        ncread(nc, param(config, "input.lateral.river.width"); type = Float64, fill = 0)
    riverwidth = riverwidth_2d[inds]
    riverlength_2d =
        ncread(nc, param(config, "input.lateral.river.length"); type = Float64, fill = 0)
    riverlength = riverlength_2d[inds]

    # read x, y coordinates and calculate cell length [m]
    y_nc = "y" in keys(nc.dim) ? ncread(nc, "y") : ncread(nc, "lat")
    x_nc = "x" in keys(nc.dim) ? ncread(nc, "x") : ncread(nc, "lon")
    if dims_xy
        y = permutedims(repeat(y_nc, outer = (1, length(x_nc))))[inds]
    else
        y = repeat(y_nc, outer = (1, length(x_nc)))[inds]
    end
    cellength = abs(mean(diff(x_nc)))

    xl = fill(mv, n)
    yl = fill(mv, n)
    riverfrac = fill(mv, n)

    for i = 1:n
        xl[i] = sizeinmetres ? cellength : lattometres(y[i])[1] * cellength
        yl[i] = sizeinmetres ? cellength : lattometres(y[i])[2] * cellength
        riverfrac[i] =
            river[i] ? min((riverlength[i] * riverwidth[i]) / (xl[i] * yl[i]), 1.0) : 0.0
    end

    sbm = initialize_sbm(nc, config, riverfrac, xl, yl, inds)

    inds_riv, rev_inds_riv = active_indices(river_2d, 0)
    nriv = length(inds_riv)

    # reservoirs
    pits = zeros(Bool, modelsize_2d)
    if do_reservoirs
        reservoirs, resindex, reservoir, pits = initialize_simple_reservoir(config, nc, inds_riv, nriv, pits)
    else
        reservoir = ()
    end

    # lakes
    if do_lakes
        lakes, lakeindex, lake, pits = initialize_natural_lake(config, nc, inds_riv, nriv, pits)
    else
        lake = ()
    end

    ldd_2d = ncread(nc, param(config, "input.ldd"); allow_missing = true)
    ldd = ldd_2d[inds]
    if do_pits
        pits_2d = ncread(nc, param(config, "input.pits"); type = Bool, fill = false)
        ldd = set_pit_ldd(pits_2d, ldd, inds)
    end

    # lateral part sbm
    khfrac = ncread(
        nc,
        param(config, "input.lateral.subsurface.ksathorfrac", nothing);
        sel = inds,
        defaults = 1.0,
        type = Float64,
    )
    βₗ = ncread(nc, param(config, "input.lateral.land.slope"); sel = inds, type = Float64)
    clamp!(βₗ, 0.00001, Inf)
    kh₀ = khfrac .* sbm.kv₀
    dl = fill(mv, n)
    dw = fill(mv, n)
    sw = fill(mv, n)

    for i = 1:n
        dl[i] = detdrainlength(ldd[i], xl[i], yl[i])
        dw[i] = (xl[i] * yl[i])/dl[i]
        sw[i] = river[i] ? max(dw[i] - riverwidth[i], 0.0) : dw[i]
    end

    ssf = LateralSSF{Float64}(
        kh₀ = kh₀,
        f = sbm.f,
        zi = sbm.zi,
        soilthickness = sbm.soilthickness,
        θₑ = sbm.θₛ .- sbm.θᵣ,
        Δt = 1.0,
        βₗ = βₗ,
        dl = dl .* 1000.0,
        dw = dw .* 1000.0,
        ssf = ((kh₀ .* βₗ) ./ sbm.f) .* (exp.(-sbm.f .* sbm.zi) - exp.(-sbm.f .* sbm.soilthickness)) .* dw .*1000.0,
        wb_pit = pits[inds],
    )

    n_land = ncread(
        nc,
        param(config, "input.lateral.land.n", nothing);
        sel = inds,
        defaults = 0.072,
        type = Float64,
    )

    olf = SurfaceFlow(
        sl = βₗ,
        n = n_land,
        dl = dl,
        Δt = tosecond(Δt),
        its = kw_land_tstep > 0 ? Int(cld(tosecond(Δt), kw_land_tstep)) : kw_land_tstep,
        width = sw,
        wb_pit = pits[inds],
    )

    pcr_dir = dims_xy ? permute_indices(pcrdir) : pcrdir
    graph = flowgraph(ldd, inds, pcr_dir)

    riverslope = ncread(
        nc,
        param(config, "input.lateral.river.slope");
        sel = inds_riv,
        type = Float64,
    )
    clamp!(riverslope, 0.00001, Inf)
    riverlength = riverlength_2d[inds_riv]
    riverwidth = riverwidth_2d[inds_riv]
    n_river = ncread(
        nc,
        param(config, "input.lateral.river.n", nothing);
        sel = inds_riv,
        defaults = 0.036,
        type = Float64,
    )
    ldd_riv = ldd_2d[inds_riv]
    if do_pits
        ldd_riv = set_pit_ldd(pits_2d, ldd_riv, inds_riv)
    end
    graph_riv = flowgraph(ldd_riv, inds_riv, pcr_dir)

    # the indices of the river cells in the land(+river) cell vector
    index_river = filter(i -> !isequal(river[i], 0), 1:n)
    frac_toriver = fraction_runoff_toriver(graph, ldd, index_river, βₗ, n)

    rf = SurfaceFlow(
        sl = riverslope,
        n = n_river,
        dl = riverlength,
        Δt = tosecond(Δt),
        its = kw_river_tstep > 0 ? ceil(Int(tosecond(Δt) / kw_river_tstep)) : kw_river_tstep,
        width = riverwidth,
        reservoir_index = do_reservoirs ? resindex : fill(0, nriv),
        lake_index = do_lakes ? lakeindex : fill(0, nriv),
        reservoir = do_reservoirs ? reservoirs : nothing,
        lake = do_lakes ? lakes : nothing,
        rivercells = river,
    )

    state_ncnames = ncnames(config.state)

    reader = prepare_reader(dynamic_path, cyclic_path, config)

    modelmap = (vertical = sbm, lateral = (subsurface = ssf, land = olf, river = rf))
    indices_reverse = (
        land = rev_inds,
        river = rev_inds_riv,
        reservoir = isempty(reservoir) ? nothing : reservoir.reverse_indices,
        lake = isempty(lake) ? nothing : lake.reverse_indices,
    )
    writer = prepare_writer(
        config,
        reader,
        output_path,
        modelmap,
        state_ncnames,
        indices_reverse,
        x_nc,
        y_nc,
        dims_xy,
        nc,
        maxlayers = sbm.maxlayers,
    )

    # for each domain save the directed acyclic graph, the traversion order,
    # and the indices that map it back to the two dimensional grid
    # for reservoirs and lakes this is information is also available (except the graph)
    # from the initialization functions
    land = (
        graph = graph,
        order = topological_sort_by_dfs(graph),
        indices = inds,
        reverse_indices = rev_inds,
    )
    river = (
        graph = graph_riv,
        order = topological_sort_by_dfs(graph_riv),
        indices = inds_riv,
        reverse_indices = rev_inds_riv,
    )

    model = Model(
        config,
        (; land, river, reservoir, lake, index_river, frac_toriver),
        (subsurface = ssf, land = olf, river = rf),
        sbm,
        Clock(config.starttime, 1, Δt),
        reader,
        writer,
    )

    # read and set states in model object if reinit=false
    if reinit == false
        set_states(
            instate_path,
            model,
            state_ncnames;
            type = Float64
        )
        @unpack lateral, vertical = model
        # update zi for vertical sbm and α for river and overland flow
        zi = max.(0.0, vertical.soilthickness .- vertical.satwaterdepth ./ vertical.θₑ)
        vertical.zi .= zi
        lateral.river.α .= lateral.river.alpha_term .* pow.(lateral.river.width .+ 2.0 .* lateral.river.h, lateral.river.alpha_pow)
        lateral.land.α .= lateral.land.alpha_term .* pow.(lateral.land.width .+ 2.0 .* lateral.land.h, lateral.land.alpha_pow)
    end

    # make sure the forcing is already loaded
    # it's fine to run twice, and may help catching errors earlier
    update_forcing!(model)
    if haskey(config.input, "cyclic")
        update_cyclic!(model)
    end
    return model
end

"update SBM model for a single timestep"
function update(model::Model{N,L,V,R,W}) where {N,L,V<:SBM,R,W}
    @unpack lateral, vertical, network, clock, config = model

    inds_riv = network.index_river
    kinwave_it = get(config.model, "kin_wave_iteration", false)

    update_forcing!(model)
    if haskey(config.input, "cyclic")
        update_cyclic!(model)
    end

    # extract water levels h_av [m] from the land and river domains
    # this is used to limit open water evaporation
    vertical.waterlevel_land .= lateral.land.h_av .* 1000.0
    vertical.waterlevel_river[inds_riv] .= lateral.river.h_av .* 1000.0

    # vertical sbm concept is updated until snow state, after that (optional)
    # snow transport is possible
    update_until_snow(vertical, config)

    # lateral snow transport
    if get(config.model, "masswasting", false)
        snowflux_frac =
            min.(0.5, lateral.land.sl ./ 5.67) .* min.(1.0, vertical.snow ./ 10000.0)
        maxflux = snowflux_frac .* vertical.snow
        vertical.snow .= accucapacityflux(network.land, vertical.snow, maxflux)
        vertical.snowwater .= accucapacityflux(network.land, vertical.snowwater, vertical.snowwater .* snowflux_frac)
    end

    # update vertical sbm concept until recharge [mm] to the saturated store
    update_until_recharge(vertical, config)

    # exchange of recharge between vertical sbm concept and subsurface flow domain
    lateral.subsurface.recharge .= vertical.recharge
    lateral.subsurface.recharge .*= lateral.subsurface.dw
    lateral.subsurface.zi .= vertical.zi

    # update lateral subsurface flow domain
    update(lateral.subsurface, network.land, network.frac_toriver, lateral.river.rivercells)

    # update vertical sbm concept (runoff, ustorelayerdepth and satwaterdepth)
    update_after_lateralflow(
        vertical,
        lateral.subsurface.zi,
        lateral.subsurface.exfiltwater,
    )

    # update overland flow based on vertical runoff [mm] from vertical sbm concept
    lateral.land.qlat .=
        (vertical.runoff .* vertical.xl .* vertical.yl .* 0.001) ./ lateral.land.Δt ./
        lateral.land.dl
    update(
        lateral.land,
        network.land,
        frac_toriver = network.frac_toriver,
        river = lateral.river.rivercells,
        do_iter = kinwave_it,
    )

    # update river domain with net runoff from vertical sbm concept, overland flow
    # and lateral subsurface flow to the river cells
    net_runoff_river =
        (vertical.net_runoff_river[inds_riv] .* vertical.xl[inds_riv] .*
        vertical.yl[inds_riv] .* 0.001) ./ vertical.Δt
    lateral.river.qlat .=
        (
            lateral.subsurface.to_river[inds_riv] ./ 1.0e9 ./ lateral.river.Δt .+
            lateral.land.to_river[inds_riv] .+ net_runoff_river
        ) ./ lateral.river.dl
    update(lateral.river, network.river, do_iter = kinwave_it, doy = dayofyear(clock.time))

    write_output(model)

    # update the clock
    advance!(clock)

    return model
end


# This elixir and indicator is only for testing purposes and does not have any practical use

using OrdinaryDiffEq
using Trixi

# Define new structs inside a module to allow re-evaluating the file.
# This module name needs to be unique among all examples, otherwise Julia will throw warnings
# if multiple test cases using the same module name are run in the same session.
module TrixiExtensionRefine

using Trixi

struct IndicatorAlwaysRefine{Cache<:NamedTuple} <: Trixi.AbstractIndicator
  cache::Cache
end

function IndicatorAlwaysRefine(semi)
  basis = semi.solver.basis
  alpha = Vector{real(basis)}()
  cache = (; semi.mesh, alpha)

  return IndicatorAlwaysRefine{typeof(cache)}(cache)
end

function (indicator::IndicatorAlwaysRefine)(u::AbstractArray{<:Any,4},
                                             equations, dg, cache;
                                             t, kwargs...)
  alpha = indicator.cache.alpha
  resize!(alpha, nelements(dg, cache))

  alpha .= 1.0

  return alpha
end

end # module TrixiExtensionRefine

import .TrixiExtensionRefine

###############################################################################
# semidiscretization of the linear advection equation

advectionvelocity = (1.0, 1.0)
# advectionvelocity = (0.2, -0.3)
equations = LinearScalarAdvectionEquation2D(advectionvelocity)

initial_condition = initial_condition_gauss

solver = DGSEM(polydeg=3, surface_flux=flux_lax_friedrichs)

coordinates_min = (-5, -5)
coordinates_max = ( 5,  5)
mesh = TreeMesh(coordinates_min, coordinates_max,
                initial_refinement_level=2,
                n_cells_max=30_000)

semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition, solver)

###############################################################################
# ODE solvers, callbacks etc.

tspan = (0.0, 2.0)
ode = semidiscretize(semi, tspan)

summary_callback = SummaryCallback()

analysis_interval = 100
analysis_callback = AnalysisCallback(semi, interval=analysis_interval,
                                     extra_analysis_integrals=(entropy,))

alive_callback = AliveCallback(analysis_interval=analysis_interval)

save_restart = SaveRestartCallback(interval=100,
                                   save_final_restart=true)

save_solution = SaveSolutionCallback(interval=100,
                                     save_initial_solution=true,
                                     save_final_solution=true,
                                     solution_variables=cons2prim)

amr_controller = ControllerThreeLevel(semi, TrixiExtensionRefine.IndicatorAlwaysRefine(semi),
                                      base_level=4, max_level=4,
                                      med_threshold=0.1, max_threshold=0.6)

amr_callback = AMRCallback(semi, amr_controller,
                           interval=5,
                           adapt_initial_condition=true,
                           adapt_initial_condition_only_refine=true)

stepsize_callback = StepsizeCallback(cfl=1.6)

callbacks = CallbackSet(summary_callback,
                        analysis_callback, alive_callback,
                        save_restart, save_solution,
                        amr_callback, stepsize_callback);

###############################################################################
# run the simulation

sol = solve(ode, CarpenterKennedy2N54(williamson_condition=false),
            dt=1.0, # solve needs some value here but it will be overwritten by the stepsize_callback
            save_everystep=false, callback=callbacks);
summary_callback() # print the timer summary


mutable struct Snake{T, R} <: AbstractGridWorld
    world::GridWorldBase{Tuple{Agent, Wall, Body, Food}}
    agent_pos::CartesianIndex{2}
    reward::T
    rng::R
    body::DS.Queue{CartesianIndex{2}}
    food_reward::T
    food_pos::CartesianIndex{2}
    terminal_reward::T
    terminal_penalty::T
    done::Bool
end

@generate_getters(Snake)
@generate_setters(Snake)

function Snake(; T = Float32, height = 8, width = 8, rng = Random.GLOBAL_RNG)
    objects = (AGENT, WALL, BODY, FOOD)
    world = GridWorldBase(objects, height, width)

    room = Room(CartesianIndex(1, 1), height, width)
    place_room!(world, room)

    food_pos = CartesianIndex(height - 1, width - 1)
    world[FOOD, food_pos] = true

    agent_pos = CartesianIndex(2, 2)
    world[AGENT, agent_pos] = true

    body = DS.Queue{CartesianIndex{2}}()
    DS.enqueue!(body, agent_pos)
    world[BODY, agent_pos] = true

    reward = zero(T)
    food_reward = one(T)
    terminal_reward = convert(T, height * width)
    terminal_penalty = convert(T, -height * width)
    done = false

    env = Snake(world, agent_pos, reward, rng, body, food_reward, food_pos, terminal_reward, terminal_penalty, done)

    RLBase.reset!(env)

    return env
end

RLBase.state_space(env::Snake, ::RLBase.Observation, ::RLBase.DefaultPlayer) = nothing
const SNAKE_LAYERS = SA.SVector(2, 3, 4)
RLBase.state(env::Snake, ::RLBase.Observation, ::RLBase.DefaultPlayer) = get_grid(get_world(env), get_agent_pos(env), get_half_size(env), SNAKE_LAYERS)

RLBase.action_space(env::Snake, player::RLBase.DefaultPlayer) = UNDIRECTED_NAVIGATION_ACTIONS
RLBase.reward(env::Snake, ::RLBase.DefaultPlayer) = get_reward(env)
RLBase.is_terminated(env::Snake) = get_done(env)

function RLBase.reset!(env::Snake{T}) where {T}
    world = get_world(env)
    rng = get_rng(env)
    body = get_body(env)

    world[AGENT, get_agent_pos(env)] = false
    world[FOOD, get_food_pos(env)] = false

    for i in 1:length(body)
        pos = DS.dequeue!(body)
        world[BODY, pos] = false
    end

    new_food_pos = rand(rng, pos -> !any(@view world[:, pos]), env)
    set_food_pos!(env, new_food_pos)
    world[FOOD, new_food_pos] = true

    new_agent_pos = rand(rng, pos -> !any(@view world[:, pos]), env)
    set_agent_pos!(env, new_agent_pos)
    world[AGENT, new_agent_pos] = true

    DS.enqueue!(body, new_agent_pos)
    world[BODY, new_agent_pos] = true

    set_reward!(env, zero(T))
    set_done!(env, false)

    return nothing
end

function (env::Snake{T})(action::AbstractMoveAction) where {T}
    world = get_world(env)
    height = get_height(env)
    width = get_width(env)
    rng = get_rng(env)
    body = get_body(env)

    agent_pos = get_agent_pos(env)
    dest = move(action, agent_pos)

    if (world[WALL, dest] || world[BODY, dest])
        set_reward!(env, get_terminal_penalty(env))
        set_done!(env, true)
    elseif world[FOOD, dest]
        world[AGENT, agent_pos] = false
        set_agent_pos!(env, dest)
        world[AGENT, dest] = true

        DS.enqueue!(body, dest)
        world[BODY, dest] = true

        world[FOOD, dest] = false

        if length(body) == (height - 2) * (width - 2)
            set_reward!(env, get_food_reward(env) + get_terminal_reward(env))
            set_done!(env, true)
        else
            new_food_pos = rand(rng, pos -> !any(@view world[:, pos]), env, max_try = 100 * height * width)
            set_food_pos!(env, new_food_pos)
            world[FOOD, new_food_pos] = true

            set_reward!(env, get_food_reward(env))
            set_done!(env, false)
        end
    else
        world[AGENT, agent_pos] = false
        set_agent_pos!(env, dest)
        world[AGENT, dest] = true

        DS.enqueue!(body, dest)
        world[BODY, dest] = true

        last_pos = DS.dequeue!(body)
        world[BODY, last_pos] = false

        set_reward!(env, zero(T))
        set_done!(env, false)
    end

    return nothing
end


@testset "Change bounds" begin
    cp = test_LP()
    change_bounds!(cp, [3; 1], xl = [-10.0; -20], xu = [10.0; 20])
    @test cp.xl == [-20; 1; -10]
    @test cp.xu == [20; 1; 10]
    change_bounds!(
        cp,
        ["gibberish1"; "r3"; "r1"; "gibberish2"],
        xl = [0; -30.0; -40; 0],
        xu = [0; 30.0; 40; 0],
    )
    @test cp.xl == [-40; 1; -30]
    @test cp.xu == [40; 1; 30]
    change_bounds!(cp, ["r1"; "r3"], xl = [-50.0; -60])
    @test cp.xl == [-50; 1; -60]
end

@testset "Verify consistency" begin
    cp = test_LP()
    @test size(cp.S) == (4, 3)
    (new_reactions, new_mets) = verify_consistency(
        cp,
        reshape(cp.S[:, end], :, 1),
        [1.0, 2.0, 3.0, 4.0],
        [2.0],
        [-1.0],
        [1.0],
        ["r4"],
        ["m1", "m2", "m3", "m6"],
        [1],
        [4],
    )
    @test new_reactions == [1]
    @test new_mets == [4]

    (new_reactions, new_mets) = verify_consistency(
        cp,
        reshape(cp.S[:, end], :, 1),
        [1.0, 2.0, 3.0, 4.0],
        [2.0],
        [-1.0],
        [1.0],
        ["r1"],
        ["m1", "m2", "m3", "m6"],
        [],
        [4],
    )
    @test new_reactions == []
    @test new_mets == [4]
end

@testset "Add reactions (checking existence and consistency)" begin
    cp = test_LP()
    @test size(cp.S) == (4, 3)
    (new_cp, new_reactions, new_mets) = add_reactions(
        cp,
        cp.S[:, end],
        [1.0, 2.0, 3.0, 4.0],
        2.0,
        -1.0,
        1.0,
        check_consistency = true,
    )
    @test n_reactions(cp) + 1 == n_reactions(new_cp)

    (new_cp, new_reactions, new_mets) = add_reactions(
        cp,
        cp.S[:, end],
        [1.0, 2.0, 3.0, 4.0],
        2.0,
        -1.0,
        1.0,
        "r1",
        ["m1", "m2", "m3", "m6"],
        check_consistency = true,
    )
    @test n_reactions(cp) == n_reactions(new_cp)
    @test n_metabolites(cp) + 1 == n_metabolites(new_cp)
end

@testset "Add reactions" begin
    cp = test_LP()
    @test size(cp.S) == (4, 3)
    cp = add_reactions(cp, 2.0 * ones(4), 3 .* ones(4), 2.0, -1.0, 1.0)
    @test size(cp.S) == (8, 4)
    cp = add_reactions(cp, 2.0 * ones(4, 1), 3 .* ones(4), 2 .* ones(1), -ones(1), ones(1))
    @test size(cp.S) == (12, 5)
    cp = add_reactions(
        cp,
        2.0 * ones(4, 10),
        3 .* ones(4),
        2 .* ones(10),
        -ones(10),
        ones(10),
    )
    @test size(cp.S) == (16, 15)

    cp = test_sparseLP()
    @test size(cp.S) == (4000, 3000)
    cp = add_reactions(cp, 2.0 * sprand(4000, 0.5), 3 .* sprand(4000, 0.5), 2.0, -1.0, 1.0)
    @test size(cp.S) == (8000, 3001)
    cp = add_reactions(
        cp,
        2.0 * sprand(4000, 1, 0.5),
        3 .* sprand(4000, 0.5),
        2 .* sprand(1, 0.5),
        -sprand(1, 0.5),
        sprand(1, 0.5),
    )
    @test size(cp.S) == (12000, 3002)
    cp = add_reactions(
        cp,
        2.0 * sprand(4000, 1000, 0.5),
        3 .* sprand(4000, 0.5),
        2 .* sprand(1000, 0.5),
        -sprand(1000, 0.5),
        sprand(1000, 0.5),
    )
    @test size(cp.S) == (16000, 4002)

    cp = test_sparseLP()
    @test size(cp.S) == (4000, 3000)
    cp = add_reactions(cp, 2.0 * ones(4000), 3 .* ones(4000), 2.0, -1.0, 1.0)
    @test size(cp.S) == (8000, 3001)
    cp = add_reactions(
        cp,
        2.0 * ones(4000, 1),
        3 .* ones(4000),
        2 .* ones(1),
        -ones(1),
        ones(1),
    )
    @test size(cp.S) == (12000, 3002)
    cp = add_reactions(
        cp,
        2.0 * ones(4000, 1000),
        3 .* ones(4000),
        2 .* ones(1000),
        -ones(1000),
        ones(1000),
    )
    @test size(cp.S) == (16000, 4002)

    # proper subset of existing metabolites
    cp = test_LP()
    new_cp = add_reactions(cp, [-1.0], zeros(1), 1.0, 0.0, 1.0, "r4", ["m1"])
    @test n_reactions(cp) + 1 == n_reactions(new_cp)

    @test_throws DimensionMismatch add_reactions(
        cp,
        2.0 * ones(4000, 1),
        3 .* ones(4000),
        2 .* ones(2),
        -ones(1),
        ones(1),
    )
end

@testset "Remove reactions" begin
    cp = test_LP()
    @test size(cp.S) == (4, 3)
    cp = remove_reactions(cp, 2)
    @test size(cp.S) == (0, 2)
    cp = remove_reactions(cp, [2, 1])
    @test size(cp.S) == (0, 0)

    cp = test_LP()
    @test size(cp.S) == (4, 3)
    cp = remove_reactions(cp, "r0")
    @test size(cp.S) == (4, 3)
    cp = remove_reactions(cp, "r1")
    @test size(cp.S) == (0, 2)
    cp = remove_reactions(cp, ["r2"])
    @test size(cp.S) == (0, 1)

    lp = CoreModel(
        [1.0 1 1 0; 1 1 1 0; 1 1 1 0; 0 0 0 1],
        collect(1.0:4),
        collect(1.0:4),
        collect(1.0:4),
        collect(1.0:4),
        ["r1"; "r2"; "r3"; "r4"],
        ["m1"; "m2"; "m3"; "m4"],
    )

    modLp = remove_reactions(lp, [4; 1])
    @test stoichiometry(modLp) == stoichiometry(lp)[1:3, 2:3]
    @test balance(modLp) == balance(lp)[1:3]
    @test objective(modLp) == objective(lp)[2:3]
    @test bounds(modLp)[1] == bounds(lp)[1][2:3]
    @test bounds(modLp)[2] == bounds(lp)[2][2:3]
    @test reactions(modLp) == reactions(lp)[2:3]
    @test metabolites(modLp) == metabolites(lp)[1:3]
end

@testset "Remove metabolites" begin
    model = load_model(CoreModel, model_paths["e_coli_core.json"])

    m1 = remove_metabolites(model, ["glc__D_e", "for_c"])
    m2 = remove_metabolites(model, "glc__D_e")
    m3 = remove_metabolites(model, indexin(["glc__D_e", "for_c"], metabolites(model)))
    m4 = remove_metabolites(model, first(indexin(["glc__D_e"], metabolites(model))))

    @test size(stoichiometry(m1)) == (70, 94)
    @test size(stoichiometry(m2)) == (71, 94)
    @test size(stoichiometry(m3)) == (70, 94)
    @test size(stoichiometry(m4)) == (71, 94)
    @test any(["glc__D_e", "for_c"] .∉ Ref(metabolites(m1)))
    @test any(["glc__D_e"] .∉ Ref(metabolites(m2)))
    @test any(["glc__D_e", "for_c"] .∉ Ref(metabolites(m3)))
    @test any(["glc__D_e"] .∉ Ref(metabolites(m4)))
end


mutable struct Snake
    body::Vector
    dir::Vector
    @init function Snake()
        this.body = [[w/2, h/2]]
        this.dir = [1, 0]
    end
end

setdir!(this::Snake, dir::Vector) = (this.dir = dir;)

function update!(this::Snake)
    head = [0., 0.]
    copy!(head, last(this.body))
    popfirst!(this.body)
    head += this.dir
    push!(this.body, head)
end

function draw(this::Snake)
    sethue(colors.snake)
    foreach(this.body) do pos
        rect(pos..., 1, 1, :fill)
    end
end

function eat(this::Snake, food::Food)
    x, y = last(this.body)
    return x == food.x && y == food.y
end

function grow(this::Snake)
    head = [0., 0.]
    copy!(head, last(this.body))
    push!(this.body, head)
end

function endgame(this::Snake)
    x, y = last(this.body)

    if x < 0 || x > w - 1 || y < 0 || y > h - 1
        return true
    end

    for pos in this.body[1:end-1]
        if pos == [x, y]
            return true
        end
    end

    return false
end


function construct_device!(ps_m::CanonicalModel,
                           device::Type{St},
                           device_formulation::Type{D},
                           system_formulation::Type{S},
                           sys::PSY.System,
                           time_range::UnitRange{Int64};
                           kwargs...) where {St <: PSY.Storage,
                                             D <: PSI.AbstractStorageForm,
                                             S <: PM.AbstractPowerFormulation}

    #wrangle initial_conditions
    if  !isempty(keys(ps_m.initial_conditions))

        if "energy_initial_conditions" in keys(ps_m.initial_conditions)
             energy_initial_conditions = ps_m.initial_conditions["energy_initial_conditions"]
        else
             @warn("No storage energy initial conditions provided")
        end

    else

        @warn("Initial Conditions not provided, this can lead to infeasible problems")

        energy_initial_conditions = zeros(length(sys.storage))

    end

    #Variables
    activepower_variables(ps_m, sys.storage, time_range);

    reactivepower_variables(ps_m, sys.storage, time_range);

    energystorage_variables(ps_m, sys.storage, time_range);

    storagereservation_variables(ps_m, sys.storage, time_range);

    #Constraints
    activepower_constraints(ps_m, sys.storage, device_formulation, system_formulation, time_range)

    reactivepower_constraints(ps_m, sys.storage, device_formulation, system_formulation, time_range)

    # Energy Balanace limits
    energy_balance_constraint(ps_m,sys.storage, device_formulation, system_formulation, time_range, energy_initial_conditions)

    return

end

function construct_device!(ps_m::CanonicalModel,
                           device::Type{St},
                           device_formulation::Type{D},
                           system_formulation::Type{S},
                           sys::PSY.System,
                           time_range::UnitRange{Int64};
                           kwargs...) where {St <: PSY.Storage,
                                             D <: PSI.AbstractStorageForm,
                                             S <: PM.AbstractActivePowerFormulation}

    #wrangle initial_conditions
    if !isempty(keys(ps_m.initial_conditions))

       if "energy_initial_conditions" in keys(ps_m.initial_conditions)
         energy_initial_conditions = ps_m.initial_conditions["energy_initial_conditions"]
       else
        @warn("No energy initial conditions provided")
       end

    else
        @warn("Initial Conditions not provided, this can lead to infeasible problems")

        energy_initial_conditions = zeros(length(sys.storage))

    end

    #Variables
    activepower_variables(ps_m, sys.storage, time_range);

    energystorage_variables(ps_m, sys.storage, time_range);

    storagereservation_variables(ps_m, sys.storage, time_range);

    #Constraints
    activepower_constraints(ps_m, sys.storage, device_formulation, system_formulation, time_range)

    # Energy Balanace limits
    energy_balance_constraint(ps_m,sys.storage, device_formulation, system_formulation, time_range, energy_initial_conditions)

    return

end


using HDF5

abstract type Solution{dType, tType, N} end
abstract type DeterministicSolution{dType, tType, N} <: Solution{dType, tType, N} end
abstract type StochasticSolution{dType, tType, NQ, NW} <: Solution{dType, tType, NQ} end

timesteps(sol::Solution) = error("time() not implemented for ", typeof(sol))
hdf5(sol::Solution)   = error("hdf5() not implemented for ", typeof(sol))
ntime(sol::Solution)  = error("ntime() not implemented for ", typeof(sol))
nsave(sol::Solution)  = error("nsave() not implemented for ", typeof(sol))
offset(sol::Solution) = error("offset() not implemented for ", typeof(sol))

create_hdf5(sol::Solution, file) = error("create_hdf5() not implemented for ", typeof(sol))
CommonFunctions.write_to_hdf5(sol::Solution, h5::HDF5File, offset=0) = error("write_to_hdf5() not implemented for ", typeof(sol))

conv(sol::StochasticSolution) = error("conv() not implemented for ", typeof(sol))

nsamples(sol::DeterministicSolution) = sol.ni
nsamples(sol::StochasticSolution) = sol.ns

CommonFunctions.eachtimestep(sol::Solution) = 1:sol.nt*sol.nsave
CommonFunctions.eachsample(sol::Solution) = 1:nsamples(sol)

"Create solution for ODE."
function Solution(equation::AbstractEquationODE, Δt, ntime::Int; kwargs...)
    SSolutionODE(equation, Δt, ntime; kwargs...)
end

"Create solution for partitioned ODE."
function Solution(equation::AbstractEquationPODE, Δt, ntime::Int; kwargs...)
    SSolutionPODE(equation, Δt, ntime; kwargs...)
end

"Create solution for DAE."
function Solution(equation::AbstractEquationDAE, Δt, ntime::Int; kwargs...)
    SSolutionDAE(equation, Δt, ntime; kwargs...)
end

"Create solution for partitioned DAE."
function Solution(equation::AbstractEquationPDAE, Δt, ntime::Int; kwargs...)
    SSolutionPDAE(equation, Δt, ntime; kwargs...)
end

"Create solution for SDE."
function Solution(equation::SDE, Δt, ntime::Int; kwargs...)
    SSolutionSDE(equation, Δt, ntime; kwargs...)
end

function Solution(equation::SDE, Δt, dW, dZ, ntime::Int; kwargs...)
    SSolutionSDE(equation, Δt, dW, dZ, ntime; kwargs...)
end

"Create solution for PSDE."
function Solution(equation::Union{PSDE,SPSDE}, Δt, ntime::Int; kwargs...)
    SSolutionPSDE(equation, Δt, ntime; kwargs...)
end

function Solution(equation::Union{PSDE,SPSDE}, Δt, dW, dZ, ntime::Int; kwargs...)
    SSolutionPSDE(equation, Δt, dW, dZ, ntime; kwargs...)
end

"Print error for solutions of equations not implemented, yet."
function Solution(equation::Equation, Δt, ntime::Int; kwargs...)
    error("No solution found for equation ", equation)
end

"Create parallel solution for ODE."
function ParallelSolution(equation::AbstractEquationODE, Δt, ntime::Int; kwargs...)
    PSolutionODE(equation, Δt, ntime; kwargs...)
end

"Create parallel solution for partitioned ODE."
function ParallelSolution(equation::AbstractEquationPODE, Δt, ntime::Int; kwargs...)
    PSolutionPODE(equation, Δt, ntime; kwargs...)
end

"Create parallel solution for DAE."
function ParallelSolution(equation::AbstractEquationDAE, Δt, ntime::Int; kwargs...)
    PSolutionDAE(equation, Δt, ntime; kwargs...)
end

"Create parallel solution for partitioned DAE."
function ParallelSolution(equation::AbstractEquationPDAE, Δt, ntime::Int; kwargs...)
    PSolutionPDAE(equation, Δt, ntime; kwargs...)
end

"Create parallel solution for SDE."
function ParallelSolution(equation::SDE, Δt, ntime::Int; kwargs...)
    PSolutionSDE(equation, Δt, ntime; kwargs...)
end

"Create parallel solution for PSDE."
function ParallelSolution(equation::Union{PSDE,SPSDE}, Δt, ntime::Int; kwargs...)
    PSolutionPSDE(equation, Δt, ntime; kwargs...)
end

"Print error for parallel solutions of equations not implemented, yet."
function ParallelSolution(equation::Equation, Δt, ntime::Int; kwargs...)
    error("No parallel solution found for equation ", equation)
end


module VisionModel

using Flux

# Models
include("alexnet.jl")
include("vgg.jl")
include("resnet.jl")
include("googlenet.jl")

export  alexnet,
        vgg11, vgg11bn, vgg13, vgg13bn, vgg16, vgg16bn, vgg19, vgg19bn,
        ResNet18, ResNet34, ResNet50, ResNet101, ResNet152,
        googlenet

end


import PeriodicGraphs

function export_dataline(f, x)
    inbetween = '\t'
    if any(isspace, x)
        if any(==('\n'), x)
            x = ';'*x*"\n;"
            inbetween = '\n'
        else
            @toggleassert x[1]!= '\''
            x = '\''*x*'\''
        end
    end
    println(f, inbetween*x)
end

_representative_atom(ty, i) = last(representative_atom(ty, i))
function PeriodicGraphEmbeddings.export_vtf(file, c::Union{CrystalNet,Crystal}, repeatedges=6, colorname=false)
    if c isa CrystalNet
        net = CrystalNet3D(c)
        return export_vtf(file, net.pge, net.types, repeatedges, colorname, string_atomtype, _representative_atom)
    end
    return export_vtf(file, c.pge, c.types, repeatedges, colorname, string_atomtype, _representative_atom)
end

function export_cif(file, __c::Union{Crystal, CIF})
    mkpath(splitdir(file)[1])
    c, info = if __c isa CIF
        ___c = expand_symmetry(__c)
        ___c, copy(__c.cifinfo)
    else
        __c, Dict{String,Union{String,Vector{String}}}("data"=>last(splitdir(file)))
    end
    loops = Dict{Int, Tuple{Vector{String}, Vector{Vector{String}}}}()
    open(file, write=true) do f
        print(f, "data_"); println(f, popstring!(info, "data")*"\n\n")
        for (id, data) in info
            if data isa String
                print(f, '_'); print(f, id)
                export_dataline(f, data)
            else
                @toggleassert data isa Vector{String}
                n = length(data)
                if haskey(loops, n)
                    push!(loops[n][1], id)
                    push!(loops[n][2], data)
                else
                    loops[n] = (String[id], Vector{String}[data])
                end
            end
        end

        cell::Cell{Rational{Int}} = c isa CIF ? c.cell : c.pge.cell
        #scale_factor::Float64 = unique!(sort(c.types)) == [:Si] && c.pge.pos[:,1] != [0,0,0] ? 1.2 : 1.0
        ((__a, __b, __c), (__α, __β, __γ)), _ = cell_parameters(cell)
        _a, _b, _c, _α, _β, _γ = Float64.((__a, __b, __c, __α, __β, __γ))

        println(f, """

        _cell_length_a\t\t$_a
        _cell_length_b\t\t$_b
        _cell_length_c\t\t$_c
        _cell_angle_alpha\t\t$_α
        _cell_angle_beta\t\t$_β
        _cell_angle_gamma\t\t$_γ

        _symmetry_space_group_name_H-M\t'P 1'
        _symmetry_Int_Tables_number\t1
        _symmetry_cell_setting\ttriclinic
        """)

        println(f, """
        loop_
        _symmetry_equiv_pos_as_xyz
        x,y,z""")

        for eq in cell.equivalents
            println(f, eq)
        end
        println(f)

        n::Int = c isa CIF ? length(c.ids) : length(c.types)
        if !haskey(loops, n)
            loops[n] = (String[], Vector{String}[])
        end
        append!(loops[n][1],
                ["atom_site_label", "atom_site_type_symbol",
                 "atom_site_fract_x", "atom_site_fract_y", "atom_site_fract_z"])
        labels = String[string_atomtype(c.types[c isa CIF ? c.ids[i] : i]::Symbol)*string(i) for i in 1:n]
        pos = string.(round.(c isa CIF ? c.pos::Matrix{Float64} : reduce(hcat, c.pge.pos)::Matrix{Float64}; sigdigits=6))
        append!(loops[n][2], [labels, string_atomtype.((c.types[x])::Symbol for x in (c isa CIF ? c.ids : collect(1:n))::Vector{Int}),
                pos[1,:], pos[2,:], pos[3,:]])

        if c isa Union{Crystal{Clusters}, Crystal{Nothing}}
            bonds = edges((c isa CIF ? c.graph : c.pge.g)::PeriodicGraph3D)
            src = String[]
            dst = String[]
            mult = Int[]
            last_bond = (0, 0)
            n = 0
            for e in bonds
                if (e.src, e.dst.v) == last_bond
                    mult[end] += 1
                else
                    push!(src, labels[e.src])
                    push!(dst, labels[e.dst.v])
                    push!(mult, 1)
                    last_bond = (e.src, e.dst.v)
                    n += 1
                end
            end
            if !haskey(loops, n)
                loops[n] = (String[], Vector{String}[])
            end
            append!(loops[n][1], ["geom_bond_atom_site_label_1", "geom_bond_atom_site_label_2", "geom_bond_multiplicity"])
            append!(loops[n][2], [src, dst, string.(mult)])
        end

        for (n, (ids, datas)) in loops
            println(f, "loop_")
            for id in ids
                print(f, '_'); println(f, id)
            end
            for i in 1:n
                for j in 1:length(datas)
                    print(f, datas[j][i]); print(f, '\t')
                end
                println(f)
            end
            println(f)
        end
        println(f, "\n#END")
    end
    nothing
end

PeriodicGraphEmbeddings.export_cgd(file, c::Crystal) = export_cgd(file, c.pge)
PeriodicGraphEmbeddings.export_cgd(file, c::CrystalNet) = export_cgd(file, PeriodicGraph3D(c.pge.g))

function export_attributions(crystal::Crystal{Clusters}, path=joinpath(tempdir(),tempname()))
    frame = Chemfiles.Frame()
    m = length(crystal.clusters.classes)
    # residues = [Chemfiles.Residue(string(i)) for i in 1:m]
    # for i in 1:m
    #     Chemfiles.set_property!(residues[i], "chainname", string(i))
    #     Chemfiles.set_property!(residues[i], "chainid", string(i))
    #     Chemfiles.add_residue!(frame, residues[i])
    # end
    ((_a, _b, _c), (_α, _β, _γ)), mat = cell_parameters(c.pge.cell)
    Chemfiles.set_cell!(frame, Chemfiles.UnitCell(Float64[_a, _b, _c], Float64[_α, _β, _γ]))
    recenter::SVector{3,Float64} = minimum(reduce(hcat, crystal.pge.pos); dims=2)
    for i in 1:length(crystal.types)
        # resid = crystal.clusters.attributions[i]
        atom = Chemfiles.Atom(string(crystal.clusters.classes[crystal.clusters.attributions[i]]))
        Chemfiles.set_type!(atom, string_atomtype(crystal.types[i]))
        Chemfiles.add_atom!(frame, atom, collect(Float64.(mat * (crystal.pge.pos[i] .- recenter))))
        # Chemfiles.add_atom!(residues[resid], i)
    end
    for e in edges(crystal.pge.g)
        iszero(last(last(e))) || continue
        Chemfiles.add_bond!(frame, first(e)-1, first(last(e))-1)
    end
    target = isempty(last(splitext(path))) ? path*".pdb" : path
    output = Chemfiles.Trajectory(target, 'w')
    write(output, frame)
    close(output)
end

"""
    export_arc(path, arc=CRYSTAL_NETS_ARCHIVE)

Export archive `arc` to the specified `path`. If unspecified, the exported archive is the
current one.
"""
function export_arc(path, arc=CRYSTAL_NETS_ARCHIVE)
    mkpath(splitdir(path)[1])
    open(path, "w") do f
        println(f, "Made by CrystalNets.jl v$CRYSTAL_NETS_VERSION")
        if !isnothing(arc)
            println(f)
            pairs = sort(collect(arc); by=last)
            for (genome, id) in pairs
                println(f, "key\t", genome)
                println(f, "id\t", id)
                println(f)
            end
        end
    end
end


struct TimestampResolver{T₁ <: AbstractString, T₂ <: Number, T₃ <: Number} <: Function
    timezone::T₁
    timestamp::T₂
    intersample_duration::T₃
    nested_interval::NestedInterval
end

function get_timezone(resolver::TimestampResolver)
    resolver.timezone
end

function get_timestamp(resolver::TimestampResolver)
    resolver.timestamp
end

function get_intersample_duration(resolver::TimestampResolver)
    resolver.intersample_duration
end

function get_nested_interval(resolver::TimestampResolver)
    resolver.nested_interval
end

function (resolve_timestamp::TimestampResolver)(x::Number)
    # All times are in milliseconds
    timezone = resolve_timestamp.timezone
    nested_interval = resolve_timestamp.nested_interval
    t₀ = resolve_timestamp.timestamp
    tₛ = get_start(nested_interval) * resolve_timestamp.intersample_duration
    tₙ = x * resolve_timestamp.intersample_duration
    # Display timestamp.
    timestamp₀ = Dates.unix2datetime(Dates.value(Second(t₀) + round(Millisecond(tₛ + tₙ), Dates.Second)))
    # TODO Make the timezone a parameter of the TimestampResolver
    timestamp₁ = ZonedDateTime(timestamp₀, TimeZone(timezone, TimeZones.Class(:ALL)); from_utc = true)
    timestr = Dates.format(timestamp₁ , dateformat"HH:MM:SS")
end


# Julia wrapper for header: /Users/dls/jl/openssl-1.0.1e/include/openssl/des.h
# Automatically generated using Clang.jl wrap_c, version 0.0.0

@c Ptr{Uint8} DES_options () libcrypto
@c None DES_ecb3_encrypt (Ptr{const_DES_cblock}, Ptr{DES_cblock}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Int32) libcrypto
@c Uint32 DES_cbc_cksum (Ptr{Uint8}, Ptr{DES_cblock}, Int64, Ptr{DES_key_schedule}, Ptr{const_DES_cblock}) libcrypto
@c None DES_cbc_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int64, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Int32) libcrypto
@c None DES_ncbc_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int64, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Int32) libcrypto
@c None DES_xcbc_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int64, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Ptr{const_DES_cblock}, Ptr{const_DES_cblock}, Int32) libcrypto
@c None DES_cfb_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int32, Int64, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Int32) libcrypto
@c None DES_ecb_encrypt (Ptr{Uint8}, Ptr{Uint8}, Ptr{DES_key_schedule}, Int32) libcrypto
@c None DES_encrypt1 (Ptr{Uint32}, Ptr{DES_key_schedule}, Int32) libcrypto
@c None DES_encrypt2 (Ptr{Uint32}, Ptr{DES_key_schedule}, Int32) libcrypto
@c None DES_encrypt3 (Ptr{Uint32}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}) libcrypto
@c None DES_decrypt3 (Ptr{Uint32}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}) libcrypto
@c None DES_ede3_cbc_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int64, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Int32) libcrypto
@c None DES_ede3_cbcm_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int64, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Ptr{DES_cblock}, Int32) libcrypto
@c None DES_ede3_cfb64_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int64, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Ptr{Int32}, Int32) libcrypto
@c None DES_ede3_cfb_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int32, Int64, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Int32) libcrypto
@c None DES_ede3_ofb64_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int64, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Ptr{Int32}) libcrypto
@c Int32 DES_enc_read (Int32, Ptr{None}, Int32, Ptr{DES_key_schedule}, Ptr{DES_cblock}) libcrypto
@c Int32 DES_enc_write (Int32, Ptr{None}, Int32, Ptr{DES_key_schedule}, Ptr{DES_cblock}) libcrypto
@c Ptr{Uint8} DES_fcrypt (Ptr{Uint8}, Ptr{Uint8}, Ptr{Uint8}) libcrypto
@c Ptr{Uint8} DES_crypt (Ptr{Uint8}, Ptr{Uint8}) libcrypto
@c None DES_ofb_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int32, Int64, Ptr{DES_key_schedule}, Ptr{DES_cblock}) libcrypto
@c None DES_pcbc_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int64, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Int32) libcrypto
@c Uint32 DES_quad_cksum (Ptr{Uint8}, Ptr{DES_cblock}, Int64, Int32, Ptr{DES_cblock}) libcrypto
@c Int32 DES_random_key (Ptr{DES_cblock},) libcrypto
@c None DES_set_odd_parity (Ptr{DES_cblock},) libcrypto
@c Int32 DES_check_key_parity (Ptr{const_DES_cblock},) libcrypto
@c Int32 DES_is_weak_key (Ptr{const_DES_cblock},) libcrypto
@c Int32 DES_set_key (Ptr{const_DES_cblock}, Ptr{DES_key_schedule}) libcrypto
@c Int32 DES_key_sched (Ptr{const_DES_cblock}, Ptr{DES_key_schedule}) libcrypto
@c Int32 DES_set_key_checked (Ptr{const_DES_cblock}, Ptr{DES_key_schedule}) libcrypto
@c None DES_set_key_unchecked (Ptr{const_DES_cblock}, Ptr{DES_key_schedule}) libcrypto
@c None DES_string_to_key (Ptr{Uint8}, Ptr{DES_cblock}) libcrypto
@c None DES_string_to_2keys (Ptr{Uint8}, Ptr{DES_cblock}, Ptr{DES_cblock}) libcrypto
@c None DES_cfb64_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int64, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Ptr{Int32}, Int32) libcrypto
@c None DES_ofb64_encrypt (Ptr{Uint8}, Ptr{Uint8}, Int64, Ptr{DES_key_schedule}, Ptr{DES_cblock}, Ptr{Int32}) libcrypto
@c Int32 DES_read_password (Ptr{DES_cblock}, Ptr{Uint8}, Int32) libcrypto
@c Int32 DES_read_2passwords (Ptr{DES_cblock}, Ptr{DES_cblock}, Ptr{Uint8}, Int32) libcrypto


# ================ INITIALIZATION CODE DO NOT EDIT ====================================== %
# some global parameters -
BIG = 1e10
SMALL = 1e-8

# Load the experiment specifications -
tmp_value = JSON.parsefile("./experiments/Experiments.json")
experiment_array = tmp_value["experiment_array"]

# preload the data -
cached_data_dictionary = Dict()
for (experiment_index,experiment_dictionary) in enumerate(experiment_array)

  # Grab the data file -
  data_file_path = experiment_dictionary["data_file"]
  experiment_id = experiment_dictionary["experiment_id"]

  # Load the data -
  experimental_data_array = readdlm(data_file_path)

  # Cache the data w/experiment_id -
  cached_data_dictionary[experiment_id] = experimental_data_array
end

# preload the error function calls -
cached_error_function_dictionary = Dict()
for (experiment_index,experiment_dictionary) in enumerate(experiment_array)

  # Grab the experimental id -
  experiment_id = experiment_dictionary["experiment_id"]

  # Grab the error functions from the experiment_dictionary -
  error_function = eval(parse(experiment_dictionary["error_function"]))

  # cache these functions -
  cached_error_function_dictionary[experiment_id] = error_function
end
# ================ INITIALIZATION CODE DO NOT EDIT ====================================== %

function objective_function(parameter_array)

  # Script to solve the balance equations -
  time_start = 0.0
  time_stop = 120.0
  time_step_size = 0.01

  # Load the data dictionary -
  data_dictionary = DataDictionary(time_start,time_stop,time_step_size)

  # Update data dictionary to match new parameters before calculating obj
  parameter_mapping_array = data_dictionary["parameter_name_mapping_array"]
  for index = 1:length(parameter_mapping_array)
    if index <= data_dictionary["number_of_binding"]
      data_dictionary["binding_parameter_dictionary"][parameter_mapping_array[index]] = parameter_array[index]
    else
      data_dictionary["control_parameter_dictionary"][parameter_mapping_array[index]] = parameter_array[index]
    end
  end

  # how many objectives do we have?
  obj_array = BIG*ones(13,1)

  # Call simulation routine -
  # (run the model to SS, and then set the ICs to the SS for this parameter set)
  (time_array,simulation_state_array) = Simulation(time_start,time_stop,time_step_size,data_dictionary);

  # Call the error functions -
  # loop through the experimental dictionary, and call the appropriate error function -
  for (experiment_index,experiment_dictionary) in enumerate(experiment_array)

    # Get the error function pointer -
    error_function_pointer = experiment_dictionary["error_function"]
    output_index = parse(Int,experiment_dictionary["output_index"])
    experiment_id = experiment_dictionary["experiment_id"]
    species_symbol = experiment_dictionary["protein_symbol"];

    # Get the experimental data array -
    experimental_data_array = cached_data_dictionary[experiment_id]

    # Call the error function -
    error_function = cached_error_function_dictionary[experiment_id]
    error_value = error_function(experimental_data_array,time_array,simulation_state_array,output_index,species_symbol,data_dictionary)

    # Add the error to the objective array -
    obj_array[experiment_index] = error_value;
  end

  # @show obj_array

  # return the error array -
  return obj_array;
end

function neighbor_function(parameter_array)

  SIGMA = 0.01
  number_of_parameters = length(parameter_array)

  # calculate new parameters -
  perturbed_parameter_array = parameter_array.*(1+SIGMA*randn(number_of_parameters));

  # Parameters -
  # "n_gene_AP1_gene_AhR"	;	# 1
  # "K_gene_AP1_gene_AhR"	;	# 2
  # "n_gene_AP1_gene_PU1"	;	# 3
  # "K_gene_AP1_gene_PU1"	;	# 4
  # "n_gene_AP1_gene_PPARg"	;	# 5
  # "K_gene_AP1_gene_PPARg"	;	# 6
  # "n_gene_AhR_gene_Trigger"	;	# 7
  # "K_gene_AhR_gene_Trigger"	;	# 8
  # "n_gene_CD11b_gene_PU1_gene_cRAF"	;	# 9
  # "K_gene_CD11b_gene_PU1_gene_cRAF"	;	# 10
  # "n_gene_CD14_gene_PPARg_gene_CEBPa_gene_EGR1_gene_cRAF"	;	# 11
  # "K_gene_CD14_gene_PPARg_gene_CEBPa_gene_EGR1_gene_cRAF"	;	# 12
  # "n_gene_CD38_gene_IRF1_gene_PPARg_gene_Trigger_gene_cRAF"	;	# 13
  # "K_gene_CD38_gene_IRF1_gene_PPARg_gene_Trigger_gene_cRAF"	;	# 14
  # "n_gene_CEBPa_gene_Trigger"	;	# 15
  # "K_gene_CEBPa_gene_Trigger"	;	# 16
  # "n_gene_CEBPa_gene_PPARg"	;	# 17
  # "K_gene_CEBPa_gene_PPARg"	;	# 18
  # "n_gene_CEBPa_gene_CEBPa"	;	# 19
  # "K_gene_CEBPa_gene_CEBPa"	;	# 20
  # "n_gene_CEBPa_gene_GFI1"	;	# 21
  # "K_gene_CEBPa_gene_GFI1"	;	# 22
  # "n_gene_E2F_gene_E2F"	;	# 23
  # "K_gene_E2F_gene_E2F"	;	# 24
  # "n_gene_E2F_gene_PPARg"	;	# 25
  # "K_gene_E2F_gene_PPARg"	;	# 26
  # "n_gene_E2F_gene_CEBPa"	;	# 27
  # "K_gene_E2F_gene_CEBPa"	;	# 28
  # "n_gene_E2F_gene_GFI1"	;	# 29
  # "K_gene_E2F_gene_GFI1"	;	# 30
  # "n_gene_E2F_gene_cRAF"	;	# 31
  # "K_gene_E2F_gene_cRAF"	;	# 32
  # "n_gene_EGR1_gene_Trigger"	;	# 33
  # "K_gene_EGR1_gene_Trigger"	;	# 34
  # "n_gene_EGR1_gene_PU1"	;	# 35
  # "K_gene_EGR1_gene_PU1"	;	# 36
  # "n_gene_EGR1_gene_PPARg"	;	# 37
  # "K_gene_EGR1_gene_PPARg"	;	# 38
  # "n_gene_EGR1_gene_GFI1"	;	# 39
  # "K_gene_EGR1_gene_GFI1"	;	# 40
  # "n_gene_GFI1_gene_CEBPa"	;	# 41
  # "K_gene_GFI1_gene_CEBPa"	;	# 42
  # "n_gene_GFI1_gene_EGR1"	;	# 43
  # "K_gene_GFI1_gene_EGR1"	;	# 44
  # "n_gene_IRF1_gene_Trigger"	;	# 45
  # "K_gene_IRF1_gene_Trigger"	;	# 46
  # "n_gene_IRF1_gene_AhR"	;	# 47
  # "K_gene_IRF1_gene_AhR"	;	# 48
  # "n_gene_IRF1_gene_PPARg"	;	# 49
  # "K_gene_IRF1_gene_PPARg"	;	# 50
  # "n_gene_OCT1_gene_PPARg"	;	# 51
  # "K_gene_OCT1_gene_PPARg"	;	# 52
  # "n_gene_OCT4_gene_Trigger"	;	# 53
  # "K_gene_OCT4_gene_Trigger"	;	# 54
  # "n_gene_OCT4_gene_AhR"	;	# 55
  # "K_gene_OCT4_gene_AhR"	;	# 56
  # "n_gene_OCT4_gene_cRAF"	;	# 57
  # "K_gene_OCT4_gene_cRAF"	;	# 58
  # "n_gene_P21_gene_Trigger_gene_AP1_gene_PPARg_gene_PU1_gene_IRF1_gene_CEBPa_gene_cRAF"	;	# 59
  # "K_gene_P21_gene_Trigger_gene_AP1_gene_PPARg_gene_PU1_gene_IRF1_gene_CEBPa_gene_cRAF"	;	# 60
  # "n_gene_P21_gene_GFI1"	;	# 61
  # "K_gene_P21_gene_GFI1"	;	# 62
  # "n_gene_P47Phox_gene_PU1_gene_CEBPa_gene_cRAF"	;	# 63
  # "K_gene_P47Phox_gene_PU1_gene_CEBPa_gene_cRAF"	;	# 64
  # "n_gene_P47Phox_gene_PPARg"	;	# 65
  # "K_gene_P47Phox_gene_PPARg"	;	# 66
  # "n_gene_PPARg_gene_Trigger"	;	# 67
  # "K_gene_PPARg_gene_Trigger"	;	# 68
  # "n_gene_PPARg_gene_CEBPa"	;	# 69
  # "K_gene_PPARg_gene_CEBPa"	;	# 70
  # "n_gene_PPARg_gene_EGR1"	;	# 71
  # "K_gene_PPARg_gene_EGR1"	;	# 72
  # "n_gene_PPARg_gene_PU1"	;	# 73
  # "K_gene_PPARg_gene_PU1"	;	# 74
  # "n_gene_PPARg_gene_AP1"	;	# 75
  # "K_gene_PPARg_gene_AP1"	;	# 76
  # "n_gene_PU1_gene_Trigger"	;	# 77
  # "K_gene_PU1_gene_Trigger"	;	# 78
  # "n_gene_PU1_gene_CEBPa"	;	# 79
  # "K_gene_PU1_gene_CEBPa"	;	# 80
  # "n_gene_PU1_gene_PU1"	;	# 81
  # "K_gene_PU1_gene_PU1"	;	# 82
  # "n_gene_PU1_gene_AP1"	;	# 83
  # "K_gene_PU1_gene_AP1"	;	# 84
  # "n_gene_PU1_gene_OCT1"	;	# 85
  # "K_gene_PU1_gene_OCT1"	;	# 86
  # "n_gene_PU1_gene_AhR"	;	# 87
  # "K_gene_PU1_gene_AhR"	;	# 88
  # "n_gene_PU1_gene_GFI1"	;	# 89
  # "K_gene_PU1_gene_GFI1"	;	# 90
  # "W_gene_AP1_RNAP"	;	# 91
  # "W_gene_AP1_gene_AhR"	;	# 92
  # "W_gene_AP1_gene_PU1"	;	# 93
  # "W_gene_AP1_gene_PPARg"	;	# 94
  # "W_gene_AhR_RNAP"	;	# 95
  # "W_gene_AhR_gene_Trigger"	;	# 96
  # "W_gene_CD11b_RNAP"	;	# 97
  # "W_gene_CD11b_gene_PU1_gene_cRAF"	;	# 98
  # "W_gene_CD14_RNAP"	;	# 99
  # "W_gene_CD14_gene_PPARg_gene_CEBPa_gene_EGR1_gene_cRAF"	;	# 100
  # "W_gene_CD38_RNAP"	;	# 101
  # "W_gene_CD38_gene_IRF1_gene_PPARg_gene_Trigger_gene_cRAF"	;	# 102
  # "W_gene_CEBPa_RNAP"	;	# 103
  # "W_gene_CEBPa_gene_Trigger"	;	# 104
  # "W_gene_CEBPa_gene_PPARg"	;	# 105
  # "W_gene_CEBPa_gene_CEBPa"	;	# 106
  # "W_gene_CEBPa_gene_GFI1"	;	# 107
  # "W_gene_E2F_RNAP"	;	# 108
  # "W_gene_E2F_gene_E2F"	;	# 109
  # "W_gene_E2F_gene_PPARg"	;	# 110
  # "W_gene_E2F_gene_CEBPa"	;	# 111
  # "W_gene_E2F_gene_GFI1"	;	# 112
  # "W_gene_E2F_gene_cRAF"	;	# 113
  # "W_gene_EGR1_RNAP"	;	# 114
  # "W_gene_EGR1_gene_Trigger"	;	# 115
  # "W_gene_EGR1_gene_PU1"	;	# 116
  # "W_gene_EGR1_gene_PPARg"	;	# 117
  # "W_gene_EGR1_gene_GFI1"	;	# 118
  # "W_gene_GFI1_RNAP"	;	# 119
  # "W_gene_GFI1_gene_CEBPa"	;	# 120
  # "W_gene_GFI1_gene_EGR1"	;	# 121
  # "W_gene_IRF1_RNAP"	;	# 122
  # "W_gene_IRF1_gene_Trigger"	;	# 123
  # "W_gene_IRF1_gene_AhR"	;	# 124
  # "W_gene_IRF1_gene_PPARg"	;	# 125
  # "W_gene_OCT1_RNAP"	;	# 126
  # "W_gene_OCT1_gene_PPARg"	;	# 127
  # "W_gene_OCT4_RNAP"	;	# 128
  # "W_gene_OCT4_gene_Trigger"	;	# 129
  # "W_gene_OCT4_gene_AhR"	;	# 130
  # "W_gene_OCT4_gene_cRAF"	;	# 131
  # "W_gene_P21_RNAP"	;	# 132
  # "W_gene_P21_gene_Trigger_gene_AP1_gene_PPARg_gene_PU1_gene_IRF1_gene_CEBPa_gene_cRAF"	;	# 133
  # "W_gene_P21_gene_GFI1"	;	# 134
  # "W_gene_P47Phox_RNAP"	;	# 135
  # "W_gene_P47Phox_gene_PU1_gene_CEBPa_gene_cRAF"	;	# 136
  # "W_gene_P47Phox_gene_PPARg"	;	# 137
  # "W_gene_PPARg_RNAP"	;	# 138
  # "W_gene_PPARg_gene_Trigger"	;	# 139
  # "W_gene_PPARg_gene_CEBPa"	;	# 140
  # "W_gene_PPARg_gene_EGR1"	;	# 141
  # "W_gene_PPARg_gene_PU1"	;	# 142
  # "W_gene_PPARg_gene_AP1"	;	# 143
  # "W_gene_PU1_RNAP"	;	# 144
  # "W_gene_PU1_gene_Trigger"	;	# 145
  # "W_gene_PU1_gene_CEBPa"	;	# 146
  # "W_gene_PU1_gene_PU1"	;	# 147
  # "W_gene_PU1_gene_AP1"	;	# 148
  # "W_gene_PU1_gene_OCT1"	;	# 149
  # "W_gene_PU1_gene_AhR"	;	# 150
  # "W_gene_PU1_gene_GFI1"	;	# 151
  # "W_gene_Trigger_RNAP"	;	# 152
  # "W_gene_cRAF_RNAP"	;	# 153

  # Setup my upper bound, and lower bounds on parameters -
  lower_bound_array = zeros(number_of_parameters)
  upper_bound_array = [
    4.0   ;   # "n_gene_AP1_gene_AhR"	;	# 1
    500.0 ;   # "K_gene_AP1_gene_AhR"	;	# 2
    4.0   ;   # "n_gene_AP1_gene_PU1"	;	# 3
    500.0 ;   # "K_gene_AP1_gene_PU1"	;	# 4
    4.0   ;   # "n_gene_AP1_gene_PPARg"	;	# 5
    500.0 ;   # "K_gene_AP1_gene_PPARg"	;	# 6
    4.0   ;   # "n_gene_AhR_gene_Trigger"	;	# 7
    500.0 ;   # "K_gene_AhR_gene_Trigger"	;	# 8
    4.0   ;  # "n_gene_CD11b_gene_PU1_gene_cRAF"	;	# 9
    500.0 ;  # "K_gene_CD11b_gene_PU1_gene_cRAF"	;	# 10
    4.0   ;  # "n_gene_CD14_gene_PPARg_gene_CEBPa_gene_EGR1_gene_cRAF"	;	# 11
    500.0 ;  # "K_gene_CD14_gene_PPARg_gene_CEBPa_gene_EGR1_gene_cRAF"	;	# 12
    4.0   ;  # "n_gene_CD38_gene_IRF1_gene_PPARg_gene_Trigger_gene_cRAF"	;	# 13
    500.0 ;  # "K_gene_CD38_gene_IRF1_gene_PPARg_gene_Trigger_gene_cRAF"	;	# 14
    4.0   ;  # "n_gene_CEBPa_gene_Trigger"	;	# 15
    500.0 ;  # "K_gene_CEBPa_gene_Trigger"	;	# 16
    4.0   ;  # "n_gene_CEBPa_gene_PPARg"	;	# 17
    500.0 ;  # "K_gene_CEBPa_gene_PPARg"	;	# 18
    4.0   ;  # "n_gene_CEBPa_gene_CEBPa"	;	# 19
    500.0 ;  # "K_gene_CEBPa_gene_CEBPa"	;	# 20
    4.0   ;  # "n_gene_CEBPa_gene_GFI1"	;	# 21
    500.0 ;  # "K_gene_CEBPa_gene_GFI1"	;	# 22
    4.0   ;  # "n_gene_E2F_gene_E2F"	;	# 23
    500.0 ;  # "K_gene_E2F_gene_E2F"	;	# 24
    4.0   ;  # "n_gene_E2F_gene_PPARg"	;	# 25
    500.0 ;  # "K_gene_E2F_gene_PPARg"	;	# 26
    4.0   ;  # "n_gene_E2F_gene_CEBPa"	;	# 27
    500.0 ;  # "K_gene_E2F_gene_CEBPa"	;	# 28
    4.0   ;  # "n_gene_E2F_gene_GFI1"	;	# 29
    500.0 ;  # "K_gene_E2F_gene_GFI1"	;	# 30
    4.0   ;  # "n_gene_E2F_gene_cRAF"	;	# 31
    500.0 ;  # "K_gene_E2F_gene_cRAF"	;	# 32
    4.0   ;  # "n_gene_EGR1_gene_Trigger"	;	# 33
    500.0 ;  # "K_gene_EGR1_gene_Trigger"	;	# 34
    4.0   ;  # "n_gene_EGR1_gene_PU1"	;	# 35
    500.0 ;  # "K_gene_EGR1_gene_PU1"	;	# 36
    4.0   ;  # "n_gene_EGR1_gene_PPARg"	;	# 37
    500.0 ;  # "K_gene_EGR1_gene_PPARg"	;	# 38
    4.0   ;  # "n_gene_EGR1_gene_GFI1"	;	# 39
    500.0 ;  # "K_gene_EGR1_gene_GFI1"	;	# 40
    4.0   ;  # "n_gene_GFI1_gene_CEBPa"	;	# 41
    500.0 ;  # "K_gene_GFI1_gene_CEBPa"	;	# 42
    4.0   ;  # "n_gene_GFI1_gene_EGR1"	;	# 43
    500.0 ;  # "K_gene_GFI1_gene_EGR1"	;	# 44
    4.0   ;  # "n_gene_IRF1_gene_Trigger"	;	# 45
    500.0 ;  # "K_gene_IRF1_gene_Trigger"	;	# 46
    4.0   ;  # "n_gene_IRF1_gene_AhR"	;	# 47
    500.0 ;  # "K_gene_IRF1_gene_AhR"	;	# 48
    4.0   ;  # "n_gene_IRF1_gene_PPARg"	;	# 49
    500.0 ;  # "K_gene_IRF1_gene_PPARg"	;	# 50
    4.0   ;  # "n_gene_OCT1_gene_PPARg"	;	# 51
    500.0 ;  # "K_gene_OCT1_gene_PPARg"	;	# 52
    4.0   ;  # "n_gene_OCT4_gene_Trigger"	;	# 53
    500.0 ;  # "K_gene_OCT4_gene_Trigger"	;	# 54
    4.0   ;  # "n_gene_OCT4_gene_AhR"	;	# 55
    500.0 ;  # "K_gene_OCT4_gene_AhR"	;	# 56
    4.0   ;  # "n_gene_OCT4_gene_cRAF"	;	# 57
    500.0 ;  # "K_gene_OCT4_gene_cRAF"	;	# 58
    4.0   ;  # "n_gene_P21_gene_Trigger_gene_AP1_gene_PPARg_gene_PU1_gene_IRF1_gene_CEBPa_gene_cRAF"	;	# 59
    500.0 ;  # "K_gene_P21_gene_Trigger_gene_AP1_gene_PPARg_gene_PU1_gene_IRF1_gene_CEBPa_gene_cRAF"	;	# 60
    4.0   ;  # "n_gene_P21_gene_GFI1"	;	# 61
    500.0 ;  # "K_gene_P21_gene_GFI1"	;	# 62
    4.0   ;  # "n_gene_P47Phox_gene_PU1_gene_CEBPa_gene_cRAF"	;	# 63
    500.0 ;  # "K_gene_P47Phox_gene_PU1_gene_CEBPa_gene_cRAF"	;	# 64
    4.0   ;  # "n_gene_P47Phox_gene_PPARg"	;	# 65
    500.0 ;  # "K_gene_P47Phox_gene_PPARg"	;	# 66
    4.0   ;  # "n_gene_PPARg_gene_Trigger"	;	# 67
    500.0 ;  # "K_gene_PPARg_gene_Trigger"	;	# 68
    4.0   ;  # "n_gene_PPARg_gene_CEBPa"	;	# 69
    500.0 ;  # "K_gene_PPARg_gene_CEBPa"	;	# 70
    4.0   ;  # "n_gene_PPARg_gene_EGR1"	;	# 71
    500.0 ;  # "K_gene_PPARg_gene_EGR1"	;	# 72
    4.0   ;  # "n_gene_PPARg_gene_PU1"	;	# 73
    500.0 ;  # "K_gene_PPARg_gene_PU1"	;	# 74
    4.0   ;  # "n_gene_PPARg_gene_AP1"	;	# 75
    500.0 ;  # "K_gene_PPARg_gene_AP1"	;	# 76
    4.0   ;  # "n_gene_PU1_gene_Trigger"	;	# 77
    500.0 ;  # "K_gene_PU1_gene_Trigger"	;	# 78
    4.0   ;  # "n_gene_PU1_gene_CEBPa"	;	# 79
    500.0 ;  # "K_gene_PU1_gene_CEBPa"	;	# 80
    4.0   ;  # "n_gene_PU1_gene_PU1"	;	# 81
    500.0 ;  # "K_gene_PU1_gene_PU1"	;	# 82
    4.0   ;  # "n_gene_PU1_gene_AP1"	;	# 83
    500.0 ;  # "K_gene_PU1_gene_AP1"	;	# 84
    4.0   ;  # "n_gene_PU1_gene_OCT1"	;	# 85
    500.0 ;  # "K_gene_PU1_gene_OCT1"	;	# 86
    4.0   ;  # "n_gene_PU1_gene_AhR"	;	# 87
    500.0 ;  # "K_gene_PU1_gene_AhR"	;	# 88
    4.0   ;  # "n_gene_PU1_gene_GFI1"	;	# 89
    500.0 ;  # "K_gene_PU1_gene_GFI1"	;	# 90
    100.0 ; # "W_gene_AP1_RNAP"	;	# 91
    100.0 ;  # "W_gene_AP1_gene_AhR"	;	# 92
    100.0 ;  # "W_gene_AP1_gene_PU1"	;	# 93
    100.0 ;  # "W_gene_AP1_gene_PPARg"	;	# 94
    100.0 ;  # "W_gene_AhR_RNAP"	;	# 95
    100.0 ;  # "W_gene_AhR_gene_Trigger"	;	# 96
    100.0 ;  # "W_gene_CD11b_RNAP"	;	# 97
    100.0 ;  # "W_gene_CD11b_gene_PU1_gene_cRAF"	;	# 98
    100.0 ;  # "W_gene_CD14_RNAP"	;	# 99
    0.0   ;  # "W_gene_CD14_gene_PPARg_gene_CEBPa_gene_EGR1_gene_cRAF"	;	# 100
    100.0 ;  # "W_gene_CD38_RNAP"	;	# 101
    100.0 ;  # "W_gene_CD38_gene_IRF1_gene_PPARg_gene_Trigger_gene_cRAF"	;	# 102
    100.0 ;  # "W_gene_CEBPa_RNAP"	;	# 103
    100.0 ;  # "W_gene_CEBPa_gene_Trigger"	;	# 104
    100.0 ;  # "W_gene_CEBPa_gene_PPARg"	;	# 105
    100.0 ;  # "W_gene_CEBPa_gene_CEBPa"	;	# 106
    100.0 ;  # "W_gene_CEBPa_gene_GFI1"	;	# 107
    100.0 ;  # "W_gene_E2F_RNAP"	;	# 108
    100.0 ;  # "W_gene_E2F_gene_E2F"	;	# 109
    100.0 ;  # "W_gene_E2F_gene_PPARg"	;	# 110
    100.0 ;  # "W_gene_E2F_gene_CEBPa"	;	# 111
    100.0 ;  # "W_gene_E2F_gene_GFI1"	;	# 112
    100.0 ;  # "W_gene_E2F_gene_cRAF"	;	# 113
    100.0 ;  # "W_gene_EGR1_RNAP"	;	# 114
    100.0 ;  # "W_gene_EGR1_gene_Trigger"	;	# 115
    100.0 ;  # "W_gene_EGR1_gene_PU1"	;	# 116
    100.0 ;  # "W_gene_EGR1_gene_PPARg"	;	# 117
    100.0 ;  # "W_gene_EGR1_gene_GFI1"	;	# 118
    100.0 ;  # "W_gene_GFI1_RNAP"	;	# 119
    100.0 ;  # "W_gene_GFI1_gene_CEBPa"	;	# 120
    100.0 ;  # "W_gene_GFI1_gene_EGR1"	;	# 121
    100.0 ;  # "W_gene_IRF1_RNAP"	;	# 122
    100.0 ;  # "W_gene_IRF1_gene_Trigger"	;	# 123
    100.0 ;  # "W_gene_IRF1_gene_AhR"	;	# 124
    100.0 ;  # "W_gene_IRF1_gene_PPARg"	;	# 125
    100.0 ;  # "W_gene_OCT1_RNAP"	;	# 126
    100.0 ;  # "W_gene_OCT1_gene_PPARg"	;	# 127
    100.0 ;  # "W_gene_OCT4_RNAP"	;	# 128
    100.0 ;  # "W_gene_OCT4_gene_Trigger"	;	# 129
    100.0 ;  # "W_gene_OCT4_gene_AhR"	;	# 130
    100.0 ;  # "W_gene_OCT4_gene_cRAF"	;	# 131
    100.0 ;  # "W_gene_P21_RNAP"	;	# 132
    100.0 ;  # "W_gene_P21_gene_Trigger_gene_AP1_gene_PPARg_gene_PU1_gene_IRF1_gene_CEBPa_gene_cRAF"	;	# 133
    100.0 ;  # "W_gene_P21_gene_GFI1"	;	# 134
    100.0 ;  # "W_gene_P47Phox_RNAP"	;	# 135
    100.0 ;  # "W_gene_P47Phox_gene_PU1_gene_CEBPa_gene_cRAF"	;	# 136
    100.0 ;  # "W_gene_P47Phox_gene_PPARg"	;	# 137
    100.0 ;  # "W_gene_PPARg_RNAP"	;	# 138
    100.0 ;  # "W_gene_PPARg_gene_Trigger"	;	# 139
    100.0 ;  # "W_gene_PPARg_gene_CEBPa"	;	# 140
    100.0 ;  # "W_gene_PPARg_gene_EGR1"	;	# 141
    100.0 ;  # "W_gene_PPARg_gene_PU1"	;	# 142
    100.0 ;  # "W_gene_PPARg_gene_AP1"	;	# 143
    100.0 ;  # "W_gene_PU1_RNAP"	;	# 144
    100.0 ;  # "W_gene_PU1_gene_Trigger"	;	# 145
    100.0 ;  # "W_gene_PU1_gene_CEBPa"	;	# 146
    100.0 ;  # "W_gene_PU1_gene_PU1"	;	# 147
    100.0 ;  # "W_gene_PU1_gene_AP1"	;	# 148
    100.0 ;  # "W_gene_PU1_gene_OCT1"	;	# 149
    100.0 ;  # "W_gene_PU1_gene_AhR"	;	# 150
    100.0 ;  # "W_gene_PU1_gene_GFI1"	;	# 151
    100.0 ;  # "W_gene_Trigger_RNAP"	;	# 152
    100.0 ;  # "W_gene_cRAF_RNAP"	;	# 153
  ];

  # return the corrected value -
  return parameter_bounds_function(perturbed_parameter_array,lower_bound_array,upper_bound_array);
end

function acceptance_probability_function(rank_array,temperature)
  return (exp(-rank_array[end]/temperature))
end

function cooling_function(temperature)

  # define my new temperature -
  alpha = 0.90
  return alpha*temperature
end

function parameter_bounds_function(parameter_array,lower_bound_array,upper_bound_array)

  # iterate through and fix the parameters -
  new_parameter_array = copy(parameter_array)
  for (index,value) in enumerate(parameter_array)

    lower_bound = lower_bound_array[index]
    upper_bound = upper_bound_array[index]

    if (value<lower_bound)
      new_parameter_array[index] = lower_bound
    elseif (value>upper_bound)
      new_parameter_array[index] = upper_bound
    end
  end

  return new_parameter_array
end


using ForwardDiff
using Setfield: Lens, set, get

using ...Bifurcations: maybe_subtract!

"""
Codimension-2 fixed point bifurcation problem wrapper for DifferentialEquations.
"""
struct DiffEqCodim2Problem{
        skind <: StateKind,
        tkind <: TimeKind,
        ASType <: AugmentedSystem,
        P, X, V, W, T, L1, L2,
        } <: Codim2Problem{skind, tkind}
    statekind::skind
    timekind::tkind
    augmented_system::ASType
    de_prob::P
    x0::X
    v0::V
    w0::W
    t0::T
    t_domain::Tuple{T, T}
    param_axis1::L1
    param_axis2::L2
end

StateKind(::Type{<: DiffEqCodim2Problem{skind}}) where skind = skind()
TimeKind(::Type{<: DiffEqCodim2Problem{_sk, tkind}}) where {_sk, tkind} = tkind()

function DiffEqCodim2Problem(
        de_prob,
        param_axis1::Lens,
        param_axis2::Lens,
        t_domain::Tuple;
        x0 = copy(de_prob.u0),
        v0 = copy(x0),  # TODO: fix it. this default won't work
        w0 = 0,
        t0 = SVector(get(de_prob.p, param_axis1),
                     get(de_prob.p, param_axis2)),
        augmented_system = BackReferencingAS(),
        )
    if param_axis1 == param_axis2
        error("""
        Same parameter is specified for `param_axis1` and `param_axis2`:
        param_axis1 = param_axis2 = $(param_axis1)
        """)
    end

    t_domain = (SVector{2, eltype(t0)}(t_domain[1]),
                SVector{2, eltype(t0)}(t_domain[2]))
    return DiffEqCodim2Problem(
        statekind(de_prob), timekind(de_prob),
        augmented_system,
        de_prob, x0, v0, w0, t0, t_domain,
        param_axis1, param_axis2)
end

AugmentedSystem(::Type{<: DiffEqCodim2Problem{_sk, _tk, ASType}}
                ) where {_sk, _tk, ASType} = ASType()

_get_augsys_cache(::BackReferencingAS, prob::DiffEqCodim2Problem) =
    BackReferencingASCache(copy(prob.v0))

# ------------------------------------------------ DiffEqCodim2BifurcationCache

struct DiffEqCodim2BifurcationCache{P, C, AC, F} <: AbstractProblemCache{P}
    prob::P
    augsys_cache::AC
    cfg::C
    residual::F
end

function DiffEqCodim2BifurcationCache(prob::DiffEqCodim2Problem)
    augsys_cache = get_augsys_cache(prob)
    return _DiffEqCodim2BifurcationCache(statekind(prob), prob, augsys_cache)
end

function _DiffEqCodim2BifurcationCache(::MutableState, prob, augsys_cache)
    x = copy(get_u0(prob))
    y = similar(x, length(x) - 1)
    residual = (y, x) -> _residual!(y, x, prob, augsys_cache)
    cfg = ForwardDiff.JacobianConfig(residual, y, x)
    return DiffEqCodim2BifurcationCache(
        prob,
        augsys_cache,
        cfg,
        residual)
end

function _DiffEqCodim2BifurcationCache(::ImmutableState, prob, augsys_cache)
    x = copy(get_u0(prob))
    residual = (x) -> _residual!(nothing, x, prob, augsys_cache)
    cfg = ForwardDiff.JacobianConfig(residual, x)
    return DiffEqCodim2BifurcationCache(
        prob,
        augsys_cache,
        cfg,
        residual)
end

# ------------------------------------------------------ continuation interface

get_prob_cache(prob::DiffEqCodim2Problem) =
    DiffEqCodim2BifurcationCache(prob)

get_u0(prob::DiffEqCodim2Problem) = _get_u0(prob, prob.x0)

function _get_u0(prob::DiffEqCodim2Problem, ::AbstractVector)
    return vcat(prob.x0, as_reals(prob.v0), imag_eigval(prob), prob.t0)
end

function _get_u0(prob::DiffEqCodim2Problem, ::SVector) :: SVector
    return vcat(prob.x0, as_reals(prob.v0), imag_eigval(prob), prob.t0)
end

function _get_u0(prob::DiffEqCodim2Problem, ::Real)
    return SVector(prob.x0, as_reals(prob.v0)...,
                   imag_eigval(prob)..., prob.t0...)
end

imag_eigval(prob) = _imag_eigval(eltype(prob.v0), prob.w0)
_imag_eigval(::Type{T}, ::Real) where {T <: Real} = SVector{0, T}()
_imag_eigval(::Type{<: Complex}, w0::Real) = SVector(w0)

isindomain(u, cache::DiffEqCodim2BifurcationCache) =
    _isindomain(u, cache, eltype(cache.prob.v0))

function _isindomain(u, cache, ::Type{<: Real})
    t = SVector(u[end-1], u[end])
    tmin, tmax = cache.prob.t_domain
    return all(tmin .<= t .<= tmax)
end

function _isindomain(u, cache, ::Type{<: Complex})
    # Constrain to positive angular velocity (w = u[end-2])
    p = SVector(u[end-2], u[end-1], u[end])
    tmin, tmax = cache.prob.t_domain
    pmin = SVector(zero(tmin[1]), tmin[1], tmin[2])
    pmax = SVector(typemax(tmax[1]), tmax[1], tmax[2])
    return all(pmin .<= p .<= pmax)
end

residual!(H, u, cache::DiffEqCodim2BifurcationCache) =
    _residual!(H, u, cache.prob, cache.augsys_cache)

_residual!(H, u, prob, augsys_cache) =
    _residual!(H, u, prob, augsys_cache, statekind(prob))

residual_jacobian!(H, J, u, cache::DiffEqCodim2BifurcationCache) =
    _residual_jacobian!(H, J, u, cache, statekind(cache.prob))

# ------------------------------------------------------------------- utilities

function modified_param!(p::DiffEqCodim2Problem, u)
    t1, t2 = u[end-1:end]
    q0 = p.de_prob.p
    q1 = set(q0, p.param_axis1, t1)
    q2 = set(q1, p.param_axis2, t2)
    return q2
end

# ------------------------------------------------------------------- residual!

eigvec_constraint(v::AbstractArray{T}, ::NormalizingASCache) where {T <: Real} =
    SVector{1, T}(v ⋅ v - 1)

eigvec_constraint(v::AbstractArray{<: Complex{T}}, ::NormalizingASCache) where {T} =
    SVector{2, T}(real(v ⋅ v) - 1, real(v) ⋅ imag(v))

eigvec_constraint(v, augsys_cache::BackReferencingASCache) =
    as_reals(augsys_cache.v ⋅ v - 1)

ds_state(prob::DiffEqCodim2Problem, u::AbstractArray) =
    _ds_state(eltype(prob.v0), u)

function _ds_state(E::Type, u)
    d = dims_from_augsys(length(u), E)
    return @view u[1:d.ds_dim]
end

@generated function _ds_state(::Type{E}, u::SVector{S, T}) where {E, S, T}
    d = dims_from_augsys(S, E)
    values = [:(u[$i]) for i in 1:d.ds_dim]
    quote
        SVector{$(length(values)), $T}($(values...))
    end
end

ds_eigvec(prob::DiffEqCodim2Problem, u::AbstractArray) =
    _ds_eigvec(eltype(prob.v0), u::AbstractArray)

ds_eigvec(::HopfCont, u::AbstractArray) = _ds_eigvec(Complex, u)
ds_eigvec(::SaddleNodeCont, u::AbstractArray) = _ds_eigvec(Real, u)

function _ds_eigvec(::Type{E}, u::AbstractArray) where {E <: Real}
    d = dims_from_augsys(length(u), E)
    return @view u[eigvec_range(d)]
end

function _ds_eigvec(::Type{E}, u::Array{T}) where {E <: Complex, T}
    d = dims_from_augsys(length(u), E)
    # return ComplexView(u, eigvec_range(d))  # TODO: implement
    ofs, r = divrem(d.ds_dim, 2)
    if r != 0 || length(u) ÷ 2 != 0
        # Can't be done without copy?  Maybe this memory layout was a
        # bad idea...
        uc = reinterpret(Complex{T}, u[d.ds_dim .+ (1:d.eigvec_dim)])
        return view(uc, :)
    end
    uc = reinterpret(Complex{T}, u)
    return @view uc[ofs .+ 1:ofs .+ d.ds_dim]
end

function _ds_eigvec_expr(C::Type{<: Real}, indices)
    values = [:(u[$i]) for i in indices]
    return Expr(:call, SVector, values...)
end

function _ds_eigvec_expr(::Type{<: Complex}, indices)
    values = [:(u[$i] + u[$i + 1] * im) for i in indices[1:2:end]]
    return Expr(:call, SVector, values...)
end

# Disambiguation:
_ds_eigvec(T::Type{<: Real}, u::SVector) = _ds_eigvec_svec(T, u)
_ds_eigvec(T::Type{<: Complex}, u::SVector) = _ds_eigvec_svec(T, u)

@generated function _ds_eigvec_svec(::Type{eigvec_eltype}, u::SVector{S, T}
                                    ) where {eigvec_eltype, S, T}
    d = dims_from_augsys(S, eigvec_eltype)
    return _ds_eigvec_expr(eigvec_eltype, eigvec_range(d))
end

ds_eigval(prob::DiffEqCodim2Problem, u::AbstractArray) =
    _ds_eigval(eltype(prob.v0), u::AbstractArray)

_ds_eigval(::Type{<: Real}, ::AbstractArray) = 0

function _ds_eigval(T::Type{<: Complex}, u::AbstractArray)
    d = dims_from_augsys(length(u), T)
    return u[eigval_index(d)] * im
end

state_cond_view(d::VarDims, H) = @view H[1:d.ds_dim]       # f(x)
eigvec_cond_view(d::VarDims, H) = @view H[eigvec_range(d)] # Jv - 1
eigvec_cons_view(d::VarDims, H) = @view H[last(eigvec_range(d)) + 1:end]

J_mul_v!(Jv, fx, x, v::AbstractVector{<: Real}, q, f) =
    ForwardDiff.jacobian!(
        reshape(view(Jv, :), :, 1),
        (fx, d) -> f(fx, (@. x + d[1] * v), q, 0),
        fx,
        SVector(zero(eltype(x))),
        # TODO: setup cache
    )

function J_mul_v!(Jv, fx, x, v::AbstractVector{<: Complex}, q, f)
    vr = real(v)
    vi = imag(v)
    JvT = reshape(view(Jv, :), 2, :)'  # not a view in Julia 0.6
    ForwardDiff.jacobian!(
        JvT,
        (fx, d) -> f(fx, (@. x + d[1] * vr + d[2] * vi), q, 0),
        fx,
        SVector(zero(eltype(x)), zero(eltype(x))),
        # TODO: setup cache
    )
    Jv .= view(JvT', :)
end
# It's a bit tricky to handle complex vector case (see also
# `H2[2:2:end]` below).  Maybe `reinterpret` -compatible memory layout
# was not the best choice?

function _residual!(H, u, prob::DiffEqCodim2Problem,
                    augsys_cache,
                    ::MutableState)
    q = modified_param!(prob, u)

    dim = dims_from_augsys(length(u), contkind(prob))
    H1 = state_cond_view(dim, H)
    H2 = eigvec_cond_view(dim, H)
    H3 = eigvec_cons_view(dim, H)
    x = ds_state(prob, u)
    v = ds_eigvec(prob, u)
    iw = ds_eigval(prob, u)

    # TODO: don't allocate x + d * v
    J_mul_v!(H2, H1, x, v, q, prob.de_prob.f)
    if iw != 0
        # "H2" -= iw * v
        w = imag(iw)
        @. H2[1:2:end] += w * imag(v)
        @. H2[2:2:end] -= w * real(v)
    end
    H3[:] = eigvec_constraint(v, augsys_cache)

    maybe_subtract!(H1, x, statekind(prob), timekind(prob))
    maybe_subtract!(H2, v, statekind(prob), timekind(prob))
    return H
end

J_mul_v(x, v::AbstractVector{<: Real}, q, f) =
    ForwardDiff.jacobian(
        (d) -> f((@. x + d[1] * v), q, 0),
        SVector(zero(eltype(x))),
        # TODO: setup cache
    )[:]

function J_mul_v(x, v::AbstractVector{<: Complex}, q, f)
    vr = real(v)
    vi = imag(v)
    Jv = ForwardDiff.jacobian(
        (d) -> f((@. x + d[1] * vr + d[2] * vi), q, 0),
        SVector(zero(eltype(x)), zero(eltype(x))),
        # TODO: setup cache
    )
    return @. Jv[:, 1] + im * Jv[:, 2]
end

function _residual!(::Any, u, prob::DiffEqCodim2Problem,
                    augsys_cache,
                    ::ImmutableState)
    q = modified_param!(prob, u)

    x = ds_state(prob, u)
    v = ds_eigvec(prob, u)
    iw = ds_eigval(prob, u)

    # TODO: Can I compute H and Jv in one go?  Or is it already
    # maximally efficient?
    H1 = prob.de_prob.f(x, q, 0)
    Jv = J_mul_v(x, v, q, prob.de_prob.f)
    H2 = as_reals(@. Jv - iw * v)

    H3 = eigvec_constraint(v, augsys_cache)

    return cat_outputs(
        maybe_subtract!(H1, x, statekind(prob), timekind(prob)),
        maybe_subtract!(H2, v, statekind(prob), timekind(prob)),
        H3)
end

# ---------------------------------------------------------- residual_jacobian!

function _residual_jacobian!(H, J, u, cache::DiffEqCodim2BifurcationCache,
                             ::MutableState)
    ForwardDiff.jacobian!(
        J,
        cache.residual,
        H,  # y
        u,  # x
        cache.cfg,
    )
    return (H, J)
end

function _residual_jacobian!(_H, _J, u, cache::DiffEqCodim2BifurcationCache,
                             ::ImmutableState)
    # TODO: Can I compute H and J in one go?  Or is it already
    # maximally efficient?
    H = cache.residual(u)
    J = ForwardDiff.jacobian(
        cache.residual,
        u,  # x
        cache.cfg,
    )
    return (H, J)
end


# These are the objects encountered in the caching experiments
@enum Food begin
    Mealworm = 1
    Waxworm
    Peanut
    SuetPellet
    Pinenut
    Kibble
    Cricket
    Pineapple
    Salami
    Stone
end
struct MaintenanceDiet end
const N_FOODTYPE = length(instances(Food))
mutable struct FoodItem
    id::Food
    n::Int
    freshness::Float64
    eatable::Bool
    cacheable::Bool
end
function Base.:(==)(f1::FoodItem, f2::FoodItem)
    f1.id == f2.id &&
    f1.n == f2.n &&
    f1.freshness == f2.freshness &&
    f1.eatable == f2.eatable &&
    f1.cacheable == f2.cacheable
end
(id::Food)(n = 1, cacheable = true, eatable = id != Stone, freshness = 1.) = FoodItem(id, n, freshness, eatable, cacheable)
for foodtype in instances(Food)
    powderedform = Symbol(:Powdered, foodtype)
    @eval @__MODULE__() begin
        $powderedform(n = 1) = $foodtype(n, false)
    end
end
isnut(f) = f == Peanut || f == Pinenut
isnut(f::FoodItem) = isnut(f.id)
isworm(f) = f == Mealworm || f == Waxworm
isworm(f::FoodItem) = isworm(f.id)
ismeat(f) = isworm(f) || f == Salami || f == Cricket || f == SuetPellet || f == Kibble
ismeat(f::FoodItem) = ismeat(f.id)
isfruit(f) = f == Pineapple
isfruit(f::FoodItem) = isfruit(f.id)
foodindex(f) = Int(f)
foodindex(f::FoodItem) = foodindex(f.id)

mutable struct Tray
	appearance::Int
    position::Int
	closed::Bool
	eatableitems::Array{FoodItem, 1}
end
function Base.:(==)(x::Tray, y::Tray)
    x.appearance == y.appearance &&
    x.position == y.position &&
    x.closed == y.closed &&
    x.eatableitems == y.eatableitems
end

Tray(p = 0, a = rand(Int); closed = false) = Tray(a, p, closed, FoodItem[])
degrade!(tray::Tray) = degrade!(tray, 0)
function degrade!(tray::Tray, foodtype)
    for o in tray.eatableitems
        (foodtype == 0 || o.id == foodtype) && (o.freshness = 0.)
    end
end
pilfer!(tray::Tray) = empty!(tray.eatableitems)
function pilfer!(tray::Tray, foodtype)
    tray.eatableitems = filter(x -> x.id != foodtype, tray.eatableitems)
end
countcache(tray::Tray) = countcache(tray, Any)
countcache(tray::Tray, typ) = countfooditems(tray.eatableitems, typ)
function countfooditems(array, typ)
	n = 0
	for o in array
		if o.id == typ || typ == Any
			n += o.n
		end
	end
	n
end
countfooditems(tray::Tray, typ) = countfooditems(tray.eatableitems, typ)
add!(model, typ, n) = add!(model, typ(n))
add!(model, objects::AbstractVector) = add!.(Ref(model), objects)
add!(list::Vector{FoodItem}, typ, n) = add!(list, typ(n))
function add!(list::Vector{FoodItem}, new_item::FoodItem; only_push = false)
    for item in list
        if item.id == new_item.id &&
           item.freshness == new_item.freshness &&
           item.eatable == new_item.eatable &&
           item.cacheable == new_item.cacheable
            only_push || (item.n += new_item.n)
            return list
        end
    end
    push!(list, new_item)
end
add!(tray::Tray, typ, n) = add!(tray.eatableitems, typ, n)
cover!(tray::Tray) = tray.closed = true
uncover!(tray::Tray) = tray.closed = false

struct InspectionObserver
	trayappearances::Array{Int, 1}
end
InspectionObserver() = InspectionObserver(Tray[])
countinspections(o) = length(o.trayappearances)
function countinspections(o::InspectionObserver, tray::Tray, N = typemax(Int))
	n = 0
    for appearance in Iterators.take(o.trayappearances, N)
		if appearance == tray.appearance
			n += 1
		end
	end
	n
end
firstinspection(o, tray) = countinspections(o, tray, 1)
observe!(o, tray) = push!(o.trayappearances, tray.appearance)

# Models need to implement the following functions
function add! end
function remove! end
function wait! end
function countfooditems end


export embedded_number_field

################################################################################
#
#  Types
#
################################################################################

mutable struct EmbeddedNumField{S, E} <: Field
  field::S
  embedding::E
end

mutable struct EmbeddedNumFieldElem{T} <: FieldElem
  parent
  element::T

  function EmbeddedNumFieldElem{T}(parent, element::T) where {T}
    @assert Hecke.parent(element) === number_field(parent)
    return new{T}(parent, element)
  end
end

function (E::EmbeddedNumField{S})(x::T) where {S, T <: NumFieldElem}
  if elem_type(S) === T
    @assert parent(x) === number_field(E)
    return EmbeddedNumFieldElem{T}(E, x)
  else
    return E(number_field(E)(x))
  end
end

function (E::EmbeddedNumField)(x::EmbeddedNumFieldElem)
  @assert parent(x) === E
  return x
end

function (E::EmbeddedNumField)(x::RingElement)
  return E(number_field(E)(x))
end

function hash(x::EmbeddedNumFieldElem, h::UInt)
  return xor(hash(data(x), h), 0x5cccaf5d1346dc53%UInt)
end

function Base.deepcopy_internal(x::EmbeddedNumFieldElem, id::IdDict)
  return parent(x)(Base.deepcopy_internal(data(x), id))
end

number_field(K::EmbeddedNumField) = K.field

embedding(K::EmbeddedNumField) = K.embedding

parent(x::EmbeddedNumFieldElem{T}) where {T} = x.parent::parent_type(x)

elem_type(::Type{EmbeddedNumField{S, E}}) where {S, E} = EmbeddedNumFieldElem{elem_type(S)}

elem_type(K::EmbeddedNumField{S, E}) where {S, E} = elem_type(EmbeddedNumField{S, E})

parent_type(::Type{EmbeddedNumFieldElem{T}}) where {T} = EmbeddedNumField{parent_type(T), embedding_type(parent_type(T))}

parent_type(x::EmbeddedNumFieldElem{T}) where {T} = parent_type(EmbeddedNumFieldElem{T})

data(x::EmbeddedNumFieldElem) = x.element

function embedded_field(K::SimpleNumField, i::NumFieldEmb)
  @assert number_field(i) === K
  E = EmbeddedNumField(K, i)
  return E, E(gen(K))
end

function embedded_field(K::NonSimpleNumField, i::NumFieldEmb)
  @assert number_field(i) === K
  E = EmbeddedNumField(K, i)
  return E, E.(gens(K))
end

(E::EmbeddedNumField)() = zero(E)

unary_ops = [:(-)]

binary_ops = [:(+), :(*), :(-), :(div), :(divexact), :(//)]

for b in unary_ops
  @eval begin
    function ($b)(x::EmbeddedNumFieldElem)
      return parent(x)($(b)(data(x)))
    end
  end
end

for b in binary_ops
  @eval begin
    function ($b)(x::EmbeddedNumFieldElem, y::EmbeddedNumFieldElem)
      return parent(x)($(b)(data(x), data(y)))
    end
  end
end

function ==(x::EmbeddedNumFieldElem, y::EmbeddedNumFieldElem)
  return ==(data(x), data(y))
end

iszero(x::EmbeddedNumFieldElem) = iszero(data(x))

isone(x::EmbeddedNumFieldElem) = isone(data(x))

isunit(x::EmbeddedNumFieldElem) = isunit(data(x))

zero(E::EmbeddedNumField) = E(zero(number_field(E)))

one(E::EmbeddedNumField) = E(one(number_field(E)))

# Now the ordering

function isless(x::EmbeddedNumFieldElem, y::EmbeddedNumFieldElem)
  i = embedding(parent(x))
  # Need to exclude equality
  if x == y
    return false
  end
  p = 32
  xe, ye = i(data(x), p), i(data(y), p)
  while overlaps(xe, ye)
    p = Int(floor(p * 1.42))
    xe, ye = i(data(x), p), i(data(y), p)
  end
  if real(xe) < real(ye)
    return true
  else
    @assert real(xe) > real(ye)
    return false
  end
end

isless(x::EmbeddedNumFieldElem, y) = isless(x, parent(x)(y))

isless(x, y::EmbeddedNumFieldElem) = isless(parent(y)(x), y)

# Support comparing with floats

function isless(x::EmbeddedNumFieldElem, y::AbstractFloat)
  i = embedding(parent(x))
  # Need to exclude equality
  p = 32
  xe = i(data(x), p)
  # check if y is "equal" to x as a rational
  if isrational(data(x))
    xq = QQ(data(x))
    d = denominator(xq)
    if isone(d)
      yd = y
    else
      yd = d * y
    end
    if isinteger(yd) && numerator(xq) == yd
      return false
    end
  end

  while contains(real(xe), BigFloat(y))
    p = Int(floor(p * 1.42))
    xe = i(data(x), p)
  end
  if real(xe) < y
    return true
  else
    @assert real(xe) > y
    return false
  end
end

function isless(y::AbstractFloat, x::EmbeddedNumFieldElem)
  i = embedding(parent(x))
  # Need to exclude equality
  p = 32
  xe = i(data(x), p)
  # check if y is "equal" to x as a rational
  if isrational(data(x))
    xq = QQ(data(x))
    d = denominator(xq)
    if isone(d)
      yd = y
    else
      yd = d * y
    end
    if isinteger(yd) && numerator(xq) == yd
      return false
    end
  end

  while contains(real(xe), BigFloat(y))
    p = Int(floor(p * 1.42))
    xe = i(data(x), p)
  end
  if real(xe) > y
    return true
  else
    @assert real(xe) < y
    return false
  end
end

################################################################################
#
#  String I/O
#
################################################################################

function Base.show(io::IO, E::EmbeddedNumField)
  print(io, "Embedded field\n$(number_field(E))\nat\n$(embedding(E))")
end

# I overload this to get the value at the embedding
# (But I don't want this in the general printing routines, e.g., over polynomial
# rings.)
function Base.show(io::IO, ::MIME"text/plain", x::EmbeddedNumFieldElem)
  print(io, "$(data(x))")
  a = Float64(real(embedding(parent(x))(data(x), 32)))
  print(io, @sprintf " (%.2f)" a)
end

function Base.show(io::IO, x::EmbeddedNumFieldElem)
  print(io, "$(data(x))")
end

function AbstractAlgebra.expressify(x::EmbeddedNumFieldElem; context = nothing)
  AbstractAlgebra.expressify(data(x), context = context)
end

################################################################################
#
#  Constructors
#
################################################################################

function embedded_number_field(f::Union{fmpq_poly, fmpz_poly}, r::Union{AbstractFloat, Tuple}, var = "a")
  K, a = number_field(f, var)
  r = real_embedding(K, r)
  return embedded_field(K, r)
end

function embedded_number_field(f::Vector{<:Union{fmpq_poly, fmpz_poly}}, r::Vector{<:Union{AbstractFloat, Tuple}}, var = "a")
  K, a = number_field(f, var)
  r = real_embedding(K, r)
  return embedded_field(K, r)
end

################################################################################
#
#  Creation of elements
#
################################################################################

(E::EmbeddedNumField)(v::Vector) = E(number_field(E)(v))

################################################################################
#
#  Promote rule
#
################################################################################

function AbstractAlgebra.promote_rule(::Type{EmbeddedNumFieldElem{T}}, ::Type{S}) where {T <: NumFieldElem, S <: RingElement}
  if T === S
    return Union{}
  else
    if AbstractAlgebra.promote_rule(T, S) === T
      return EmbeddedNumFieldElem{T}
    else
      return Union{}
    end
  end
end

function AbstractAlgebra.promote_rule(::Type{EmbeddedNumFieldElem{T}}, ::Type{EmbeddedNumFieldElem{T}}) where {T <: NumFieldElem}
  return EmbeddedNumFieldElem{T}
end


struct CompilerMetadata{StaticNDRange, CheckBounds, I, NDRange, Iterspace}
    groupindex::I
    ndrange::NDRange
    iterspace::Iterspace

    # CPU variant
    function CompilerMetadata{NDRange, CB}(idx, ndrange, iterspace) where {NDRange, CB}
        if ndrange !== nothing
            ndrange = CartesianIndices(ndrange)
        end
        return new{NDRange, CB, typeof(idx), typeof(ndrange), typeof(iterspace)}(idx, ndrange, iterspace)
    end

    # GPU variante: index is given implicit
    function CompilerMetadata{NDRange, CB}(ndrange, iterspace) where {NDRange, CB}
        if ndrange !== nothing
            ndrange = CartesianIndices(ndrange)
        end
        return new{NDRange, CB, Nothing, typeof(ndrange), typeof(iterspace)}(nothing, ndrange, iterspace)
    end
end

@inline __iterspace(cm::CompilerMetadata)  = cm.iterspace
@inline __groupindex(cm::CompilerMetadata) = cm.groupindex
@inline __groupsize(cm::CompilerMetadata) = size(workitems(__iterspace(cm)))
@inline __dynamic_checkbounds(::CompilerMetadata{NDRange, CB}) where {NDRange, CB} = CB <: DynamicCheck
@inline __ndrange(cm::CompilerMetadata{NDRange}) where {NDRange<:StaticSize}  = CartesianIndices(get(NDRange))
@inline __ndrange(cm::CompilerMetadata{NDRange}) where {NDRange<:DynamicSize} = cm.ndrange

include("compiler/contract.jl")
include("compiler/pass.jl")

function generate_overdubs(Ctx)
   @eval begin
        @inline Cassette.overdub(ctx::$Ctx, ::typeof(groupsize)) = __groupsize(ctx.metadata)
        @inline Cassette.overdub(ctx::$Ctx, ::typeof(__workitems_iterspace)) = workitems(__iterspace(ctx.metadata))

        ###
        # Cassette fixes
        ###
        @inline Cassette.overdub(::$Ctx, ::typeof(Core.kwfunc), f) = return Core.kwfunc(f)
        @inline Cassette.overdub(::$Ctx, ::typeof(Core.apply_type), args...) = return Core.apply_type(args...)
        @inline Cassette.overdub(::$Ctx, ::typeof(StaticArrays.Size), x::Type{<:AbstractArray{<:Any, N}}) where {N} = return StaticArrays.Size(x)

        @inline Cassette.overdub(::$Ctx, ::typeof(+), a::T, b::T) where T<:Union{Float32, Float64} = add_float_contract(a, b)
        @inline Cassette.overdub(::$Ctx, ::typeof(-), a::T, b::T) where T<:Union{Float32, Float64} = sub_float_contract(a, b)
        @inline Cassette.overdub(::$Ctx, ::typeof(*), a::T, b::T) where T<:Union{Float32, Float64} = mul_float_contract(a, b)

        function Cassette.overdub(::$Ctx, ::typeof(:), start::T, step::T, stop::T) where T<:Union{Float16,Float32,Float64}
            lf = (stop-start)/step
            if lf < 0
                len = 0
            elseif lf == 0
                len = 1
            else
                len = round(Int, lf) + 1
                stop′ = start + (len-1)*step
                # if we've overshot the end, subtract one:
                len -= (start < stop < stop′) + (start > stop > stop′)
            end
            Base.steprangelen_hp(T, start, step, 0, len, 1)
        end
    end
end


# TODO: Test arbitrary profile waveguide, for now have only tested simple waveguide
module Waveguides

export add_halfspace_waveguide,
add_halfspace_waveguides,
add_planar_waveguide,
add_planar_waveguides,
add_pc_waveguide,
add_pc_waveguides

using ...Bravais,
...BoundaryConditions,
...Shapes,
...DefineSimulation,
..ConstructionToolsBase,
..PhotonicCrystal

include("Waveguides/waveguides_planar.jl")
include("Waveguides/waveguides_halfspace.jl")
include("Waveguides/waveguides_pc.jl")

include("DefineSimulation/iss.jl")

end # module


struct ResNetModel
    entry
    pooling
    layers
    head

    size::Int64
end
Flux.@functor ResNetModel

(m::ResNetModel)(x) = x |> m.entry |> m.pooling |> m.layers |> m.head

function (m::ResNetModel)(x, ::Val{:stages})
    stages = typeof(x)[]

    o = x |> m.entry
    push!(stages, o)
    o = o |> m.pooling

    for l in m.layers
        o = o |> l
        push!(stages, o)
    end
    stages
end

const Config = Dict(
    18=>((2, 2, 2, 2), BasicBlock, 1),
    34=>((3, 4, 6, 3), BasicBlock, 1),
    50=>((3, 4, 6, 3), Bottleneck, 4),
    101=>((3, 4, 23, 3), Bottleneck, 4),
    152=>((3, 8, 36, 3), Bottleneck, 4),
)

function ResNetModel(
    model_size::Int64 = 18;
    in_channels::Int64 = 3,
    classes::Union{Int64, Nothing} = 1000,
)
    if !(model_size in keys(Config))
        throw(
            "Invalid mode size [$model_size]. " *
            "Supported sizes are $(keys(Config))."
        )
    end
    repeats, block, expansion = Config[model_size]

    entry = Chain(
        Conv((7, 7), in_channels=>64, pad=(3, 3), stride=(2, 2), bias=false),
        BatchNorm(64, relu),
    )
    pooling = MaxPool((3, 3), pad=(1, 1), stride=(2, 2))

    head = nothing
    if classes ≢ nothing
        head = Chain(
            MeanPool((7, 7)), flatten, Dense(512 * expansion, classes),
        )
    end

    in_channels = 64
    channels = (64, 128, 256, 512)
    strides = (1, 2, 2, 2)

    layers = []
    for (out_channels, repeat, stride) in zip(channels, repeats, strides)
        push!(layers, make_layer(
            block, in_channels=>out_channels, repeat, expansion, stride,
        ))
        in_channels = out_channels * expansion
    end

    ResNetModel(entry, pooling, Chain(layers...), head, model_size)
end

function stages_channels(r::ResNetModel)
    e = Config[r.size][3]
    (64, 64 * e, 128 * e, 256 * e, 512 * e)
end


import KernelAbstractions: CPU

using ..Mesh.Grids
using ..MPIStateArrays
using ..DGMethods
using ..TicToc

"""
    writevtk(prefix, Q::MPIStateArray, dg::DGModel [, fieldnames])

Write a vtk file for all the fields in the state array `Q` using geometry and
connectivity information from `dg.grid`. The filename will start with `prefix`
which may also contain a directory path. The names used for each of the fields
in the vtk file can be specified through the collection of strings `fieldnames`;
if not specified the fields names will be `"Q1"` through `"Qk"` where `k` is the
number of states in `Q`, i.e., `k = size(Q,2)`.

"""
function writevtk(prefix, Q::MPIStateArray, dg::DGModel, fieldnames = nothing)
    vgeo = dg.grid.vgeo
    device = array_device(Q)
    (h_vgeo, h_Q) = device isa CPU ? (vgeo, Q.data) : (Array(vgeo), Array(Q))
    writevtk_helper(prefix, h_vgeo, h_Q, dg.grid, fieldnames)
    return nothing
end

"""
    writevtk(prefix, Q::MPIStateArray, dg::DGModel, fieldnames,
             state_auxiliary::MPIStateArray, auxfieldnames)

Write a vtk file for all the fields in the state array `Q` and auxiliary state
`state_auxiliary` using geometry and connectivity information from `dg.grid`. The
filename will start with `prefix` which may also contain a directory path. The
names used for each of the fields in the vtk file can be specified through the
collection of strings `fieldnames` and `auxfieldnames`.

If `fieldnames === nothing` then the fields names will be `"Q1"` through `"Qk"`
where `k` is the number of states in `Q`, i.e., `k = size(Q,2)`.

If `auxfieldnames === nothing` then the fields names will be `"aux1"` through
`"auxk"` where `k` is the number of states in `state_auxiliary`, i.e., `k =
size(state_auxiliary,2)`.

"""
function writevtk(
    prefix,
    Q::MPIStateArray,
    dg::DGModel,
    fieldnames,
    state_auxiliary,
    auxfieldnames,
)
    vgeo = dg.grid.vgeo
    device = array_device(Q)
    (h_vgeo, h_Q, h_aux) =
        device isa CPU ? (vgeo, Q.data, state_auxiliary.data) :
        (Array(vgeo), Array(Q), Array(state_auxiliary))
    writevtk_helper(
        prefix,
        h_vgeo,
        h_Q,
        dg.grid,
        fieldnames,
        h_aux,
        auxfieldnames,
    )
    return nothing
end

"""
    writevtk_helper(prefix, vgeo::Array, Q::Array, grid, fieldnames)

Internal helper function for `writevtk`
"""
function writevtk_helper(
    prefix,
    vgeo::Array,
    Q::Array,
    grid,
    fieldnames,
    state_auxiliary = nothing,
    auxfieldnames = nothing,
)

    dim = dimensionality(grid)
    N = polynomialorder(grid)
    Nq = N + 1

    nelem = size(Q)[end]
    Xid = (grid.x1id, grid.x2id, grid.x3id)
    X = ntuple(
        j -> reshape((@view vgeo[:, Xid[j], :]), ntuple(j -> Nq, dim)..., nelem),
        dim,
    )
    if fieldnames == nothing
        fields = ntuple(
            i -> ("Q$i", reshape((@view Q[:, i, :]), ntuple(j -> Nq, dim)..., nelem)),
            size(Q, 2),
        )
    else
        fields = ntuple(
            i -> (
                fieldnames[i],
                reshape((@view Q[:, i, :]), ntuple(j -> Nq, dim)..., nelem),
            ),
            size(Q, 2),
        )
    end
    if state_auxiliary !== nothing
        if auxfieldnames === nothing
            auxfields = ntuple(
                i -> (
                    "aux$i",
                    reshape(
                        (@view state_auxiliary[:, i, :]),
                        ntuple(j -> Nq, dim)...,
                        nelem,
                    ),
                ),
                size(state_auxiliary, 2),
            )
        else
            auxfields = ntuple(
                i -> (
                    auxfieldnames[i],
                    reshape(
                        (@view state_auxiliary[:, i, :]),
                        ntuple(j -> Nq, dim)...,
                        nelem,
                    ),
                ),
                size(state_auxiliary, 2),
            )
        end
        fields = (fields..., auxfields...)
    end
    writemesh(
        prefix,
        X...;
        fields = fields,
        realelems = grid.topology.realelems,
    )
end

"""
    writegrid(prefix, grid::DiscontinuousSpectralElementGrid)

Write a vtk file for the grid.  The filename will start with `prefix` which
may also contain a directory path.
"""
function writevtk(prefix, grid::DiscontinuousSpectralElementGrid)
    dim = dimensionality(grid)
    N = polynomialorder(grid)
    Nq = N + 1

    vgeo = grid.vgeo isa Array ? grid.vgeo : Array(grid.vgeo)

    nelem = size(vgeo)[end]
    Xid = (grid.x1id, grid.x2id, grid.x3id)
    X = ntuple(
        j -> reshape((@view vgeo[:, Xid[j], :]), ntuple(j -> Nq, dim)..., nelem),
        dim,
    )
    writemesh(prefix, X...; realelems = grid.topology.realelems)
end


using Jtac

chain = @stack Morris stack_input=true begin
  Conv(128, relu)
  Dense(200, relu)
end

model = NeuralModel(Morris, chain)
player = MCTSPlayer(model, power = 100)

loss = Loss(value = 10, regularization = 1e-5)

opponents = [MCTSPlayer(power = p) for p in [10, 25, 50, 100, 250, 500]]

with_contest( train_self!
            , player
            , loss = loss
            , playings = 500
            , batchsize = 100
            , branching = 0.1
            , epochs = 10
            , opponents = opponents
            , interval = 3
            , length = 1000
            , cache = 2000 )


export buildPyNNModel_01_FromElements, buildPyNNModel_01_FromWeights, loadTfModelIntoFlux
export buildPose2OdoNN_01_FromElements, buildPose2OdoNN_01_FromWeights, loadPose2OdoNNModelIntoFlux

function buildPose2OdoNN_01_FromElements( W1::AbstractMatrix{<:Real}=zeros(4,8),
                                          b1::AbstractVector{<:Real}=zeros(8),
                                          W2::AbstractMatrix{<:Real}=zeros(8,48),
                                          b2::AbstractVector{<:Real}=zeros(8),
                                          W3::AbstractMatrix{<:Real}=zeros(2,8),
                                          b3::AbstractVector{<:Real}=zeros(2),
                                          pad::Bool=false)
  #
  # W1 = randn(Float32, 4,8)
  # b1 = randn(Float32,8)
  # outPad = pad ? x->[x;0] : x->x
  modjl = Chain(
    x -> (x*W1)' .+ b1 .|> relu,
    x -> reshape(x', 25,8,1),
    x -> maxpool(x, PoolDims(x, 4)),
    x -> reshape(x[:,:,1]',:),
    Dense(48,8,relu),
    Dense(8,2),
    x->[x;0]
  )

  modjl[5].W .= W2
  modjl[5].b .= b2

  modjl[6].W .= W3
  modjl[6].b .= b3

  return modjl
end

# As loaded from tensorflow get_weights
# Super specialized function
function buildPose2OdoNN_01_FromWeights(pywe; pad::Bool=false)
  buildPose2OdoNN_01_FromElements(pywe[1], pywe[2][:], pywe[3]', pywe[4][:], pywe[5]', pywe[6][:], pad)
end

# function buildPose2OdoNN_01_FromWeightsGPU(pywe)
#   buildPose2OdoNN_01_FromElements(collect(pywe[1]) |> gpu, collect(pywe[2][:]) |> gpu, collect(pywe[3]') |> gpu, collect(pywe[4][:]) |> gpu, collect(pywe[5]') |> gpu, collect(pywe[6][:]) |> gpu) |> gpu
# end

@deprecate buildPyNNModel_01_FromElements(args...;kw...) buildPose2OdoNN_01_FromElements(args...;kw...)
@deprecate buildPyNNModel_01_FromWeights(args...;kw...) buildPose2OdoNN_01_FromWeights(args...;kw...)

#