module Aswing
using OffsetArrays

import Base.eltype

function __init__()
    global UNITS = UNITS_s()
    global ASWING = ASWING_s()
    global EIGEN = EIGEN_s()
    global THCALC = THCALC_s()
    global CONLAW = CONLAW_s()
    initialize()

    global GlobalUNITS = UNITS
    global GlobalASWING = ASWING
    global GlobalEIGEN = EIGEN
    global GlobalTHCALC = THCALC
    global GlobalCONLAW = CONLAW
end

const depsfile = joinpath(dirname(@__FILE__), "..", "deps", "deps.jl")
if isfile(depsfile)
    include(depsfile)
else
    error("Aswing not properly installed. Please run Pkg.build(\"Aswing\") then restart Julia.")
end

function initialize()
    ccall((:init_, libaswing), Nothing, ())
    ccall((:setdef_, libaswing), Nothing, ())
    ccall((:iidef_, libaswing), Nothing, ())
    ccall((:nndefp_, libaswing), Nothing, ())
    ccall((:vginit_, libaswing), Nothing, ())
    ccall((:dscheck_, libaswing), Nothing, ())
    THCALC.CUSTOMINTERVALS[1] = false
    THCALC.INTERVALS[0:NBRKX+1] .= 0
end

# Global variable wrappers and fortran pointer definitions
include("globals/dimen.jl")
include("globals/indexb.jl")
include("globals/indexc.jl")
include("globals/indexp.jl")
include("globals/units.jl")
include("globals/aswing.jl")
include("globals/eigen.jl")
include("globals/thcalc.jl")
include("globals/conlaw.jl")

# defines ASWING geometry input
include("geom.jl")

# defines ASWING operating point definitions
include("pnt.jl")

# defines Aswing solution output
include("sol.jl")

# implements Aswing file i/o
include("io.jl")

# implements main Aswing calling methods
include("main.jl")

# deprecated functions
include("deprecated.jl")

end #modules


abstract type AxisObject end

mutable struct Title <: AxisObject
    text::String
    #
    textstyle::TextStyle
    dist     ::Option{F64}  # distance labels - title
end
Title(t) = default(Title, t)

mutable struct TicksLabels <: AxisObject
    names::Vector{String}
    #
    textstyle::TextStyle
    angle    ::Option{F64}     # rotation of labels
    format   ::Option{String}  # format of the ticks labels
    shift    ::Option{F64}     # move labels to left/right
    dist     ::Option{F64}     # ⟂ distance to spine
    off      ::Bool            # whether to suppress the labels
end
TicksLabels(n) = default(TicksLabels, n)

mutable struct Ticks <: AxisObject
    places::Vector{F64}     # where the ticks are
    labels::TicksLabels     # their label
    #
    linestyle::LineStyle    # how the ticks marks look
    length   ::Option{F64}  # how long the ticks spine (negative for outside)
    symticks ::Bool         # draws ticks on 2 sides of
    off      ::Bool         # whether to suppress them
    grid     ::Bool         # ticks increased to mirrorred axis
end
Ticks(p=Float64[], l=fl2str.(p)) = default(Ticks, p, TicksLabels(l))

abstract type AxesObject end

mutable struct Legend <: AxesObject
    handles::Vector{DrawingHandle}
    labels::Vector{Union{String,Vector{String}}}
    #
    position::Option{String}
    textstyle::TextStyle
    offset  ::T2F
    bgcolor ::Option{Colorant}
    margins ::Option{T2F}
    nobox   ::Bool
    off     ::Bool
end
Legend(h, l) = default(Legend, h, l)


using ImageView, Images   # TestImages  just a reminder poop stinks!
using Gtk.ShortNames

grid, frames, canvases = canvasgrid((1,1))  # 1 row, 2 columns
img = fill(RGBA(0.65625,0.796875,1,1), 500, 800)
imshow(canvases[1,1], img)
win = Window(grid)
showall(win)
# ==============================================================================
# This draws a pixel with Alpha chanel
# ==============================================================================
function pixel(image, X, Y, color )
    a = alpha(color)
    width, height = size(image)
    if X>0 && X < width && Y>0 && Y < height
        image[ X,Y ] = image[ X,Y ]*(1-a) + (color*a)
    end
end
# ==============================================================================
# This draws a rectrangle to a picture!
# ==============================================================================
function rect(image, left, top, width, height, color)
    for y in 1:height, x in 1:width
        pixel(image, top+y, left+x, color )
    end
end
# ==============================================================================
# This draws random rectrangles!
# ==============================================================================
function RandomPaint()
    left, top = rand( 1:800), rand( 1:500)
    rect(img, left, top,
              rand( 1:300), rand( 1:300),
              rand(RGBA{Float32})  )
    imshow(canvases[1,1], img)
end

u1 = Timer(u1 -> RandomPaint(), 0.01, 0.1)

#imshow(canvases[1,2], testimage("mandrill"))
#win = Window(grid)
# showall(win)


# Types using the base types

"""
    Gain(value, frequency)
    Gain(; value, frequency)

Complex type for sensitivity and frequency ranges. This complex type
can be used to represent both overall sensitivities and individual stage gains. The
`FrequencyRangeGroup` is an optional construct that defines a pass band in Hertz
(`frequency_start` and `frequency_nd`) in which the `SensitivityValue` is valid within the
number of decibels specified in FrequencyDBVariation.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw mutable struct Gain
    "A scalar that, when applied to the data values, converts the data to different units (e.g. Earth units)."
    value::Float64
    "The frequency (in Hertz) at which the Value is valid."
    frequency::Float64
end

function parse_node(::Type{Gain}, node::EzXML.Node, warn::Bool=false)
    local value, frequency
    for elm in EzXML.eachelement(node)
        if elm.name == "Value"
            value = parse(Float64, elm.content)
        elseif elm.name == "Frequency"
            frequency = parse(Float64, elm.content)
        else
            warn && @warn("unexpected element name \"$(elm.name)\" in Gain")
        end
    end
    Gain(value, frequency)
end

"""
    Sensitivity(value, frequency, input_units, output_units)
    Sensitivity(; value, frequency, input_units, output_units, frequency_start=missing, frequency_end=missing, frequency_db_variation=missing)

Sensitivity and frequency ranges.

Optionally specify a frequency range that defines a pass band in Hertz
`frequency_start` and `frequency_end` in which the `value` is valid within the
number of decibels specified in `frequency_db_variation`.

Note that either all or none of `frequency_start`, `frequency_end` and
`frequency_db_variation` must be specified upon construction.
This is **not** enforced however when mutating the fields of a `Sensitivity`
object.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw mutable struct Sensitivity
    "A scalar that, when applied to the data values, converts the data to different units (e.g. Earth units).
     Although this must be present according to the specification, it can be
     `missing` here."
    value::M{Float64} = missing
    "The frequency (in Hertz) at which the `value` is valid.
     Although this must be present according to the specification, it can be
     `missing` here."
    frequency::M{Float64} = missing
    "The units of the data as input from the perspective
	 of data acquisition. After correcting data for this response, these
	 would be the resulting units."
    input_units::Units
    "The units of the data as output from the perspective
	 of data acquisition. These would be the units of the data prior to
	 correcting for this response."
    output_units::Units
    frequency_start::M{Float64} = missing
    frequency_end::M{Float64} = missing
    frequency_db_variation::M{Float64} = missing

    function Sensitivity(value, frequency, input_units, output_units,
            frequency_start, frequency_end, frequency_db_variation)
        freq_group = (frequency_start, frequency_end, frequency_db_variation)
        count(x->x===missing, freq_group) in (0, 3) ||
            throw(ArgumentError("either all or none of $freq_group must be given"))
        new(value, frequency, input_units, output_units,
            frequency_start, frequency_end, frequency_db_variation)
    end
end

"""
    Equipment(; kwargs...)

Sensor equipment installed at a Station.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw mutable struct Equipment
    type::M{String} = missing
    description::M{String} = missing
    manufacturer::M{String} = missing
    vendor::M{String} = missing
    model::M{String} = missing
    serial_number::M{String} = missing
    installation_date::M{DateTime} = missing
    removal_date::M{DateTime} = missing
    calibration_date::Vector{DateTime} = DateTime[]
    other::M{String} = missing
    "This field contains a string that should serve as a unique
	 resource identifier. This identifier can be interpreted differently depending on
	 the datacenter/software that generated the document. Also, we recommend to use
	 something like GENERATOR:Meaningful ID. As a common behaviour equipment with the
	 same ID should contains the same information/be derived from the same base
	 instruments."
    resource_id::M{String} = missing
end

attribute_fields(::Type{Equipment}) = (:resource_id,)

"""
    Log(; entry=Comment[])

Container for log entries.

!!! note
    The `Log` type is no longer present in StationXML v1.1 and later.
    It is included here for compatibility with v1.0 files.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct Log
    entry::Vector{Comment} = Comment[]
end

@enumerated_struct(PZTransferFunction,
    ("LAPLACE (RADIANS/SECOND)", "LAPLACE (HERTZ)", "DIGITAL (Z-TRANSFORM)"))

"""
    PolesZeros

Response: complex poles and zeros. Corresponds to SEED blockette 53.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw mutable struct PolesZeros
    @BaseFilter
    pz_transfer_function_type::PZTransferFunction
    normalization_factor::Float64 = 1.0
    normalization_frequency::Float64
    zero::Vector{PoleZero} = PoleZero[]
    pole::Vector{PoleZero} = PoleZero[]
end

attribute_fields(::Type{PolesZeros}) = BASE_FILTER_ATTRIBUTES

"""
    NumeratorCoefficient

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct NumeratorCoefficient <: NumberType
    value::Float64
    i::M{Int} = missing
    NumeratorCoefficient(value, i=missing) = new(value, i)
end

attribute_fields(::Type{NumeratorCoefficient}) = (:i,)

function parse_node(::Type{NumeratorCoefficient}, node::EzXML.Node, warn::Bool=false)
    value = parse(Float64, node.content)
    i = missing
    for att in EzXML.eachattribute(node)
        if att.name == "i"
            i = parse(Int, att.content)
        else
            warn && @warn("unexpected attribute $(att.name) for NumeratorCoefficient")
        end
    end
    NumeratorCoefficient(value, i)
end

@enumerated_struct(FIRSymmetry,
    ("NONE", "EVEN", "ODD"))

"""
    FIR(; kwargs...)

Response: FIR filter. Corresponds to SEED blockette 61. FIR filters
are also commonly documented using the `Coefficients` type.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw mutable struct FIR
    @BaseFilter
    "Symmetry of filter.  Must be one of `\"NONE\"`, `\"EVEN\"` or `\"ODD\"`.
     See [`StationXML.FIR`](@ref)."
    symmetry::FIRSymmetry
    "Set of [`NumeratorCoefficient`](@ref StationXML.NumeratorCoefficient)s."
    numerator_coefficient::Vector{NumeratorCoefficient} = NumeratorCoefficient[]
end

attribute_fields(::Type{FIR}) = BASE_FILTER_ATTRIBUTES

@enumerated_struct(CfTransferFunction,
    ("ANALOG (RADIANS/SECOND)", "ANALOG (HERTZ)", "DIGITAL"))

"""
    Coefficient

Extension of [`FloatNoUnit`](@ref StationXML.FloatNoUnit) with an
additional `number` attribute, used as the `numerator` and `denominator`
of [`Coefficients`](@ref StationXML.Coefficients) and
[`Polynomial`](@ref StationXML.Polynomial).

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct Coefficient <: NumberType
    "Value of the coefficient (no unit)"
    value::Float64
    "Absolute error in the positive direction."
    plus_error::M{Float64} = missing
    "Absolute error in the negative direction (i.e., this value should be positive)."
    minus_error::M{Float64} = missing
    "Method used to make measurement"
    measurement_method::M{String} = missing
    "Number of the coefficient"
    number::M{Int} = missing

    function Coefficient(value, plus_error=missing, minus_error=missing,
            measurement_method=missing, number=missing)
        number !== missing && number < 0 &&
            throw(ArgumentError("number must be 0 or greater"))
        new(value, plus_error, minus_error, measurement_method, number)
    end
end

attribute_fields(::Type{Coefficient}) = (:plus_error, :minus_error,
    :measurement_method, :number)

function parse_node(::Type{Coefficient}, node::EzXML.Node, warn::Bool=false)
    value = parse(Float64, node.content)
    plus_error = minus_error = measurement_method = number = missing
    for att in EzXML.eachattribute(node)
        name = att.name
        if name == "number"
            number = parse(Int, att.content)
        elseif name == "plusError"
            plus_error = parse(Float64, att.content)
        elseif name == "minusError"
            minus_error = parse(Float64, att.content)
        elseif name == "measurementMethod"
            measurement_method = att.content
        else
            warn && @warn("unexpected attribute $(att.name) for NumeratorCoefficient")
        end
    end
    Coefficient(value, plus_error, minus_error, measurement_method, number)
end

const Numerator = Coefficient
const Denominator = Coefficient

"""
    Coefficients(; kwargs...)

Response: coefficients for FIR filter. Laplace transforms or IIR
filters can be expressed using type as well but the PolesAndZerosType should be used
instead. Corresponds to SEED blockette 54.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw mutable struct Coefficients
    @BaseFilter
    "Type of the transfer function (see [`CfTransferFunction`](@ref)."
    cf_transfer_function_type::CfTransferFunction
    numerator::Vector{Numerator} = Numerator[]
    denominator::Vector{Denominator} = Denominator[]
end

attribute_fields(::Type{Coefficients}) = BASE_FILTER_ATTRIBUTES

"""
    ResponseListElement(frequency, amplitude, phase)

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct ResponseListElement
    "Frequency at which the `amplitude` and `phase` was measured."
    frequency::Frequency
    "Amplitude response at the given `frequency`."
    amplitude::Float
    "Phase angle response at the given `frequency`"
    phase::Angle
end

"""
    ResponseList

Response: list of frequency, amplitude and phase values. Corresponds to SEED blockette 55.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct ResponseList
    @BaseFilter
    "Set of [`ResponseListElement`](@ref)s."
    response_list_element::Vector{ResponseListElement} = ResponseListElement[]
end

attribute_fields(::Type{ResponseList}) = BASE_FILTER_ATTRIBUTES

@enumerated_struct(Approximation, ("MACLAURIN",))

"""
    Polynomial

Response: expressed as a polynomial (allows non-linear sensors to be
described). Corresponds to SEED blockette 62. Can be used to describe a stage of
acquisition or a complete system.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct Polynomial
    @BaseFilter
    approximation_type::Approximation = Approximation("MACLAURIN")
    frequency_lower_bound::Frequency
    frequency_upper_bound::Frequency
    "The low frequency corner for which the sensor is valid."
    approximation_lower_bound::Float64
    "The high frequency corner for which the sensor is valid."
    approximation_upper_bound::Float64
    maximum_error::Float64
    coefficient::Vector{Coefficient} = Coefficient[]
end

"""
    Decimation

Description of the decimation stage of a response.

Corresponds to SEED blockette 57.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct Decimation
    "Sample rate of the signal before decimation."
    input_sample_rate::Frequency
    "Number of times the signal is decimated.  The output sample rate
     is therefore `input_sample_rate ÷ factor`."
    factor::Int
    "Which sample is chosen for use when decimating, starting at 0 for
     the first sample in the window of `factor` samples.
     (This implies `0 <= offset < factor`.)"
    offset::Int
    "The estimated pure delay of the signal, where positive values
     mean a time delay or shift to later in time."
    delay::Float64
    "Time shift applied to correct for the delay introduced in this
     stage.  Positive numbers mean a time advance or shift to earlier in time."
    correction::Float64
end

"""
    ResponseStage

Represents a channel's response and covers SEED blockettes 53 to 56.

!!! note
    Although the `stage_gain` field is mandatory in v1.0 of the StationXML specification,
    it is common in StationXML files that the field is absent for certain
    types of channel, such as state-of-health channels.  Hence it is an
    optional field of `ResponseStage` in StationXML.jl.
    Version 1.1 of the specification removed the requirement for a `stage_gain`
    when a `polynomial` stage is defined, rather than one of `poles_zeros`,
    `coefficients`, `response_list` or `fir`.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct ResponseStage
    "A choice of `polynomial` versus all other response types.
     There should be one response per stage."
    poles_zeros::M{PolesZeros} = missing
    coefficients::M{Coefficients} = missing
    response_list::M{ResponseList} = missing
    fir::M{FIR} = missing
    polynomial::M{Polynomial} = missing
    decimation::M{Decimation} = missing
    "The gain at the stage of the encapsulating
     response element and corresponds to SEED blockette 58. In the SEED
     convention, stage 0 gain represents the overall sensitivity of the channel.
     In this schema, stage 0 gains are allowed but are considered deprecated.
     Overall sensitivity should be specified in the InstrumentSensitivity
     element."
    stage_gain::M{Gain} = missing
    "Stage sequence number. This is used in all the response SEED blockettes."
    number::Int
    "Same meaning as the `resource_id` field of [`Equipment`](@ref StationXML.Equipment)."
    resource_id::M{String} = missing
    function ResponseStage(poles_zeros, coefficients, response_list, fir, polynomial,
        decimation, stage_gain, number, resource_id)
        count(!ismissing, (poles_zeros, coefficients, response_list, fir, polynomial)) <= 1 ||
            throw(ArgumentError("none or only one response type can be specified"))
        number >= 0 || throw(ArgumentError("number must be 0 or more"))
        polynomial !== missing && stage_gain !== missing &&
            @warn("stage_gain cannot be specified for polynomial stages in StationXML v1.1")
        new(poles_zeros, coefficients, response_list, fir, polynomial,
            decimation, stage_gain, number, resource_id)
    end
end

attribute_fields(::Type{ResponseStage}) = (:number, :resource_id)

"""
    Response

Instrument sensitivities, or the complete system sensitivity,
can be expressed using either a sensitivity value or a polynomial. The
information can be used to convert raw data to real units at a specified
frequency or within a range of frequencies.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct Response
    "The total sensitivity for a channel, representing the
     complete acquisition system expressed as a scalar. Equivalent to SEED
     stage 0 gain with (blockette 58) with the ability to specify a frequency
     range."
    instrument_sensitivity::M{Sensitivity} = missing
    "The total sensitivity for a channel, representing the
	complete acquisition system expressed as a
    [`Polynomial`](@ref StationXML.Polynomial). Equivalent to
	SEED stage 0 polynomial (blockette 62). "
    instrument_polynomial::M{Polynomial} = missing
    "Set of [`ResponseStage`](@ref StationXML.ResponseStage)s describing
     each stage in the "
    stage::Vector{ResponseStage} = ResponseStage[]
    resource_id::M{String} = missing
end

attribute_fields(::Type{Response}) = (:resource_id,)

"""
    Operator

An operating agency and associated contact persons. If
there are multiple operators, each one should be encapsulated within an
Operator tag. Since the Contact element is a generic type that
represents any contact person, it also has its own optional `agency`
field.  It is expected that typically the contact’s optional `agency`
field will match the `Operator` `agency`. Only contacts appropriate for the
enclosing element should be included in the `Operator` object.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct Operator
    "Only one agency is allowed in StationXML v1.1."
    agency::Vector{String} = String[]
    contact::Vector{Person} = Person[]
    web_site::M{String} = missing
end

"""
    DataAvailability

A description of time series data availability. This information should be
considered transient and is primarily useful as a guide for generating time
series data requests. The information for a DataAvailability:Span may be
specific to the time range used in a request that resulted in the document
or limited to the availability of data withing the request range. These details
may or may not be retained when synchronizing metadata between data centers.

# List of fields
$(DocStringExtensions.TYPEDFIELDS)
"""
@with_kw struct DataAvailability
    extent::M{DataAvailabilityExtent} = missing
    span::Vector{DataAvailabilitySpan} = DataAvailabilitySpan[]
end


include("functions_cvlm.jl")

#Cl_spn,nspn,spn_map,spn_loc,θ,rhs,AIC,AICₜ,AICₘ,Λ,sbar,ebar,ds,Γ,chord,cbar,mbar,nbar,Cl,CL,CDind,CDind_ff,SpnLd = main("input.yaml");
#Px = Py = Pz = Qx = Qy = Qz = zeros(6,6)
sol,Px,Py,Pz,Qx,Qy,Qz,lat2wing_map = main("input.yaml","motion_param.yaml");

nsteps= size(sol.u,1)
xyz = zeros(3,nsteps)
xyz1= zeros(3,nsteps)
for i in 1:nsteps
    xyz[:,i]  = sol.u[i][10:12]
    #xyz1[:,i] = solwing.u[i][10:12]
end

#p = plot3d(2,labels=["Wing+tail" "Wing only"],xlabel="X-coord",ylabel="Y-coord",zlabel="Z-coord",xlim=(minimum(xyz[1,:]),maximum(xyz[1,:])),ylim=(minimum(xyz[2,:]),maximum(xyz[2,:])),zlim=(minimum(xyz[3,:]),maximum(xyz[3,:])),marker=2)
#p = plot3d!(p,2)
p = plot3d(1,labels=["Wing+tail"],xlabel="X-coord",ylabel="Y-coord",zlabel="Z-coord",xlim=(minimum(xyz[1,:]),maximum(xyz[1,:])),ylim=(minimum(xyz[2,:]),maximum(xyz[2,:])),zlim=(minimum(xyz[3,:]),maximum(xyz[3,:])),marker=2)
gif = @gif for i in 1:nsteps
    push!(p,1,xyz[1,i],xyz[2,i],xyz[3,i])
    #push!(p,2,xyz1[1,i],xyz1[2,i],xyz1[3,i])
end every 1


"""
    join(r1::Relation, r2::Relation)

Performs the natural join between two `Relation`s. If two columns share the same name, they
must match on the output.
"""
function join(r1::Relation, r2::Relation)
    joinlabels = labels(r1) ∩ labels(r2)
    _join(r1, r2, indexes(r1, joinlabels), indexes(r2, joinlabels))
end

#const ⨝ = join


# TODO: move this next line to MLJBase:
MLJBase.iterator(r::NominalRange, ::Nothing) = iterator(r)

"""
    MLJTuning.grid([rng, ] prototype, ranges, resolutions)

Given an iterable `ranges` of `ParamRange` objects, and an iterable
`resolutions` of the same length, return a vector of models generated
by cloning and mutating the hyperparameters (fields) of `prototype`,
according to the Cartesian grid defined by the specifed
one-dimensional `ranges` (`ParamRange` objects) and specified
`resolutions`. A resolution of `nothing` for a `NominalRange`
indicates that all values should be used.

Specification of an `AbstractRNG` object `rng` implies shuffling of
the results. Otherwise models are ordered, with the first
hyperparameter referenced cycling fastest.

"""
grid(rng::AbstractRNG, prototype::Model, ranges, resolutions) =
    shuffle(rng, grid(prototype, ranges, resolutions))

function grid(prototype::Model, ranges, resolutions)

    iterators = broadcast(iterator, ranges, resolutions)

    A = MLJBase.unwind(iterators...)

    N = size(A, 1)
    map(1:N) do i
        clone = deepcopy(prototype)
        for k in eachindex(ranges)
            field = ranges[k].field
            recursive_setproperty!(clone, field, A[i,k])
        end
        clone
    end
end

"""
    process_user_range(user_specified_range, resolution, verbosity)

Utility to convert user-specified range (see [`Grid`](@ref)) into a
pair of tuples `(ranges, resolutions)`.

For example, if `r1`, `r2` are `NumericRange`s and `s` is a
NominalRange` with 5 values, then we have:

    julia> MLJTuning.process_user_range([(r1, 3), r2, s], 42, 1) ==
                            ((r1, r2, s), (3, 42, 5))
    true

If `verbosity` > 0, then a warning is issued if a `Nominal` range is
paired with a resolution.

"""
process_user_range(user_specified_range, resolution, verbosity) =
    process_user_range([user_specified_range, ], resolution, verbosity)
function process_user_range(user_specified_range::AbstractVector,
                    resolution, verbosity)
    stand(r) = throw(ArgumentError("Unsupported range. "))
    stand(r::NumericRange) = (r, resolution)
    stand(r::NominalRange) = (r, length(r.values))
    stand(t::Tuple{NumericRange,Integer}) = t
    function stand(t::Tuple{NominalRange,Integer})
        verbosity < 0 ||
            @warn  "Ignoring a resolution specified for a `NominalRange`. "
        return (first(t), length(first(t).values))
    end

    ret = zip(stand.(user_specified_range)...) |> collect
    return first(ret), last(ret)
end


# Defines the core Node abstraction
# Defines the core Node and operations on the graph

using Base
importall Base

typealias Float Float64
typealias TensorValue Union{Real, Array}

###############
# Basic types #
###############
abstract OpType

type Node
    op::OpType
    inputs::Vector{Node}
    outputs::Vector{Node}
    name::AbstractString
end

pred(node::Node) = node.inputs
succ(node::Node) = node.outputs

# TODO: Actual scoping on naming
function Node(data::OpType, name::AbstractString="")
    newName = length(name) == 0 ? "$(gensym())" : name
    Node(data, Vector{Node}(), Vector{Node}(), newName)
end

function Node(op::OpType, inputs::Vector{Node}, name::AbstractString="")
    newName = length(name) == 0 ? "$(gensym())" : name
    node = Node(op, inputs, Vector{Node}(), newName)
    for i in inputs
        push!(i.outputs, node)
    end
    node
end

immutable Graph
    nodes::Set{Node}
end

# May use to have a notion of "default" graph
# const Dict{Symbol, Graph} context
# context[:default] = Graph(Set{Node}())
########################
# Gradient computation #
########################

# out w.r.t. each element of wrt
function grad(out::Node, wrt::Vector{Node})
    # input gradients = derivative w.r.t. input - may be different for the nth and n+1th argument
    # output gradient = derivative w.r.t. output (i.e. before we apply this node's grad)

    # When we process a node, associate its input gradient with the appropriate input
    node_to_grad_vec = Dict{Node, Vector{Node}}(out => [fill_like(cg.constant(1.0), out)])

    # Map a node to its single output (usually sum of all output gradients)
    node_to_grad = Dict{Node, Node}()

    # Set of nodes on all paths between the set `wrt` and `out`
    for node = reverse(toposort_on_path([out], wrt))
        # Compute the gradient w.r.t. the node's output
        @assert haskey(node_to_grad_vec, node)
        grads = node_to_grad_vec[node]
        if (length(grads) == 1)
            output_grad = node_to_grad_vec[node][1]
        else
            # Sum up the gradients of all the outputs
            output_grad = foldr((sum, next) -> sum + next, grads)
        end
        if length(node.name) > 0
            output_grad.name = "Gin:$(node.name)"
        end
        node_to_grad[node] = output_grad

        if (length(node.inputs) > 0)
            # If the node has inputs, calculate the gradient w.r.t their outputs along this path
            input_grads = grad(node.op, node, output_grad, node.inputs...)
            for (input_node, grad) = zip(node.inputs, input_grads)
                if (!haskey(node_to_grad_vec, input_node))
                    node_to_grad_vec[input_node] = Vector{Node}()
                end
                if length(grad.name) > 0
                    output_grad.name = "Gout:$(input_node.name)"
                end
                push!(node_to_grad_vec[input_node], grad)
            end
        end
    end
    [node_to_grad[x] for x in wrt]
end

####################
# Graph operations #
####################

# Node hashing for use in deduping computation graphs
# Hash node itself, then recursively add in hashes of parents
function hash_node(node::Node)
    res = UInt64(node.op)
    for input = node.inputs
        res = hash(hash_node(input), res)
    end
    res
end

# Return set of nodes that are influenced in the DAG from any in set `nodes`
# i.e. that would be influenced by in a computation
# child=true means go to children in dag, false means go to parents
function influenced_by(nodes::Vector{Node}, child::Bool)
    queue = copy(nodes)
    influenced = Set{Node}(queue)
    next_method = child ? succ : pred
    while !isempty(queue)
        next = pop!(queue)
        for node = next_method(next)
            if node in influenced
                continue
            end
            push!(queue, node)
            push!(influenced, node)
        end
    end
    influenced
end

function nodes_on_path(sink::Vector{Node}, source::Vector{Node}=Vector{Node}())
    downstream = influenced_by(source, true)
    upstream = influenced_by(sink, false)
    if length(source) > 0
        intersect(upstream, downstream)
    else
        upstream
    end

end

function toposort_on_path(sink::Vector{Node}, source::Vector{Node}=Vector{Node}())
    nodes = nodes_on_path(sink, source)
    toposorted = toposort(get_graph(union(sink, source)))
    intersect(toposorted, nodes)
end

# Returns nodes in topological order
function toposort(graph::Graph)
    result = Vector{Node}()
    marks = Dict{Node, Symbol}() # :marked, :temp
    function visit(cur::Node)
        mark = get(marks, cur, :unmarked)
        if isequal(mark, :temp)
            @assert false && "Graph is not a DAG!"
            return
        elseif isequal(mark, :unmarked)
            marks[cur] = :temp
            for child = succ(cur)
                visit(child)
            end
            marks[cur] = :marked
            push!(result, cur)
        end
    end

    for node = graph.nodes
        if isequal(get(marks, node, :unmarked), :unmarked)
            visit(node)
        end
    end
    reverse!(result)
    result
end

# Return graph consisting of all nodes connected to given Variables
function get_connected(nodes::Set{Node})
    stack = collect(Node, nodes)
    seen = Set{Node}(nodes)
    while !isempty(stack)
        cur = pop!(stack)
        prev = pred(cur)
        next = succ(cur)
        # Would like the chain(.) function
        for n in prev
            if !(n in seen)
                push!(seen, n)
                push!(stack, n)
            end
        end
        for n in next
            if !(n in seen)
                push!(seen, n)
                push!(stack, n)
            end
        end
    end
    seen
end

function get_graph(nodes::Vector{Node})
    Graph(get_connected(Set(nodes)))
end

function group_nodes(nodes::Vector{Node}, group::AbstractString)
    for node in nodes
        node.name = string(group, "/", node.name)
    end
end

function group_between(inputs::Vector{Node}, outputs::Vector{Node}, group::AbstractString ; include_in=true, include_out=true)
    set = nodes_on_path(outputs, inputs)
    if !include_in
        set = setdiff(set, Set(inputs))
    end
    if !include_out
        set = setdiff(set, Set(outputs))
    end
    group_nodes(collect(set), group)
end


using Nettle

testdict = Dict("abc" => "a9993e364706816aba3e25717850c26c9cd0d89d",
                "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" =>
                    "84983e441c3bd26ebaae4aa1f95129e5e54670f1",
                "a" ^ 1_000_000 => "34aa973cd4c4daa4f61eeb2bdbad27316534016f",)

for (text, expect) in testdict
    digest = hexdigest("sha1", text)
    if length(text) > 50 text = text[1:50] * "..." end
    println("# $text\n -> digest: $digest\n -> expect: $expect")
end


#=
function fprop!(t::Convexity, g::DAT, b::VexityCache)
end
=#


using PyCall
@pyimport numpy

x = numpy.load("test_x.npy")
y = numpy.load("test_y.npy")

num_tests = 2246
println("Loading model...")
using Keras
m = Keras.load("model-structure.json", "model-weights.h5")
println("Model loaded, Testing....")
count = 0
for i=1:num_tests
    if (findmax(y[i,:])[2] == findmax(m(x[i,:]))[2])
        count += 1
    end
end

println("Model tested: $(count*100/num_tests)% accuracy achieved.")


VERSION >= v"0.4.0-dev+6521" && __precompile__()

module Meshes

using Compat
using GeometryTypes
using Meshing
using MeshIO
using FileIO

export HomogenousMesh
export Mesh
export isosurface
export load, save #FileIO

typealias Mesh HomogenousMesh

#include("simplification.jl")

end # module Meshes


# Single decision #
# ========================== #
const FixingConstraint{T} = CI{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}
mutable struct DecisionBridge{T} <: MOIB.Variable.AbstractBridge
    decision::Decision{T}
    variable::MOI.VariableIndex
    fixing_constraint::FixingConstraint{T}

    function DecisionBridge(decision::Decision{T},
                            variable::MOI.VariableIndex,
                            fixing_constraint::FixingConstraint{T}) where T
        return new{T}(decision, variable, fixing_constraint)
    end
end

function MOIB.Variable.bridge_constrained_variable(::Type{DecisionBridge{T}},
                                                   model::MOI.ModelLike,
                                                   set::SingleDecisionSet{T}) where T
    if set.constraint isa NoSpecifiedConstraint
        variable = MOI.add_variable(model)
    else
        variable, constraint = MOI.add_constrained_variable(model, set.constraint)
    end
    # Check state of decision
    if state(set.decision) == NotTaken
        fixing_constraint = FixingConstraint{T}(0)
    else
        # Decision initially fixed
        fixing_constraint =
            MOI.add_constraint(model,
                               MOI.ScalarAffineFunction{T}([MOI.ScalarAffineTerm(one(T), variable)], zero(T)),
                               MOI.EqualTo(set.decision.value))
    end
    return DecisionBridge(set.decision, variable, fixing_constraint)
end

function MOIB.Variable.supports_constrained_variable(
    ::Type{DecisionBridge{T}}, ::Type{SingleDecisionSet{T}}) where T
    return true
end

function MOIB.added_constrained_variable_types(::Type{<:DecisionBridge})
    return Tuple{DataType}[]
end
function MOIB.added_constraint_types(::Type{<:DecisionBridge})
    return Tuple{DataType, DataType}[]
end

# Attributes, Bridge acting as a model
MOI.get(bridge::DecisionBridge, ::MOI.NumberOfVariables) = 1

function MOI.get(bridge::DecisionBridge, ::MOI.ListOfVariableIndices)
    return [bridge.variable]
end

# References
function MOI.delete(model::MOI.ModelLike, bridge::DecisionBridge{T}) where T
    if bridge.fixing_constraint.value != 0
        # Remove the fixing constraint
        MOI.delete(model, bridge.fixing_constraint)
        bridge.fixing_constraint = FixingConstraint{T}(0)
    end
    MOI.delete(model, bridge.variable)
end

# Attributes, Bridge acting as a constraint
function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet,
                 bridge::DecisionBridge)
    return SingleDecisionSet(1, bridge.decision)
end

function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintPrimal,
                 bridge::DecisionBridge{T}) where T
    if bridge.decision.state == Taken
        return bridge.decision.value
    end
    return MOI.get(model, MOI.VariablePrimal(), bridge.variable)
end

function MOI.get(model::MOI.ModelLike, attr::Union{MOI.VariablePrimal, MOI.VariablePrimalStart},
                 bridge::DecisionBridge{T}) where T
    if bridge.decision.state == Taken
        return bridge.decision.value
    end
    return MOI.get(model, attr, bridge.variable)
end

function MOI.get(model::MOI.ModelLike, ::DecisionIndex,
                 bridge::DecisionBridge{T}) where T
    return bridge.variable
end

function MOI.set(model::MOI.ModelLike, attr::MOI.VariablePrimalStart,
                 bridge::DecisionBridge{T}, value) where T
    MOI.set(model, attr, bridge.variable, value)
end

function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintSet,
                 bridge::DecisionBridge{T}, new_set::SingleDecisionSet{T}) where T
    bridge.decision = new_set.decision
end

function MOI.modify(model::MOI.ModelLike, bridge::DecisionBridge{T}, change::DecisionStateChange) where T
    # Switch on state transition
    if change.new_state == NotTaken
        if bridge.fixing_constraint.value != 0
            # Remove the fixing constraint
            MOI.delete(model, bridge.fixing_constraint)
            bridge.fixing_constraint = FixingConstraint{T}(0)
        end
    else
        set = MOI.EqualTo(bridge.decision.value)
        if bridge.fixing_constraint.value != 0
            # Update existing
            MOI.set(model.model, MOI.ConstraintSet(), bridge.fixing_constraint, set)
        else
            # Add new fixing constraint
            bridge.fixing_constraint = MOI.add_constraint(model,
                                                          MOI.ScalarAffineFunction{T}([MOI.ScalarAffineTerm(one(T), bridge.variable)], zero(T)),
                                                          set)
        end
    end
    return nothing
end

function MOI.modify(model::MOI.ModelLike, bridge::DecisionBridge{T}, ::KnownValuesChange) where T
    # Update fixing constraint if decision is known
    if state(bridge.decision) == Known
        set = MOI.EqualTo(bridge.decision.value)
        MOI.set(model.model, MOI.ConstraintSet(), bridge.fixing_constraint, set)
    end
    return nothing
end

function MOIB.bridged_function(bridge::DecisionBridge{T}) where T
    # Return mapped variable
    return MOI.ScalarAffineFunction{T}(MOI.SingleVariable(bridge.variable))
end

function MOIB.Variable.unbridged_map(bridge::DecisionBridge, vi::MOI.VariableIndex)
    return (bridge.variable => MOI.SingleVariable(vi),)
end

# Multiple decisions #
# ========================== #
struct DecisionsBridge{T} <: MOIB.Variable.AbstractBridge
    decisions::Vector{Decision{T}}
    variables::Vector{MOI.VariableIndex}
    fixing_constraints::Vector{FixingConstraint{T}}
end

function MOIB.Variable.bridge_constrained_variable(::Type{DecisionsBridge{T}},
                                                   model::MOI.ModelLike,
                                                   set::MultipleDecisionSet{T}) where T
    if set.constraint isa NoSpecifiedConstraint
        variables = MOI.add_variables(model, length(set.decisions))
    else
        variables, constraints = MOI.add_constrained_variables(model, set.constraint)
    end
    fixing_constraints = FixingConstraint{T}[]
    # Check decision states
    for (variable, decision) in zip(variables, set.decisions)
        if state(decision) == NotTaken
            push!(fixing_constraints, FixingConstraint{T}(0))
        else
            # Decision initially fixed
            fixing_constraint =
                MOI.add_constraint(model,
                                   MOI.ScalarAffineFunction{T}([MOI.ScalarAffineTerm(one(T), variable)], zero(T)),
                                   MOI.EqualTo(decision.value))
            push!(fixing_constraints, fixing_constraint)
        end
    end
    return DecisionsBridge(set.decisions, variables, fixing_constraints)
end

function MOIB.Variable.supports_constrained_variable(
    ::Type{DecisionsBridge{T}}, ::Type{MultipleDecisionSet{T}}) where T
    return true
end

function MOIB.added_constrained_variable_types(::Type{<:DecisionsBridge})
    return Tuple{DataType}[]
end
function MOIB.added_constraint_types(::Type{<:DecisionsBridge})
    return Tuple{DataType, DataType}[]
end

# Attributes, Bridge acting as a model
MOI.get(bridge::DecisionsBridge, ::MOI.NumberOfVariables) = length(bridge.variables)

function MOI.get(bridge::DecisionsBridge, ::MOI.ListOfVariableIndices)
    return bridge.variables
end

# References
function MOI.delete(model::MOI.ModelLike, bridge::DecisionsBridge{T}) where T
    for (i,fixing_constraint) in enumerate(bridge.fixing_constraints)
        if fixing_constraint.value != 0
            # Remove the fixing constraint
            MOI.delete(model, fixing_constraint)
            bridge.fixing_constraints[i] = FixingConstraint{T}(0)
        end
    end
    MOI.delete(model, bridge.variables)
end

function MOI.delete(model::MOI.ModelLike, bridge::DecisionsBridge{T}, i::MOIB.Variable.IndexInVector) where T
    if bridge.fixing_constraints[i].value != 0
        # Remove the fixing constraint
        MOI.delete(model, bridge.fixing_constraints[i])
        bridge.fixing_constraints[i] = FixingConstraint{T}(0)
    end
    MOI.delete(model, bridge.variables[i.value])
    deleteat!(bridge.variables, i.value)
end

# Attributes, Bridge acting as a constraint
function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet,
                 bridge::DecisionsBridge)
    return MultipleDecisionSet(bridge.decisions)
end

function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintPrimal,
                 bridge::DecisionsBridge{T}) where T
    return [decision.state == Taken ? decision_value.(bridge.decisions) :
            MOI.get(model,
                    MOI.VariablePrimal(),
                    bridge.variables[i]) for (i,decision) in bridge.decisions]
end

function MOI.get(model::MOI.ModelLike,
                 attr::Union{MOI.VariablePrimal, MOI.VariablePrimalStart},
                 bridge::DecisionsBridge, i::MOIB.Variable.IndexInVector)
    decision = bridge.decisions[i.value]
    if decision.state == Taken
        return decision.value
    else
        return MOI.get(model, attr, bridge.variables[i.value])
    end
end

function MOI.get(model::MOI.ModelLike, ::DecisionIndex,
                 bridge::DecisionBridge{T}, i::MOIB.Variable.IndexInVector) where T
    return bridge.variables[i]
end

function MOI.set(model::MOI.ModelLike, attr::MOI.VariablePrimalStart,
                 bridge::DecisionsBridge{T}, val, i::MOIB.Variable.IndexInVector) where T
    MOI.set(model, attr, bridge.variables[i.value], val)
end

function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintSet,
                 bridge::DecisionsBridge{T}, new_set::MultipleDecisionSet{T}) where T
    bridge.decisions .= new_set.decisions
end

function MOI.modify(model::MOI.ModelLike, bridge::DecisionsBridge{T}, change::DecisionStateChange) where T
    # Switch on state transition
    if change.new_state == NotTaken
        if bridge.fixing_constraints[change.index].value != 0
            # Remove the fixing constraint
            MOI.delete(model, bridge.fixing_constraints[change.index])
            bridge.fixing_constraints[change.index] = FixingConstraint{T}(0)
        end
    else
        set = MOI.EqualTo(bridge.decisions[change.index].value)
        if bridge.fixing_constraints[change.index].value != 0
            # Update existing
            MOI.set(model.model, MOI.ConstraintSet(), bridge.fixing_constraints[change.index], set)
        else
            # Add new fixing constraint
            bridge.fixing_constraints[change.index] = MOI.add_constraint(model,
                                                                         MOI.ScalarAffineFunction{T}([MOI.ScalarAffineTerm(one(T), bridge.variables[change.index])], zero(T)),
                                                                         set)
        end
    end
    return nothing
end

function MOI.modify(model::MOI.ModelLike, bridge::DecisionsBridge{T}, ::KnownValuesChange) where T
    # Update fixing constraints of known decisions
    for (decision,fixing_constraint) in zip(bridge.decisions, bridge.fixing_constraints)
        if state(decision) == Known
            set = MOI.EqualTo(decision.value)
            MOI.set(model.model, MOI.ConstraintSet(), fixing_constraint, set)
        end
    end
    return nothing
end

function MOIB.bridged_function(bridge::DecisionsBridge{T}, i::MOIB.Variable.IndexInVector) where T
    # Return mapped variable
    return MOI.ScalarAffineFunction{T}(MOI.SingleVariable(bridge.variables[i.value]))
end
function MOIB.Variable.unbridged_map(bridge::DecisionsBridge, vi::MOI.VariableIndex, i::MOIB.Variable.IndexInVector)
    return (bridge.variables[i.value] => MOI.SingleVariable(vi),)
end

# Modifications #
# ========================== #
# To enable certain modifications (decision coefficient changes and decision state updates)
# a few methods must be added to MathOptInterface
function MOIB.is_bridged(b::MOIB.AbstractBridgeOptimizer,
                         change::Union{DecisionCoefficientChange, DecisionMultirowChange})
    return MOIB.is_bridged(b, change.decision)
end

function MOIB.is_bridged(b::MOIB.AbstractBridgeOptimizer,
                         change::Union{DecisionStateChange, KnownValuesChange})
    # These modifications should not be handled by the variable bridges
    return false
end

function MOIB.modify_bridged_change(b::MOIB.AbstractBridgeOptimizer, obj,
                                    change::DecisionCoefficientChange)
    f = MOIB.bridged_variable_function(b, change.decision)
    # Continue modification with mapped variable
    MOI.modify(b, obj, DecisionCoefficientChange(only(f.terms).variable_index, change.new_coefficient))
    return nothing
end

function MOIB.modify_bridged_change(b::MOIB.AbstractBridgeOptimizer, obj,
                                    change::DecisionMultirowChange)
    f = MOIB.bridged_variable_function(b, change.decision)
    # Continue modification with mapped variable
    MOI.modify(b, obj, DecisionMultirowChange(only(f.terms).variable_index, change.new_coefficients))
    return nothing
end


# =========================================================================
#    BASIC STRUCTURES
# =========================================================================

struct vtx
    #IDENTIFIERS
    name::String
    type::String

    #VALS
    x::Float64 #xcoor
    y::Float64 #ycoor
    MAX::Float64 #max inventory level
    MIN::Float64 #min inventory level
    START::Float64 #starting inventory level

    #COSTS
    h::Float64 #inv cost per unit
end

struct veh
    #IDENTIFIERS
    name::String
    type::String

    #CHAR
    cover::Vector{Int64}
    BP::Dict{Int64,Int64}
    Q::Int64

    #COSTS
    vx::Float64
    vl::Float64
    fp::Float64
    fd::Float64
end

struct col
    #quantity
    u::JuMP.Containers.DenseAxisArray
    v::JuMP.Containers.DenseAxisArray
    l::JuMP.Containers.DenseAxisArray

    #decision
    o::JuMP.Containers.DenseAxisArray
    x::JuMP.Containers.DenseAxisArray
end

struct dv
    #three master constraints
    λ::JuMP.Containers.DenseAxisArray
    δ::JuMP.Containers.SparseAxisArray
    ϵ::JuMP.Containers.DenseAxisArray

    #bounding constraints
    ρ::JuMP.Containers.DenseAxisArray
    σ::JuMP.Containers.DenseAxisArray
end

struct β
    i::Int64
    v::Int64
end

struct S
    k::Int64
    t::Int64
    seq::Vector{β}
end

struct bound
    S::S #component bound sequence
    sense::String #≳ or ≲
    κ::Int64 #value of aggregation
end

struct stabilizer
    slCoeff::Float64
    suCoeff::Float64
    slLim::Dict
    suLim::Dict
end

struct node
    #IDENTIFIER
    parent::UUID
    self::UUID

    #Dynamic SET
    bounds::Vector{bound}
    columns::Vector{Dict}

    #SUPPORT
    stab::stabilizer

    #STATUS
    status::Vector{String}
end


import GeoStatsBase: solve, solvesingle
import MLJModelInterface
import TableOperations

const MI = MLJModelInterface
const TO = TableOperations

########################
# DUMMY LEARNING MODEL
########################

struct DummyModel <: MI.Supervised end
MI.fit(m::DummyModel, v, X, y) = rand(unique(y),length(y)), 0, 0
MI.predict(m::DummyModel, θ, X) = rand(θ, Tables.rowcount(X))
MI.target_scitype(m::DummyModel) = AbstractVector{<:MI.Finite}

#################
# DUMMY SOLVERS
#################

@estimsolver DummyEstimSolver begin end
function solve(problem::EstimationProblem, solver::DummyEstimSolver)
  sdat = data(problem)
  sdom = domain(problem)
  npts = nelements(sdom)
  vars = name.(variables(problem))
  μs = [v => fill(mean(sdat[v]), npts) for v in vars]
  σs = [Symbol(v,:Var) => fill(var(sdat[v]), npts) for v in vars]
  georef((; μs..., σs...), sdom)
end

@estimsolver ESolver begin
  @param A=1.0
  @param B=2
  @jparam J="foo"
  @global C=true
end

@simsolver SSolver begin
  @param A=1.0
  @param B=2
  @jparam J="foo"
  @global C=true
end

function solvesingle(problem::SimulationProblem, covars, ::SSolver, preproc)
  Dict(var => rand(100) for var in covars.names)
end

struct DummyLearnSolver{M} <: LearningSolver
  model::M
end

function solve(problem::LearningProblem, solver::DummyLearnSolver)
  sdata = sourcedata(problem)
  tdata = targetdata(problem)
  ptask = task(problem)
  model = solver.model

  # learn task with source data
  stable = values(sdata)
  if issupervised(ptask)
    X = TO.select(stable, features(ptask)...)
    y = Tables.getcolumn(stable, label(ptask))
    θ, _, __ = MI.fit(model, 0, X, y)
  else
    X = TO.select(stable, features(ptask)...)
    θ, _, __ = MI.fit(model, 0, X)
  end

  # perform task with target data
  ttable = values(tdata)
  X = TO.select(ttable, features(ptask)...)
  ŷ = MI.predict(model, θ, X)

  # post-process result
  var = outputvars(ptask)[1]
  val = if issupervised(ptask)
    isprobabilistic(model) ? mode.(ŷ) : ŷ
  else
    ŷ
  end

  # georeference on target domain
  ctor = constructor(typeof(tdata))
  dom  = domain(tdata)
  dat  = Dict(paramdim(dom) => (; var=>val))

  ctor(dom, dat)
end


# VTK cell definitions

# Definitions copied from the vtkCellType.h file of the VTK source code:
# https://raw.githubusercontent.com/Kitware/VTK/master/Common/DataModel/vtkCellType.h

__precompile__()

module VTKCellTypes

import Compat.UTF8String

immutable VTKCellType
    vtk_name::UTF8String
    vtk_id::UInt8
    nodes::Int
end

# Linear cells
const VTK_EMPTY_CELL       = VTKCellType("VTK_EMPTY_CELL",      UInt8(0),  0)
const VTK_VERTEX           = VTKCellType("VTK_VERTEX",          UInt8(1),  1)
const VTK_POLY_VERTEX      = VTKCellType("VTK_POLY_VERTEX",     UInt8(2), -1)
const VTK_LINE             = VTKCellType("VTK_LINE",            UInt8(3),  2)
const VTK_POLY_LINE        = VTKCellType("VTK_POLY_LINE",       UInt8(4), -1)
const VTK_TRIANGLE         = VTKCellType("VTK_TRIANGLE",        UInt8(5),  3)
const VTK_TRIANGLE_STRIP   = VTKCellType("VTK_TRIANGLE_STRIP",  UInt8(6), -1)
const VTK_POLYGON          = VTKCellType("VTK_POLYGON",         UInt8(7), -1)
const VTK_PIXEL            = VTKCellType("VTK_PIXEL",           UInt8(8),  4)
const VTK_QUAD             = VTKCellType("VTK_QUAD",            UInt8(9),  4)
const VTK_TETRA            = VTKCellType("VTK_TETRA",           UInt8(10), 4)
const VTK_VOXEL            = VTKCellType("VTK_VOXEL",           UInt8(11), 8)
const VTK_HEXAHEDRON       = VTKCellType("VTK_HEXAHEDRON",      UInt8(12), 8)
const VTK_WEDGE            = VTKCellType("VTK_WEDGE",           UInt8(13), 6)
const VTK_PYRAMID          = VTKCellType("VTK_PYRAMID",         UInt8(14), 5)
const VTK_PENTAGONAL_PRISM = VTKCellType("VTK_PENTAGONAL_PRISM",UInt8(15),10)
const VTK_HEXAGONAL_PRISM  = VTKCellType("VTK_HEXAGONAL_PRISM", UInt8(16),12)

# Quadratic, isoparametric cells
const VTK_QUADRATIC_EDGE                   = VTKCellType("VTK_QUADRATIC_EDGE",                  UInt8(21), 3)
const VTK_QUADRATIC_TRIANGLE               = VTKCellType("VTK_QUADRATIC_TRIANGLE",              UInt8(22), 6)
const VTK_QUADRATIC_QUAD                   = VTKCellType("VTK_QUADRATIC_QUAD",                  UInt8(23), 8)
const VTK_QUADRATIC_POLYGON                = VTKCellType("VTK_QUADRATIC_POLYGON",               UInt8(36),-1)
const VTK_QUADRATIC_TETRA                  = VTKCellType("VTK_QUADRATIC_TETRA",                 UInt8(24),10)
const VTK_QUADRATIC_HEXAHEDRON             = VTKCellType("VTK_QUADRATIC_HEXAHEDRON",            UInt8(25),20)
const VTK_QUADRATIC_WEDGE                  = VTKCellType("VTK_QUADRATIC_WEDGE",                 UInt8(26),15)
const VTK_QUADRATIC_PYRAMID                = VTKCellType("VTK_QUADRATIC_PYRAMID",               UInt8(27),13)
const VTK_BIQUADRATIC_QUAD                 = VTKCellType("VTK_BIQUADRATIC_QUAD",                UInt8(28), 9)
const VTK_TRIQUADRATIC_HEXAHEDRON          = VTKCellType("VTK_TRIQUADRATIC_HEXAHEDRON",         UInt8(29),27)
const VTK_QUADRATIC_LINEAR_QUAD            = VTKCellType("VTK_QUADRATIC_LINEAR_QUAD",           UInt8(30), 6)
const VTK_QUADRATIC_LINEAR_WEDGE           = VTKCellType("VTK_QUADRATIC_LINEAR_WEDGE",          UInt8(31),12)
const VTK_BIQUADRATIC_QUADRATIC_WEDGE      = VTKCellType("VTK_BIQUADRATIC_QUADRATIC_WEDGE",     UInt8(32),18)
const VTK_BIQUADRATIC_QUADRATIC_HEXAHEDRON = VTKCellType("VTK_BIQUADRATIC_QUADRATIC_HEXAHEDRON",UInt8(33),24)
const VTK_BIQUADRATIC_TRIANGLE             = VTKCellType("VTK_BIQUADRATIC_TRIANGLE",            UInt8(34), 7)

# Cubic, isoparametric cell
const VTK_CUBIC_LINE                       = VTKCellType("VTK_CUBIC_LINE",UInt8(35),4)

# Special class of cells formed by convex group of points
const VTK_CONVEX_POINT_SET = VTKCellType("VTK_CONVEX_POINT_SET",UInt8(41),-1)

# Polyhedron cell (consisting of polygonal faces)
const VTK_POLYHEDRON = VTKCellType("VTK_POLYHEDRON",UInt8(42),-1)

# Higher order cells in parametric form
const VTK_PARAMETRIC_CURVE        = VTKCellType("VTK_PARAMETRIC_CURVE",       UInt8(51),-1)
const VTK_PARAMETRIC_SURFACE      = VTKCellType("VTK_PARAMETRIC_SURFACE",     UInt8(52),-1)
const VTK_PARAMETRIC_TRI_SURFACE  = VTKCellType("VTK_PARAMETRIC_TRI_SURFACE", UInt8(53),-1)
const VTK_PARAMETRIC_QUAD_SURFACE = VTKCellType("VTK_PARAMETRIC_QUAD_SURFACE",UInt8(54),-1)
const VTK_PARAMETRIC_TETRA_REGION = VTKCellType("VTK_PARAMETRIC_TETRA_REGION",UInt8(55),-1)
const VTK_PARAMETRIC_HEX_REGION   = VTKCellType("VTK_PARAMETRIC_HEX_REGION",  UInt8(56),-1)

# Higher order cells
const VTK_HIGHER_ORDER_EDGE        = VTKCellType("VTK_HIGHER_ORDER_EDGE",       UInt8(60),-1)
const VTK_HIGHER_ORDER_TRIANGLE    = VTKCellType("VTK_HIGHER_ORDER_TRIANGLE",   UInt8(61),-1)
const VTK_HIGHER_ORDER_QUAD        = VTKCellType("VTK_HIGHER_ORDER_QUAD",       UInt8(62),-1)
const VTK_HIGHER_ORDER_POLYGON     = VTKCellType("VTK_HIGHER_ORDER_POLYGON",    UInt8(63),-1)
const VTK_HIGHER_ORDER_TETRAHEDRON = VTKCellType("VTK_HIGHER_ORDER_TETRAHEDRON",UInt8(64),-1)
const VTK_HIGHER_ORDER_WEDGE       = VTKCellType("VTK_HIGHER_ORDER_WEDGE",      UInt8(65),-1)
const VTK_HIGHER_ORDER_PYRAMID     = VTKCellType("VTK_HIGHER_ORDER_PYRAMID",    UInt8(66),-1)
const VTK_HIGHER_ORDER_HEXAHEDRON  = VTKCellType("VTK_HIGHER_ORDER_HEXAHEDRON", UInt8(67),-1)

end # module


using JuliaFEM
using JuliaFEM.Preprocess
using JuliaFEM.Postprocess
using JuliaFEM.Abaqus: create_surface_elements

Logging.configure(level=Logging.DEBUG)

function create_body(mesh, name, E, nu, rho)
    body = Problem(mesh, Elasticity, name, 3)
    update!(body, "youngs modulus", E)
    update!(body, "poissons ratio", nu)
    update!(body, "density", rho)
    return body
end

""" Create boundary condition from surface set. """
function create_bc_from_surface_set(mesh, name, u1, u2, u3)
    bc = Problem(Dirichlet, string(name), 3, "displacement")
    bc.elements = create_surface_elements(mesh, name)
    update!(bc, "displacement 1", u1)
    update!(bc, "displacement 2", u2)
    update!(bc, "displacement 3", u3)
    return bc
end

""" Create boundary condition from node set. """
function create_bc_from_node_set(mesh, name, u1, u2, u3)
    bc = Problem(Dirichlet, string(name), 3, "displacement")
    bc.elements = [Element(Poi1, [nid]) for nid in mesh.node_sets[name]]
    update!(bc, "geometry", mesh.nodes)
    update!(bc, "displacement 1", u1)
    update!(bc, "displacement 2", u2)
    update!(bc, "displacement 3", u3)
    return bc
end

function create_bc(mesh, name, u1=0.0, u2=0.0, u3=0.0)
    name = Symbol(name)
    if haskey(mesh.surface_sets, name)
        return create_bc_from_surface_set(mesh, name, u1, u2, u3)
    elseif haskey(mesh.node_sets, name)
        return create_bc_from_node_set(mesh, name, u1, u2, u3)
    else
        error("Mesh does not contain node or surface set $name")
    end
end

function create_interface(mesh, slave_surface::String, master_surface::String)
    interface = Problem(Mortar, "interface between $slave_surface and $master_surface", 3, "displacement")
    interface.properties.dual_basis = true
    slave_elements = create_surface_elements(mesh, Symbol(slave_surface))
    master_elements = create_surface_elements(mesh, Symbol(master_surface))
    nslaves = length(slave_elements)
    nmasters = length(master_elements)
    info("$nslaves slaves, $nmasters masters")
    update!(slave_elements, "master elements", master_elements)
    interface.elements = [slave_elements; master_elements]
    return interface
end

function create_interface(mesh, slave::Problem, master::Problem)
    slave_surface = slave.name * "_TO_" * master.name
    master_surface = master.name * "_TO_" * slave.name
    return create_interface(mesh, slave_surface, master_surface)
end

""" Convert Mesh object from quadratic to linear. """
function to_linear!(mesh)
    mapping = Dict(:Tet10 => :Tet4, :Tri6 => :Tri3)
    nnodes = Dict(:Tet4 => 4, :Tri3 => 3)
    for elid in keys(mesh.elements)
        eltype = mesh.element_types[elid]
        if haskey(mapping, eltype)
            mesh.element_types[elid] = mapping[eltype]
            nnodes_new = nnodes[mapping[eltype]]
            mesh.elements[elid] = mesh.elements[elid][1:nnodes_new]
        end
    end
end

# start of simulation
#mesh = abaqus_read_mesh("beam_3d_1st_order_tetra_30mm.inp")
mesh = abaqus_read_mesh("beam_3d_2nd_order_tetra_30mm.inp")
#to_linear!(mesh)
info("element sets = ", collect(keys(mesh.element_sets)))
info("surface sets = ", collect(keys(mesh.surface_sets)))

# parts
beam = create_body(mesh, "beam", 210.0e3, 0.3, 7.85e-9)

# boundary conditions
bc1 = create_bc(mesh, "fixed")

# load
load = Problem(Elasticity, "pressure load", 3)
load.elements = create_surface_elements(mesh, :load)
nload = length(load.elements)
info("$nload elements in load surface")
area = 0.0
time = 0.0
for element in load.elements
    for ip in get_integration_points(element)
        detJ = element(ip, time, Val{:detJ})
        area += ip.weight*detJ
    end
end
info("load surface area: $area")

update!(load, "surface pressure", 20.0)

# solution
isfile("results.h5") && rm("results.h5")
isfile("results.xmf") && rm("results.xmf")
solver = Solver(Linear, beam, bc1, load)
solver.xdmf = Xdmf("results")
solver()

# calculate stresses in integration points and use least-squares fitting to
# extrapolate results to nodes

""" Return stress tensor. """
function get_stress_tensor(element, ip, time)
    haskey(element, "displacement") || return nothing
    gradu = element("displacement", ip, time, Val{:Grad})
    eps = 0.5*(gradu' + gradu)
    E = element("youngs modulus", ip, time)
    nu = element("poissons ratio", ip, time)
    mu = E/(2.0*(1.0+nu))
    la = E*nu/((1.0+nu)*(1.0-2.0*nu))
    S = la*trace(eps)*I + 2.0*mu*eps
    return S
end

""" Return stress vector in "ABAQUS" order 11, 22, 33, 12, 23, 13. """
function get_stress(element, ip, time)
    S = get_stress_tensor(element, ip, time)
    return [S[1,1], S[2,2], S[3,3], S[1,2], S[2,3], S[1,3]]
end

""" Return principal stresses. """
function get_stress_principal(element, ip, time)
    S = get_stress_tensor(element, ip, time)
    return sort(eigvals(S))
end

""" Make least squares fit for some field to nodes. """
function lsq_fit(elements, field)
    A = SparseMatrixCOO()
    b = SparseMatrixCOO()
    volume = 0.0
    for element in elements
        gdofs = get_connectivity(element)
        # increase integration order by 1 from default
        for ip in get_integration_points(element, 1)
	        detJ = element(ip, time, Val{:detJ})
	        w = ip.weight*detJ
	        N = element(ip, time)
	        f = field(element, ip, time)
	        add!(A, gdofs, gdofs, w*kron(N', N))
	        for i=1:length(f)
                add!(b, gdofs, w*f[i]*N, i)
            end
            volume += w
	    end
    end
    info("Mass matrix for least-squares fit is assembled. Total volume to fit: $volume")
    A = sparse(A)
    b = sparse(b)
    A = 1/2*(A + A')

    SparseArrays.droptol!(A, 1.0e-6)
    SparseArrays.dropzeros!(A)
    nz = get_nonzero_rows(A)
    F = ldltfact(A[nz,nz])

    x = zeros(size(b)...)
    x[nz, :] = F \ b[nz, :]

    nodal_values = Dict(i => vec(x[i,:]) for i in nz)
    return nodal_values
end

xdmf = get(solver.xdmf)
S = lsq_fit(beam.elements, get_stress)
#Sp = lsq_fit(beam.elements, get_stress_principal)

# Calculate principal stresses in nodes
Sp = Dict()
Sa = Dict()
Sm = Dict()
for (nid, s) in S
    # order is: 11, 22, 33, 12, 23, 13
    stress_tensor = [
        s[1] s[4] s[6]
        s[4] s[2] s[5]
        s[6] s[5] s[3]]
    principle = sort(eigvals(stress_tensor))
    Sp[nid] = principle
    Sa[nid] = (maximum(principle) - minimum(principle)) / 2
    Sm[nid] = (maximum(principle) + minimum(principle)) / 2
end

using JuliaFEM: new_dataitem, new_child, set_attribute, add_child, save!

""" Store values to xml/h5. """
function add_field_to_xdmf!(xdmf, field_name, path, data::Dict; field_type="Vector")
    node_ids = sort(collect(keys(data)))
    info("Storing field $field_name to path $path")
    println("$path in nodes:")
    for (i, nid) in enumerate(node_ids)
        println("$nid -> $(data[nid])")
        if i > 2
            println("...")
            break
        end
    end
    datavec = hcat([data[nid] for nid in node_ids]...)
    dataitem = new_dataitem(xdmf, path, datavec)
    frame = read(xdmf, "/Domain/Grid/Grid")
    attribute = new_child(frame, "Attribute")
    set_attribute(attribute, "Name", field_name)
    set_attribute(attribute, "Center", "Node")
    set_attribute(attribute, "AttributeType", field_type)
    add_child(attribute, dataitem)
end

add_field_to_xdmf!(xdmf, "Stress", "/Results/Time $(solver.time)/Nodal Fields/Stress", S; field_type="Tensor6")
add_field_to_xdmf!(xdmf, "Principal Stress", "/Results/Time $(solver.time)/Nodal Fields/Principal Stress", Sp; field_type="Vector")
add_field_to_xdmf!(xdmf, "Stress Amplitude", "/Results/Time $(solver.time)/Nodal Fields/Stress Amplitude", Sa; field_type="Scalar")
add_field_to_xdmf!(xdmf, "Mean Stress", "/Results/Time $(solver.time)/Nodal Fields/Mean Stress", Sm; field_type="Scalar")
save!(xdmf)

close(xdmf.hdf)


"Evaluate formula as fully as possible."
function partialeval(domain::Domain, state::GenericState, term::Term)
    funcs = merge(GLOBAL_FUNCTIONS, state.values, get_funcdefs(domain))
    return eval_term(term, Subst(), funcs)
end

"Get domain constant type declarations as a list of clauses."
function get_const_clauses(domain::Domain)
   return [@julog($ty(:o) <<= true) for (o, ty) in get_constypes(domain)]
end

"Get domain type hierarchy as a list of clauses."
function get_type_clauses(domain::Domain)
    clauses = [[Clause(@julog($ty(X)), Term[@julog($s(X))]) for s in subtys]
               for (ty, subtys) in get_typetree(domain) if length(subtys) > 0]
    return length(clauses) > 0 ? reduce(vcat, clauses) : Clause[]
end

"Get all proof-relevant Horn clauses for PDDL domain."
function get_clauses(domain::Domain)
   return [collect(values(get_axioms(domain)));
           get_const_clauses(domain); get_type_clauses(domain)]
end

"""
    find_matches(term, state, domain=nothing)

Returns a list of all matching substitutions of `term` with respect to
a given `state` and `domain`.
"""
function find_matches(domain::GenericDomain, state::GenericState, term::Term)
    if term.name in keys(state.values)
        clauses = Vector{Clause}(get_fluents(state))
        _, subst = resolve(term, clauses; mode=:all)
    else
        clauses = isnothing(domain) ? Clause[] : get_clauses(domain)
        clauses = Clause[clauses; collect(state.types); collect(state.facts)]
        funcs = state.values
        _, subst = resolve(term, clauses; funcs=funcs, mode=:all)
    end
    matches = Term[substitute(term, s) for s in subst]
    return matches
end


action_types_dict = Dict(
:RIGHT_ONLY => [
    (:NOOP,),
    (:right,),
    (:right, :A),
    (:right, :B),
    (:right, :A, :B)
],

:SIMPLE_MOVEMENT => [
    (:NOOP,),
    (:right,),
    (:right, :A),
    (:right, :B),
    (:right, :A, :B),
    (:A,),
    (:left,)
],

:COMPLEX_MOVEMENT => [
    (:NOOP,),
    (:right,),
    (:right, :A),
    (:right, :B),
    (:right, :A, :B),
    (:A,),
    (:left, :A),
    (:left, :B),
    (:left, :A, :B),
    (:down,),
    (:up,)
])
button_map = Dict{Symbol, UInt8}(
    :right  => 0b10000000,
    :left   => 0b01000000,
    :down   => 0b00100000,
    :up     => 0b00010000,
    :start  => 0b00001000,
    :select => 0b00000100,
    :B      => 0b00000010,
    :A      => 0b00000001,
    :NOOP   => 0b00000000,
)


mutable struct D4COption
    threshold::Float64

    # allow uninitialized creation
    D4COption() = new()
end

function d4c(x::Vector{Float64}, fs::Int, temporal_positions::Vector{Float64}, f0::Vector{Float64}, fft_size::Int)

    d4c_option = Ref(D4COption())
    @ccall libworld.InitializeD4COption(d4c_option::Ptr{D4COption})::Cvoid

    nbins = length(temporal_positions)
    nfreqs = (fft_size ÷ 2) + 1
    aperiodicity = Matrix{Float64}(undef, nfreqs, nbins) # transposed for C

    aperiodicity_pointer_vector = [Ref(aperiodicity, offset)
        for offset in 1:nfreqs:length(aperiodicity)]

    @ccall libworld.D4C(
        x::Ptr{Float64},
        length(x)::Cint,
        fs::Cint,
        temporal_positions::Ptr{Float64},
        f0::Ptr{Float64},
        length(f0)::Cint,
        fft_size::Cint,
        d4c_option::Ptr{D4COption},
        aperiodicity_pointer_vector::Ptr{Ptr{Cdouble}})::Cvoid

    aperiodicity
end


import Distributions.sample
import Base.append!
using Flux: Zygote.@adjoint

function test(f, env, maxt=100)
    R = 0f0
    s = reset!(env)
    for _ in 1:maxt
        s, r, done, _ = step!(env, f(s))
        R += r
        done && break
    end
    return R
end

gather(y, a) = y[CartesianIndex.(a, axes(a, 1))]
gather(y, a::AbstractMatrix) = gather(y, vec(a))

@adjoint gather(y, a) = gather(y, a),
    ȳ -> begin
        o = zeros(eltype(ȳ), size(y))
        @inbounds for (i, j) in enumerate(a)
            o[j, i] = ȳ[i]
        end
        return (o, nothing)
    end

function softupdate!(target::T, model::T, τ=1f-2) where T
    for f in fieldnames(T)
        softupdate!(getfield(target, f), getfield(model, f), τ)
    end
end

function softupdate!(dst::A, src::A, τ=T(1f-2)) where {T, A<:AbstractArray{T}}
    dst .= τ .* src .+ (one(T) - τ) .* dst
end

mutable struct ReplayMemory{S<:Real, A<:Real, R<:Real}
    n::Int64
    maxsize::Int64
    batchsize::Int64
    s::Matrix{S}
    a::Matrix{A}
    r::Vector{R}
    d::Vector{Bool}
    function ReplayMemory{S, A, R}(obssize, actiondim, maxsize, batchsize) where {S<:Real, A<:Real, R<:Real}
        new(
            0,
            maxsize,
            batchsize,
            zeros(S, obssize, maxsize),
            zeros(A, actiondim, maxsize),
            zeros(R, maxsize),
            zeros(Bool, maxsize)
        )
    end
end

function append!(memory::ReplayMemory, s, a, r, d)
    ind = memory.n % memory.maxsize + 1
    memory.n += 1
    memory.s[:, ind] .= s
    memory.a[:, ind] .= a
    memory.r[ind] = r
    memory.d[ind] = d
end

function sample(memory::ReplayMemory)
    inds = sample(1:(min(memory.n, memory.maxsize)-1), min(memory.n-1, memory.batchsize), replace=false)
    return (
        memory.s[:, inds],
        memory.a[:, inds],
        memory.r[inds],
        memory.s[:, inds .+ 1],
        memory.d[inds]
    )
end


module Solvent

using Adapt
using CUDA
using LinearAlgebra
using LazyArrays
using StaticArrays
using KernelAbstractions

array_device(::Union{Array, SArray, MArray}) = CPU()
array_device(::CuArray) = CUDADevice()
realview(x::Union{Array, SArray, MArray}) = x
realview(x::CuArray) = x

# Just for testing Solvent
LinearAlgebra.norm(A::MVector, p::Real, weighted::Bool) = norm(A, p)
LinearAlgebra.norm(A::MVector, weighted::Bool) = norm(A, 2, weighted)
LinearAlgebra.dot(A::MVector, B::MVector, weighted) = dot(A, B)
LinearAlgebra.norm(A::AbstractVector, p::Real, weighted::Bool) = norm(A, p)
LinearAlgebra.norm(A::AbstractVector, weighted::Bool) = norm(A, 2, weighted)
LinearAlgebra.dot(A::AbstractVector, B::AbstractVector, weighted) = dot(A, B)

export linearsolve!, settolerance!, prefactorize
export AbstractSystemSolver, AbstractIterativeSystemSolver

"""
    AbstractSystemSolver

This is an abstract type representing a generic linear solver.
"""
abstract type AbstractSystemSolver end

include(joinpath("Preconditioners", "Preconditioners.jl"))
include(joinpath("LinearSolvers", "LinearSolvers.jl"))

end  # End of module


bothat_ref(img, dims, r) = bothat_ref(img, strel_box(img, dims; r))
bothat_ref(img, se) = float.(closing(img, se)) - float.(img)

@testset "bothat" begin
    test_ranges = [Bool, 1:10, Float64, Gray{N0f8}, Gray{Float64}]
    for T in test_ranges
        for N in (1, 2, 3)
            sz = ntuple(_ -> 32, N)
            img = rand(T, sz...)

            out = bothat(img)
            @test out == bothat(img, strel_box(img, ntuple(identity, N); r=1))
            @test out == bothat_ref(img, ntuple(identity, N), 1)

            @test bothat(img; dims=(1,), r=2) == bothat_ref(img, (1,), 2)

            se = centered(rand(Bool, ntuple(_ -> 3, N)))
            @test bothat(img, se) == bothat_ref(img, se)
        end
    end

    img = rand(1:5, 7, 7)
    out = similar(img)
    bothat!(out, img, similar(img))
    @test out == bothat(img)

    img = rand(RGB, 7, 7)
    @test_throws ArgumentError("color image is not supported") bothat(img)
end


include("parameter.jl")

using Random
using StaticArrays

PID = length(ARGS) > 0 ? PID = parse(Int, ARGS[1]) : rand(100_000:999_999)
Random.seed!(PID) #initialize rand(), which is already global and by default using MersenneTwister
@assert PID >= 0 "RNG seed must be >=0"

@flush println("Seed/PID: $PID")

include("./parquetMC/oneBody/group.jl")
using .OneBody
include("./markov.jl")

state, groups, updates=OneBody.init(para, grids)
markov(state, groups, updates, 1000_000, Random.GLOBAL_RNG)

# include("markov.jl")
# Markov.init()

################# Test and Benchmark ############################
# Markov.test()

# import BenchmarkTools: @btime
# for _order = 1:Order
#     println("Benchmark Order $_order")
#     @btime Markov.eval(o) samples = 1 evals = 100 setup = (o = $_order)
# println(sum(Markov.ver4[_order].weight))
# end
# exit()

# println("Start Simulation ...")
# block = 0

# printTimer = StopWatch(PrintTime, Markov.printStatus)
# saveTimer = StopWatch(SaveTime, Markov.save)
# reweightTimer = StopWatch(ReWeightTime, Markov.reweight)
# messageTimer = StopWatch(MessageTime, Markov.save)

# for block = 1:TotalBlock
#     for i = 1:1000_000
#         Curr.step += 1
#         x = rand(Curr.rng)
#         if x < 1.0 / 6.0
#             Markov.increaseOrder()
#         elseif x < 2.0 / 6.0
#             Markov.decreaseOrder()
#         elseif x < 3.0 / 6.0
#             # Markov.changeK()
#         elseif x < 4.0 / 6.0
#             Markov.changeTau()
#         elseif x < 5.0 / 6.0
#             Markov.changeExtK()
#         else
#             Markov.changeExtTau()
#         end
#         # println(Curr.T[1], "-->", Curr.T[LastTidx])

#         i % 8 == 0 && Markov.measure()

#         if i % 1000 == 0
#             check(printTimer)
#             check(saveTimer)
#             check(reweightTimer)
#         end
#     end
# end

# Markov.printStatus()
# Markov.save()
# println("End Simulation. ")


#===========================================================m====================
    calibration.jl

    calibration procedure:
    1. multichannel deconvolution
    2. normalize variation between wells in absolute fluorescence

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

import DataStructures.OrderedDict
import StaticArrays: MArray, SArray, SMatrix, SVector
import Memento: debug, error

#===============================================================================
    constants >>
===============================================================================#

## default values
const DEFAULT_CAL_FIRST_WELL        = 1

## preset values
const NORMALIZATION_SCALING_FACTOR  = 3.7           ## used: 9e5, 1e5, 1.2e6, 3.0
const DECONVOLUTION_SCALING_FACTOR  = [1.0, 4.2]    ## used: [1, oneof(1, 2, 3.5, 8, 7, 5.6, 4.2)]
const DECONVOLUTION_BACKUP_K        = K4DCV         ## not used

## convenience array indices
const WATER  = 1
const SIGNAL = 2

#===============================================================================
    function definitions >>
===============================================================================#

"Perform deconvolution between channels and normalize variation between wells."
function calibrate(
    i                       ::Input,
    calibration_data        ::CalibrationData{<: NumberOfChannels, <: Union{Int_T,Float_T}},
    calibration_args        ::CalibrationParameters,
    raw                     ::RawData{<: Union{Int_T,Float_T}},         ## 3D array of raw fluorescence
    data_format             ::DataFormat,               ## array, dict, both
)
    debug(logger, "at calibrate()")

    ## remove MySql dependency
    # calib_info = ensure_ci(db_conn, calib_info)
    # norm_data, norm_well_nums = prep_normalize(db_conn,
    #     calib_info, well_nums_in_req, dye_in, dyes_to_fill)

    ## prepare data for normalization
    (norm_data, norm_wells) = prep_normalize(calibration_data)

    # if length(well_nums_found_in_req) == 0
    #     well_nums_found_in_req = norm_wells
    # end

    ## remove MySql dependency
    #
    # matched_well_idc = find(norm_well_nums) do norm_well_num
    #     norm_well_num in well_nums_found_in_raw
    # end ## do norm_well_num

    ## match (explicit) well numbers in the experimental data
    ## with (presumed complete) set of well numbers in calibration data
    matched_wells = indexin(i.wells, norm_wells)
    matched_exp_well_idc = find(matched_wells)
    matched_calib_well_idc = SVector{length(matched_exp_well_idc)}(
        matched_wells[matched_exp_well_idc])

    #
    ## subtract background
    background_subtracted_data =
        subtract_background(
            raw.data,
            norm_data[:,:,WATER],
            matched_wells,
            matched_exp_well_idc)

    #
    (k_deconv, deconvoluted_data) =
        if  calibration_args.dcv &&
            isa(calibration_data, CalibrationData{DualChannel,<: Union{Int_T,Float_T}})
            ## addition with flexible ratio instead of deconvolution (commented out)
            # k_inv_vec = fill(reshape(DataArray([1, 0, 1, 0]), 2, 2), 16)

            ## removing MySql dependency
            # k4dcv, deconvoluted_data = deconvolute(
            #     1. * background_subtracted_data,
            #     channel_nums, matched_well_idc,
            #     db_conn, calib_info, well_nums_in_req;
            #     out_format="array")
            deconvolute(
                calibration_data,
                calibration_args,
                background_subtracted_data |> cast(bless(Float_T)),
                matched_calib_well_idc,
                norm_wells[matched_calib_well_idc],
                DECONVOLUTION_BACKUP_K,
                DECONVOLUTION_SCALING_FACTOR,
                array)
        else ## !dcv
            DeconvolutionMatrices(calibration_data, calibration_args),
                background_subtracted_data
        end
    #
    calibrated_array =
        map(1:i.num_channels) do channel
            perform_normalize(
                deconvoluted_data[:, :, channel],
                norm_data,
                matched_exp_well_idc,
                matched_calib_well_idc,
                channel,
                calibration_args.subtract_water,
                NORMALIZATION_SCALING_FACTOR)
        end #= do channel =# |>
        splat(tie(3)) ## not gather(tie(3)) because we need 3 dimensions for single channel data
    #
    ## format output
    ## the following line of code needs the keys of calibrated_dict to be in sort order
    calibrated_dict()  =
        OrderedDict(
            map(1:i.num_channels) do channel
                channel => calibrated_array[:, :, channel]
            end)
    calibrated_data =
        if      (data_format == array)  tuple(calibrated_array)
        elseif  (data_format == dict)   tuple(calibrated_dict())
        elseif  (data_format == both)   tuple(calibrated_array, calibrated_dict())
        else                            throw(ArgumentException(
                                            "`data_format` must be array, dict or both"))
        end ## if
    return (
        background_subtracted_data,
        k_deconv,
        deconvoluted_data,
        norm_data,
        norm_wells,
        calibrated_data...)
end ## calibrate()

## called by calibrate()
function subtract_background(
    raw_data                ::Array{<: Union{Int_T,Float_T}, 3},
    water_data              ::SArray{S,<: Union{Int_T,Float_T},2} where {S},
    matched_wells           ::Array{Int_T},
    matched_exp_wells_idc   ::Array{Int_T},
)
    ## vectorized
    # raw_data[:,matched_exp_wells_idc,:] .-
    #     permutedims(cat(3,water_data[matched_wells[matched_exp_wells_idc],:]),[3,1,2])
    ## devectorized
    [   raw_data[ui, wi, ci] - water_data[matched_wells[wi], ci]
        for ui in 1:size(raw_data, 1),
            wi in matched_exp_wells_idc,
            ci in 1:size(raw_data, 3)   ]
end

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

"Check validity of optical calibration data."
function prep_normalize(calibration_data ::CalibrationData{DualChannel, <: Union{Int_T,Float_T}})
    debug(logger, "at prep_normalize()")
    norm_data = get_norm_data(calibration_data)
    ## index wells in calibration data starting at `DEFAULT_CAL_FIRST_WELL` = 0
    ## issue:
    ## using the current format for the request body there is no well_num information
    ## associated with the calibration data
    num_wells = size(calibration_data.array, 1)
    num_channels = 2
    signal_wells = num_wells |> from(DEFAULT_CAL_FIRST_WELL) |>
        mold(Symbol) |> SVector{num_wells}
    ## check whether signal fluorescence > water fluorescence
    failed = norm_data[:, :, SIGNAL] .<= norm_data[:, :, WATER]
    if any(failed)
        err_msgs = Vector{String}()
        for channel in 1:num_channels
            invalid = find(failed[:, channel])
            if length(invalid) > 0
                failed_wells = join(signal_wells[invalid], ", ")
                push!(err_msgs, "invalid calibration data in channel $channel: " *
                    "fluorescence value of water is greater than or equal to that of dye " *
                    "in the following well(s) - " * failed_wells)
            end ## if invalid
        end ## next channel
        throw(ArgumentError(join(err_msgs, "; ")))
    end ## if any(failed)

    ## issue:
    ## this feature has been temporarily disabled while
    ## removing MySql dependency in get_wva_data because
    ## using the current format for the request body
    ## we cannot subset the calibration data by step_id
    # if length(dyes_to_fill) > 0 ## extrapolate well-to-well variation data for missing channels
    #     println("Preset well-to-well variation data is used to extrapolate " *
    #         "calibration data for missing channels.")
    #     channels_missing = setdiff(channels_in_water, channels_in_signal)
    #     dyes_dyes_to_fill_channels =
    #         map(dye -> DYE2CHST[dye]["channel"], dyes_to_fill) ## DYE2CHST is defined in module scope
    #     check_subset(
    #         Ccsc(channels_missing, "Channels missing well-to-well variation data"),
    #         Ccsc(dyes_dyes_to_fill_channels, "channels corresponding to the dyes " *
    #             "of which well-to-well variation data is needed")
    #     )
    #     # process preset calibration data
    #     preset_step_ids = OrderedDict([
    #         dye => DYE2CHST[dye]["step_id"]
    #         for dye in keys(DYE2CHST)
    #     ])
    #     preset_signal_data_dict, dyes_in_preset =
    #         get_wva_data(PRESET_calib_ids["signal"], preset_step_ids, db_conn, "dye") ## `well_nums` is not passed on
    #     pivot_preset = preset_signal_data_dict[dye_in]
    #     pivot_in = signal_data_dict[DYE2CHST[dye_in]["channel"]]
    #     in2preset = pivot_in ./ pivot_preset
    #     for dye in dyes_to_fill
    #         signal_data_dict[DYE2CHST[dye]["channel"]] = preset_signal_data_dict[dye] .* in2preset
    #     end
    # end ## if

    return (norm_data, signal_wells)
end ## prep_normalize()

function prep_normalize(calibration_data ::CalibrationData{SingleChannel, <: Union{Int_T,Float_T}})
    debug(logger, "at prep_normalize()")
    norm_data = calibration_data.array
    num_wells = size(calibration_data.array, 1)
    signal_wells = num_wells |> from(DEFAULT_CAL_FIRST_WELL) |>
        mold(Symbol) |> SVector{num_wells}
    ## check whether signal fluorescence > water fluorescence
    failed = norm_data[:, 1, SIGNAL] .<= norm_data[:, 1, WATER]
    if any(failed)
        err_msg = "invalid calibration data in channel 1: " *
            "fluorescence value of water is greater than or equal to that of dye " *
            "in the following well(s) - " * join(signal_wells[find(failed)], ", ")
        throw(ArgumentError(err_msg))
    end ## if any(failed)
    return (norm_data, signal_wells)
end ## prep_normalize()

## called by prep_normalize() >>

get_norm_data(data ::CalibrationData{SingleChannel, <: Union{Int_T,Float_T}}) =
    data.array

function get_norm_data(data ::CalibrationData{DualChannel,R}) where {R <: Union{Int_T,Float_T}}
    local (water, dye1, dye2) = 1:3
    SArray{Tuple{size(data.array,1),2,2},R}( ## hcat converts from SArray to Array
        hcat(
            data.array[:, SVector{1}(1), SVector{2}(water, dye1)],    ## channel 1
            data.array[:, SVector{1}(2), SVector{2}(water, dye2)]))   ## channel 2
end

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

#=
    perform_normalize(): each dye only has data for its target channel;
    calibration/calib/oc - used for `deconvolute` and `normalize`,
    each dye has data for both target and non-target channels.
    Input `fluo` and output: dim2 indexed by well and dim1 indexed by unit,
    which can be cycle (amplification) or temperature point (melt curve).
    Output does not include the automatically created column at index 1
    from rownames of input array as R does
=#

## called from calibrate()
"Normalize variation between wells in absolute fluorescence values."
function perform_normalize(
    fluorescence                    ::Array{<: Union{Int_T,Float_T},2},
    norm_data                       ::SArray{S,<: Union{Int_T,Float_T},3} where {S},
    matched_exp_well_idc            ::Array{Int_T},
    matched_calib_well_idc          ::SVector{L,Int_T} where {L},
    channel                         ::Int_T,
    subtract_water                  ::Bool,
    normalization_scaling_factor    ::Float_T,
)
    debug(logger, "at perform_normalize()")
    #
    ## devectorized code avoids transposing data matrix
    if subtract_water == false
        smw = norm_data[matched_calib_well_idc, channel, SIGNAL]
        return ([
            normalization_scaling_factor * mean(smw) *
                fluorescence[u, matched_exp_well_idc[wi]] / smw[wi]
                    for u  in 1:size(fluorescence, 1),
                        wi in eachindex(matched_exp_well_idc)])
    end
    #
    ## subtract_water == true
    norm_water = norm_data[matched_calib_well_idc, channel, WATER]
    smw = norm_data[matched_calib_well_idc, channel, SIGNAL] .- norm_water
    return ([
        normalization_scaling_factor * mean(smw) *
            (fluorescence[u, matched_exp_well_idc[wi]] - norm_water[wi]) / smw[wi]
                for u  in 1:size(fluorescence, 1),
                    wi in eachindex(matched_exp_well_idc)])
end ## perform_normalize

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

## called from calibrate()
"Perform multi-channel deconvolution of fluorescence values."
function deconvolute(
    ## ary2dcv dim1 is unit, which can be cycle (amplification), temperature point (melting curve),
    ## or step type (like "water", "channel_1", "channel_2" for calibration experiment);
    ## ary2dcv dim2 must be well, ary2dcv dim3 must be channel
    calibration_data        ::CalibrationData{DualChannel, <: Union{Int_T,Float_T}},
    calibration_args        ::CalibrationParameters,
    ary2dcv                 ::Array{Float_T,3},
    matched_calib_well_idc  ::SVector{L,Int_T} where {L},
    wells                   ::SVector{L,Symbol} where {L},
    k_deconv_backup         ::DeconvolutionMatrices, ## argument not used
    scaling_factor_dcv_vec  ::Vector{Float_T},
    data_format             ::DataFormat, ## array, dict, both

    ## remove MySql dependency
    #
    ## arguments needed if `k` matrix needs to be computed
    ## `db_conn_default` is defined in "__init__.jl"
    # db_conn ::MySQL.MySQLHandle=db_conn_default,
    # calib_info ::Union{Integer,OrderedDict}=calib_info_AIR,
    # well_nums ::AbstractVector=[];
)
    debug(logger, "at deconvolute()")

    ## remove MySql dependency
    # k_deconv = (isa(calib_info, Integer) || begin
    #     step_ids = map(ci_value -> ci_value["step_id"], values(calib_info))
    #     length_step_ids = length(step_ids)
    #     length_step_ids <= 2 || length(unique(step_ids)) < length_step_ids
    # end) ? k_deconv_backup : get_k(db_conn, calib_info, well_nums) ## use default `well_proc` value

    k_deconv = get_k(
        calibration_data,
        calibration_args,
        matched_calib_well_idc,
        wells)
    (num_units, num_channels) = size(ary2dcv,1,3)
    scaled_k_inv_vecs =
        map(eachindex(matched_calib_well_idc)) do wi
            k_deconv.k_inv_vec[wi] .* scaling_factor_dcv_vec
        end
    deconvoluted_array = similar(ary2dcv)
    for ui in 1:num_units, wi in eachindex(matched_calib_well_idc)
        deconvoluted_array[ui, wi, :] =
            scaled_k_inv_vecs[wi] * ary2dcv[ui, wi, :] ## matrix * vector
    end
    deconvoluted_dict() =
        OrderedDict(
            channel => deconvoluted_array[:, :, channel]
            for channel in 1:num_channels)
    ## format output
    deconvoluted_data =
        if      (data_format == array)  tuple(deconvoluted_array)
        elseif  (data_format == dict)   tuple(deconvoluted_dict())
        elseif  (data_format == both)   tuple(deconvoluted_array, deconvoluted_dict())
        else
            throw(ArgumentError("`out_format` must be array, dict or both"))
        end ## if
    return (k_deconv, deconvoluted_data...)
end ## deconvolute()

deconvolute(
    calibration_data        ::CalibrationData{SingleChannel, <: Union{Int_T,Float_T}},
    calibration_args        ::CalibrationParameters,
    ary2dcv                 ::Array{Float_T,3},
    matched_calib_well_idc  ::SVector{L,Int_T} where {L},
    wells                   ::SVector{L,Symbol} where {L},
    k_deconv_backup         ::DeconvolutionMatrices, ## argument not used
    scaling_factor_dcv_vec  ::Vector{Float_T},
    data_format             ::DataFormat, ## array, dict, both
) =
    throw(ArgumentError("cannot perform multi-channel deconvolution " *
        "using single channel calibration data"))

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

"Calculate the matrix `k` that describes crosstalk between channels."
function get_k(
    calibration_data        ::CalibrationData{DualChannel, <: Union{Int_T,Float_T}},
    calibration_args        ::CalibrationParameters,
    matched_calib_well_idc  ::SVector{L,Int_T} where {L},
    wells                   ::SVector{L,Symbol} where {L};
    save_to                 ::String = "" ## used: "k.jld"

    ## remove MySql dependency
    #
    # db_conn ::MySQL.MySQLHandle,
    ## info on experiment(s) used to calculate matrix K
    ## OrderedDict(
    ##    "water"    =OrderedDict(calibration_id=..., step_id=...),
    ##    "channel_1"=OrderedDict(calibration_id=..., step_id=...),
    ##    "channel_2"=OrderedDict(calibration_id=..., step_id=...))
    # dcv_exp_info ::OrderedDict,
    #
    ## possible  issue:
    ## originally step_ids were provided together with calibration data
    ## I'm not sure that this is a problem now because the calibration data
    ## in the request body is already specific to a single step.
)
    debug(logger, "at get_k()")

    ## remove MySql dependency
    #
    # dcv_exp_info = ensure_ci(db_conn, dcv_exp_info)
    #
    # calib_key_vec = get_ordered_keys(dcv_exp_info)
    # cd_key_vec = calib_key_vec[2:end] # cd = channel of dye, "water" is index 1
    #
    # dcv_data_dict = get_full_calib_data(db_conn, dcv_exp_info, well_nums)
    #
    # water_data, water_well_nums = dcv_data_dict["water"]
    # num_wells = length(water_well_nums)
    #
    ## `dcv_well_nums` is not passed on because it is
    ## expected to be the same as `water_well_nums`,
    ## otherwise error will be raised by `get_full_calib_data`
    # smw_bydye = OrderedDict(map(cd_key_vec) do cd_key
    #    k_data_1dye, dcv_well_nums = dcv_data_dict[cd_key]
    #    return cd_key => k_data_1dye .- water_data
    # end)

    ## water-subtracted calibration data
    smw = SArray{Tuple{2,2,length(matched_calib_well_idc)},Float_T}([
        calibration_data.array[well, channel, dye] - calibration_data.array[well, channel, WATER]
        for channel in 1:2,
            dye     in 2:3,
            well    in matched_calib_well_idc])
    # const smw2 = SArray{Tuple{2,2,length(matched_calib_well_idc)},Float_T}(
    #
    ## check that the water-subtracted signal in the target channel
    ## is greater than that in the non-target channel(s) for each well and each dye
    failed =
        map(DYES) do dye
            target     = dye
            non_target = 3 - target
            smw[target, dye, :] .<= smw[non_target, dye, :]
        end #= next channel =# |>
        gather(hcat)
    if any(failed)
        err_msgs = Vector{String}()
        for dye in DYES
            invalid = find(failed[:, dye])
            if length(invalid) > 0
                target     = dye
                non_target = 3 - target
                wells_invalid = join(wells[invalid], ", ")
                push!(err_msgs,
                    "invalid deconvolution data for the dye targeting channel $target: " *
                    "fluorescence value of non-target channel $non_target " *
                    "is greater than or equal to that of target channel $target " *
                    "in the following well(s) - $wells_invalid")
            end ## if
        end ## next dye
        throw(ArgumentError(join(err_msgs, "; ")))
    end ## if any(failed)
    #
    ## compute inverses and return
    INV_NOTE_PT2 = ": K matrix is singular, using `pinv` instead of `inv` " *
    "to compute inverse matrix of K. Deconvolution result may not be accurate. " *
    "This may be caused by using the same or a similar set of solutions " *
    "in the steps for different dyes."
    (k_s, k_inv_vec, inv_note) =
        calc_kinv(Val{calibration_args.k_method}, smw, DYES, wells)
    k_deconv =
        DeconvolutionMatrices(
            k_s,
            k_inv_vec,
            (length(inv_note) > 0 ? inv_note * INV_NOTE_PT2 : ""))
    (length(save_to) > 0) && save(save_to, "k_deconv", k_deconv)
    return k_deconv
end ## get_k()

get_k(
    calibration_data        ::CalibrationData{SingleChannel, <: Union{Int_T,Float_T}},
    calibration_args        ::CalibrationParameters,
    matched_calib_well_idc  ::SVector{L,Int_T} where {L},
    wells                   ::SVector{L,Symbol} where {L};
    save_to                 ::String = "" ## used: "k.jld"
) =
    throw(ArgumentError("cannot calculate deconvolution matrices " *
        "using single channel calibration data"))

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

## functions called by get_k() >>

"Calculate a single deconvolution matrix K, averaging across all wells"
function calc_kinv(
    ::Type{Val{well_proc_mean}},
    smw             ::SArray{S,<: Float_T,3} where {S},
    dyes            ::SVector{L,Int_T},
    wells           ::SVector{L,Symbol}
) where {L}
    inv_note = false
    k_s =
        map(eachindex(dyes)) do dye
            SVector{L,Float_T}(sweep(sum)(/)(mean(smw[:,dye,:], 2)))
        end #= next dye =# |>
        gather(hcat)
    k_inv = try
        inv(k_s)
    catch err
        if isa(err, Base.LinAlg.SingularException)
            inv_note = true
            pinv(k_s)
        else
            rethrow(err)
        end ## if isa(err,
    end ## try
    return
        [k_s],
        fill(k_inv, length(wells)),
        inv_note ? "" : "Well mean"
end

"Calculate deconvolution matrix K for each well"
function calc_kinv(
    ::Type{Val{well_proc_vec}},
    smw             ::SArray{S,<: Float_T,3} where {S},
    dyes            ::SVector{L,Int_T},
    wells           ::SVector{M,Symbol} where {M}
) where {L}
    singular_wells = Vector{Symbol}()
    k_s =
        map(eachindex(wells)) do wi
            map(eachindex(dyes)) do dye
                sweep(sum)(/)(smw[:, dye, wi])
            end #= next dye =# |>
            gather(hcat)
        end ## next wi
    k_inv_vec =
        map(eachindex(wells)) do wi
            try
                ## `inv()` is supposedly faster with StaticArray
                inv(k_s[wi])
            catch err
                if  isa(err, Base.LinAlg.SingularException) ||
                    isa(err, Base.LinAlg.LAPACKException)
                        push!(singular_wells, wells[wi])
                        pinv(k_s[wi])
                else
                    rethrow(err)
                end ## if isa(err
            end ## try
        end ## next wi
    inv_note = (length(singular_wells) > 0) ?
        "Well(s) " * string(join(singular_wells, ", ")) : ""
    return k_s, k_inv_vec, inv_note
end

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

## unused function
# function calib_calib(
#     ## remove MySql dependency
#     #
#     # db_conn_1 ::MySQL.MySQLHandle,
#     # db_conn_2 ::MySQL.MySQLHandle,
#     # calib_info_1 ::OrderedDict,
#     # calib_info_2 ::OrderedDict,
#     # well_nums_1 ::AbstractVector=[],
#     # well_nums_2 ::AbstractVector=[];
#     dye_in      ::Symbol =:FAM,
#     dyes_to_fill ::AbstractVector =[]
# )
#     ## This function is expected to handle situations where `calib_info_1` and `calib_info_2`
#     ## have different combinations of wells, but the number of wells should be the same.
#     if length(well_nums_1) != length(well_nums_2)
#         throw(DimensionMismatch("length(well_nums_1) != length(well_nums_2)"))
#     end

#     ## remove MySql dependency
#     #
#     # calib_dict_1 = get_full_calib_data(db_conn_1, calib_info_1, well_nums_1)
#     # water_well_nums_1 = calib_dict_1["water"][2]
#     #
#     # calib_key_vec_1 = get_ordered_keys(calib_info_1)
#     # cd_key_vec_1 = calib_key_vec_1[2:end] ## cd = channel of dye. "water" is index 1 per original order.
#     # channel_nums_1 = map(cd_key_vec_1) do cd_key
#     #     parse(Int_T, split(cd_key, "_")[2])
#     # end
#
#     const ary2dcv_1 =
#         cat(1,
#             map(values(calib_dict_1)) do value_1
#                 reshape(transpose(fluo_data), 1, size(value_1[1],2,1)...)
#             end...) ## do value_1
#     const (background_subtracted_data_1, k4dcv_2, deconvoluted_data_1,
#         norm_data_2, norm_well_nums_2, calibrated_data_1) =
#         calibrate(
#             ary2dcv_1,
#             db_conn_2,
#             calib_info_2,
#             well_nums_2,
#             well_nums_2,
#             channel_nums_1,
#             true,
#             dye_in,
#             dyes_to_fill;
#             data_format = array)
#     return CalibCalibOutput(
#         ary2dcv_1,
#         background_subtracted_data_1,
#         k4dcv_2,
#         deconvoluted_data_1,
#         norm_data_2,
#         calibrated_data_1)
# end ## calib_calib

## deprecated to remove MySql dependency
#
## get all the data from a calibration experiment,
## including data from all the channels for all the steps
# function get_full_calib_data(
#    db_conn::MySQL.MySQLHandle,
#    calib_info::OrderedDict,
#    well_nums::AbstractVector=[]
#    )
#
#    calib_info = ensure_ci(db_conn, calib_info)
#
#    calib_key_vec = get_ordered_keys(calib_info)
#    cd_key_vec = calib_key_vec[2:end] ## cd = channel of dye. "water" is index 1 per original order.
#    channel_nums = map(cd_key_vec) do cd_key
#        parse(Int_T, split(cd_key, "_")[2])
#    end
#    num_channels = length(channel_nums)
#
#    calib_dict = OrderedDict(map(calib_key_vec) do calib_key
#        exp_id = calib_info[calib_key]["calibration_id"]
#        step_id = calib_info[calib_key]["step_id"]
#        k_qry_2b = "
#            SELECT fluorescence_value, well_num, channel
#                FROM fluorescence_data
#                WHERE
#                    experiment_id = $exp_id AND
#                    step_id = $step_id AND
#                    cycle_num = 1 AND
#                    step_id is not NULL
#                    well_constraint
#                ORDER BY well_num, channel
#        "
#        calib_data_1key, calib_well_nums = get_mysql_data_well(
#            well_nums, k_qry_2b, db_conn, false
#        )
#        if length(well_nums) > 0 && Set(calib_well_nums) != Set(well_nums)
#            error(logger, "experiment $exp_id, step $step_id: " *
#                " calibration data is not found for all the wells requested")
#        end ## if
#        calib_data_1key_chwl = vcat(map(channel_nums) do channel
#            transpose(calib_data_1key[:fluorescence_value][calib_data_1key[:channel] .== channel])
#        end...) ## do channel
#            return an array where rows indexed by channels and columns indexed by wells
#
#        return calib_key => (calib_data_1key_chwl, calib_well_nums)
#    end)
#
#    return calib_dict ## share the same keys as `calib_info`
#
# end ## get_full_calib_data