function read_asdf!(  S::GphysData,
                      hdf::String,
                      id::String,
                      s::TimeSpec,
                      t::TimeSpec,
                      msr::Bool,
                      v::Int64  )

  SX = SeisData() # for XML
  idr = isa(id, String) ? id_to_regex(id) : id
  (v > 2) && println("Reading IDs that match ", idr)

  if typeof(s) == String && typeof(t) == String
    d0 = s
    d1 = t
    ts = DateTime(s).instant.periods.value*1000 - dtconst
    te = DateTime(t).instant.periods.value*1000 - dtconst
  else
    (d0, d1) = parsetimewin(s, t)
    ts = DateTime(d0).instant.periods.value*1000 - dtconst
    te = DateTime(d1).instant.periods.value*1000 - dtconst
  end
  ts *= 1000
  te *= 1000
  Δ = 0
  fs = 0.0

  # this nesting is a mess
  netsta = netsta_to_regex(id)
  idr = id_to_regex(id)
  f = h5open(hdf, "r")
  W = f["Waveforms"]
  A = names(W)
  sort!(A)
  (v > 2) && println("Net.sta found: ", A)

  for i in A
    if occursin(netsta, i)
      w = W[i]
      N = names(w)
      sort!(N)
      (v > 2) && println("Traces found: ", N)
      for n in N
        if n == "StationXML"
          sxml = String(UInt8.(read(w[n])))
          read_station_xml!(SX, sxml, msr=msr, s=s, t=t, v=v)
        elseif occursin(idr, n)
          x = w[n]
          nx = length(x)
          t0 = read(x["starttime"])
          fs = read(x["sampling_rate"])

          # convert fs to sampling interval in ns
          Δ = round(Int64, 1.0e9/fs)
          t1 = t0 + (nx-1)*Δ
          (v > 2) && println("t0 = ", t0,"; t1 = ", t1, "; ts = ", ts, "; te = ", te)

          if (ts ≤ t1) && (te ≥ t0)
            xi = 0
            i0, i1, t2 = get_trace_bounds(ts, te, t0, t1, Δ, nx)
            ni = i1-i0+1
            cid = String(split(n, "_", limit=2, keepempty=true)[1])
            j = findid(cid, S.id)
            (v > 2) && println("cid = ", cid, " (found at index in S = ", j, ")")
            nX = div(te-ts, Δ)+1
            if j == 0
              T = eltype(x)
              push!(S, SeisChannel(id = cid,
                                   fs = fs,
                                   x = Array{T,1}(undef, nX)))
              j = S.n
              if has(x, "event_id")
                S.misc[j]["event_id"] = read(x["event_id"])
              end
            else
              ct = getindex(getfield(S, :t), j)
              nt = div(lastindex(ct), 2)
              L = lastindex(S.x[j])
              if nt > 0
                xi = getindex(ct, nt)
                check_for_gap!(S, j, div(t2, 1000), ni, v)
              end
              if xi + ni > L
                resize!(S.x[j], xi + max(ni, nX))
              end
              if S.fs[j] == 0.0
                S.fs[j] = fs
              end
            end

            if xi == 0
              ct = Array{Int64, 2}(undef, 2, 2)
              setindex!(ct, one(Int64), 1)
              setindex!(ct, ni, 2)
              setindex!(ct, div(t2, 1000), 3)
              setindex!(ct, zero(Int64), 4)
              setindex!(getfield(S, :t), ct, j)
            end

            si = xi+1
            ei = si+ni-1
            X = S.x[j]
            load_data!(X, x, i0:i1, si:ei)
          end
        end
      end
    end
  end

  # Ensure data source is logged accurately
  fill!(S.src, realpath(hdf))

  # merge in the XML that we read
  sxml_mergehdr!(S, SX, app=false, nofs=true, v=v)

  trunc_x!(S)

  # Done
  close(f)
  return S
end

function read_asdf(hdf::String, id::Union{String,Regex}, s::TimeSpec, t::TimeSpec, msr::Bool, v::Int64)
  S = SeisData()
  read_asdf!(S, hdf, id, s, t, msr, v)
  return S
end


module SDPT3

using SparseArrays
using MATLAB

export sdpt3

#function dim(c::Char, n::Float64)
#    if c == 's'
#        return div(n * (n + 1), 2)
#    else
#        return n
#    end
#end

# See Solver/sqlparameters.m
const ALLOWED_OPTIONS = [
    "vers",
    "gam",
    "predcorr",
    "expon",
    "gaptol",
    "inftol",
    "steptol",
    "maxit",
    "printlevel",
    "stoplevel",
    "scale_data",
    "rmdepconstr",
    "smallblkdim",
    "parbarrier",
    "schurfun",
    "schurfun_par"
]

# `SparseMatrixCSC` is returned in SumOfSquares.jl test `sos_horn`
_array(x::AbstractMatrix) = x
_array(x::Vector) = x
_array(x::Float64) = [x]

# TODO log in objective, OPTION, initial iterates X0, y0, Z0
# Solve the primal/dual pair
# min c'x,      max b'y
# s.t. Ax = b,   c - A'x ∈ K
#       x ∈ K
function sdpt3(blk::Matrix,
               At::Vector{<:Union{Matrix{Float64}, SparseMatrixCSC{Float64}}},
               C::Vector{<:Union{Matrix{Float64}, Vector{Float64}}},
               b::Vector{Float64}; kws...)
               #C::Vector{<:Union{Vector{Float64}, SparseVector{Float64}}}, b::Vector{Float64})
    options = Dict{String, Any}(string(key) => value for (key, value) in kws)
    @assert all(i -> size(At[i], 2) == length(b), 1:length(At))
    @assert length(At) == size(blk, 1)
    #@assert all(i -> size(A[i], 1) == dim(blk[i, 1], blk[i, 2]), 1:length(A))
    #@assert all(i -> length(C[i], 1) == dim(blk[i, 1], blk[i, 2]), 1:length(A))
    # There are 6 output arguments so we use `6` below
    obj, X, y, Z, info, runhist = mxcall(:sdpt3, 6, blk, At, C, b, options)
    return obj, _array.(X), _array(y), _array.(Z), info, runhist
end

include("MOI_wrapper.jl")

end # module


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/UMAT.jl/blob/master/LICENSE

module UMAT

using Libdl
using Materials
using Parameters
using LinearAlgebra
using Tensors

const abaqus = [1 4 6; 0 2 5; 0 0 3]

pkg_dir = dirname(Base.find_package("UMAT"))
if Sys.iswindows()
    lib_dir = joinpath(pkg_dir,"..","deps","usr","bin")
else
    lib_dir = joinpath(pkg_dir,"..","deps","usr","lib")
end

"""
Variables updated by UMAT routine.
"""
@with_kw struct UmatVariableState <: AbstractMaterialState
    NTENS :: Int
    NSTATV :: Int = zero(Int)
    jacobian :: SymmetricTensor = zero(SymmetricTensor{4,convert(Int,NTENS/2),Float64})
    stress :: SymmetricTensor = zero(SymmetricTensor{2,convert(Int,NTENS/2),Float64})
    STATEV :: Array{Float64,1} = zeros(Float64, NSTATV)
    SSE :: Array{Float64,1} = zeros(Float64, 1)
    SPD :: Array{Float64,1} = zeros(Float64, 1)
    SCD :: Array{Float64,1} = zeros(Float64, 1)
    RPL :: Array{Float64,1} = zeros(Float64, 1)
    DDSDDT :: Array{Float64,1} = zeros(Float64, NTENS)
    DRPLDE :: Array{Float64,1} = zeros(Float64, NTENS)
    DRPLDT :: Array{Float64,1} = zeros(Float64, 1)
    PNEWDT :: Array{Float64,1} = ones(Float64, 1)
end

"""
Material parameters in order that is specific to chosen UMAT.
"""
@with_kw struct UmatParameterState <: AbstractMaterialState
    NPROPS :: Int = zero(Int)
    PROPS :: Array{Float64,1} = zeros(Float64, NPROPS)
end

"""
Variables passed in for information.
These drive evolution of the material state.
"""
@with_kw mutable struct UmatDriverState <: AbstractMaterialState
    NTENS :: Int
    strain :: SymmetricTensor = zero(SymmetricTensor{2,convert(Int,NTENS/2),Float64})
    time :: Float64 = zero(Float64)
    dtime :: Float64 = zero(Float64)
    TIME_ :: Array{Float64,1} = zeros(Float64, 2)
    TEMP :: Float64 = zero(Float64)
    PREDEF :: Float64 = zero(Float64)
end

"""
Other Abaqus UMAT variables passed in for information.
"""
@with_kw mutable struct UmatOtherState <: AbstractMaterialState
    CMNAME :: String = "U" # Ptr{Cuchar} # = 'U'
    NDI :: Int = 3
    NSHR :: Int = 3
    NTENS :: Int  # NDI + NSHR
    NSTATV :: Int
    NPROPS :: Int
    COORDS :: Array{Float64,1} = zeros(Float64, 3)
    DROT :: Array{Float64,2} = I + zeros(Float64, 3, 3)
    CELENT :: Float64 = zero(Float64)
    DFGRD0 :: Array{Float64,2} = I + zeros(Float64, 3, 3)
    DFGRD1 :: Array{Float64,2} = I + zeros(Float64, 3, 3)
    NOEL :: Int = zero(Int)
    NPT :: Int = zero(Int)
    LAYER :: Int = zero(Int)
    KSPT :: Int = zero(Int)
    JSTEP :: Array{Float64,1} = zeros(Int, 3)
    KSTEP :: Int = zero(Int)
    KINC :: Int = zero(Int)
end

"""
UMAT material structure.

`lib_path` is the path to the compiled shared library.
`behaviour` is the function name to call from the shared library
    generally this is compiler dependent, by default `umat_`.
    MFront Abaqus interface produces specific name, e.g. `ELASTICITY_3D`.
"""
@with_kw mutable struct UmatMaterial <: AbstractMaterial
    NTENS :: Int
    NSTATV :: Int = zero(Int)
    NPROPS :: Int = zero(Int)

    drivers :: UmatDriverState = UmatDriverState(NTENS=NTENS)
    ddrivers :: UmatDriverState = UmatDriverState(NTENS=NTENS)
    variables :: UmatVariableState = UmatVariableState(NTENS=NTENS, NSTATV=NSTATV)
    variables_new :: UmatVariableState = UmatVariableState(NTENS=NTENS, NSTATV=NSTATV)
    parameters :: UmatParameterState = UmatParameterState(NPROPS=NPROPS)
    dparameters :: UmatParameterState = UmatParameterState(NPROPS=NPROPS)

    umat_other :: UmatOtherState = UmatOtherState(NTENS=NTENS, NSTATV=NSTATV, NPROPS=NPROPS)

    lib_path :: String
    behaviour :: Symbol = :umat_

end

"""
Wrapper function to ccall the UMAT subroutine from the compiled shared library.
"""
function call_umat!(func_umat::Symbol, lib_path::String, STRESS_,STATEV,DDSDDE,
        SSE,SPD,SCD,RPL,DDSDDT,DRPLDE,DRPLDT,
        STRAN,DSTRAN,TIME,DTIME,TEMP,DTEMP,PREDEF,DPRED,CMNAME,NDI,NSHR,
        NTENS,NSTATV,PROPS,NPROPS,COORDS,DROT,PNEWDT,CELENT,DFGRD0,DFGRD1,
        NOEL,NPT,LAYER,KSPT,KSTEP,KINC)

    lib_umat = Libdl.dlopen(lib_path) # Open the library explicitly.
    sym_umat = Libdl.dlsym(lib_umat, func_umat) # Get a symbol for the umat function to call.

    ccall(sym_umat, Nothing,
         (Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},
          Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},
          Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},
          Ref{Float64},Ref{Float64},Ref{Float64},Ptr{Cuchar},Ref{Int},Ref{Int},
          Ref{Int},Ref{Int},Ref{Float64},Ref{Int},Ref{Float64},Ref{Float64},
          Ref{Float64},Ref{Float64},Ref{Float64},Ref{Float64},
          Ref{Int},Ref{Int},Ref{Int},Ref{Int},Ref{Int},Ref{Int}),
         STRESS_,STATEV,DDSDDE,SSE,SPD,SCD,RPL,DDSDDT,DRPLDE,DRPLDT,
         STRAN,DSTRAN,TIME,DTIME,TEMP,DTEMP,PREDEF,DPRED,CMNAME,NDI,NSHR,
         NTENS,NSTATV,PROPS,NPROPS,COORDS,DROT,PNEWDT,CELENT,DFGRD0,DFGRD1,
         NOEL,NPT,LAYER,KSPT,KSTEP,KINC)

    Libdl.dlclose(lib_umat) # Close the library explicitly.

    return Nothing
end

"""
Calls UMAT and updates MaterialVariableState writing the result to material.variables_new
"""
function Materials.integrate_material!(material::UmatMaterial)
    @unpack CMNAME,NDI,NSHR,NTENS,NSTATV,NPROPS,COORDS,DROT,CELENT,DFGRD0,DFGRD1,NOEL,NPT,LAYER,KSPT,KSTEP,KINC = material.umat_other
    @unpack PROPS = material.parameters
    strain, time, TEMP, PREDEF = material.drivers.strain, material.drivers.time, material.drivers.TEMP, material.drivers.PREDEF
    dstrain, dtime, DTEMP, DPRED = material.ddrivers.strain, material.drivers.dtime, material.ddrivers.TEMP, material.ddrivers.PREDEF
    @unpack stress,STATEV,jacobian,SSE,SPD,SCD,RPL,DDSDDT,DRPLDE,DRPLDT,PNEWDT = material.variables

    # TODO need to replace with toabaqus etc.
    STRAN = tovoigt(strain; order=abaqus, offdiagscale = 2.0)
    DSTRAN = tovoigt(dstrain; order=abaqus, offdiagscale = 2.0)
    STRESS_ = tovoigt(stress, order=abaqus)
    DDSDDE = tovoigt(jacobian, order=abaqus)
    TIME_ = [(time - floor(time)) time]
    DTIME_ = dtime
    call_umat!(material.behaviour, material.lib_path, STRESS_, STATEV, DDSDDE, SSE, SPD, SCD, RPL, DDSDDT, DRPLDE, DRPLDT,
        STRAN, DSTRAN, TIME_, DTIME_, TEMP, DTEMP, PREDEF, DPRED, CMNAME, NDI, NSHR,
        NTENS, NSTATV, PROPS, NPROPS, COORDS, DROT, PNEWDT, CELENT, DFGRD0, DFGRD1,
        NOEL, NPT, LAYER, KSPT, KSTEP, KINC)

    stress = fromvoigt(SymmetricTensor{2,convert(Int,NTENS/2)}, STRESS_, order=abaqus)
    jacobian = fromvoigt(SymmetricTensor{4,convert(Int,NTENS/2)}, DDSDDE, order=abaqus)

    variables_new = UmatVariableState(NSTATV=NSTATV,NTENS=NTENS,stress=stress,
                                      STATEV=STATEV,jacobian=jacobian,SSE=SSE,
                                      SPD=SPD,SCD=SCD,RPL=RPL,DDSDDT=DDSDDT,
                                      DRPLDE=DRPLDE,DRPLDT=DRPLDT,PNEWDT=PNEWDT)
    material.variables_new = variables_new
end

function Materials.reset_material!(material::UmatMaterial)
    material.ddrivers = UmatDriverState(NTENS=material.NTENS)
    material.dparameters = UmatParameterState(NPROPS=material.NPROPS)
    material.variables_new = UmatVariableState(NTENS=material.NTENS, NSTATV=material.NSTATV)
    return nothing
end

include("gurson_model.jl")
include("druckerprager_model.jl")

export UmatMaterial, UmatDriverState, UmatParameterState, UmatVariableState
export UmatOtherState, GursonMaterial, DruckerPragerMaterial, integrate_material!

# Re-export update_material! and uniaxial_increment! from Materials.jl for convenience
export update_material!, uniaxial_increment!
end # module


using ObjectDetector, FileIO
using Profile

Profile.init(n=10000000, delay=0.01)
yolomod = YOLO.v3_416_COCO()

batch = emptybatch(yolomod)
img = load(joinpath(dirname(dirname(pathof(ObjectDetector))),"test","images","dog-cycle-car.png"))

batch[:,:,:,1] .= gpu(resizePadImage(img, yolomod)) # Send resized image to the batch
res = yolomod(batch) # Run the model on the length-1 batch

imgBoxes = drawBoxes(img, res)

save(joinpath(@__DIR__,"test.png"), imgBoxes)

ObjectDetector.benchmark()

save(joinpath(@__DIR__,"test.png"), resizePadImage(img, yolomod))


module Soundcard
using SharedArrays
using Libaudio

# int record(float * pcm_record, int64_t record_channels, int64_t record_frames, int64_t samplerate);
# int play(const float * pcm_play, int64_t play_channels, int64_t play_frames, int64_t samplerate);
# int playrecord(const float * pcm_play, int64_t play_channels, float * pcm_record, int64_t record_channels, int64_t common_frames, int64_t samplerate);

modulepath(name) = realpath(joinpath(dirname(pathof(name)),".."))
folder() = "C:/DRIVERS/Julia/"

"""
    init()

install binary dependencies to "C:\\Drivers\\Julia\\"
"""
function __init__()
    root = folder()
    isdir(root) || mkpath(root)
    mp = modulepath(Soundcard)

    t1 = joinpath(root, "portaudio_x64.dll")
    t2 = joinpath(root, "soundcard_api.dll")

    isfile(t1) || cp(joinpath(mp, "deps/usr/lib/portaudio_x64.dll"), t1, force=true)
    isfile(t2) || cp(joinpath(mp, "deps/usr/lib/soundcard_api.dll"), t2, force=true)
end

"""
    device()

populate soundcard devices attached to the workstation
"""
function device()
    buffer = zeros(Int8, 8192)
    numdev = ccall((:list_devices, "C:\\Drivers\\Julia\\soundcard_api"), Int32, (Ptr{Int8},), buffer)
    digest = ""
    for i in buffer
        digest = digest * string(Char(i))
    end
    report = split(digest,'\n')
    numdev, report[1:end-1]
end

"""
    record(dim, fs)

low level api for recording
"""
function record(dim::Tuple{Int64, Int64}, fs::Int64)    # -> Matrix{Float32}
    pcm = zeros(Float32, dim[2] * dim[1])
    ccall((:record, "C:\\Drivers\\Julia\\soundcard_api"), Int32, (Ptr{Float32}, Int64, Int64, Int64), pcm, dim[2], dim[1], fs)
    return transpose(reshape(pcm, dim[2], dim[1]))
end

"""
    play(dat, fs)

low level api for playback
"""
function play(dat::Matrix{Float32}, fs::Int64)
    pcm = interleave(dat)
    ccall((:play, "C:\\Drivers\\Julia\\soundcard_api"), Int32, (Ptr{Float32}, Int64, Int64, Int64), pcm, size(dat)[2], size(dat)[1], fs)
    return nothing
end

"""
    playrecord(dat, ch, fs)

low level api for simultaneous playback and recording
usually we have ch = size(mixmic,1)
"""
function playrecord(dat::Matrix{Float32}, ch::Int64, fs::Int64)    # -> Matrix{Float32}
    pcmo = interleave(dat)
    pcmi = zeros(Float32, size(dat)[1] * ch)
    ccall((:playrecord, "C:\\Drivers\\Julia\\soundcard_api"), Int32, (Ptr{Float32}, Int64, Ptr{Float32}, Int64, Int64, Int64), pcmo, size(dat)[2], pcmi, ch, size(dat)[1], fs)
    return transpose(reshape(pcmi, ch, size(dat)[1]))
end

function interleave(x::Matrix{T}) where T <: Number
    fr,ch = size(x)
    y = zeros(T, ch * fr)
    k::Int64 = 0
    for i = 1:fr
        y[k+1:k+ch] = x[i,:]
        k += ch
    end
    return y
end

"""
    mixer(x, mix)

apply mixing matrix 'mix' to input signal matrix 'x' for logical signal routing,
mixing matrix will be converted to the same type as the input matrix, note that
this function is the last gate to the physical world of soundcard so use clamp
"""
function mixer(x::AbstractMatrix{T}, mix::AbstractMatrix) where T <: AbstractFloat
    mm = convert(Matrix{T}, mix)
    y = x * mm
    maximum(abs.(y)) >= one(T) && Libaudio.printl(joinpath(Libaudio.folder(), Libaudio.logfile()), :light_red, Libaudio.nows() * " | Soundcard.mixer: sample amplitude clipping")
    return clamp.(y, -one(T), one(T))
end

                                    # --------------------------
                                    #    API for ease of use
                                    # --------------------------

function playrecord(playing::Matrix, ms::Matrix, mm::Matrix, fs)::Matrix{Float32}
    out = convert(Matrix{Float32}, playing)
    recording = mixer(playrecord(mixer(out,ms), size(mm,1), convert(Int64,fs)), mm)
end

function play(playing::Matrix, ms::Matrix, fs)
    out = convert(Matrix{Float32}, playing)
    play(mixer(out, ms), Int64(fs))
end

function record(samples::Integer, mm::Matrix, fs)::Matrix{Float32}
    recording = mixer(record((convert(Int64,samples),size(mm,1)), Int64(fs)), mm)
end

                                    # --------------------------
                                    #  API for ultralow latency
                                    # --------------------------
# x = Soundcard.mixer(convert(Matrix{Float32},out), ms)
# y = SharedArray{Float32,1}(Soundcard.interleave(x))
play(size_x::Tuple{Int64,Int64}, y::SharedArray{Float32,1}, fs) =
    ccall((:play,"C:\\Drivers\\Julia\\soundcard_api"), Int32, (Ptr{Float32}, Int64, Int64, Int64), y, size_x[2], size_x[1], fs)  # remotecall
# async tasks...
# fetch(done)

# y = SharedArray{Float32,1}(zeros(Float32, size(mm,1)*samples))
record(y::SharedArray{Float32,1}, size_mm::Tuple{Int64,Int64}, samples, fs) =
    ccall((:record,"C:\\Drivers\\Julia\\soundcard_api"), Int32, (Ptr{Float32}, Int64, Int64, Int64), y, size_mm[1], samples, fs)  # remotecall
# async tasks...
# fetch(done)
# recording = Soundcard.mixer(transpose(reshape(y,size(mm,1),samples)), mm)

# samples = size(out,1)
# x = Soundcard.mixer(convert(Matrix{Float32},out), ms)
# y = SharedArray{Float32,1}(Soundcard.interleave(x))
# z = SharedArray{Float32,1}(zeros(Float32,size(mm,1)*samples))
playrecord(size_x::Tuple{Int64,Int64}, y::SharedArray{Float32,1}, z::SharedArray{Float32,1}, size_mm::Tuple{Int64,Int64}, fs) =
    ccall((:playrecord, "C:\\Drivers\\Julia\\soundcard_api"), Int32, (Ptr{Float32}, Int64, Ptr{Float32}, Int64, Int64, Int64), y, size_x[2], z, size_mm[1], size_x[1], fs)
# async tasks...
# fetch(done)
# recording = Soundcard.mixer(transpose(reshape(z,size(mm,1),samples)), mm)

end # module


"""
    couple_models(aero::LiftingLine, stru::GEBT, dyn::RigidBody, flap::LiftingLineFlaps)

Create a coupled model using a lifting line aerodynamic model, a
geometrically exact beam theory model, a rigid body dynamics model, and a lifting
line control surface model.  This model introduces additional parameters
corresponding to the freestream air density ``\\rho_\\infty``, followed by the
external loads ``F_{x,i}, F_{y,i}, F_{z,i}, M_{x,i}, M_{y,i}, M_{z,i}`` or
displacements ``u_{x,i}, u_{y,i}, u_{z,i}, \\theta_{x,i}, \\theta_{y,i},
\\theta_{z,i}`` for each node, followed by the constant distributed loads
``f_{x,i}, f_{y,i}, f_{z,i}, m_{x,i}, m_{y,i}, m_{z,i}`` applied on each beam
element (excluding aerodynamic loads), followed by the control surface
deflections ``\\delta_1, \\delta_2, \\dots, \\delta_N``.

**NOTE: When using this model, the local frame for each beam element should be
oriented with the x-axis along the beam's axis, the y-axis forward, and the
z-axis normal to the surface**
"""
function couple_models(aero::LiftingLine, stru::GEBT, dyn::RigidBody,
    flap::LiftingLineFlaps)

    return (aero, stru, dyn, flap)
end

# --- traits --- #

function number_of_additional_parameters(aero::LiftingLine, stru::GEBT, dyn::RigidBody,
    flap::LiftingLineFlaps)

    return 1 + 6*length(stru.icol_point) + 6*length(stru.icol_elem) + length(flap.gains)
end

function coupling_inplaceness(::Type{<:LiftingLine}, ::Type{<:GEBT}, ::Type{<:RigidBody},
    ::Type{<:LiftingLineFlaps})

    return InPlace()
end

function coupling_rate_jacobian_type(::Type{LiftingLine{NA,TA}}, ::Type{<:GEBT},
    ::Type{<:RigidBody}, ::Type{LiftingLineFlaps{NF,NG,TF,TG}}) where {NA,NF,NG,TA,TF,TG}

    aero_model_types = TA.parameters
    flap_model_types = TF.parameters
    if all(islinear.(coupling_rate_jacobian_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Linear()
    else
        return Nonlinear()
    end
end

function coupling_state_jacobian_type(::Type{LiftingLine{NA,TA}}, ::Type{<:GEBT},
    ::Type{<:RigidBody}, ::Type{LiftingLineFlaps{NF,NG,TF,TG}}) where {NA,NF,NG,TA,TF,TG}

    aero_model_types = TA.parameters
    flap_model_types = TF.parameters
    if all(islinear.(coupling_state_jacobian_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Linear()
    else
        return Nonlinear()
    end
end

coupling_parameter_jacobian_type(::Type{<:LiftingLine}, ::Type{<:GEBT},
    ::Type{RigidBody}, ::Type{LiftingLineFlaps}) = Nonlinear()

function coupling_time_gradient_type(::Type{LiftingLine{NA,TA}}, ::Type{<:GEBT},
    ::Type{<:RigidBody}, ::Type{LiftingLineFlaps{NF,NG,TF,TG}}) where {NA,NF,NG,TA,TF,TG}

    aero_model_types = TA.parameters
    flap_model_types = TF.parameters
    if all(isempty.(coupling_time_gradient_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Empty()
    elseif all(iszero.(coupling_time_gradient_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Zeros()
    elseif all(isconstant.(coupling_time_gradient_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Constant()
    elseif all(islinear.(coupling_time_gradient_type.(aero_model_types, Ref(LiftingLineSection),
        flap_model_types, Ref(LiftingLineSectionControl))))
        return Linear()
    else
        return Nonlinear()
    end
end

# --- methods --- #

function get_coupling_inputs!(y, aero::LiftingLine{NA,TA}, stru::GEBT, dyn::RigidBody,
    flap::LiftingLineFlaps{NF,NG,TF,TG}, dx, x, p, t) where {NA,NF,NG,TA,TF,TG}

    # extract number of state variables, inputs, and parameters
    nxa = number_of_states(aero) # number of aerodynamic states
    nxs = number_of_states(stru) # number of structural states
    nxd = number_of_states(dyn) # number of rigid body states
    nxf = number_of_states(flap) # number of control surface states
    nya = number_of_inputs(aero) # number of aerodynamic inputs
    nys = number_of_inputs(stru) # number of structural inputs
    nyd = number_of_inputs(dyn) # number of rigid body inputs
    nyf = number_of_inputs(flap) # number of control surface inputs
    npa = number_of_parameters(aero) # number of aerodynamic parameters
    nps = number_of_parameters(stru) # number of structural parameters
    npd = number_of_parameters(dyn) # number of rigid body parameters
    npf = number_of_parameters(flap) # number of control surface parameters
    npadd = number_of_additional_parameters(aero, stru, dyn, flap) # number of additional parameters

    # get indices for state variables, inputs, and parameters
    ixa = 1:nxa # indices of aerodynamic states
    iya = 1:nya # indices of aerodynamic inputs
    ipa = 1:npa # indices of aerodynamic parameters
    ixs = nxa + 1 : nxa + nxs # indices of structural states
    iys = nya + 1 : nya + nys # indices of structural inputs
    ips = npa + 1 : npa + nps # indices of structural parameters
    ixd = nxa + nxs + 1 : nxa + nxs + nxd # indices of rigid body states
    iyd = nya + nys + 1 : nya + nys + nyd # indices of rigid body inputs
    ipd = npa + nps + 1 : npa + nps + npd # indices of rigid body parameters
    ixf = nxa + nxs + nxd + 1 : nxa + nxs + nxd + nxf # indices of control surface states
    iyf = nya + nys + nyd + 1 : nya + nys + nyd + nyf # indices of control surface inputs
    ipf = npa + nps + npd + 1 : npa + nps + npd + npf # indices of control surface parameters
    ipadd = npa + nps + npd + npf + 1 : npa + nps + npd + npf + npadd # indices of additional parameters
    ixas = state_indices(aero.models) # indices of aerodynamic states for each section
    iyas = input_indices(aero.models) # indices of aerodynamic inputs for each section
    ipas = parameter_indices(aero.models) # indices of aerodynamic inputs for each section
    ixfs = state_indices(flap.models) # indices of control surface states for each section
    iyfs = input_indices(flap.models) # indices of control surface inputs for each section
    ipfs = parameter_indices(flap.models) # indices of control surface parameters for each section

    # separate state variables, inputs, and parameters
    dxa = view(dx, ixa) # aerodynamic rates
    xa = view(x, ixa) # aerodynamic states
    ya = view(y, iya) # aerodynamic inputs
    pa = view(p, ipa) # aerodynamic parameters
    dxs = view(dx, ixs) # structural rates
    xs = view(x, ixs) # structural states
    ys = view(y, iys) # structural inputs
    ps = view(p, ips) # structural parameters
    dxd = view(dx, ixd) # rigid body rates
    xd = view(x, ixd) # rigid body states
    yd = view(y, iyd) # rigid body inputs
    pd = view(p, ipd) # rigid body parameters
    dxf = view(dx, ixf) # structural rates
    xf = view(x, ixf) # structural states
    yf = view(y, iyf) # structural inputs
    pf = view(p, ipf) # structural parameters
    padd = view(p, ipadd) # additional parameters for coupled model
    dxas = view.(Ref(dxa), ixas) # aerodynamic rates for each section
    xas = view.(Ref(xa), ixas) # aerodynamic states for each section
    yas = view.(Ref(ya), iyas) # aerodynamic inputs for each section
    pas = view.(Ref(pa), ipas) # aerodynamic parameters for each section
    dxfs = view.(Ref(dxf), ixfs) # control surface rates for each section
    xfs = view.(Ref(xf), ixfs) # control surface states for each section
    yfs = view.(Ref(yf), iyfs) # control surface inputs for each section
    pfs = view.(Ref(pf), ipfs) # control surface parameters for each section

    # number of points and elements
    npoint = length(stru.icol_point)
    nelem = length(stru.icol_elem)

    # rigid body states
    xr, yr, zr, ϕr, θr, ψr, ur, vr, wr, pr, qr, rr = xd

    # rigid body state rates
    dxr, dyr, dzr, dϕr, dθr, dψr, dur, dvr, dwr, dpr, dqr, drr = dxd

    # extract global parameters
    ρ = padd[1] # freestream air density
    δ = padd[SVector{NG}(1 + 6*npoint + 6*nelem + 1 : 1 + 6*npoint + 6*nelem + NG)]
    dδ = zero(δ)

    # rigid body linear and angular velocity
    V = SVector(ur, vr, wr)
    Ω = SVector(pr, qr, rr)

    # rigid body linear and angular acceleration
    dV = SVector(dur, dvr, dwr)
    dΩ = SVector(dpr, dqr, drr)

    # construct assembly from parameters
    assembly = gxbeam_assembly(ps, npoint, nelem, stru.start, stru.stop)

    # initialize total forces and moments
    Ftot = @SVector zeros(3)
    Mtot = @SVector zeros(3)

    # initialize rigid body properties
    mass = 0.0
    Ir = @SMatrix zeros(3,3)

    # save prescribed point loads/displacements
    for ip = 1:npoint
        yoff = 6*(ip-1)
        poff = 1 + 6*(ip-1)
        ys[yoff+1] = padd[poff+1]
        ys[yoff+2] = padd[poff+2]
        ys[yoff+3] = padd[poff+3]
        ys[yoff+4] = padd[poff+4]
        ys[yoff+5] = padd[poff+5]
        ys[yoff+6] = padd[poff+6]
    end

    # add prescribed loads to total forces and moments
    for ip = 1:npoint
        # point location
        point = assembly.points[ip]
        # prescribed condition identities
        prescribed_forces = SVector{6}(view(stru.displacement, :, ip)) .== false
        # point loads
        poff = 1 + 6*(ip-1)
        F1 = ifelse(prescribed_forces[1], padd[poff+1], zero(padd[poff+1]))
        F2 = ifelse(prescribed_forces[2], padd[poff+2], zero(padd[poff+2]))
        F3 = ifelse(prescribed_forces[3], padd[poff+3], zero(padd[poff+3]))
        M1 = ifelse(prescribed_forces[4], padd[poff+4], zero(padd[poff+4]))
        M2 = ifelse(prescribed_forces[5], padd[poff+5], zero(padd[poff+5]))
        M3 = ifelse(prescribed_forces[6], padd[poff+6], zero(padd[poff+6]))
        Fp = SVector(F1, F2, F3)
        Mp = SVector(M1, M2, M3)
        Ftot += Fp
        Mtot += cross(point, Fp) + Mp
    end

    # calculate aerodynamic inputs, distributed loads, and element properties
    for i = 1:NA
        # models for this section
        section_aero = aero.models[i]
        section_stru = LiftingLineSection()
        section_flap = flap.models[i]
        section_ctrl = LiftingLineSectionControl()
        section_models = (section_aero, section_stru, section_flap, section_ctrl)

        # model dimensions for this section
        Nxai = number_of_states(section_aero)
        Nyai = number_of_inputs(section_aero)
        Npai = number_of_parameters(section_aero)
        Nxsi = number_of_states(section_stru)
        Nysi = number_of_inputs(section_stru)
        Npsi = number_of_parameters(section_stru)
        Nxfi = number_of_states(section_flap)
        Nyfi = number_of_inputs(section_flap)
        Npfi = number_of_parameters(section_flap)
        Nxci = number_of_states(section_ctrl)
        Nyci = number_of_inputs(section_ctrl)
        Npci = number_of_parameters(section_ctrl)

        # section properties
        icol = stru.icol_elem[i]
        element = assembly.elements[i]
        u_elem = SVector(xs[icol], xs[icol+1], xs[icol+2]) # linear displacement
        θ_elem = SVector(xs[icol+3], xs[icol+4], xs[icol+5]) # angular displacement
        F_elem = SVector(xs[icol+6], xs[icol+7], xs[icol+8]) .* stru.force_scaling # internal force
        M_elem = SVector(xs[icol+9], xs[icol+10], xs[icol+11]) .* stru.force_scaling # internal moment
        P_elem = SVector(xs[icol+12], xs[icol+13], xs[icol+14]) .* stru.mass_scaling # linear momentum
        H_elem = SVector(xs[icol+15], xs[icol+16], xs[icol+17]) .* stru.mass_scaling # angular momentum

        dF_elem = SVector(dxs[icol+6], dxs[icol+7], dxs[icol+8]) .* stru.force_scaling
        dM_elem = SVector(dxs[icol+9], dxs[icol+10], dxs[icol+11]) .* stru.force_scaling
        dP_elem = SVector(dxs[icol+12], dxs[icol+13], dxs[icol+14]) .* stru.mass_scaling
        dH_elem = SVector(dxs[icol+15], dxs[icol+16], dxs[icol+17]) .* stru.mass_scaling

        # convert rotation parameter to Wiener-Milenkovic parameters
        scaling = GXBeam.rotation_parameter_scaling(θ_elem)
        θ_elem *= scaling # angular displacement (Wiener-Milenkovic parameters)

        # length and location of the element
        ΔL = element.L # element length
        pe = element.x + u_elem # location of element (in body frame)

        # local to body transformation
        CtCab = GXBeam.get_C(θ_elem)'*element.Cab

        # local structural to aerodynamic transformation
        R = @SMatrix [0 -1 0; 1 0 0; 0 0 1]

        # local freestream linear and angular velocities
        vi = -R*(CtCab'*V + GXBeam.element_linear_velocity(element, P_elem, H_elem)) # local linear freestream velocity
        ωi = R*GXBeam.element_angular_velocity(element, P_elem, H_elem) # local angular freestream velocity

        # local freestream linear and angular accelerations
        dvi = -R*(CtCab'*dV + CtCab'*cross(dΩ, pe) + GXBeam.element_linear_velocity(element, dP_elem, dH_elem))
        dωi = R*CtCab'*dΩ + R*GXBeam.element_angular_velocity(element, dP_elem, dH_elem)

        # element inertial properties
        poff = 3*npoint + 36*(i-1)
        μ = ps[poff + 31] # mass per unit length
        xm2 = ps[poff + 32] # center of mass location
        xm3 = ps[poff + 33] # center of mass location
        i22 = ps[poff + 34] # rotational inertia
        i33 = ps[poff + 35] # rotational inertia
        i23 = ps[poff + 36] # product of inertia
        me = ΔL*μ # element mass
        Ie = ΔL*(@SMatrix [i22+i33 0 0; 0 i22 -i23; 0 -i23 i33]) # local frame inertia
        Ie = (CtCab*R')*Ie*(R*CtCab') # body frame inertia
        Ie = me*(pe'*pe*I - pe*pe') + Ie # body frame inertia about origin

        # NOTE: We assume that the local beam frame y-axis is oriented towards
        # the leading edge and the z-axis is oriented up
        # NOTE: We assume the local aerodynamic frame is oriented with the x-axis
        # in the chordwise direction and the z-axis in the (airfoil) normal
        # direction.

        # section state variable rates
        duai = SVector{Nxai}(dxas[i]) # aerodynamic state variables
        dusi = vcat(dvi, dωi) # structural state variables
        dufi = SVector{Nxfi}(dxfs[i]) # control surface state variables
        duci = SMatrix{1,NG}(getindex.(flap.gains, i)) * dδ # controller state variables
        dui = vcat(duai, dusi, dufi, duci)

        # section state variables
        uai = SVector{Nxai}(xas[i]) # aerodynamic state variables
        usi = vcat(vi, ωi) # structural state variables
        ufi = SVector{Nxfi}(xfs[i]) # control surface state variables
        uci = SMatrix{1,NG}(getindex.(flap.gains, i)) * δ # controller state variables
        ui = vcat(uai, usi, ufi, uci)

        # section parameters
        pai = SVector{Npai}(pas[i]) # aerodynamic parameters
        psi = SVector(ρ) # structural parameters
        pfi = SVector{Npfi}(pfs[i]) # control surface parameters
        pci = SVector{0,Float64}() # controller parameters
        pi = vcat(pai, psi, pfi, pci)

        # section inputs
        yi = get_coupling_inputs(section_models, dui, ui, pi, t)

        # separate inputs
        yai = view(yi, 1:Nyai)
        ysi = view(yi, Nyai + 1 : Nyai + Nysi)
        yfi = view(yi, Nyai + Nysi + 1 : Nyai + Nysi + Nyfi)
        yci = view(yi, Nyai + Nysi + Nyfi + 1 : Nyai + Nysi + Nyfi + Nyci)

        # save local inputs
        y[iya[iyas[i]]] .= yai # aerodynamic inputs
        y[iyf[iyfs[i]]] .= yfi # control surface inputs

        # section aerodynamic loads (in body frame)
        fi = CtCab*R'*SVector(ysi[1], ysi[2], ysi[3])
        mi = CtCab*R'*SVector(ysi[4], ysi[5], ysi[6])

        # add apparent forces due to body frame linear and angular acceleration
        fi += me*dV - cross(me*dΩ, pe)
        mi += me*dΩ

        # add constant distributed loads (in body frame)
        poff = 1 + 6*npoint + 6*(i-1)
        fi += SVector(padd[poff+1], padd[poff+2], padd[poff+3])
        mi += SVector(padd[poff+4], padd[poff+5], padd[poff+6])

        # save distributed loads for this element (in body frame)
        yoff = 6*npoint + 6*(i-1)
        y[iys[yoff+1 : yoff+3]] = fi
        y[iys[yoff+4 : yoff+6]] = mi

        # add distributed loads to total forces and moments
        Ftot += ΔL*fi
        Mtot += cross(pe, ΔL*fi) + ΔL*mi

        # add element mass and inertia to total mass and inertia
        mass += me
        Ir += Ie
    end

    # save body frame linear and angular velocities
    yoff = 6*npoint + 6*nelem
    y[iys[yoff+1]] = ur
    y[iys[yoff+2]] = vr
    y[iys[yoff+3]] = wr
    y[iys[yoff+4]] = pr
    y[iys[yoff+5]] = qr
    y[iys[yoff+6]] = rr

    # save rigid body model inputs
    y[iyd[1]] = mass
    y[iyd[2]] = Ixx = Ir[1,1]
    y[iyd[3]] = Iyy = Ir[2,2]
    y[iyd[4]] = Izz = Ir[3,3]
    y[iyd[5]] = Ixz = Ir[1,3]
    y[iyd[6]] = Ixy = Ir[1,2]
    y[iyd[7]] = Iyz = Ir[2,3]
    y[iyd[8:10]] = Ftot
    y[iyd[11:13]] = Mtot

    return y
end

# --- performance overloads --- #

# TODO

# --- convenience methods --- #

function set_additional_parameters!(padd, model::LiftingLine, stru::GEBT,
    dyn::RigidBody, flap::LiftingLineFlaps{NF,NG,TF,TG}; rho, point_conditions,
    element_loads, delta) where {NF,NG,TF,TG}

    np = length(stru.icol_point)
    ne = length(stru.icol_elem)

    padd[1] = rho

    for ip = 1:np
        padd[1+6*(ip-1)+1] = point_conditions[6*(ip-1)+1]
        padd[1+6*(ip-1)+2] = point_conditions[6*(ip-1)+2]
        padd[1+6*(ip-1)+3] = point_conditions[6*(ip-1)+3]
        padd[1+6*(ip-1)+4] = point_conditions[6*(ip-1)+4]
        padd[1+6*(ip-1)+5] = point_conditions[6*(ip-1)+5]
        padd[1+6*(ip-1)+6] = point_conditions[6*(ip-1)+6]
    end

    for ie = 1:ne
        padd[1+6*np+6*(ie-1)+1] = element_loads[6*(ie-1)+1]
        padd[1+6*np+6*(ie-1)+2] = element_loads[6*(ie-1)+2]
        padd[1+6*np+6*(ie-1)+3] = element_loads[6*(ie-1)+3]
        padd[1+6*np+6*(ie-1)+4] = element_loads[6*(ie-1)+4]
        padd[1+6*np+6*(ie-1)+5] = element_loads[6*(ie-1)+5]
        padd[1+6*np+6*(ie-1)+6] = element_loads[6*(ie-1)+6]
    end

    padd[1 + 6*np + 6*ne + 1 : 1 + 6*np + 6*ne + NG] = delta

    return padd
end

function separate_additional_parameters(model::LiftingLine, stru::GEBT,
    dyn::RigidBody, flap::LiftingLineFlaps{NF,NG,TF,TG}, padd) where {NF,NG,TF,TG}

    np = length(stru.icol_point)
    ne = length(stru.icol_elem)

    rho = padd[1]

    point_conditions = reshape(view(padd, 1 + 1 : 1 + 6*np), 6, np)

    element_loads = reshape(view(padd, 1 + 6*np + 1 : 1 + 6*np + 6*ne), 6, ne)

    delta = view(padd, 1 + 6*np + 6*ne + 1 : 1 + 6*np + 6*ne + NG)

    return (rho = rho, point_conditions = point_conditions, element_loads = element_loads,
        delta = delta)
end


"""
Code declaring Spectra1DBasic, Spectra1DBasic, and their abstract versions.

Author: Eric Ford
Created: August 2020
"""

#=
const min_orders_in_spectra = 1
const max_orders_in_spectra = 128
const min_pixels_in_order = 1
const max_pixels_in_order = 9128
const min_pixels_in_spectra = min_orders_in_spectra*min_pixels_in_order
const max_pixels_in_spectra = max_orders_in_spectra*max_pixels_in_order
min_usable_pixels_in_order = 128
=#

""" Abstract type for any Spectrum (or region of spectrum) """
abstract type AbstractSpectra end
""" Abstract type for any 1-d Spectrum (or region of spectrum) """
abstract type AbstractSpectra1D <: AbstractSpectra end
""" Abstract type for any 2-d Spectrum """
abstract type AbstractSpectra2D <: AbstractSpectra end

MetadataT = Dict{Symbol,Any}

""" Basic struct for Spectra1D (or region of specturm)
    Instruments can specialize their own if additional data is avaliable. """
struct Spectra1DBasic{T1<:Real,T2<:Real,T3<:Real,
    AA1<:AbstractArray{T1,1},AA2<:AbstractArray{T2,1},AA3<:AbstractArray{T3,1},
    InstT<:AbstractInstrument
    } <: AbstractSpectra1D
    λ::AA1
    flux::AA2
    var::AA3
    inst::InstT
    metadata::MetadataT #Dict{Symbol,Any}
end

""" Basic struct for Spectra2D (or region of specturm)
    Instruments can specialize their own if additional data is avaliable. """
struct Spectra2DBasic{T1<:Real,T2<:Real,T3<:Real,
     AA1<:AbstractArray{T1,2},AA2<:AbstractArray{T2,2},AA3<:AbstractArray{T3,2},
     InstT<:AbstractInstrument
    } <: AbstractSpectra2D
    λ::AA1
    flux::AA2
    var::AA3
    inst::InstT
    metadata::MetadataT # Dict{Symbol,Any}
end

""" Basic struct for Spectra2D (or region of specturm)
    Instruments can specialize their own if additional data is avaliable. """
struct Spectra2DExtended{T1<:Real,T2<:Real,T3<:Real,
     AA1<:AbstractArray{T1,2},AA2<:AbstractArray{T2,2},AA3<:AbstractArray{T3,2},
     InstT<:AbstractInstrument
    } <: AbstractSpectra2D
    λ::AA1
    λ_obs::AA1
    flux::AA2
    var::AA3
    inst::InstT
    metadata::MetadataT # Dict{Symbol,Any}
end

function Spectra1DBasic(λ::A1, flux::A2, var::A3, inst::InstT;
        metadata::Dict{Symbol,Any} = Dict{Symbol,Any}() ) where {
          T1<:Real, T2<:Real, T3<:Real, A1<:AbstractArray{T1,1}, A2<:AbstractArray{T2,1}, A3<:AbstractArray{T3,1},
          InstT<:AbstractInstrument }
    @assert size(λ) == size(flux)
    @assert size(λ) == size(var)
    @assert 1 <= length(λ) <= max_pixel(inst)-min_pixel(inst)+1
    Spectra1DBasic{eltype(λ),eltype(flux),eltype(var),typeof(λ),typeof(flux),typeof(var),typeof(inst)}(λ,flux,var,inst,metadata)
end

function Spectra2DBasic(λ::A1, flux::A2, var::A3, inst::InstT;
     metadata::MetadataT = MetadataT() ) where {
     T1<:Real, T2<:Real, T3<:Real, A1<:AbstractArray{T1,2}, A2<:AbstractArray{T2,2}, A3<:AbstractArray{T3,2},
     InstT<:AbstractInstrument  }
    @assert size(λ) == size(flux)
    @assert size(λ) == size(var)
    @assert 1 <= size(λ,1) <= max_pixel_in_order(inst)
    @assert 1 <= size(λ,2) <= max_order(inst)
    @assert 1 <= min_order(inst) <= max_order(inst)
    @assert 1 <= min_pixel_in_order(inst) <= max_pixel_in_order(inst)
    Spectra2DBasic{eltype(λ),eltype(flux),eltype(var),typeof(λ),typeof(flux),typeof(var),typeof(inst)}(λ,flux,var,inst,metadata)
end

function Spectra2DExtended(λ::A1, λ_obs::A1, flux::A2, var::A3, inst::InstT;
     metadata::MetadataT = MetadataT() ) where {
     T1<:Real, T2<:Real, T3<:Real, A1<:AbstractArray{T1,2}, A2<:AbstractArray{T2,2}, A3<:AbstractArray{T3,2},
     InstT<:AbstractInstrument  }
    @assert size(λ) == size(λ_obs) == size(flux) == size(var)
    @assert 1 <= size(λ,1) <= max_pixel_in_order(inst)-min_pixel_in_order(inst)+1
    @assert 1 <= size(λ,2) <= max_order(inst)-min_order(inst)+1
    Spectra2DExtended{eltype(λ),eltype(flux),eltype(var),typeof(λ),typeof(flux),typeof(var),typeof(inst)}(λ,λ_obs,flux,var,inst,metadata)
end


include("Include.jl")

function update_model_dictionary(parameter_guess_array,model_data_dictionary)

    # what is the host_type?
    host_type = :cell_free

     # Phase 1: parameter update =========================================================================== #
    # update the paramaters in the model data dictionary -
    # for now - lets only search over dG's -
    R = model_data_dictionary["R"]
    T_K = model_data_dictionary["T_K"]

    # compute W -
    tmp_W_array = Float64[]
    for index = 1:8
        parameter_guess = parameter_guess_array[index]
        value = exp(-1*parameter_guess/(R*T_K))
        push!(tmp_W_array,value)
    end

    # update the control W's -
    control_parameter_dictionary = model_data_dictionary["control_parameter_dictionary"]
    control_parameter_dictionary["W_clssrA_RNAP"] = tmp_W_array[1]          # 1
	control_parameter_dictionary["W_clssrA_sigma_28"] = tmp_W_array[2]      # 2
	control_parameter_dictionary["W_deGFPssrA_RNAP"] = tmp_W_array[3]       # 3
	control_parameter_dictionary["W_deGFPssrA_sigma_70"] = tmp_W_array[4]   # 4
	control_parameter_dictionary["W_deGFPssrA_clssrA"] = tmp_W_array[5]     # 5
	control_parameter_dictionary["W_sigma_28_RNAP"] = tmp_W_array[6]        # 6
	control_parameter_dictionary["W_sigma_28_sigma_70"] = tmp_W_array[7]    # 7
	control_parameter_dictionary["W_sigma_28_clssrA"] = tmp_W_array[8]      # 8
    model_data_dictionary["control_parameter_dictionary"] = control_parameter_dictionary

    binding_parameter_dictionary = model_data_dictionary["binding_parameter_dictionary"]
    binding_parameter_dictionary["n_clssrA_sigma_28"] = parameter_guess_array[9]
	binding_parameter_dictionary["K_clssrA_sigma_28"] = parameter_guess_array[10]
	binding_parameter_dictionary["n_deGFPssrA_sigma_70"] = parameter_guess_array[11]
	binding_parameter_dictionary["K_deGFPssrA_sigma_70"] = parameter_guess_array[12]
	binding_parameter_dictionary["n_deGFPssrA_clssrA"] = parameter_guess_array[13]
	binding_parameter_dictionary["K_deGFPssrA_clssrA"] = parameter_guess_array[14]
	binding_parameter_dictionary["n_sigma_28_sigma_70"] = parameter_guess_array[15]
	binding_parameter_dictionary["K_sigma_28_sigma_70"] = parameter_guess_array[16]
	binding_parameter_dictionary["n_sigma_28_clssrA"] = parameter_guess_array[17]
    binding_parameter_dictionary["K_sigma_28_clssrA"] = parameter_guess_array[18]
    model_data_dictionary["binding_parameter_dictionary"] = binding_parameter_dictionary

    # time constant modifier -
    time_constant_modifier_array = [
		0.0	                          ;	  # 1	clssrA
		0.0	                          ;	  # 2	deGFPssrA
		0.0	                          ;	  # 3	sigma_28
		0.0                           ;	  # 4	sigma_70
		parameter_guess_array[19]     ;	  # 5	mRNA_clssrA
		parameter_guess_array[20]     ;	  # 6	mRNA_deGFPssrA
		parameter_guess_array[21]     ;	  # 7	mRNA_sigma_28
		1.0 	                      ;	  # 8	mRNA_sigma_70
		parameter_guess_array[22]	  ;	  # 9	protein_clssrA
		parameter_guess_array[23]	  ;	  # 10	protein_deGFPssrA
		parameter_guess_array[24]	  ;	  # 11	protein_sigma_28
		1.0	                          ;	  # 12	protein_sigma_70
	]
    model_data_dictionary["time_constant_modifier_array"] = time_constant_modifier_array

    # setup degradation_modifier_array -
    degradation_modifier_array = [
		0.0	                            ;	# 1	clssrA
		0.0	                            ;	# 2	deGFPssrA
		0.0	                            ;	# 3	sigma_28
		0.0	                            ;	# 4	sigma_70
		parameter_guess_array[25]	    ;	# 5	mRNA_clssrA 0.0085
		parameter_guess_array[26]		;	# 6	mRNA_deGFPssrA 0.05
		parameter_guess_array[27]		;	# 7	mRNA_sigma_28
		0.0                             ;   # 8 mRNA_sigma_70
		parameter_guess_array[28]		;	# 9	protein_clssrA
		parameter_guess_array[29]		;	# 10	protein_deGFPssrA
		parameter_guess_array[30]		;	# 11	protein_sigma_28
	    parameter_guess_array[31]		;	# 12	protein_sigma_70
    ]

    # update the translation time -
    model_data_dictionary["half_life_translation_capacity"] = parameter_guess_array[32]

    # lastly, update KL -
    biophysical_constants_dictionary = model_data_dictionary["biophysical_constants_dictionary"]
    biophysical_constants_dictionary["translation_saturation_constant"] = parameter_guess_array[33]
    model_data_dictionary["biophysical_constants_dictionary"] = biophysical_constants_dictionary

    # grab defaults -
    species_symbol_type_array = model_data_dictionary["species_symbol_type_array"]
    protein_coding_length_array = model_data_dictionary["protein_coding_length_array"]
    gene_coding_length_array = model_data_dictionary["gene_coding_length_array"]
    time_constant_modifier_array = model_data_dictionary["time_constant_modifier_array"]
    initial_condition_array = model_data_dictionary["initial_condition_array"]

    # # get gene IC -
    idx_gene = findall(x->x==:gene,species_symbol_type_array)
    gene_abundance_array = initial_condition_array[idx_gene]

    # Precompute the translation parameters -
	translation_parameter_array = precompute_translation_parameter_array(biophysical_constants_dictionary, protein_coding_length_array, time_constant_modifier_array,host_type)
    model_data_dictionary["translation_parameter_array"] = translation_parameter_array

	# Precompute the kinetic limit of transcription -
	transcription_kinetic_limit_array = precompute_transcription_kinetic_limit_array(biophysical_constants_dictionary, gene_coding_length_array, gene_abundance_array, time_constant_modifier_array, host_type)
    model_data_dictionary["transcription_kinetic_limit_array"] = transcription_kinetic_limit_array

    # Dilution degrdation matrix -
    dilution_degradation_matrix = build_dilution_degradation_matrix(biophysical_constants_dictionary,species_symbol_type_array,degradation_modifier_array)
    model_data_dictionary["dilution_degradation_matrix"] = dilution_degradation_matrix
    # ===================================================================================================== #

    # return -
    return model_data_dictionary
end

# Function to set some values for the weights, gene lengths etc -
function customize_data_dictionary(default_data_dictionary::Dict{String,Any}, host_type::Symbol)::Dict{String,Any}

    # make a deepcopy -
    customized_data_dictionary = deepcopy(default_data_dictionary)

    # set the gene lengths -
    gene_length_array = customized_data_dictionary["gene_coding_length_array"]
    gene_length_array[1] = 744.0    # clssrA
    gene_length_array[2] = 711.0    # deGFPssrA
    gene_length_array[3] = 720.0    # S28
    gene_length_array[4] = 720.0    # S70 => doesn't matter, gene expression = 0

    # set the protein lengths -
    protein_coding_length_array = customized_data_dictionary["protein_coding_length_array"]
    protein_coding_length_array[1] = 248.0  # clssrA
    protein_coding_length_array[2] = 237.0  # deGFPssrA
    protein_coding_length_array[3] = 240.0  # S28
    protein_coding_length_array[4] = 240.0  # S70 => doesn't matter, translation = 0

    # set ICs -
    initial_condition_array = customized_data_dictionary["initial_condition_array"]
    initial_condition_array[1] = 0.001  # clssrA gene
    initial_condition_array[2] = 0.008  # deGFP gene
    initial_condition_array[3] = 0.0015 # S28 gene
    initial_condition_array[4] = 0.0    # no S70 gene, just S70 protein in the extract
    #initial_condition_array[11] = 0.020 # sigma28 protein in muM
    initial_condition_array[12] = 0.035 # S70 protein in muM

    # setup the W's -
    control_parameter_dictionary = customized_data_dictionary["control_parameter_dictionary"]
    control_parameter_dictionary["W_clssrA_RNAP"] = 0.000014
	control_parameter_dictionary["W_clssrA_sigma_28"] = 1.0
	control_parameter_dictionary["W_deGFPssrA_RNAP"] = 0.000014
	control_parameter_dictionary["W_deGFPssrA_sigma_70"] = 10.0
	control_parameter_dictionary["W_deGFPssrA_clssrA"] = 100.0
	control_parameter_dictionary["W_sigma_28_RNAP"] = 0.00014
	control_parameter_dictionary["W_sigma_28_sigma_70"] = 100.0
	control_parameter_dictionary["W_sigma_28_clssrA"] = 0.1
	control_parameter_dictionary["W_sigma_70_RNAP"] = 0.0

    # setup the binding parameters -
    bP = readdlm("bP-Opt.dat")
    binding_parameter_dictionary = customized_data_dictionary["binding_parameter_dictionary"]
    bp_symbol_name_array = String[]
    push!(bp_symbol_name_array, "n_clssrA_sigma_28")
    push!(bp_symbol_name_array, "K_clssrA_sigma_28")
    push!(bp_symbol_name_array, "n_deGFPssrA_sigma_70")
    push!(bp_symbol_name_array, "K_deGFPssrA_sigma_70")
    push!(bp_symbol_name_array, "n_deGFPssrA_clssrA")
    push!(bp_symbol_name_array, "K_deGFPssrA_clssrA")
    push!(bp_symbol_name_array, "n_sigma_28_sigma_70")
    push!(bp_symbol_name_array, "K_sigma_28_sigma_70")
    push!(bp_symbol_name_array, "n_sigma_28_clssrA")
    push!(bp_symbol_name_array, "K_sigma_28_clssrA")
    for (index,key_text) in enumerate(bp_symbol_name_array)

        # get value -
        value = bP[index]

        # cache -
        binding_parameter_dictionary[key_text] = value
    end

    # setup degradation_modifier_array -
    degradation_modifier_array = [
		0.0	    ;	# 1	clssrA
		0.0	    ;	# 2	deGFPssrA
		0.0	    ;	# 3	sigma_28
		0.0	    ;	# 4	sigma_70
		0.05    ;	# 5	mRNA_clssrA 0.0085
		0.05	;	# 6	mRNA_deGFPssrA 0.05
		0.05	;	# 7	mRNA_sigma_28
		1.0	    ;	# 8	mRNA_sigma_70
		30.0	;	# 9	protein_clssrA
		28.0	;	# 10	protein_deGFPssrA
		1.5	    ;	# 11	protein_sigma_28
		1.0	    ;	# 12	protein_sigma_70
	]
    customized_data_dictionary["degradation_modifier_array"] = degradation_modifier_array
    biophysical_constants_dictionary = customized_data_dictionary["biophysical_constants_dictionary"]
    species_symbol_type_array = customized_data_dictionary["species_symbol_type_array"]
    protein_coding_length_array = customized_data_dictionary["protein_coding_length_array"]
    gene_coding_length_array = customized_data_dictionary["gene_coding_length_array"]

    # get gene IC -
    idx_gene = findall(x->x==:gene,species_symbol_type_array)
    gene_abundance_array = initial_condition_array[idx_gene]

    # Dilution degrdation matrix -
	dilution_degradation_matrix = build_dilution_degradation_matrix(biophysical_constants_dictionary,species_symbol_type_array,degradation_modifier_array)
    customized_data_dictionary["dilution_degradation_matrix"] = dilution_degradation_matrix

    # time constant modifier -
    time_constant_modifier_array = [
		0.0	                    ;	  # 1	clssrA
		0.0	                    ;	  # 2	deGFPssrA
		0.0	                    ;	  # 3	sigma_28
		0.0                     ;	  # 4	sigma_70
        0.5549555127597618      ;	  # 5	mRNA_clssrA
        0.9961201331095983      ;	  # 6	mRNA_deGFPssrA
		0.9573079006791378      ;	  # 7	mRNA_sigma_28
        2.47595196225463	    ;	  # 8	mRNA_sigma_70
		1.2340314042937286	    ;	  # 9	protein_clssrA
		2.0833904410438735 	    ;	  # 10	protein_deGFPssrA
		4.99999996459193	    ;	  # 11	protein_sigma_28
        2.47595196225463	    ;	  # 12	protein_sigma_70
	]
    customized_data_dictionary["time_constant_modifier_array"] = time_constant_modifier_array

    # Precompute the translation parameters -
	translation_parameter_array = precompute_translation_parameter_array(biophysical_constants_dictionary, protein_coding_length_array, time_constant_modifier_array,host_type)
    customized_data_dictionary["translation_parameter_array"] = translation_parameter_array

	# Precompute the kinetic limit of transcription -
	transcription_kinetic_limit_array = precompute_transcription_kinetic_limit_array(biophysical_constants_dictionary, gene_coding_length_array, gene_abundance_array, time_constant_modifier_array, host_type)
    customized_data_dictionary["transcription_kinetic_limit_array"] = transcription_kinetic_limit_array

    # return -
    return customized_data_dictionary
end

function main(path_to_ensemble_file::String, path_to_sim_dir::String; sample_fraction::Float64=1.0)

    # load the default data_dictionary -
    time_start = 0.0
    time_stop = 16.0
    time_step_size = 0.01

    # what is the host_type?
    host_type = :cell_free

    # path to parameters -
    path_to_biophysical_constants_file = "./CellFree.json"

    # Load the data dictionary (uses the default biophysical_constants file)
    default_data_dictionary = build_data_dictionary((time_start,time_stop,time_step_size), path_to_biophysical_constants_file, host_type)

    # Set some values for the weights, gene lengths etc -
    custom_data_dictionary = customize_data_dictionary(default_data_dictionary,host_type)

    # load the ensemble file -
    parameter_ensemble_array = readdlm(path_to_ensemble_file)

    # sort the array -
    parameter_ensemble_array = sort_parameter_ensemble_array(parameter_ensemble_array)

    # what is the size of the ensemble -
    (number_of_parameters, number_of_samples) = size(parameter_ensemble_array)

    # take the top sample fraction -
    number_of_samples = round(Int,sample_fraction*number_of_samples)
    for sample_index = 1:number_of_samples

        # grab the parameter array -
        local_parameter_array = parameter_ensemble_array[:,sample_index]

        # update the default data_dictionary to reflect the new parameters -
        model_data_dictionary = update_model_dictionary(local_parameter_array, custom_data_dictionary)

        # solve the model equations -
        (T,X) = SolveBalances(time_start,time_stop,time_step_size,model_data_dictionary)

        # dump -
        data_array = [T X]
        filename = "$(path_to_sim_dir)/simulation-P$(sample_index).dat"
        writedlm(filename, data_array)

        # give user some notification -
        @show sample_index
    end
end

# setup paths -
path_to_sim_file = "$(pwd())/simulations"
path_to_ensemble_file = "$(pwd())/Ensemble-c1-restriction-T20.dat"

# call -
main(path_to_ensemble_file, path_to_sim_file; sample_fraction = 1.0)


function NumericalPropagatorBuilder(arg0::Orbit, arg1::ODEIntegratorBuilder, arg2::PositionAngle, arg3::jdouble)
    return NumericalPropagatorBuilder((Orbit, ODEIntegratorBuilder, PositionAngle, jdouble), arg0, arg1, arg2, arg3)
end

function NumericalPropagatorBuilder(arg0::Orbit, arg1::ODEIntegratorBuilder, arg2::PositionAngle, arg3::jdouble, arg4::AttitudeProvider)
    return NumericalPropagatorBuilder((Orbit, ODEIntegratorBuilder, PositionAngle, jdouble, AttitudeProvider), arg0, arg1, arg2, arg3, arg4)
end

function add_force_model(obj::NumericalPropagatorBuilder, arg0::ForceModel)
    return jcall(obj, "addForceModel", void, (ForceModel,), arg0)
end

function build_kalman_model(obj::NumericalPropagatorBuilder, arg0::List, arg1::List, arg2::ParameterDriversList)
    return jcall(obj, "buildKalmanModel", KalmanModel, (List, List, ParameterDriversList), arg0, arg1, arg2)
end

function build_ls_model(obj::NumericalPropagatorBuilder, arg0::Vector{IntegratedPropagatorBuilder}, arg1::List, arg2::ParameterDriversList, arg3::ModelObserver)
    return jcall(obj, "buildLSModel", BatchLSModel, (Vector{IntegratedPropagatorBuilder}, List, ParameterDriversList, ModelObserver), arg0, arg1, arg2, arg3)
end

function build_propagator(obj::NumericalPropagatorBuilder, arg0::Vector{jdouble})
    return jcall(obj, "buildPropagator", NumericalPropagator, (Vector{jdouble},), arg0)
end

function copy(obj::NumericalPropagatorBuilder)
    return jcall(obj, "copy", NumericalPropagatorBuilder, ())
end

function get_all_force_models(obj::NumericalPropagatorBuilder)
    return jcall(obj, "getAllForceModels", List, ())
end

function get_integrator_builder(obj::NumericalPropagatorBuilder)
    return jcall(obj, "getIntegratorBuilder", ODEIntegratorBuilder, ())
end

function get_mass(obj::NumericalPropagatorBuilder)
    return jcall(obj, "getMass", jdouble, ())
end

function set_mass(obj::NumericalPropagatorBuilder, arg0::jdouble)
    return jcall(obj, "setMass", void, (jdouble,), arg0)
end


__precompile__()
module ReinforcementLearning

using POMDPs
using POMDPToolbox
using Discretizers

import POMDPs: isterminal, discount
import POMDPs: action, actions, action_index, n_actions
import POMDPs: states, state_index, n_states

export

    isterminal,
    actions,
    step!,
    reset!,

    DiscretizedEnvironment

include("abstract.jl")
include("environments.jl")

end # module


@noinline function old_cfunction(f, r, a)
  ccall(:jl_function_ptr, Ptr{Cvoid}, (Any, Any, Any), f, r, a)
end

## Common Interface Solve Functions

mutable struct CommonFunction{F,P}
    func::F
    p::P
    neq::Cint
end

function commonfun(t::T1,y::T2,yp::T3,comfun::CommonFunction) where {T1,T2,T3}
  y_ = unsafe_wrap(Array,y,comfun.neq)
  ydot_ = unsafe_wrap(Array,yp,comfun.neq)
  comfun.func(ydot_,y_,comfun.p,t)
  return Int32(0)
end

function DiffEqBase.__solve(
    prob::DiffEqBase.AbstractODEProblem{uType,tupType,isinplace},
    alg::LSODAAlgorithm,
    timeseries=[],ts=[],ks=[];

    verbose=true,
    abstol=1/10^6,reltol=1/10^3,
    tstops=Float64[],
    saveat=Float64[], maxiter=Int(1e5),
    callback=nothing,
    timeseries_errors=true,
    save_everystep=isempty(saveat),
    save_start = save_everystep || isempty(saveat) || typeof(saveat) <: Number ? true : prob.tspan[1] in saveat,
    userdata=nothing,
    alias_u0=false,
    kwargs...) where {uType,tupType,isinplace}

    tType = eltype(tupType)

    if verbose
        warned = !isempty(kwargs) && check_keywords(alg, kwargs, warnlist)
        if !(typeof(prob.f) <: DiffEqBase.AbstractParameterizedFunction)
            if DiffEqBase.has_tgrad(prob.f)
                @warn("Explicit t-gradient given to this stiff solver is ignored.")
                warned = true
            end
            if DiffEqBase.has_jac(prob.f)
                @warn("Explicit Jacobian given to this stiff solver is ignored.")
                warned = true
            end
        end
        warned && warn_compat()
    end

    if prob.f.mass_matrix != I
        error("This solver is not able to use mass matrices.")
    end

    if callback != nothing || :callback in keys(prob.kwargs)
      error("LSODA is not compatible with callbacks.")
    end

    tspan = prob.tspan
    t0 = tspan[1]
    T = tspan[end]

    if typeof(saveat) <: Number
        if (tspan[1]:saveat:tspan[end])[end] == tspan[end]
          saveat_vec = convert(Vector{tType},collect(tType,tspan[1]+saveat:saveat:tspan[end]))
        else
          saveat_vec = convert(Vector{tType},collect(tType,tspan[1]+saveat:saveat:(tspan[end]-saveat)))
        end
    else
        saveat_vec =  convert(Vector{tType},collect(saveat))
    end

    if !isempty(saveat_vec) && saveat_vec[end] == tspan[2]
        pop!(saveat_vec)
    end

    if !isempty(saveat_vec) && saveat_vec[1] == tspan[1]
        save_ts = sort(unique([saveat_vec;T]))
    else
        save_ts = sort(unique([t0;saveat_vec;T]))
    end

    if T < save_ts[end]
        error("Final saving timepoint is past the solving timespan")
    end
    if t0 > save_ts[1]
        error("First saving timepoint is before the solving timespan")
    end

    if !isempty(tstops)
        error("tstops is not supported for this solver. Please use saveat instead")
    end

    if typeof(prob.u0) <: Number
        u0 = [prob.u0]
    else
        if alias_u0
            u0 = vec(prob.u0)
        else
            u0 = vec(deepcopy(prob.u0))
        end
    end

    sizeu = size(prob.u0)

    ### Fix the more general function to Sundials allowed style
    if !isinplace && (typeof(prob.u0)<:Vector{Float64} || typeof(prob.u0)<:Number)
        f! = (du,u,p,t) -> (du[:] = prob.f(u,p,t); nothing)
    elseif !isinplace && typeof(prob.u0)<:AbstractArray
        f! = (du,u,p,t) -> (du[:] = vec(prob.f(reshape(u,sizeu),p,t)); nothing)
    elseif typeof(prob.u0)<:Vector{Float64}
        f! = prob.f
    else # Then it's an in-place function on an abstract array
        f! = (du,u,p,t) -> (prob.f(reshape(du,sizeu),reshape(u,sizeu),p,t); nothing)
    end

    ures = Vector{Float64}[]
    push!(ures,u0)
    utmp = copy(u0)
    utmp2= copy(u0)
    ttmp = [t0]
    t    = [t0]
    t2   = [t0]
    save_start ? ts = [t0] : ts = typeof(t0)[]

    neq = Int32(length(u0))
    comfun = CommonFunction(f!,prob.p,neq)
    atol = ones(Float64,neq)
    rtol = ones(Float64,neq)

    if typeof(abstol) == Float64
        atol *= abstol
    else
        atol = copy(abstol)
    end

    if typeof(reltol) == Float64
        rtol *= reltol
    else
        rtol = copy(reltol)
    end

    GC.@preserve comfun atol rtol begin

    global ___ref = comfun

    opt = lsoda_opt_t(mxstep = maxiter)
    opt.ixpr = 0
    opt.rtol = pointer(rtol)
    opt.atol = pointer(atol)
    if save_everystep
        itask_tmp = 2
    else
        itask_tmp = 1
    end
    opt.itask = itask_tmp

    function get_cfunction(comfun::T) where T
        @cfunction commonfun Cint (Cdouble, Ptr{Cdouble}, Ptr{Cdouble}, Ref{T})
    end

    fex_c = get_cfunction(comfun)

    ctx = lsoda_context_t()
    ctx.function_ = fex_c
    ctx.neq = neq
    ctx.state = 1
    ctx.data = pointer_from_objref(comfun)

    ch = ContextHandle(ctx)

    lsoda_prepare(ctx,opt)

    for k in 2:length(save_ts)
        ttmp[1] = save_ts[k]
        if t[1] < ttmp[1]
            while t[1] < ttmp[1]
                lsoda(ctx, utmp, t, ttmp[1])
                if t[1] > ttmp[1] # overstepd, interpolate back
                    t2[1] = t[1] # save step values
                    copyto!(utmp2,utmp) # save step values
                    opt.itask = 1 # change to interpolating
                    lsoda(ctx, utmp, t, ttmp[1])
                    opt.itask = itask_tmp
                    push!(ures, copy(utmp))
                    push!(ts, t[1])
                    # don't overstep the last timestep
                    if k != length(save_ts) && save_ts[k+1] > t2[1]
                        push!(ures, copy(utmp2))
                        push!(ts, t2[1])
                    end
                    copyto!(utmp, utmp2)
                    t[1] = t2[1]
                else
                    push!(ures, copy(utmp))
                    push!(ts,t[1])
                end
            end
      else
            t2[1] = t[1] # save step values
            copyto!(utmp2, utmp) # save step values
            opt.itask = 1 # change to interpolating
            lsoda(ctx, utmp, t, ttmp[1])
            opt.itask = itask_tmp
            push!(ures, copy(utmp))
            push!(ts, t[1])
            if k != length(save_ts) && save_ts[k+1] > t2[1] # don't overstep the last timestep
                push!(ures,copy(utmp2))
                push!(ts,t2[1])
            end
            copyto!(utmp,utmp2)
            t[1] = t2[1]
        end
    end

    ### Finishing Routine

    timeseries = uType[]
    save_start ? start_idx = 1 : start_idx = 2
    if typeof(prob.u0)<:Number
        for i=start_idx:length(ures)
            push!(timeseries,ures[i][1])
        end
    else
        for i=start_idx:length(ures)
            push!(timeseries,reshape(ures[i],sizeu))
        end
    end

    lsoda_free(ch)
    global ___ref = nothing
    end

    DiffEqBase.build_solution(prob, alg, ts, timeseries,
                   timeseries_errors = timeseries_errors,
                   retcode = :Success)
end


"""
    VanillaNetStats()

Network statistics for `VanillaConfig`.
"""
mutable struct VanillaNetStats <: Statistics
    # network traffic
    uplink::Vector{Int}
    downlink::Vector{Int}

    delta_earned::Int

    VanillaNetStats() = new(Vector{Int}(undef, 0), Vector{Int}(undef, 0), 0)
end

"""
    update_stats!(stats, round_num)

Updates the network traffic stats of a `VanillaConfig`.
"""
function update_stats!(stats::VanillaNetStats, round_num::Int)
    num_comm = round_num * STATS.base.num_clients_per_round
    updown = num_comm * STATS.base.num_weights * sizeof(STATS.T)
    push!(stats.uplink, updown)
    push!(stats.downlink, updown - stats.delta_earned)
end


base_dir = dirname(dirname(pathof(PowerSystems)))

sys = System(PowerSystems.PowerModelsData(joinpath(MATPOWER_DIR, "case5_re.m")))

@testset "Check bus index" begin
    @test sort([b.number for b in collect(get_components(Bus, sys))]) == [1, 2, 3, 4, 10]
    @test sort(collect(Set([
        b.arc.from.number for b in collect(get_components(Branch, sys))
    ]))) == [1, 2, 3, 4]
    @test sort(collect(Set([
        b.arc.to.number for b in collect(get_components(Branch, sys))
    ]))) == [2, 3, 4, 10]

    # TODO: add test for loadzones testing MAPPING_BUSNUMBER2INDEX

end

@testset "Test unique bus numbers" begin
    number = 100
    bus1 = Bus(;
        number = number,
        name = "bus100",
        bustype = BusTypes.PV,
        angle = 1.0,
        voltage = 1.0,
        voltagelimits = (min = -1.0, max = 1.0),
        basevoltage = 1.0,
        area = nothing,
        load_zone = nothing,
    )
    bus2 = Bus(;
        number = number,
        name = "bus101",
        bustype = BusTypes.PV,
        angle = 1.0,
        voltage = 1.0,
        voltagelimits = (min = -1.0, max = 1.0),
        basevoltage = 1.0,
        area = nothing,
        load_zone = nothing,
    )

    add_component!(sys, bus1)
    @test_throws ArgumentError add_component!(sys, bus2)
end


module NISTCrossSectionData
using ArgCheck
using Unitful
const UF = Unitful
using Unitful: MeV, g, cm

const uenergy  = MeV
const umassatt = cm^2/g
const umassrange = inv(umassatt)
const ustoppow = uenergy * umassatt
const unone    = Unitful.NoUnits

export MeV, g, cm

include("api.jl")
include("tables.jl")
include("csv.jl")
include("xaamdi.jl")
include("xcom.jl")
include("estar.jl")

end # module


using ServerMetrics
using Sockets
using Dates
using ServerMetrics: AbstractMetric, NumericMetric, MetricGroup
using ServerMetrics: register!, unregister!, clear_registry!
using ServerMetrics: get_metric
using ServerMetrics: get_cells
using DataStructures: SortedDict
using Test: @test, @test_throws, @test_logs

get_cell_values(m) = Dict(cell.labels => cell.value[] for cell in get_cells(m))
metric_values(m::NumericMetric) = m.value[]
metric_values(m::MetricGroup) = Dict(k => v.value[] for (k, v) in m.cells)
metric_values(m::AbstractMetric) = metric_values(m.content)
is_dummy(x) = isa(x, ServerMetrics.DummyCell)

@testcase "get_cell! with invalid labels" begin
    cnt = Counter(;action=String, code=Int64)
    @test is_dummy(ServerMetrics.get_cell!(cnt; action="get"))
    @test is_dummy(ServerMetrics.get_cell!(cnt; action="get", code=200, unknown="foo"))
    @test is_dummy(ServerMetrics.get_cell!(cnt; action="get", code=nothing))
end

@testcase "get_cell_if_exists does not create new cells" begin
    cnt = Counter(;day=String)
    @test ServerMetrics.get_cell_if_exists(cnt; day="Monday") == nothing
    @test metric_values(cnt) == Dict()

    @test ServerMetrics.get_cell!(cnt; day="Monday") isa NumericMetric
    @test ServerMetrics.get_cell_if_exists(cnt; day="Monday") isa NumericMetric
    @test metric_values(cnt) ==Dict(Dict(:day => "Monday") => 0.0)
end

@testcase "Invalid access is ignored" begin
    cnt = Counter(;action=String)
    inc!(cnt; action="get")
    inc!(cnt; unknown=nothing) # This is invalid
    inc!(cnt; action="put", mystery=1) # This is invalid
    inc!(cnt) # This is also invalid (missing action label)
    @test metric_values(cnt) == Dict(
        Dict(:action => "get") => 1.0
    )
end

@testcase "get_cell! for singular counter" begin
    cnt = Counter()
    @test is_dummy(ServerMetrics.get_cell!(cnt; label=10))
    @test ServerMetrics.get_cell_if_exists(cnt; label=10) == nothing
    @test ServerMetrics.get_cell_if_exists(cnt) == cnt.content
    @test ServerMetrics.get_cell!(cnt) == cnt.content
end

@testcase "get_cell with Counter" begin
    cnt = Counter(;action=String, code=Int64)
    @test metric_values(cnt) == Dict()

    @test ServerMetrics.get_cell!(cnt; action="get", code=200) isa NumericMetric
    @test metric_values(cnt) == Dict(
        Dict(:action => "get", :code => 200) => 0.0,
    )

    ServerMetrics.get_cell!(cnt; action="get", code=500)
    ServerMetrics.get_cell!(cnt; action="put", code=200)
    @test metric_values(cnt) == Dict(
        Dict(:action => "get", :code => 200) => 0.0,
        Dict(:action => "get", :code => 500) => 0.0,
        Dict(:action => "put", :code => 200) => 0.0,
    )
end

@testcase "Counter with labels " begin
    c = Counter(;action=String, response_code=Int64)
    inc!(c; action="get", response_code=404)
    inc!(c; action="put", response_code=200)
    inc!(c, 2.0; action="get", response_code=404)

    @test metric_values(c) == Dict(
        Dict(:action => "get", :response_code => 404) => 3.0,
        Dict(:action => "put", :response_code => 200) => 1.0,
    )
end

@testcase "Gauge with labels" begin
    g = Gauge(;resource=String)
    set!(g, 20.0; resource="disk")
    inc!(g; resource="disk")
    dec!(g, 5.0; resource="water_level")
    inc!(g, 2.0; resource="water_level")
    @test metric_values(g) == Dict(
        Dict(:resource => "disk") => 21.0,
        Dict(:resource => "water_level") => -3.0,
    )
end

@testcase "Counter with no labels has one cell" begin
    c = Counter()
    @test length(get_cells(c)) == 1
    inc!(c, 2.5)
    @test metric_values(c) == 2.5
end

@testcase "Simple counter manipulations" begin
    c = ServerMetrics.Counter()
    @test metric_values(c) == 0.0
    inc!(c)
    @test metric_values(c) == 1.0
    inc!(c, 2)
    @test metric_values(c) == 3.0

    # Incrementing by negative value is logged and ignored
    inc!(c, -1.0)
    @test metric_values(c) == 3.0
end

@testcase "Gauge constructors" begin
    g1 = ServerMetrics.Gauge()
    @test metric_values(g1) == 0.0

    g2 = ServerMetrics.Gauge(10.0)
    @test metric_values(g2) == 10.0
end

@testcase "Simple gauge manipulations" begin
    g = ServerMetrics.Gauge()
    @test metric_values(g) == 0.0
    inc!(g)
    @test metric_values(g) == 1.0
    inc!(g, 2)
    @test metric_values(g) == 3.0

    inc!(g, -1.0)
    @test metric_values(g) == 3.0

    dec!(g, -1.0)
    @test metric_values(g) == 3.0
    # TODO(janrous): Perhaps we might consider chg!(g, v) which will
    # inc! or dec! based on the sign?
end

@testcase "Registry enforces unique names" begin
    r = ServerMetrics.MetricRegistry()
    c1 = ServerMetrics.Counter()
    register!(r, c1, "first_name")
    c2 = ServerMetrics.Counter()
    register!(r, c2, "second_name")
    @test ServerMetrics.name(c1) == "first_name"
    @test ServerMetrics.name(c2) == "second_name"
    c3 = ServerMetrics.Counter()
    @test_throws KeyError register!(r, c3, "first_name")
    @test ServerMetrics.name(c3) == nothing
end

@testcase "Once registered, name remains fixed" begin
    r = ServerMetrics.MetricRegistry()
    c = ServerMetrics.Counter()
    @test ServerMetrics.name(c) === nothing
    register!(r, c, "my_name")
    @test ServerMetrics.name(c) == "my_name"
    unregister!(r, "my_name")
    @test ServerMetrics.name(c) == "my_name"
end

@testcase "overwriting existing metrics" begin
    r = ServerMetrics.MetricRegistry()
    c1 = ServerMetrics.Counter()
    c2 = ServerMetrics.Counter()
    register!(r, c1, "first_name")
    @test_throws KeyError register!(r, c2, "first_name")
    @test get_metric(r, "first_name") == c1
    @test_logs (:warn,) register!(r, c2, "first_name"; overwrite=true)
    @test get_metric(r, "first_name") == c2
end

# Returns sorted Array of metric names registered within given registry.
list_metrics(r::ServerMetrics.MetricRegistry) = sort(collect(keys(r.metrics)))

# Uniqueness of metric name is enforced within a single registry. Two registries
# can have metric with the same name.
@testcase "Metrics unique within registry" begin
    r1 = ServerMetrics.MetricRegistry()
    r2 = ServerMetrics.MetricRegistry()
    g1 = ServerMetrics.Gauge(1.0)
    g2 = ServerMetrics.Gauge(3.0)
    register!(r1, g1, "unique")
    register!(r2, g2, "unique")
    @test list_metrics(r1) == ["unique"]
    @test list_metrics(r2) == ["unique"]
    @test metric_values(get_metric(r1, "unique")) == 1.0
    @test metric_values(get_metric(r2, "unique")) == 3.0
end

# Single metric can only be registered once.
@testcase "Name constraints on metrics are enforced" begin
    r1 = ServerMetrics.MetricRegistry()
    r2 = ServerMetrics.MetricRegistry()
    c = ServerMetrics.Counter()
    register!(r1, c, "my_counter")

    # Registering under the same name twice is okay
    @test_logs register!(r2, c, "my_counter")

    # Registering under different name is not okay
    @test_throws AssertionError register!(r2, c, "my_counter_2")
end

@testcase "Multiple registration with same name okay" begin
    r1 = ServerMetrics.MetricRegistry()
    r2 = ServerMetrics.MetricRegistry()
    c = ServerMetrics.Counter()
    register!(r1, c, "my_counter")
    @test_logs register!(r2, c, "my_counter")
end

# Metric could be registered sequentially in two registries if it's unregistered from one.
@testcase "Sequential multiple registration" begin
    r1 = ServerMetrics.MetricRegistry()
    r2 = ServerMetrics.MetricRegistry()
    c = ServerMetrics.Counter()
    register!(r1, c, "my_counter")
    unregister!(r1, "my_counter")
    register!(r2, c, "my_counter")
    @test list_metrics(r1) == []
    @test list_metrics(r2) == ["my_counter"]
end

@testcase "Metric name length enforced" begin
    @test_throws ArgumentError register!(
        MetricRegistry(),
        ServerMetrics.Counter(),
        "c"^201
    )
end

@testcase "Metric name validation" begin
    r = MetricRegistry()
    c = ServerMetrics.Counter()
    @test_throws ArgumentError register!(r, c, "hyphens-not-permitted")
    @test_throws ArgumentError register!(r, c, ".tralala")
    @test_throws ArgumentError register!(r, c, "2tralala")
    @test_throws ArgumentError register!(r, c, "tra\\lala")
    @test_throws ArgumentError register!(r, c, "t,r()a22ala")
    @test_throws ArgumentError register!(r, c, "prometheus.dislikes.this")
end

# Turn Dict keys into Return sorted array of keys from Dict
key_set(d::SortedDict) = Set([first(kv) for kv in d])

Base.@kwdef struct ThreeMetrics <: AbstractMetricCollection
    a = Counter()
    b = Counter()
    c = Gauge(5.0)
end

Base.@kwdef struct CounterAndTwoGauges <: AbstractMetricCollection
    c = Counter()
    g1 = Gauge()
    g2 = Gauge()
end

Base.@kwdef struct CounterAndGauge <: AbstractMetricCollection
    c = Counter()
    g = Gauge()
end

@testcase "Int64 counter manipulations" begin
    c = Counter()
    inc!(c, 2)
    @test metric_values(c) == 2.0
end

@testcase "Int64 gauge manipulations" begin
    g = Gauge(0)

    inc!(g, 1)
    @test metric_values(g) == 1.0

    dec!(g, 2)
    @test metric_values(g) == -1.0

    set!(g, 5)
    @test metric_values(g) == 5.0
end

@testcase "Unregister from registry" begin
    r = MetricRegistry()
    @test list_metrics(r) == []
    register!(r, Counter(), "aaa")
    register!(r, Counter(), "bbb")
    @test list_metrics(r) == ["aaa", "bbb"]
    @test_throws KeyError unregister!(r, "nonexistent")
    unregister!(r, "aaa")
    @test list_metrics(r) == ["bbb"]
    unregister!(r, "bbb")
    @test list_metrics(r) == []
end

@testcase "Clear registry" begin
    r = MetricRegistry()
    @test list_metrics(r) == []
    clear_registry!(r)
    @test list_metrics(r) == []
    register!(r, Counter(), "aaa")
    register!(r, Counter(), "bbb")
    @test list_metrics(r) == ["aaa", "bbb"]
    clear_registry!(r)
    @test list_metrics(r) == []
    register!(r, Counter(), "bbb")
    @test list_metrics(r) == ["bbb"]
end

Base.@kwdef struct MixedMetrics <: AbstractMetricCollection
    my_counter = Counter()
    my_gauge = Gauge()

    other_stuff::String = "default_value"
    anything::Any = nothing
end

@testcase "Collection with nonmetric fields" begin
    r = MetricRegistry(MixedMetrics())
    @test list_metrics(r) == ["my_counter", "my_gauge"]
end

Base.@kwdef struct ArbitraryStruct
    my_counter_1 = Counter()
    name::String = "String thingy"
end

@testcase "Collection of any type" begin
    r = MetricRegistry(ArbitraryStruct())
    @test list_metrics(r) == ["my_counter_1"]
end

Base.@kwdef struct FirstGroup <: AbstractMetricCollection
    first_counter = Counter()
    first_gauge = Gauge()
end

Base.@kwdef struct SecondGroup <: AbstractMetricCollection
    second_counter = Counter()
    second_gauge = Gauge()
end

@testcase "Register non-overlapping collections" begin
    first_group = FirstGroup()
    second_group = SecondGroup()
    r = MetricRegistry()

    @test list_metrics(r) == []

    register_collection!(r, first_group)
    @test list_metrics(r) == ["first_counter", "first_gauge"]

    register_collection!(r, second_group)
    @test list_metrics(r) == ["first_counter", "first_gauge", "second_counter", "second_gauge"]
end

@testcase "MAX_CELLS_PER_METRIC triggers" begin
    cnt = Counter(;order=Int64)
    for i in 1:205
        inc!(cnt, i; order=i)
    end
    # only the last 200 cells should be present. Timestmaps may not change fast enough
    # to ensure that LRU will behave in a stable manner.
    @test length(metric_values(cnt)) == 200
end

@testcase "Zero all metrics" begin
    r = MetricRegistry()
    c = register!(r, ServerMetrics.Counter(), "my_counter")
    g = register!(r, ServerMetrics.Gauge(), "my_gauge")
    c_free = ServerMetrics.Counter()

    inc!(c, 2.0)
    set!(g, 5.0)
    inc!(c_free)

    @test metric_values(c) == 2.0
    @test metric_values(g) == 5.0
    @test metric_values(c_free) == 1.0

    ServerMetrics.zero_all_metrics(r)

    @test metric_values(c) == 0.0
    @test metric_values(g) == 0.0
    @test metric_values(c_free) == 1.0
end

@testcase "value_of for simple metrics" begin
    r = MetricRegistry()
    c = register!(r, ServerMetrics.Counter(), "my_counter")
    g = register!(r, ServerMetrics.Gauge(), "my_gauge")
    inc!(c)
    set!(g, 50)
    @test ServerMetrics.value_of(r, "my_counter") == 1.0
    @test ServerMetrics.value_of(r, "my_gauge") == 50.0
    @test ServerMetrics.value_of(r, "unknown") == nothing
end

@testcase "value_of for labelled metrics" begin
    r = MetricRegistry()
    c = register!(r, ServerMetrics.Counter(;response_code=Int64, action=String), "my_counter")
    inc!(c; action="GET", response_code=200)
    inc!(c; action="PUT")  # This inc! is invalid due to bad labels and should be silently ignored.

    # The following doesn't work because all labels are missing.
    @test ServerMetrics.value_of(r, "my_counter") == nothing

    # The following doesn't work because unknown "badlabel" is present.
    @test ServerMetrics.value_of(r, "my_counter"; action="GET", response_code=200, badlabel=1) == nothing

    # The following doesn't work because "response_code" label is missing.
    @test ServerMetrics.value_of(r, "my_counter"; action="PUT") == nothing

    # The following works and refers to the cell created above.
    @test ServerMetrics.value_of(r, "my_counter"; action="GET", response_code=200) == 1.0
end


# Load packages

using Vann
using PyPlot
using DataFrames
using ExcelReaders
using JLD
using CSV

# Settings

file_exper  = "C:/Work/Studies/vann/experiments.xlsx"
path_inputs = "C:/Work/Studies/vann/data/norway"
path_save   = "C:/Work/Studies/vann/calib_res"

df_exper = readxlsheet(DataFrame, file_exper, "Calib")

# Run one experiment

function run_single_experiment(path_inputs, path_save, df_exper, i_exper)

  # Folder for saving results

  name_experiment = "experiment_" * string(i_exper)

  path_save = joinpath(path_save, name_experiment)

  mkpath(path_save * "/calib_txt")
  mkpath(path_save * "/calib_png")
  mkpath(path_save * "/valid_txt")
  mkpath(path_save * "/valid_png")
  mkpath(path_save * "/param_snow")
  mkpath(path_save * "/param_hydro")
  mkpath(path_save * "/model_data")

  # Get settings

  opt = Dict()

  opt["epot_choice"]  = parse(df_exper[:Epot][i_exper])
  opt["snow_choice"]  = parse(df_exper[:Snow][i_exper])
  opt["hydro_choice"] = parse(df_exper[:Hydro][i_exper])
  opt["force_states"] = parse(df_exper[:force_states][i_exper])

  opt["calib_start"] = df_exper[:calib_start][i_exper]
  opt["calib_stop"]  = df_exper[:calib_stop][i_exper]
  opt["valid_start"] = df_exper[:valid_start][i_exper]
  opt["valid_stop"]  = df_exper[:valid_stop][i_exper]

  opt["tstep"] = df_exper[:tstep][i_exper]

  opt["warmup"] = round(Int64, df_exper[:warmup][i_exper])

  opt["path_save"]   = path_save
  opt["path_inputs"] = path_inputs

  # Run for all stations

  calib_all_stations(opt)

end

# Calibrate all stations for one experiment

function calib_all_stations(opt)

  # Empty dataframes for summary statistics

  df_calib = DataFrame(Station = String[], NSE = Float64[], KGE = Float64[])
  df_valid = DataFrame(Station = String[], NSE = Float64[], KGE = Float64[])

  # Loop over all watersheds

  dir_all = readdir(opt["path_inputs"])

  for dir_cur in dir_all

    # Only run calibration for watersheds without glaciers

    df_meta = CSV.read("$(opt["path_inputs"])/$dir_cur/metadata.txt", delim = ";")

    perc_glacier = df_meta[:perc_glacier]

    if perc_glacier[1].value < 0.5

      print("Calibration for data in $(dir_cur)\n")

      # Run for calibration period

      # Load data

      try
        date, tair, prec, q_obs, frac = load_data("$(opt["path_inputs"])/$dir_cur")
      catch
        info("Unable to read files in directory $(dir_cur)\n")
        continue
      end

      # Crop data

      date, tair, prec, q_obs = crop_data(date, tair, prec, q_obs, opt["calib_start"], opt["calib_stop"])

      # Compute potential evapotranspiration

      epot = eval(Expr(:call, opt["epot_choice"], date))

      # Precipitation correction step

      if sum(~isnan(q_obs)) > (3*365)

        ikeep = ~isnan(q_obs)

        prec_tmp = sum(prec .* repmat(frac, 1, size(prec,2)), 1)

        prec_sum = sum(prec_tmp[ikeep])
        q_sum = sum(q_obs[ikeep])
        epot_sum = sum(epot[ikeep])

        pcorr = (q_sum + 0.5 * epot_sum) / prec_sum

        prec = pcorr * prec

      else

        warn("Not enough runoff data for calibration (see folder $dir_cur)")
        continue

      end

      # Initilize model

      st_snow = eval(Expr(:call, opt["snow_choice"], opt["tstep"], date[1], frac))
      st_hydro = eval(Expr(:call, opt["hydro_choice"], opt["tstep"], date[1]))

      # Run calibration

      param_snow, param_hydro = run_model_calib(st_snow, st_hydro, tair, prec, epot, q_obs;
                                                force_states = opt["force_states"], warmup = opt["warmup"])

      println("Snow model parameters: $param_snow")
      println("Hydro model parameters: $param_hydro")

      # Reinitilize model

      st_snow = eval(Expr(:call, opt["snow_choice"], opt["tstep"], date[1], param_snow, frac))
      st_hydro = eval(Expr(:call, opt["hydro_choice"], opt["tstep"], date[1], param_hydro))

      # Run model with best parameter set

      q_sim, st_snow, st_hydro = run_model(st_snow, st_hydro, tair, prec, epot; return_all = true)

      # Store results in data frame

      q_obs = round(q_obs, 2)
      q_sim = round(q_sim, 2)

      df_res = DataFrame(date = Dates.format(date,"yyyy-mm-dd"), q_sim = q_sim, q_obs = q_obs)

      # Save results to txt file

      file_save = dir_cur[1:end-5]

      writetable(string(opt["path_save"], "/calib_txt/", file_save, "_station.txt"), df_res, quotemark = '"', separator = '\t')

      # Plot results

      ioff()

      file_name = string(opt["path_save"], "/calib_png/", file_save, "_hydro.png")

      plot_sim(st_hydro; q_obs = q_obs, file_name = file_name)

      file_name = string(opt["path_save"], "/calib_png/", file_save, "_snow.png")

      plot_sim(st_snow; file_name = file_name)

      # Compute summary statistics

      station = dir_cur[1:end-5]
      kge_res = kge(q_sim[opt["warmup"]:end], q_obs[opt["warmup"]:end])
      nse_res = nse(q_sim[opt["warmup"]:end], q_obs[opt["warmup"]:end])

      push!(df_calib, [station nse_res kge_res])

      # Run for validation period

      # Reload data

      date, tair, prec, q_obs, frac = load_data("$(opt["path_inputs"])/$dir_cur")

      # Crop data

      date, tair, prec, q_obs = crop_data(date, tair, prec, q_obs, opt["valid_start"], opt["valid_stop"])

      # Compute potential evapotranspiration

      epot = eval(Expr(:call, opt["epot_choice"], date))

      # Reinitilize model

      st_snow = eval(Expr(:call, opt["snow_choice"], opt["tstep"], date[1], param_snow, frac))
      st_hydro = eval(Expr(:call, opt["hydro_choice"], opt["tstep"], date[1], param_hydro))

      # Precipitation correction step

      prec = pcorr * prec

      # Run model with best parameter set

      q_sim, st_snow, st_hydro = run_model(st_snow, st_hydro, tair, prec, epot; return_all = true)

      # Store results in data frame

      q_obs = round(q_obs, 2)
      q_sim = round(q_sim, 2)

      df_res = DataFrame(date = Dates.format(date,"yyyy-mm-dd"), q_sim = q_sim, q_obs = q_obs)

      # Save results to txt file

      file_save = dir_cur[1:end-5]

      writetable(string(opt["path_save"], "/valid_txt/", file_save, "_station.txt"), df_res, quotemark = '"', separator = '\t')

      # Plot results

      ioff()

      file_name = string(opt["path_save"], "/valid_png/", file_save, "_hydro.png")

      plot_sim(st_hydro; q_obs = q_obs, file_name = file_name)

      file_name = string(opt["path_save"], "/valid_png/", file_save, "_snow.png")

      plot_sim(st_snow; file_name = file_name)

      # Save parameter values

      writedlm(opt["path_save"] * "/param_snow/" * file_save * "_param_snow.txt", param_snow)
      writedlm(opt["path_save"] * "/param_hydro/" * file_save * "_param_hydro.txt", param_hydro)

      st_snow = eval(Expr(:call, opt["snow_choice"], opt["tstep"], date[1], param_snow, frac))
      st_hydro = eval(Expr(:call, opt["hydro_choice"], opt["tstep"], date[1], param_hydro))

      jldopen(opt["path_save"] * "/model_data/" * file_save * "_modeldata.jld", "w") do file
        addrequire(file, Vann)
        write(file, "st_snow", st_snow)
        write(file, "st_hydro", st_hydro)
      end

      # Compute summary statistics

      station = dir_cur[1:end-5]
      kge_res = kge(q_sim[opt["warmup"]:end], q_obs[opt["warmup"]:end])
      nse_res = nse(q_sim[opt["warmup"]:end], q_obs[opt["warmup"]:end])

      push!(df_valid, [station nse_res kge_res])

    end

  end

  writetable(string(opt["path_save"], "/summary_calib_period.txt"), df_calib, quotemark = '"', separator = '\t')
  writetable(string(opt["path_save"], "/summary_valid_period.txt"), df_valid, quotemark = '"', separator = '\t')

end

# Run all experiments

for i_exper = 1:size(df_exper, 1)

  run_single_experiment(path_inputs, path_save, df_exper, i_exper)

end


#=
This file is auto-generated. Do not edit.
=#
"""
    mutable struct Bus <: Topology
        number::Int64
        name::String
        bustype::Union{Nothing, BusTypes.BusType}
        angle::Union{Nothing, Float64}
        voltage::Union{Nothing, Float64}
        voltagelimits::Union{Nothing, Min_Max}
        basevoltage::Union{Nothing, Float64}
        area::Union{Nothing, Area}
        load_zone::Union{Nothing, LoadZone}
        ext::Dict{String, Any}
        internal::InfrastructureSystemsInternal
    end

A power-system bus.

# Arguments
- `number::Int64`: number associated with the bus
- `name::String`: the name of the bus
- `bustype::Union{Nothing, BusTypes.BusType}`: bus type
- `angle::Union{Nothing, Float64}`: angle of the bus in radians, validation range: (-1.571, 1.571), action if invalid: error
- `voltage::Union{Nothing, Float64}`: voltage as a multiple of basevoltage, validation range: voltagelimits, action if invalid: warn
- `voltagelimits::Union{Nothing, Min_Max}`: limits on the voltage variation as multiples of basevoltage
- `basevoltage::Union{Nothing, Float64}`: the base voltage in kV, validation range: (0, nothing), action if invalid: error
- `area::Union{Nothing, Area}`: the area containing the bus
- `load_zone::Union{Nothing, LoadZone}`: the load zone containing the bus
- `ext::Dict{String, Any}`
- `internal::InfrastructureSystemsInternal`: power system internal reference, do not modify
"""
mutable struct Bus <: Topology
    "number associated with the bus"
    number::Int64
    "the name of the bus"
    name::String
    "bus type"
    bustype::Union{Nothing, BusTypes.BusType}
    "angle of the bus in radians"
    angle::Union{Nothing, Float64}
    "voltage as a multiple of basevoltage"
    voltage::Union{Nothing, Float64}
    "limits on the voltage variation as multiples of basevoltage"
    voltagelimits::Union{Nothing, Min_Max}
    "the base voltage in kV"
    basevoltage::Union{Nothing, Float64}
    "the area containing the bus"
    area::Union{Nothing, Area}
    "the load zone containing the bus"
    load_zone::Union{Nothing, LoadZone}
    ext::Dict{String, Any}
    "power system internal reference, do not modify"
    internal::InfrastructureSystemsInternal

    function Bus(number, name, bustype, angle, voltage, voltagelimits, basevoltage, area, load_zone, ext, internal, )
        (number, name, bustype, angle, voltage, voltagelimits, basevoltage, area, load_zone, ext, internal, ) = CheckBusParams(
            number,
            name,
            bustype,
            angle,
            voltage,
            voltagelimits,
            basevoltage,
            area,
            load_zone,
            ext,
            internal,
        )
        new(number, name, bustype, angle, voltage, voltagelimits, basevoltage, area, load_zone, ext, internal, )
    end
end

function Bus(number, name, bustype, angle, voltage, voltagelimits, basevoltage, area, load_zone, ext=Dict{String, Any}(), )
    Bus(number, name, bustype, angle, voltage, voltagelimits, basevoltage, area, load_zone, ext, InfrastructureSystemsInternal(), )
end

function Bus(; number, name, bustype, angle, voltage, voltagelimits, basevoltage, area, load_zone, ext=Dict{String, Any}(), )
    Bus(number, name, bustype, angle, voltage, voltagelimits, basevoltage, area, load_zone, ext, )
end

# Constructor for demo purposes; non-functional.
function Bus(::Nothing)
    Bus(;
        number=0,
        name="init",
        bustype=nothing,
        angle=0.0,
        voltage=0.0,
        voltagelimits=(min=0.0, max=0.0),
        basevoltage=nothing,
        area=Area(nothing),
        load_zone=LoadZone(nothing),
        ext=Dict{String, Any}(),
    )
end

"""Get Bus number."""
get_number(value::Bus) = value.number

InfrastructureSystems.get_name(value::Bus) = value.name
"""Get Bus bustype."""
get_bustype(value::Bus) = value.bustype
"""Get Bus angle."""
get_angle(value::Bus) = value.angle
"""Get Bus voltage."""
get_voltage(value::Bus) = value.voltage
"""Get Bus voltagelimits."""
get_voltagelimits(value::Bus) = value.voltagelimits
"""Get Bus basevoltage."""
get_basevoltage(value::Bus) = value.basevoltage
"""Get Bus area."""
get_area(value::Bus) = value.area
"""Get Bus load_zone."""
get_load_zone(value::Bus) = value.load_zone
"""Get Bus ext."""
get_ext(value::Bus) = value.ext
"""Get Bus internal."""
get_internal(value::Bus) = value.internal