struct FaceToCellGlue{A,B,C,D} <: GridapType
  face_to_bgface::A
  bgface_to_lcell::B
  face_to_cell::Vector{Int32}
  face_to_lface::Vector{Int8}
  face_to_lcell::Vector{Int8}
  face_to_ftype::C
  cell_to_ctype::D
  cell_to_lface_to_pindex::Table{Int8,Vector{Int8},Vector{Int32}}
  ctype_to_lface_to_ftype::Vector{Vector{Int8}}
end

function FaceToCellGlue(
  topo::GridTopology,
  cell_grid::Grid,
  face_grid::Grid,
  face_to_bgface::AbstractVector,
  bgface_to_lcell::AbstractVector)

  D = num_cell_dims(cell_grid)
  cD = num_cell_dims(face_grid)
  bgface_to_cells = get_faces(topo,cD,D)
  cell_to_bgfaces = get_faces(topo,D,cD)
  cell_to_lface_to_pindex = Table(get_cell_permutations(topo,cD))

  bgface_to_cell = lazy_map(getindex,bgface_to_cells, bgface_to_lcell)
  bgface_to_lface = find_local_index(bgface_to_cell, cell_to_bgfaces)

  face_to_cell = collect(Int32,lazy_map(Reindex(bgface_to_cell), face_to_bgface))
  face_to_lface = collect(Int8,lazy_map(Reindex(bgface_to_lface), face_to_bgface))
  face_to_lcell = collect(Int8,lazy_map(Reindex(bgface_to_lcell), face_to_bgface))

  f = (p)->fill(Int8(UNSET),num_faces(p,cD))
  ctype_to_lface_to_ftype = map( f, get_reffes(cell_grid) )
  face_to_ftype = get_cell_type(face_grid)
  cell_to_ctype = get_cell_type(cell_grid)

  _fill_ctype_to_lface_to_ftype!(
    ctype_to_lface_to_ftype,
    face_to_cell,
    face_to_lface,
    face_to_ftype,
    cell_to_ctype)

  FaceToCellGlue(
    face_to_bgface,
    bgface_to_lcell,
    face_to_cell,
    face_to_lface,
    face_to_lcell,
    face_to_ftype,
    cell_to_ctype,
    cell_to_lface_to_pindex,
    ctype_to_lface_to_ftype)
end

function _fill_ctype_to_lface_to_ftype!(
  ctype_to_lface_to_ftype,
  face_to_cell,
  face_to_lface,
  face_to_ftype,
  cell_to_ctype)
  for (face, cell) in enumerate(face_to_cell)
    ctype = cell_to_ctype[cell]
    lface = face_to_lface[face]
    ftype = face_to_ftype[face]
    ctype_to_lface_to_ftype[ctype][lface] = ftype
  end
end

function get_children(n::TreeNode, a::FaceToCellGlue)
  (
    similar_tree_node(n,a.face_to_cell),
    similar_tree_node(n,a.face_to_lface),
    similar_tree_node(n,a.face_to_lcell),
    similar_tree_node(n,a.face_to_ftype),
    similar_tree_node(n,a.cell_to_ctype),
    similar_tree_node(n,a.cell_to_lface_to_pindex),
    similar_tree_node(n,a.ctype_to_lface_to_ftype)
  )
end

"""
"""
struct BoundaryTriangulation{Dc,Dp,A,B} <: Triangulation{Dc,Dp}
  trian::A
  glue::B

  function BoundaryTriangulation(
    trian::BodyFittedTriangulation,
    glue::FaceToCellGlue)

    Dc = num_cell_dims(trian)
    Dp = num_point_dims(trian)
    A = typeof(trian)
    B = typeof(glue)
    new{Dc,Dp,A,B}(trian,glue)
  end
end

function Boundary(args...;kwargs...)
  BoundaryTriangulation(args...;kwargs...)
end

# Constructors

"""
    BoundaryTriangulation(model::DiscreteModel,face_to_mask::Vector{Bool})
    BoundaryTriangulation(model::DiscreteModel)
"""
function BoundaryTriangulation(
  model::DiscreteModel,
  face_to_bgface::AbstractVector{<:Integer},
  bgface_to_lcell::AbstractVector{<:Integer})

  D = num_cell_dims(model)
  topo = get_grid_topology(model)
  bgface_grid = Grid(ReferenceFE{D-1},model)

  face_grid = view(bgface_grid,face_to_bgface)
  cell_grid = get_grid(model)
  glue = FaceToCellGlue(topo,cell_grid,face_grid,face_to_bgface,bgface_to_lcell)
  trian = BodyFittedTriangulation(model,face_grid,face_to_bgface)

  BoundaryTriangulation(trian,glue)
end

function BoundaryTriangulation(
  model::DiscreteModel,
  face_to_bgface::AbstractVector{<:Integer})
  BoundaryTriangulation(model,face_to_bgface,Fill(1,num_facets(model)))
end

function BoundaryTriangulation(
  model::DiscreteModel,
  bgface_to_mask::AbstractVector{Bool},
  bgface_to_lcell::AbstractVector{<:Integer})

  face_to_bgface = findall(bgface_to_mask)
  BoundaryTriangulation(model,face_to_bgface,bgface_to_lcell)
end

function BoundaryTriangulation(
  model::DiscreteModel, bgface_to_mask::AbstractVector{Bool}, lcell::Integer=1)
  BoundaryTriangulation(model,bgface_to_mask, Fill(lcell,num_facets(model)) )
end

"""
    BoundaryTriangulation(model::DiscreteModel,labeling::FaceLabeling;tags::Vector{Int})
    BoundaryTriangulation(model::DiscreteModel,labeling::FaceLabeling;tags::Vector{String})
    BoundaryTriangulation(model::DiscreteModel,labeling::FaceLabeling;tag::Int)
    BoundaryTriangulation(model::DiscreteModel,labeling::FaceLabeling;tag::String)
"""
function BoundaryTriangulation(model::DiscreteModel,labeling::FaceLabeling;tags=nothing)
  D = num_cell_dims(model)
  if tags == nothing
    topo = get_grid_topology(model)
    face_to_mask = get_isboundary_face(topo,D-1)
  else
    face_to_mask = get_face_mask(labeling,tags,D-1)
  end
  BoundaryTriangulation(model,face_to_mask)
end

"""
    BoundaryTriangulation(model::DiscreteModel,tags::Vector{Int})
    BoundaryTriangulation(model::DiscreteModel,tags::Vector{String})
    BoundaryTriangulation(model::DiscreteModel,tag::Int)
    BoundaryTriangulation(model::DiscreteModel,tag::String)
"""
function BoundaryTriangulation(model::DiscreteModel;tags=nothing)
  labeling = get_face_labeling(model)
  BoundaryTriangulation(model,labeling,tags=tags)
end

function BoundaryTriangulation(rtrian::Triangulation,args...;kwargs...)
  rmodel = get_active_model(rtrian)
  dtrian = BoundaryTriangulation(rmodel,args...;kwargs...)
  CompositeTriangulation(rtrian,dtrian)
end

# API

get_background_model(t::BoundaryTriangulation) = get_background_model(t.trian)
get_grid(t::BoundaryTriangulation) = get_grid(t.trian)
get_glue(t::BoundaryTriangulation{D},::Val{D}) where D = get_glue(t.trian,Val(D))

function get_glue(trian::BoundaryTriangulation,::Val{Dp}) where Dp
  model = get_background_model(trian)
  Dm = num_cell_dims(model)
  get_glue(trian,Val(Dp),Val(Dm))
end

function get_glue(trian::BoundaryTriangulation,::Val{Dp},::Val{Dm}) where {Dp,Dm}
  nothing
end

function get_glue(trian::BoundaryTriangulation,::Val{D},::Val{D}) where D
  tface_to_mface = trian.glue.face_to_cell
  face_to_q_vertex_coords = _compute_face_to_q_vertex_coords(trian)
  f(p) = get_shapefuns(LagrangianRefFE(Float64,get_polytope(p),1))
  ftype_to_shapefuns = map( f, get_reffes(trian) )
  face_to_shapefuns = expand_cell_data(ftype_to_shapefuns,trian.glue.face_to_ftype)
  face_s_q = lazy_map(linear_combination,face_to_q_vertex_coords,face_to_shapefuns)
  tface_to_mface_map = face_s_q
  mface_to_tface = nothing
  FaceToFaceGlue(tface_to_mface,tface_to_mface_map,mface_to_tface)
end

function get_facet_normal(trian::BoundaryTriangulation)

  glue = trian.glue
  cell_grid = get_grid(get_background_model(trian.trian))

  ## Reference normal
  function f(r)
    p = get_polytope(r)
    lface_to_n = get_facet_normal(p)
    lface_to_pindex_to_perm = get_face_vertex_permutations(p,num_cell_dims(p)-1)
    nlfaces = length(lface_to_n)
    lface_pindex_to_n = [ fill(lface_to_n[lface],length(lface_to_pindex_to_perm[lface])) for lface in 1:nlfaces ]
    lface_pindex_to_n
  end
  ctype_lface_pindex_to_nref = map(f, get_reffes(cell_grid))
  face_to_nref = FaceCompressedVector(ctype_lface_pindex_to_nref,glue)
  face_s_nref = lazy_map(constant_field,face_to_nref)

  # Inverse of the Jacobian transpose
  cell_q_x = get_cell_map(cell_grid)
  cell_q_Jt = lazy_map(∇,cell_q_x)
  cell_q_invJt = lazy_map(Operation(pinvJt),cell_q_Jt)
  face_q_invJt = lazy_map(Reindex(cell_q_invJt),glue.face_to_cell)

  # Change of domain
  D = num_cell_dims(cell_grid)
  glue = get_glue(trian,Val(D))
  face_s_q = glue.tface_to_mface_map
  face_s_invJt = lazy_map(∘,face_q_invJt,face_s_q)
  face_s_n = lazy_map(Broadcasting(Operation(push_normal)),face_s_invJt,face_s_nref)
  Fields.MemoArray(face_s_n)
end

@inline function push_normal(invJt,n)
  v = invJt⋅n
  m = sqrt(inner(v,v))
  if m < eps()
    return zero(n)
  else
    return v/m
  end
end

function _compute_face_to_q_vertex_coords(trian::BoundaryTriangulation)
    d = num_cell_dims(trian)
    cell_grid = get_grid(get_background_model(trian.trian))
    polytopes = map(get_polytope, get_reffes(cell_grid))
    cell_to_ctype = trian.glue.cell_to_ctype
    ctype_to_lvertex_to_qcoords = map(get_vertex_coordinates, polytopes)
    ctype_to_lface_to_lvertices = map((p)->get_faces(p,d,0), polytopes)
    ctype_to_lface_to_pindex_to_perm = map( (p)->get_face_vertex_permutations(p,d), polytopes)

    P = eltype(eltype(ctype_to_lvertex_to_qcoords))
    D = num_components(P)
    T = eltype(P)
    ctype_to_lface_to_pindex_to_qcoords = Vector{Vector{Vector{Point{D,T}}}}[]

    for (ctype, lface_to_pindex_to_perm) in enumerate(ctype_to_lface_to_pindex_to_perm)
      lvertex_to_qcoods = ctype_to_lvertex_to_qcoords[ctype]
      lface_to_pindex_to_qcoords = Vector{Vector{Point{D,T}}}[]
      for (lface, pindex_to_perm) in enumerate(lface_to_pindex_to_perm)
        cfvertex_to_lvertex = ctype_to_lface_to_lvertices[ctype][lface]
        nfvertices = length(cfvertex_to_lvertex)
        pindex_to_qcoords = Vector{Vector{Point{D,T}}}(undef,length(pindex_to_perm))
        for (pindex, cfvertex_to_ffvertex) in enumerate(pindex_to_perm)
          ffvertex_to_qcoords = zeros(Point{D,T},nfvertices)
          for (cfvertex, ffvertex) in enumerate(cfvertex_to_ffvertex)
            lvertex = cfvertex_to_lvertex[cfvertex]
            qcoords = lvertex_to_qcoods[lvertex]
            ffvertex_to_qcoords[ffvertex] = qcoords
          end
          pindex_to_qcoords[pindex] = ffvertex_to_qcoords
        end
        push!(lface_to_pindex_to_qcoords,pindex_to_qcoords)
      end
      push!(ctype_to_lface_to_pindex_to_qcoords,lface_to_pindex_to_qcoords)
    end

    FaceCompressedVector(ctype_to_lface_to_pindex_to_qcoords,trian.glue)
end

struct FaceCompressedVector{T,G<:FaceToCellGlue} <: AbstractVector{T}
  ctype_lface_pindex_to_value::Vector{Vector{Vector{T}}}
  glue::G
end

Base.size(a::FaceCompressedVector) = (length(a.glue.face_to_cell),)

Base.IndexStyle(::Type{<:FaceCompressedVector}) = IndexLinear()

function Base.getindex(a::FaceCompressedVector,face::Integer)
  cell = a.glue.face_to_cell[face]
  ctype = a.glue.cell_to_ctype[cell]
  lface = a.glue.face_to_lface[face]
  p = a.glue.cell_to_lface_to_pindex.ptrs[cell]-1
  pindex = a.glue.cell_to_lface_to_pindex.data[p+lface]
  value = a.ctype_lface_pindex_to_value[ctype][lface][pindex]
  value
end

function get_children(n::TreeNode, a::FaceCompressedVector)
  (similar_tree_node(n,a.ctype_lface_pindex_to_value),similar_tree_node(n,a.glue))
end

function lazy_map(k::Fields.LinearCombinationMap,::Type{T},b::FaceCompressedVector,c::Fill) where T
  d = CompressedArray([c.value,],Fill(1,length(c)))
  lazy_map(k,T,a,b,d)
end

function lazy_map(k::Fields.LinearCombinationMap,::Type{T},b::FaceCompressedVector,c::CompressedArray) where T
  if c.ptrs === b.glue.face_to_ftype || c.ptrs == b.glue.face_to_ftype

    ctype_lface_pindex_to_r = Vector{Vector{Vector{T}}}(undef,length(b.ctype_lface_pindex_to_value))
    for (ctype, lface_pindex_to_value) in enumerate(b.ctype_lface_pindex_to_value)
      lface_pindex_to_r = Vector{Vector{T}}(undef,length(lface_pindex_to_value))
      for (lface, pindex_to_value) in enumerate(lface_pindex_to_value)
        pindex_to_r = Vector{T}(undef,length(pindex_to_value))
        ftype = b.glue.ctype_to_lface_to_ftype[ctype][lface]
        if ftype != UNSET
          for (pindex, value) in enumerate(pindex_to_value)
            pindex_to_r[pindex] = k(value,c.values[ftype])
          end
        end
        lface_pindex_to_r[lface] = pindex_to_r
      end
      ctype_lface_pindex_to_r[ctype] = lface_pindex_to_r
    end
    FaceCompressedVector(ctype_lface_pindex_to_r,b.glue)

  else
    @notimplemented
  end
end

function lazy_map(k::typeof(evaluate),::Type{T},a::Fill,b::FaceCompressedVector) where T
  @check length(a) == length(b)
  ctype_lface_pindex_to_r = Vector{Vector{Vector{T}}}(undef,length(b.ctype_lface_pindex_to_value))
  for (ctype, lface_pindex_to_value) in enumerate(b.ctype_lface_pindex_to_value)
    lface_pindex_to_r = Vector{Vector{T}}(undef,length(lface_pindex_to_value))
    for (lface, pindex_to_value) in enumerate(lface_pindex_to_value)
      pindex_to_r = Vector{T}(undef,length(pindex_to_value))
      ftype = b.glue.ctype_to_lface_to_ftype[ctype][lface]
      if ftype != UNSET
        for (pindex, value) in enumerate(pindex_to_value)
          pindex_to_r[pindex] = evaluate(a.value,value)
        end
      end
      lface_pindex_to_r[lface] = pindex_to_r
    end
    ctype_lface_pindex_to_r[ctype] = lface_pindex_to_r
  end
  FaceCompressedVector(ctype_lface_pindex_to_r,b.glue)
end

function lazy_map(
  k::typeof(evaluate),
  ::Type{T},
  a::CompressedArray,
  b::FaceCompressedVector) where T

  @check length(a) == length(b)
  ctype_lface_pindex_to_r = Vector{Vector{Vector{T}}}(undef,length(b.ctype_lface_pindex_to_value))
  for (ctype, lface_pindex_to_value) in enumerate(b.ctype_lface_pindex_to_value)
    lface_pindex_to_r = Vector{Vector{T}}(undef,length(lface_pindex_to_value))
    for (lface, pindex_to_value) in enumerate(lface_pindex_to_value)
      pindex_to_r = Vector{T}(undef,length(pindex_to_value))
      ftype = b.glue.ctype_to_lface_to_ftype[ctype][lface]
      if ftype != UNSET
        for (pindex, value) in enumerate(pindex_to_value)
          pindex_to_r[pindex] = evaluate(a.values[ctype],value)
        end
      end
      lface_pindex_to_r[lface] = pindex_to_r
    end
    ctype_lface_pindex_to_r[ctype] = lface_pindex_to_r
  end
  FaceCompressedVector(ctype_lface_pindex_to_r,b.glue)
end

function lazy_map(k::typeof(evaluate),::Type{T},b::FaceCompressedVector,a::Fill) where T
  @check length(a) == length(b)
  ctype_lface_pindex_to_r = Vector{Vector{Vector{T}}}(undef,length(b.ctype_lface_pindex_to_value))
  for (ctype, lface_pindex_to_value) in enumerate(b.ctype_lface_pindex_to_value)
    lface_pindex_to_r = Vector{Vector{T}}(undef,length(lface_pindex_to_value))
    for (lface, pindex_to_value) in enumerate(lface_pindex_to_value)
      pindex_to_r = Vector{T}(undef,length(pindex_to_value))
      ftype = b.glue.ctype_to_lface_to_ftype[ctype][lface]
      if ftype != UNSET
        for (pindex, value) in enumerate(pindex_to_value)
          pindex_to_r[pindex] = evaluate(value,a.value)
        end
      end
      lface_pindex_to_r[lface] = pindex_to_r
    end
    ctype_lface_pindex_to_r[ctype] = lface_pindex_to_r
  end
  FaceCompressedVector(ctype_lface_pindex_to_r,b.glue)
end

function lazy_map(k::typeof(evaluate),::Type{T},a::Fill,b::FaceCompressedVector,c::FaceCompressedVector) where T
  if b.glue !== c.glue
    return LazyArray(T,a,b,c)
  end
  @check length(a) == length(b)
  ctype_lface_pindex_to_r = Vector{Vector{Vector{T}}}(undef,length(b.ctype_lface_pindex_to_value))
  for (ctype, lface_pindex_to_value) in enumerate(b.ctype_lface_pindex_to_value)
    lface_pindex_to_r = Vector{Vector{T}}(undef,length(lface_pindex_to_value))
    for (lface, pindex_to_value) in enumerate(lface_pindex_to_value)
      pindex_to_r = Vector{T}(undef,length(pindex_to_value))
      ftype = b.glue.ctype_to_lface_to_ftype[ctype][lface]
      if ftype != UNSET
        for (pindex, bvalue) in enumerate(pindex_to_value)
          cvalue = c.ctype_lface_pindex_to_value[ctype][lface][pindex]
          pindex_to_r[pindex] = evaluate(a.value,bvalue,cvalue)
        end
      end
      lface_pindex_to_r[lface] = pindex_to_r
    end
    ctype_lface_pindex_to_r[ctype] = lface_pindex_to_r
  end
  FaceCompressedVector(ctype_lface_pindex_to_r,b.glue)
end


function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    ::DeviceModel{T, FixedOutput},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)
    add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)
    add_to_expression!(
        container,
        ActivePowerBalance,
        ActivePowerTimeSeriesParameter,
        devices,
        model,
        S,
    )
    return
end

function construct_device!(
    ::OptimizationContainer,
    ::PSY.System,
    ::ModelConstructStage,
    ::DeviceModel{T, FixedOutput},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    # FixedOutput doesn't add any constraints to the model. This function covers
    # AbstractPowerModel and AbtractActivePowerModel
    return
end

"""
This function creates the arguments for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, D},
    ::Type{S},
) where {
    T <: PSY.ThermalGen,
    D <: AbstractStandardUnitCommitment,
    S <: PM.AbstractPowerModel,
}
    devices = get_available_components(T, sys)

    add_variables!(container, ActivePowerVariable, devices, D())
    add_variables!(container, ReactivePowerVariable, devices, D())
    add_variables!(container, OnVariable, devices, D())
    add_variables!(container, StartVariable, devices, D())
    add_variables!(container, StopVariable, devices, D())

    add_variables!(container, TimeDurationOn, devices, D())
    add_variables!(container, TimeDurationOff, devices, D())

    initial_conditions!(container, devices, D())

    add_to_expression!(
        container,
        ActivePowerBalance,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ReactivePowerBalance,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_feedforward_arguments!(container, model, devices)
    return
end

"""
This function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, D},
    ::Type{S},
) where {
    T <: PSY.ThermalGen,
    D <: AbstractStandardUnitCommitment,
    S <: PM.AbstractPowerModel,
}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ReactivePowerVariableLimitsConstraint,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )
    add_constraints!(container, CommitmentConstraint, devices, model, S)
    add_constraints!(container, RampConstraint, devices, model, S)
    add_constraints!(container, DurationConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

"""
This function creates the arguments model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, D},
    ::Type{S},
) where {
    T <: PSY.ThermalGen,
    D <: AbstractStandardUnitCommitment,
    S <: PM.AbstractActivePowerModel,
}
    devices = get_available_components(T, sys)

    add_variables!(container, ActivePowerVariable, devices, D())
    add_variables!(container, OnVariable, devices, D())
    add_variables!(container, StartVariable, devices, D())
    add_variables!(container, StopVariable, devices, D())

    add_variables!(container, TimeDurationOn, devices, D())
    add_variables!(container, TimeDurationOff, devices, D())

    initial_conditions!(container, devices, D())

    add_to_expression!(
        container,
        ActivePowerBalance,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

"""
This function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, D},
    ::Type{S},
) where {
    T <: PSY.ThermalGen,
    D <: AbstractStandardUnitCommitment,
    S <: PM.AbstractActivePowerModel,
}
    devices = get_available_components(T, sys)
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(container, CommitmentConstraint, devices, model, S)
    add_constraints!(container, RampConstraint, devices, model, S)
    add_constraints!(container, DurationConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)

    add_constraint_dual!(container, sys, model)
    return
end

"""
This function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, ThermalBasicUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(T, sys)

    add_variables!(container, ActivePowerVariable, devices, ThermalBasicUnitCommitment())
    add_variables!(container, ReactivePowerVariable, devices, ThermalBasicUnitCommitment())
    add_variables!(container, OnVariable, devices, ThermalBasicUnitCommitment())
    add_variables!(container, StartVariable, devices, ThermalBasicUnitCommitment())
    add_variables!(container, StopVariable, devices, ThermalBasicUnitCommitment())

    initial_conditions!(container, devices, ThermalBasicUnitCommitment())

    add_to_expression!(
        container,
        ActivePowerBalance,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ReactivePowerBalance,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

"""
This function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, ThermalBasicUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(
        container,
        ReactivePowerVariableLimitsConstraint,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )
    add_constraints!(container, CommitmentConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

"""
This function creates the arguments for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, ThermalBasicUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)

    add_variables!(container, ActivePowerVariable, devices, ThermalBasicUnitCommitment())
    add_variables!(container, OnVariable, devices, ThermalBasicUnitCommitment())
    add_variables!(container, StartVariable, devices, ThermalBasicUnitCommitment())
    add_variables!(container, StopVariable, devices, ThermalBasicUnitCommitment())

    initial_conditions!(container, devices, ThermalBasicUnitCommitment())

    add_to_expression!(
        container,
        ActivePowerBalance,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

"""
This function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, ThermalBasicUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(container, CommitmentConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

"""
This function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, ThermalStandardDispatch},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(T, sys)

    add_variables!(container, ActivePowerVariable, devices, ThermalStandardDispatch())
    add_variables!(container, ReactivePowerVariable, devices, ThermalStandardDispatch())

    initial_conditions!(container, devices, ThermalStandardDispatch())

    add_to_expression!(
        container,
        ActivePowerBalance,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ReactivePowerBalance,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

"""
This function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, ThermalStandardDispatch},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(
        container,
        ReactivePowerVariableLimitsConstraint,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )
    add_constraints!(container, RampConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

"""
This function creates the arguments for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, ThermalStandardDispatch},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)

    add_variables!(container, ActivePowerVariable, devices, ThermalStandardDispatch())

    initial_conditions!(container, devices, ThermalStandardDispatch())

    add_to_expression!(
        container,
        ActivePowerBalance,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

"""
This function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation
"""
function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, ThermalStandardDispatch},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(container, RampConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, D},
    ::Type{S},
) where {
    T <: PSY.ThermalGen,
    D <: AbstractThermalDispatchFormulation,
    S <: PM.AbstractPowerModel,
}
    devices = get_available_components(T, sys)

    add_variables!(container, ActivePowerVariable, devices, D())
    add_variables!(container, ReactivePowerVariable, devices, D())

    add_to_expression!(
        container,
        ActivePowerBalance,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ReactivePowerBalance,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, D},
    ::Type{S},
) where {
    T <: PSY.ThermalGen,
    D <: AbstractThermalDispatchFormulation,
    S <: PM.AbstractPowerModel,
}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(
        container,
        ReactivePowerVariableLimitsConstraint,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, D},
    ::Type{S},
) where {
    T <: PSY.ThermalGen,
    D <: AbstractThermalDispatchFormulation,
    S <: PM.AbstractActivePowerModel,
}
    devices = get_available_components(T, sys)

    add_variables!(container, ActivePowerVariable, devices, D())

    add_to_expression!(
        container,
        ActivePowerBalance,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        ActivePowerVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, D},
    ::Type{S},
) where {
    T <: PSY.ThermalGen,
    D <: AbstractThermalDispatchFormulation,
    S <: PM.AbstractActivePowerModel,
}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment},
    ::Type{S};
    kwargs...,
) where {S <: PM.AbstractPowerModel}
    devices = get_available_components(PSY.ThermalMultiStart, sys)

    add_variables!(
        container,
        PowerAboveMinimumVariable,
        devices,
        ThermalMultiStartUnitCommitment(),
    )
    add_variables!(
        container,
        ReactivePowerVariable,
        devices,
        ThermalMultiStartUnitCommitment(),
    )
    add_variables!(container, OnVariable, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, StopVariable, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, StartVariable, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, ColdStartVariable, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, WarmStartVariable, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, HotStartVariable, devices, ThermalMultiStartUnitCommitment())

    add_variables!(container, TimeDurationOn, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, TimeDurationOff, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, PowerOutput, devices, ThermalMultiStartUnitCommitment())

    initial_conditions!(container, devices, ThermalMultiStartUnitCommitment())

    add_to_expression!(
        container,
        ActivePowerBalance,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(container, ActivePowerBalance, OnVariable, devices, model, S)
    add_to_expression!(
        container,
        ReactivePowerBalance,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment},
    ::Type{S};
    kwargs...,
) where {S <: PM.AbstractPowerModel}
    devices = get_available_components(PSY.ThermalMultiStart, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(
        container,
        ReactivePowerVariableLimitsConstraint,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )
    add_constraints!(container, CommitmentConstraint, devices, model, S)
    add_constraints!(container, RampConstraint, devices, model, S)
    add_constraints!(container, DurationConstraint, devices, model, S)
    add_constraints!(container, StartupTimeLimitTemperatureConstraint, devices, model, S)
    add_constraints!(container, StartTypeConstraint, devices, model, S)
    add_constraints!(container, StartupInitialConditionConstraint, devices, model, S)
    add_constraints!(container, MustRunConstraint, devices, model, S)
    add_constraints!(container, ActiveRangeICConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment},
    ::Type{S},
) where {S <: PM.AbstractActivePowerModel}
    devices = get_available_components(PSY.ThermalMultiStart, sys)

    add_variables!(
        container,
        PowerAboveMinimumVariable,
        devices,
        ThermalMultiStartUnitCommitment(),
    )
    add_variables!(container, OnVariable, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, StopVariable, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, StartVariable, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, ColdStartVariable, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, WarmStartVariable, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, HotStartVariable, devices, ThermalMultiStartUnitCommitment())

    add_variables!(container, TimeDurationOn, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, TimeDurationOff, devices, ThermalMultiStartUnitCommitment())
    add_variables!(container, PowerOutput, devices, ThermalMultiStartUnitCommitment())

    add_to_expression!(
        container,
        ActivePowerBalance,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(container, ActivePowerBalance, OnVariable, devices, model, S)

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment},
    ::Type{S},
) where {S <: PM.AbstractActivePowerModel}
    devices = get_available_components(PSY.ThermalMultiStart, sys)

    initial_conditions!(container, devices, ThermalMultiStartUnitCommitment())

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(container, CommitmentConstraint, devices, model, S)
    add_constraints!(container, RampConstraint, devices, model, S)
    add_constraints!(container, DurationConstraint, devices, model, S)
    add_constraints!(container, StartupTimeLimitTemperatureConstraint, devices, model, S)
    add_constraints!(container, StartTypeConstraint, devices, model, S)
    add_constraints!(container, StartupInitialConditionConstraint, devices, model, S)
    add_constraints!(container, MustRunConstraint, devices, model, S)
    add_constraints!(container, ActiveRangeICConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, ThermalCompactUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(T, sys)

    add_variables!(
        container,
        PowerAboveMinimumVariable,
        devices,
        ThermalCompactUnitCommitment(),
    )
    add_variables!(
        container,
        ReactivePowerVariable,
        devices,
        ThermalCompactUnitCommitment(),
    )
    add_variables!(container, OnVariable, devices, ThermalCompactUnitCommitment())
    add_variables!(container, StartVariable, devices, ThermalCompactUnitCommitment())
    add_variables!(container, StopVariable, devices, ThermalCompactUnitCommitment())

    add_variables!(container, TimeDurationOn, devices, ThermalCompactUnitCommitment())
    add_variables!(container, TimeDurationOff, devices, ThermalCompactUnitCommitment())
    add_variables!(container, PowerOutput, devices, ThermalCompactUnitCommitment())

    initial_conditions!(container, devices, ThermalCompactUnitCommitment())

    add_to_expression!(
        container,
        ActivePowerBalance,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(container, ActivePowerBalance, OnVariable, devices, model, S)

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, ThermalCompactUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(
        container,
        ReactivePowerVariableLimitsConstraint,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )
    add_constraints!(container, CommitmentConstraint, devices, model, S)
    add_constraints!(container, RampConstraint, devices, model, S)
    add_constraints!(container, DurationConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, ThermalCompactUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)

    add_variables!(
        container,
        PowerAboveMinimumVariable,
        devices,
        ThermalCompactUnitCommitment(),
    )
    add_variables!(container, OnVariable, devices, ThermalCompactUnitCommitment())
    add_variables!(container, StartVariable, devices, ThermalCompactUnitCommitment())
    add_variables!(container, StopVariable, devices, ThermalCompactUnitCommitment())

    add_variables!(container, TimeDurationOn, devices, ThermalCompactUnitCommitment())
    add_variables!(container, TimeDurationOff, devices, ThermalCompactUnitCommitment())
    add_variables!(container, PowerOutput, devices, ThermalCompactUnitCommitment())

    initial_conditions!(container, devices, ThermalCompactUnitCommitment())

    add_to_expression!(
        container,
        ActivePowerBalance,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(container, ActivePowerBalance, OnVariable, devices, model, S)

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_feedforward_arguments!(container, model, devices)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, ThermalCompactUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(container, CommitmentConstraint, devices, model, S)
    add_constraints!(container, RampConstraint, devices, model, S)
    add_constraints!(container, DurationConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, ThermalBasicCompactUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(T, sys)

    add_variables!(
        container,
        PowerAboveMinimumVariable,
        devices,
        ThermalBasicCompactUnitCommitment(),
    )
    add_variables!(
        container,
        ReactivePowerVariable,
        devices,
        ThermalBasicCompactUnitCommitment(),
    )
    add_variables!(container, OnVariable, devices, ThermalBasicCompactUnitCommitment())
    add_variables!(container, StartVariable, devices, ThermalBasicCompactUnitCommitment())
    add_variables!(container, StopVariable, devices, ThermalBasicCompactUnitCommitment())

    add_variables!(container, PowerOutput, devices, ThermalBasicCompactUnitCommitment())

    initial_conditions!(container, devices, ThermalBasicCompactUnitCommitment())

    add_to_expression!(
        container,
        ActivePowerBalance,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(container, ActivePowerBalance, OnVariable, devices, model, S)

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )

    add_feedforward_arguments!(container, model, devices)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, ThermalBasicCompactUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(
        container,
        ReactivePowerVariableLimitsConstraint,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )
    add_constraints!(container, CommitmentConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, ThermalBasicCompactUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)

    add_variables!(
        container,
        PowerAboveMinimumVariable,
        devices,
        ThermalBasicCompactUnitCommitment(),
    )
    add_variables!(container, OnVariable, devices, ThermalBasicCompactUnitCommitment())
    add_variables!(container, StartVariable, devices, ThermalBasicCompactUnitCommitment())
    add_variables!(container, StopVariable, devices, ThermalBasicCompactUnitCommitment())

    add_variables!(container, PowerOutput, devices, ThermalBasicCompactUnitCommitment())

    initial_conditions!(container, devices, ThermalBasicCompactUnitCommitment())

    add_to_expression!(
        container,
        ActivePowerBalance,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(container, ActivePowerBalance, OnVariable, devices, model, S)

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_feedforward_arguments!(container, model, devices)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, ThermalBasicCompactUnitCommitment},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(container, CommitmentConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, ThermalCompactDispatch},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(T, sys)

    add_variables!(container, PowerAboveMinimumVariable, devices, ThermalCompactDispatch())
    add_variables!(container, ReactivePowerVariable, devices, ThermalCompactDispatch())

    add_variables!(container, PowerOutput, devices, ThermalCompactDispatch())

    add_parameters!(container, OnStatusParameter(), devices, model)

    add_feedforward_arguments!(container, model, devices)

    initial_conditions!(container, devices, ThermalCompactDispatch())

    add_to_expression!(
        container,
        ActivePowerBalance,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ReactivePowerBalance,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(container, ActivePowerBalance, OnStatusParameter, devices, model, S)
    add_to_expression!(
        container,
        ReactivePowerBalance,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(container, ActivePowerBalance, OnStatusParameter, devices, model, S)
    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, ThermalCompactDispatch},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(
        container,
        ReactivePowerVariableLimitsConstraint,
        ReactivePowerVariable,
        devices,
        model,
        S,
    )
    add_constraints!(container, RampConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ArgumentConstructStage,
    model::DeviceModel{T, ThermalCompactDispatch},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)

    add_variables!(container, PowerAboveMinimumVariable, devices, ThermalCompactDispatch())

    add_variables!(container, PowerOutput, devices, ThermalCompactDispatch())

    add_parameters!(container, OnStatusParameter(), devices, model)

    add_feedforward_arguments!(container, model, devices)

    add_to_expression!(
        container,
        ActivePowerBalance,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )

    add_to_expression!(container, ActivePowerBalance, OnStatusParameter, devices, model, S)

    initial_conditions!(container, devices, ThermalCompactDispatch())

    add_expressions!(container, ProductionCostExpression, devices, model)

    add_to_expression!(
        container,
        ActivePowerRangeExpressionLB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    add_to_expression!(
        container,
        ActivePowerRangeExpressionUB,
        PowerAboveMinimumVariable,
        devices,
        model,
        S,
    )
    return
end

function construct_device!(
    container::OptimizationContainer,
    sys::PSY.System,
    ::ModelConstructStage,
    model::DeviceModel{T, ThermalCompactDispatch},
    ::Type{S},
) where {T <: PSY.ThermalGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(T, sys)

    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionLB,
        devices,
        model,
        S,
    )
    add_constraints!(
        container,
        ActivePowerVariableLimitsConstraint,
        ActivePowerRangeExpressionUB,
        devices,
        model,
        S,
    )

    add_constraints!(container, RampConstraint, devices, model, S)

    add_feedforward_constraints!(container, model, devices)

    objective_function!(container, devices, model, S)
    add_constraint_dual!(container, sys, model)
    return
end


using GLAbstraction, GLWindow, GeometryTypes, ColorTypes, GLFW, Reactive, ModernGL
const vert = vert"""
{{GLSL_VERSION}}
in vec2 pos;
out vec2 g_position;

void main() {
    g_position = pos;
}
"""

const frag = frag"""
{{GLSL_VERSION}}

out vec4 outColor;

void main() {
    outColor = vec4(1,0,0,1);
}
"""

const geom = geom"""

{{GLSL_VERSION}}

layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
in vec2 g_position[];

void main(void)
{
  // get the four vertices passed to the shader:
  vec2 p0 = g_position[0];   // start of previous segment

  gl_Position = vec4(p0, 0, 1);
  EmitVertex();

  gl_Position = vec4(p0+vec2(0,0.1), 0, 1);
  EmitVertex();

  gl_Position = vec4(p0+vec2(0.1,0), 0, 1);
  EmitVertex();

  gl_Position = vec4(p0+vec2(0.1), 0, 1);
  EmitVertex();

  EndPrimitive();
}
"""
const window = GLWindow.create_glcontext("Geometry Shader")

const b = Point2f0[(-0.5,0),(0.0, 0.0),(0.4, 0.3)]

data = Dict{Symbol, Any}(
    :pos => GLBuffer(b),
)

program = GLAbstraction.LazyShader(vert, geom, frag)
robj = std_renderobject(data, program, Signal(AABB(Vec3f0(0), Vec3f0(1))), GL_POINTS)
println(robj)
glClearColor(0,0,0,1)
glClearColor(0, 0, 0, 1)

while isopen(window)
  	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    render(robj)
  	swapbuffers(window)
  	pollevents()
end


export iauApci13
"""
For a terrestrial observer, prepare star-independent astrometry
parameters for transformations between ICRS and geocentric CIRS
coordinates.  The caller supplies the date, and SOFA models are used
to predict the Earth ephemeris and CIP/CIO.

The parameters produced by this function are required in the
parallax, light deflection, aberration, and bias-precession-nutation
parts of the astrometric transformation chain.

This function is part of the International Astronomical Union's
SOFA (Standards of Fundamental Astronomy) software collection.

Status:  support function.

Given:
   date1  double      TDB as a 2-part...
   date2  double      ...Julian Date (Note 1)

Returned:
   astrom iauASTROM*  star-independent astrometry parameters:
      pmt    double       PM time interval (SSB, Julian years)
      eb     double[3]    SSB to observer (vector, au)
      eh     double[3]    Sun to observer (unit vector)
      em     double       distance from Sun to observer (au)
      v      double[3]    barycentric observer velocity (vector, c)
      bm1    double       sqrt(1-|v|^2): reciprocal of Lorenz factor
      bpn    double[3][3] bias-precession-nutation matrix
      along  double       unchanged
      xpl    double       unchanged
      ypl    double       unchanged
      sphi   double       unchanged
      cphi   double       unchanged
      diurab double       unchanged
      eral   double       unchanged
      refa   double       unchanged
      refb   double       unchanged
   eo     double*     equation of the origins (ERA-GST)

Notes:

   1. The TDB date date1+date2 is a Julian Date, apportioned in any
   convenient way between the two arguments.  For example,
   JD(TDB)=2450123.7 could be expressed in any of these ways, among
   others:

         date1          date2

      2450123.7           0.0       (JD method)
      2451545.0       -1421.3       (J2000 method)
      2400000.5       50123.2       (MJD method)
      2450123.5           0.2       (date & time method)

   The JD method is the most natural and convenient to use in cases
   where the loss of several decimal digits of resolution is
   acceptable.  The J2000 method is best matched to the way the
   argument is handled internally and will deliver the optimum
   resolution.  The MJD method and the date & time methods are both
   good compromises between resolution and convenience.  For most
   applications of this function the choice will not be at all
   critical.

   TT can be used instead of TDB without any significant impact on
   accuracy.

   2. All the vectors are with respect to BCRS axes.

   3. In cases where the caller wishes to supply his own Earth
   ephemeris and CIP/CIO, the function iauApci can be used instead
   of the present function.

   4. This is one of several functions that inserts into the astrom
   structure star-independent parameters needed for the chain of
   astrometric transformations ICRS <-> GCRS <-> CIRS <-> observed.

   The various functions support different classes of observer and
   portions of the transformation chain:

         functions         observer        transformation

      iauApcg iauApcg13    geocentric      ICRS <-> GCRS
      iauApci iauApci13    terrestrial     ICRS <-> CIRS
      iauApco iauApco13    terrestrial     ICRS <-> observed
      iauApcs iauApcs13    space           ICRS <-> GCRS
      iauAper iauAper13    terrestrial     update Earth rotation
      iauApio iauApio13    terrestrial     CIRS <-> observed

   Those with names ending in "13" use contemporary SOFA models to
   compute the various ephemerides.  The others accept ephemerides
   supplied by the caller.

   The transformation from ICRS to GCRS covers space motion,
   parallax, light deflection, and aberration.  From GCRS to CIRS
   comprises frame bias and precession-nutation.  From CIRS to
   observed takes account of Earth rotation, polar motion, diurnal
   aberration and parallax (unless subsumed into the ICRS <-> GCRS
   transformation), and atmospheric refraction.

   5. The context structure astrom produced by this function is used by
   iauAtciq* and iauAticq*.

Called:
   iauEpv00     Earth position and velocity
   iauPnm06a    classical NPB matrix, IAU 2006/2000A
   iauBpn2xy    extract CIP X,Y coordinates from NPB matrix
   iauS06       the CIO locator s, given X,Y, IAU 2006
   iauApci      astrometry parameters, ICRS-CIRS
   iauEors      equation of the origins, given NPB matrix and s

This revision:   2013 October 9

SOFA release 2018-01-30

Copyright (C) 2018 IAU SOFA Board.  See notes at end.
"""
function iauApci13(date1::Real, date2::Real)
   # Allocate return value
   ref_astrom = Ref{iauASTROM}(iauASTROM())
   ref_eo     = Ref{Float64}(0.0)

   ccall((:iauApci13, libsofa_c), Cvoid,
           (Cdouble, Cdouble, Ref{iauASTROM}, Ref{Cdouble}),
           convert(Float64, date1),
           convert(Float64, date2),
           ref_astrom, ref_eo)

   return ref_astrom[], ref_eo[]
end


export load_ts_file, load_wf_file, spk_read_channel, spk_read_header,
    spk_channel_labels, spk_read_par

import JSON

# ============================================================================ #
# SpkHeader type for internal use only
# ============================================================================ #
mutable struct SpkHeader
    nchan::UInt16
    offset::Vector{UInt64}
    file::AbstractString
end
# ============================================================================ #
function load_ts_file(ifile::AbstractString)
    ext = splitext(ifile)[2]
    if ext == ".txt"
        return float(open(readlines, ifile))
    elseif ext != ".ts"
        warn("Input file does not have the proper extension, results may vary")
    end

    open(ifile, "r") do io
        seekend(io)
        nbyte = position(io)
        nel = floor(Int64, nbyte / sizeof(Float64))
        seekstart(io)

        return read(io, Float64, nel)
    end
end
# ============================================================================ #
function load_wf_file(ifile::AbstractString)
    open(ifile, "r") do io
        row = read(io, UInt64)
        col = read(io, UInt64)
        x = read(io, Float64, row, col)

        return x
    end
end
# ============================================================================ #
function spk_read_channel(ifile::AbstractString, label::AbstractString)
    hdr = spk_read_header(ifile)
    labels = spk_channel_labels(hdr)

    label in labels || error("Given channel does not exist")

    kchan = findfirst(isequal(label), labels)

    open(ifile, "r") do io

        seek(io, hdr.offset[kchan] + 64)

        nts = read(io, UInt64)
        wf_pts = read(io, UInt64)
        nmrk = read(io, UInt64)

        data = Dict{Symbol, Any}()

        data[:ts] = Vector{Float64}(undef, nts)
        read!(io, data[:ts])

        if wf_pts > 0
            data[:wf] = Matrix{Float64}(undef, wf_pts, nts)
            read!(io, data[:wf])
        else
            data[:wf] = Matrix{Float64}(undef, 0, 0)
        end
        data[:mrk] = Vector{Float64}(undef, nmrk)
        read!(io, data[:mrk])

        return data
    end
end
# ============================================================================ #
function spk_read_header(ifile::AbstractString)
    isfile(ifile) || error("File \"$(ifile)\" does not exist")

    open(ifile, "r") do io
        nchan = read(io, UInt16)
        offset = Vector{UInt64}(undef, Int64(nchan))
        read!(io, offset)

        return SpkHeader(nchan, offset, ifile)
    end
end
# ============================================================================ #
function spk_channel_labels(ifile::AbstractString)
    return spk_channel_labels(spk_read_header(ifile))
end
# ---------------------------------------------------------------------------- #
function spk_channel_labels(hdr::SpkHeader)
    open(hdr.file, "r") do io
        labels = Vector{AbstractString}(undef, hdr.nchan)
        for k = 1:hdr.nchan
            seek(io, hdr.offset[k])
            tmp = Vector{UInt8}(undef, 64)
            read!(io, tmp)
            labels[k] = strip(String(tmp), '\0')
        end

        return labels
    end
end
# ============================================================================ #
function spk_read_par(ifile::String)
    file, ext = splitext(ifile)
    parfile = file * ".json"
    if !isfile(parfile)
        error("Failed to locate PAR/JSON file: \"$(parfile)\"")
    end
    return JSON.parse(read(parfile, String))
end
# ============================================================================ #


__precompile__()

module FastStyleTransfer

using Flux, Metalhead, Images, CUDA, NNlib, BSON

CUDA.allowscalar(false)

export train, stylize

include("utils.jl")
include("layers.jl")
include("vgg.jl")
include("transformer_net.jl")
include("neural_style.jl")

end # module


@testset "NodeResult" begin end

@testset "CurrentState" begin
    fm = _build_dmip_formulation()

    nvars = Cerberus.num_variables(fm)
    pb_float = 12.4
    pb_int = 12

    cs1 = @inferred _CurrentState()
    cs2 = @inferred _CurrentState(primal_bound = pb_float)
    cs3 = @inferred _CurrentState(primal_bound = pb_int)

    @test length(cs1.tree) == 1
    @test length(cs2.tree) == 1
    @test length(cs3.tree) == 1

    @test Cerberus._is_root_node(Cerberus.pop_node!(cs1.tree))
    @test Cerberus._is_root_node(Cerberus.pop_node!(cs2.tree))
    @test Cerberus._is_root_node(Cerberus.pop_node!(cs3.tree))

    @test cs1.primal_bound == Inf
    @test cs2.primal_bound == pb_float
    @test cs3.primal_bound == pb_int

    @test cs1.dual_bound == -Inf
    @test cs2.dual_bound == -Inf
    @test cs3.dual_bound == -Inf

    @test isempty(cs1.best_solution)
    @test isempty(cs2.best_solution)
    @test isempty(cs3.best_solution)

    @test cs1.total_node_count == 0
    @test cs2.total_node_count == 0
    @test cs3.total_node_count == 0

    @test cs1.total_simplex_iters == 0
    @test cs2.total_simplex_iters == 0
    @test cs3.total_simplex_iters == 0
end

@testset "copy(::Basis)" begin
    src = Cerberus.Basis(
        [MOI.NONBASIC_AT_LOWER, MOI.BASIC, MOI.NONBASIC_AT_LOWER],
        [MOI.BASIC],
        MOI.BasisStatusCode[],
        [MOI.NONBASIC],
        MOI.BasisStatusCode[],
        [MOI.NONBASIC_AT_UPPER],
    )
    dest = copy(src)
    @test src.base_var_constrs == dest.base_var_constrs
    @test src.base_lt_constrs == dest.base_lt_constrs
    @test src.base_gt_constrs == dest.base_gt_constrs
    @test src.base_et_constrs == dest.base_et_constrs
    @test src.branch_lt_constrs == dest.branch_lt_constrs
    @test src.branch_gt_constrs == dest.branch_gt_constrs
    empty!(src.base_var_constrs)
    empty!(src.base_lt_constrs)
    empty!(src.base_gt_constrs)
    empty!(src.base_et_constrs)
    empty!(src.branch_lt_constrs)
    empty!(src.branch_gt_constrs)
    @test dest.base_var_constrs ==
          [MOI.NONBASIC_AT_LOWER, MOI.BASIC, MOI.NONBASIC_AT_LOWER]
    @test dest.base_lt_constrs == [MOI.BASIC]
    @test isempty(dest.base_gt_constrs)
    @test dest.base_et_constrs == [MOI.NONBASIC]
    @test isempty(dest.branch_lt_constrs)
    @test dest.branch_gt_constrs == [MOI.NONBASIC_AT_UPPER]
end

@testset "reset_formulation_state!" begin
    form = _build_formulation_with_single_disjunction()
    state = Cerberus.CurrentState()
    node = Cerberus.Node(
        [Cerberus.BoundUpdate(_CVI(5), _LT(0.0))],
        [Cerberus.BoundUpdate(_CVI(3), _GT(1.0))],
        [Cerberus.AffineConstraint(_CSAF([2.3], [_CVI(2)], 0.0), _LT(2.3))],
        [Cerberus.AffineConstraint(_CSAF([2.3], [_CVI(2)], 0.0), _GT(2.3))],
        4,
    )
    node_result = Cerberus.NodeResult(node)
    Cerberus.populate_lp_model!(state, form, node, node_result, CONFIG)
    Cerberus.apply_branchings!(state, node)
    Cerberus.formulate_disjunctions!(state, form, node, node_result, CONFIG)
    @test length(state._variable_indices) == 5
    @test length(state.constraint_state.base_state.var_constrs) == 5
    @test isempty(state.constraint_state.base_state.lt_constrs)
    @test length(state.constraint_state.base_state.gt_constrs) == 1
    @test isempty(state.constraint_state.base_state.et_constrs)
    @test state.constraint_state.branch_state.num_lt_branches == 1
    @test state.constraint_state.branch_state.num_gt_branches == 1
    @test length(state.constraint_state.branch_state.lt_general_constrs) == 1
    @test length(state.constraint_state.branch_state.lt_general_constrs) == 1
    @test length(state.disjunction_state) == 1

    Cerberus.reset_formulation_state!(state)
    @test isempty(state._variable_indices)
    @test isempty(state.constraint_state.base_state.var_constrs)
    @test isempty(state.constraint_state.base_state.lt_constrs)
    @test isempty(state.constraint_state.base_state.gt_constrs)
    @test isempty(state.constraint_state.base_state.et_constrs)
    @test state.constraint_state.branch_state.num_lt_branches == 0
    @test state.constraint_state.branch_state.num_gt_branches == 0
    @test isempty(state.constraint_state.branch_state.lt_general_constrs)
    @test isempty(state.constraint_state.branch_state.lt_general_constrs)
    @test isempty(state.disjunction_state)
end

@testset "instantiate" begin
    form = _build_dmip_formulation()
    state = Cerberus.CurrentState()
    vis = state._variable_indices
    @test isempty(vis)
    node = Cerberus.Node()
    node_result = Cerberus.NodeResult(node)
    Cerberus.populate_lp_model!(state, form, node, node_result, CONFIG)
    @test length(vis) == 3
    vi_1 = @inferred Cerberus.instantiate(_CVI(1), state)
    @test vi_1 == vis[1]
    @test Cerberus.instantiate(_CVI(2), state) == vis[2]
    @test Cerberus.instantiate(_CVI(3), state) == vis[3]
    svs = _SV.(vis)
    @test _is_equal(
        Cerberus.instantiate(
            _CSAF([1.2, 3.4, 5.6], [_CVI(3), _CVI(1), _CVI(2)], 7.8),
            state,
        ),
        1.2 * svs[3] + 3.4 * svs[1] + 5.6 * svs[2] + 7.8,
    )
    @test_throws BoundsError Cerberus.instantiate(_CVI(4), state)

    cvi = @inferred Cerberus.attach_index!(state, _VI(7))
    @test cvi == _CVI(4)
end


##############################################
### SRID "datum" conversions               ###
### (other point types don't have datums)  ###
### Done by Proj4                          ###
##############################################

# CRS{SRID} -> CRS{SRID}
function geotransform{T,U}(::Type{T}, X::U, ::Type{Proj4Handler}, ::Type{Proj4Handler})
    if (T == U)
        out = T(getX(X), getY(X), getZ(X))
    else
        v = [getX(X), getY(X), getZ(X)]
        Proj4.transform!(get_projection(U), get_projection(T), v, false)
        out = T(v[1], v[2], v[3])
    end
end

# CRS{SRID} -> CRS{SRID}, with srids specified by an extra variable (output SRID, input SRID)
function geotransform{T, U, S1, S2}(::Type{T}, X::U, srids::Tuple{S1, S2}, ::Type{Proj4Handler}, ::Type{Proj4Handler})
    v = [getX(X), getY(X), getZ(X)]
    Proj4.transform!(get_projection(srids[2]), get_projection(srids[1]), v)
    out = CRS{UnknownSRID}(v[1], v[2], v[3])
end

# X -> CRS{SRID}
function geotransform{T, U}(::Type{T}, X::U, ::Type{Proj4Handler}, ::Type{GeodesyHandler})
    oT = add_param(T, U)
    if false # get_srid(oT) == get_srid(U)  # not valid since the geoid addition
        out = oT(getX(X), getY(X), getZ(X))  # not actually a geotransform.  Should probably be a convert method?
    else
        v = [getX(X), getY(X), getZ(X)]
        Proj4.transform!(get_projection(U), get_projection(oT), v, false)
        out = oT(v[1], v[2], v[3])
    end
end

# X -> CRS{SRID}, with srids specified by an extra variable (output SRID, input SRID)
function geotransform{T, U, S1, S2}(::Type{T}, X::U, srids::Tuple{S1, S2}, ::Type{Proj4Handler}, ::Type{GeodesyHandler})
    v = [getX(X), getY(X), getZ(X)]
    Proj4.transform!(get_projection(srids[2]), get_projection(srids[1]), v, false)
    out = CRS{UnknownSRID}(v[1], v[2], v[3])
end

# CRS{SRID} -> X
function geotransform{T, U}(::Type{T}, X::U, ::Type{GeodesyHandler}, ::Type{Proj4Handler})
    oT = add_param(T)
    if false # get_srid(oT) == get_srid(U)  # not valid since the geoid addition
        out = oT(getX(X), getY(X), getZ(X))  # not actually a geotransform.  Should probably be a convert method?
    else
        v = [getX(X), getY(X), getZ(X)]
        Proj4.transform!(get_projection(U), get_projection(oT), v, false)
        out = oT(v[1], v[2], v[3])
    end
end

# # CRS{SRID} -> X
function geotransform{T, U, S1, S2}(::Type{T}, X::U, srids::Tuple{S1, S2}, ::Type{GeodesyHandler}, ::Type{Proj4Handler})
    v = [getX(X), getY(X), getZ(X)]
    Proj4.transform!(get_projection(srids[2]), get_projection(srids[1]), v, false)
    out = CRS{UnknownSRID}(v[1], v[2], v[3])
end

#
# Vectorized forms of the above
#
geotransform_vector{T, U}(::Type{T}, X::Vector{U}, ::Type{Proj4Handler}, ::Type{GeodesyHandler}) = proj4_vectorized(T, X)
geotransform_vector{T, U}(::Type{T}, X::Vector{U}, ::Type{GeodesyHandler}, ::Type{Proj4Handler}) = proj4_vectorized(T, X)
geotransform_vector{T, U}(::Type{T}, X::Vector{U}, ::Type{Proj4Handler}, ::Type{Proj4Handler}) = proj4_vectorized(T, X)

geotransform_vector{T, U, S1, S2}(::Type{T}, X::Vector{U}, srids::Tuple{S1, S2}, ::Type{Proj4Handler}, ::Type{GeodesyHandler}) = proj4_vectorized(T, X, srids)
function geotransform_vector{T, U, S1, S2}(::Type{T}, X::Vector{U}, srids::Tuple{S1, S2}, ::Type{GeodesyHandler}, ::Type{Proj4Handler})
    proj4_vectorized(T, X, srids)
end
geotransform_vector{T, U, S1, S2}(::Type{T}, X::Vector{U}, srids::Tuple{S1, S2}, ::Type{Proj4Handler}, ::Type{Proj4Handler}) = proj4_vectorized(T, X, srids)

# vectorizied transform for proj4 (looping calls to proj4 is slow)
function proj4_vectorized{T, U}(::Type{T}, X::Vector{U})

    # convert to a matrix
    mat = Matrix{Float64}(length(X), 3)
    @inbounds for i = 1:length(X)
        mat[i, 1] = getX(X[i])
        mat[i, 2] = getY(X[i])
        mat[i, 3] = getZ(X[i])
    end

    # perform it
    Proj4.transform!(Geodesy.get_projection(U), Geodesy.get_projection(T), mat, false)

    # and assign the output
    X = Vector{T}(length(X))
    @inbounds for i = 1:length(X)
        X[i] = T(mat[i,1], mat[i,2], mat[i,3])
    end

    return X
end

# vectorizied transform for proj4 (looping calls to proj4 is slow)
function proj4_vectorized{T, U, S1, S2}(::Type{T}, X::Vector{U}, srids::Tuple{S1, S2})

    # convert to a matrix
    mat = Matrix{Float64}(length(X), 3)
    @inbounds for i = 1:length(X)
        mat[i, 1] = getX(X[i])
        mat[i, 2] = getY(X[i])
        mat[i, 3] = getZ(X[i])
    end

    # perform it
    Proj4.transform!(Geodesy.get_projection(srids[2]), Geodesy.get_projection(srids[1]), mat, false)

    # and assign the output
    X = Vector{T}(length(X))
    @inbounds for i = 1:length(X)
        X[i] = T(mat[i,1], mat[i,2], mat[i,3])
    end

    return X
end


#=

Methods for offline testing

=#

function Debug(filepath::AbstractString, filetype::AbstractString; delay=0.0)

    if delay==0.0
        delay_b = false
        delay_t = 0.0
    else
        delay_b = true
        delay_t = delay
    end

    if filetype=="qq"

        a=matread(filepath)
        md=squeeze(a["data"]',2).*-1000
        d=Debug(true,"qq",filepath,md,1,floor(length(md)/SAMPLES_PER_DATA_BLOCK)*SAMPLES_PER_DATA_BLOCK,delay_b,delay_t)
    elseif filetype=="Intan"

        myheader = read_v_header(filepath)

        f=open(filepath, "r+")

        seekend(f)

        endpos = position(f)

        close(f)

        d=Debug(true,"Intan",filepath,zeros(Int16,1),10,endpos,delay_b,delay_t)
    end
    d
end

function readDataBlocks(rhd::RHD2000,s::Array{T,1}) where T<:Sorting
    fillFromOffline!(rhd)
    applySorting(rhd,s)
    true
end

function readDataBlocks(rhd::RHD2000,s::DArray{T,1,Array{T,1}}) where T<:Sorting
    fillFromOffline!(rhd)
    if rhd.cal<3
        offline_cal(s,rhd.v,rhd.buf,rhd.nums)
    else
        offline_sort(s,rhd.v,rhd.buf,rhd.nums)
    end
    true
end

function offline_cal(s,v,buf,nums)
    @sync for p in procs(s)
        @spawnat p begin
            cal!(localpart(s),v,buf,nums)
        end
    end
end

function offline_sort(s,v,buf,nums)
    @sync for p in procs(s)
        @spawnat p begin
            onlinesort!(localpart(s),v,buf,nums)
        end
    end
end

function fillFromOffline!(rhd::RHD2000)

    toff=rhd.time[end,1]

    if rhd.debug.m == "qq"

        for j=1:SAMPLES_PER_DATA_BLOCK
            for i=1:size(rhd.v,2)
                rhd.v[j,i]=round(Int16,rhd.debug.data[rhd.debug.ind])
            end
            rhd.debug.ind+=1
            rhd.time[j,1]=toff+j
        end
        if rhd.debug.ind>=rhd.debug.maxind
            rhd.debug.ind=1
        end
    elseif rhd.debug.m == "Intan"

        f=open(rhd.debug.filepath, "r+")

        seek(f,rhd.debug.ind)

        for i=1:size(rhd.v,2)
            for j=1:SAMPLES_PER_DATA_BLOCK
                rhd.v[j,i]=read(f,Int16)
            end
        end

        for j=1:SAMPLES_PER_DATA_BLOCK
            rhd.time[j,1]=toff+j
        end

        rhd.debug.ind = position(f)

        close(f)
        if rhd.debug.ind>=rhd.debug.maxind
            rhd.debug.ind=10
        end
    end
    #Filter
    for i=1:length(rhd.filts)
        for j=1:length(rhd.filts[i])
            apply_filter(rhd,rhd.filts[i][j],rhd.filts[i][j].chan)
        end
    end

    if rhd.debug.delay
        sleep(rhd.debug.delay_t)
    end
    nothing
end


# Methods for site counting in sequences using a naive method
# ============================================================
#
# This file is a part of BioJulia.
# License is MIT: https://github.com/BioJulia/Bio.jl/blob/master/LICENSE.md

# Methods for a single sequence.

"""
    count_sites_naive{T<:Site,A<:FourBitAlphs}(::Type{T}, a::BioSequence{A})
"""
function count_sites_naive{T<:Site,A<:FourBitAlphs}(::Type{T}, a::BioSequence{A})
    k = 0
    @inbounds for idx in eachindex(a)
        k += issite(T, a[idx])
    end
    return k
end

count_sites_naive{T<:Union{Gap,Ambiguous},A<:TwoBitAlphs}(::Type{T}, a::BioSequence{A}) = 0
count_sites_naive{A<:TwoBitAlphs}(::Type{Certain}, a::BioSequence{A}) = length(a)

"""
    count_sites_naive{T<:Site,A<:DNA_OR_RNA}(::Type{T}, a::BioSequence{A}, b::BioSequence{A})
This method counts the number of sites between a pair of aligned sequences of
type `T`.
"""
function count_sites_naive{T<:Site,A<:Alphabet,B<:Alphabet}(::Type{T},
                                                       a::BioSequence{A},
                                                       b::BioSequence{B})
    k = 0
    @inbounds for idx in 1:min(endof(a), endof(b))
        k += issite(T, a[idx], b[idx])
    end
    return k
end

"""
    count_sites_naive{T<:Mutation,A<:DNA_OR_RNA}(::Type{T}, a::BioSequence{A}, b::BioSequence{A})
This method counts the number of sites between a pair of aligned sequences of
SiteCase `T`.
Since the types of SiteCase counted by this function may not always be
unambiguously determined, this method returns a tuple of the count as well as
the number of sites which could not be unambiguously determined.
This second count is important for some downstream purposes, for example
evolutionary/genetic distance computations in which pairwise deletion of
ambiguous sites is nessecery.
"""
function count_sites_naive{T<:Mutation,A<:Alphabet,B<:Alphabet}(::Type{T},
                                                             a::BioSequence{A},
                                                             b::BioSequence{B})
    k = 0
    j = 0
    @inbounds for idx in 1:min(endof(a), endof(b))
        an = a[idx]
        bn = b[idx]
        k += issite(T, an, bn)
        j += !issite(Certain, an, bn)
    end
    return k, j
end

count_sites_naive{T<:Union{Gap,Ambiguous},A<:TwoBitAlphs}(::Type{T}, a::BioSequence{A}, b::BioSequence{A}) = 0
count_sites_naive{A<:TwoBitAlphs}(::Type{Certain}, a::BioSequence{A}, b::BioSequence{A}) = min(length(a), length(b))


abstract type AbstractTechnology{G} end

struct ThermalGenerators{G1} <: AbstractTechnology{G1}

    name::Vector{String}             # g
    owner::Vector{String}            # g

    mingen::Vector{Float64}          # (MW/unit, g)
    maxgen::Vector{Float64}          # (MW/unit, g)

    minuptime::Vector{Int}           # (hours, g)
    mindowntime::Vector{Int}         # (hours, g)
    maxrampup::Vector{Float64}       # (MW/unit/hour, g)
    maxrampdown::Vector{Float64}     # (MW/unit/hour, g)

    capacitycredit::Vector{Float64}  # (fraction, g)

    function ThermalGenerators{}(args...)
        G = length(first(args))
        @assert all(a -> length(a) == G, args)
        new{G}(args...)
    end

end

struct VariableGenerators{G2} <: AbstractTechnology{G2}

    name::Vector{String}    # g
    owner::Vector{String}   # g

    maxgen::Vector{Float64} # (MW/unit, g)

    capacitycredit::Vector{Float64} # (fraction, g)

    function VariableGenerators{}(args...)
        G = length(first(args))
        @assert all(a -> length(a) == G, args)
        new{G}(args...)
    end

end

struct StorageDevices{G3} <: AbstractTechnology{G3}

    name::Vector{String}  # g
    owner::Vector{String} # g

    maxcharge::Vector{Float64}    # (MW, g)
    maxdischarge::Vector{Float64} # (MW, g)
    maxenergy::Vector{Float64}    # (MWh, g)

    chargeefficiency::Vector{Float64}    # (fraction, g)
    dischargeefficiency::Vector{Float64} # (fraction, g)
    carryoverefficiency::Vector{Float64}  # (fraction, g)

    capacitycredit::Vector{Float64}  # (fraction, g)

    function StorageDevices{}(args...)
        G = length(first(args))
        @assert all(a -> length(a) == G, args)
        new{G}(args...)
    end

end

struct Interfaces{I,R} <: AbstractTechnology{I}

    name::Vector{String}
    regions::Vector{Tuple{Int,Int}}

    function Interfaces{R}(
        name::Vector{String},
        regions::Vector{Tuple{Int,Int}}
    ) where R

        I = length(name)
        @assert I == length(regions)

        @assert all(x -> 1 <= first(x) <= R, regions)
        @assert all(x -> 1 <= last(x) <= R, regions)
        @assert allunique(tuple.(minimum.(regions), maximum.(regions)))

        new{I,R}(name, regions)

    end

end

struct Technologies{G1,G2,G3,I,R}

    thermal::ThermalGenerators{G1}
    variable::VariableGenerators{G2}
    storage::StorageDevices{G3}
    interface::Interfaces{I,R}

    function Technologies{}(
        thermal::ThermalGenerators{G1},
        variable::VariableGenerators{G2},
        storage::StorageDevices{G3},
        interface::Interfaces{I,R}
    ) where {G1,G2,G3,I,R}

        allunique(vcat(thermal.name, variable.name,
                       storage.name, interface.name)) ||
            error("Resource class names must be unique")

        new{G1,G2,G3,I,R}(thermal, variable, storage, interface)

    end

end


# Particle IDs used for testing. The definition is the same as in
# the Python package 'particle' from the SciKit-HEP group:
# https://github.com/scikit-hep/particle/blob/master/tests/conftest.py

@enum PDGIDS begin
    # Gauge and Higgs bosons
    Gluon = 21
    Photon = 22
    Z0 = 23
    WMinus = -24
    HiggsBoson = 25
    ZPrime = 32
    # Charged leptons
    Electron = 11
    Positron = -11
    Muon = 13
    AntiMuon = -13
    Tau = 15
    TauPrime = 17
    # Neutrinos
    Nu_e = 12
    NuBar_tau = -16
    # Quarks
    DQuark = 1
    UQuark = 2
    SQuark = 3
    CQuark = 4
    BQuark = 5
    TQuark = 6
    BPrimeQuark = 7  # 4th generation
    TPrimeQuark = 8
    # Quarkonia
    jpsi = 443
    psi_2S = 100443
    Upsilon_1S = 553
    Upsilon_4S = 300553
    # Light hadrons
    Pi0 = 111
    PiPlus = 211
    eta = 221
    eta_prime = 331
    a_0_1450_plus = 10211
    KL = 130
    KS = 310
    KMinus = -321
    rho_770_minus = -213
    phi = 333
    omega = 223
    K1_1270_0 = 10313
    K1_1400_0 = 20313
    rho_1700_0 = 30113
    a2_1320_minus = -215
    omega_3_1670 = 227
    f_4_2300 = 9010229  # example of a not-well-known meson
    Proton = 2212
    AntiNeutron = -2112
    Lambda = 3122
    Sigma0 = 3212
    SigmaPlus = 3222
    SigmaMinus = 3112
    Xi0 = 3322
    AntiXiMinus = -3312
    OmegaMinus = 3334
    # Charm hadrons
    D0 = 421
    DPlus = 411
    DsPlus = 431
    LcPlus = 4122
    # Beauty hadrons
    B0 = 511
    BPlus = 521
    Bs = 531
    BcPlus = 541
    Lb = 5122
    # Top hadrons
    T0 = 621
    LtPlus = 6122
    # Special particles
    Graviton = 39
    Reggeon = 110
    Pomeron = 990
    Odderon = 9990
    # Supersymmetric particles
    Gluino = 1000021
    Gravitino = 1000039
    STildeL = 1000003
    CTildeR = 2000004
    # R-hadrons
    RPlus_TTildeDbar = 1000612
    R0_GTildeG = 1000993
    RPlusPlus_GTildeUUU = 1092224
    # Q-balls
    QBall1 = 10000150
    QBall2 = -10000200
    # Dyons
    DyonSameMagElecChargeSign = 4110010
    DyonOppositeMagElecChargeSign = 4120010
    # Di-quarks
    DD1 = 1103
    SD0 = 3101
    # Nuclei
    HydrogenNucleus = 1000010010
    Carbon12 = 1000060120
    # Pentaquarks
    AntiUCbarCUDPentaquark = -9422144
    # example of spin 3/2 u-cbar-c-u-d pentaquark decaying to J/psi proton
    UCbarCUDPentaquark = 9422144
    # Technicolor
    Pi0TC = 3000111
    PiMinusTC = -3000211
    # Composite quarks and leptons
    UQuarkStar = 4000002
    AntiElectronStar = -4000011
    # Generator specific pseudoparticles or concepts
    AntiCHadron = -84
    # Invalid ID
    Invalid1 = 0  # illegal ID
    Invalid2 = 99999999  # general form is a 7-digit number
end


using HDF5
using Unitful

const PYPHOT_DATADEP = DataDep(
    "filters",
    "pyphot filter library",
    "https://rawcdn.githack.com/mfouesneau/pyphot/943b2a5e35c88b4419d5e93a613e44f22994eae3/pyphot/libs/new_filters.hd5",
    "9078486ec989bc0ffddb60f9cda686dff887c5d69627bc4703fd41d78a9e7d46";
)

function read_filter(name::AbstractString, units=true)
    local wave, through, dtype, fname
    h5open(datadep"filters/new_filters.hd5") do fh
        node = fh["filters/$name"]
        data = read(node)
        wave = map(i -> i.WAVELENGTH, data)
        if units
            unitstr = read(attributes(node)["WAVELENGTH_UNIT"])
            wave *= parse_unit(unitstr)
        end
        through = map(i -> i.THROUGHPUT, data)
        dtype = read(attributes(node)["DETECTOR"])
        fname = read(attributes(node)["NAME"])
    end
    D = parse(DetectorType, dtype)
    return PhotometricFilter(wave, through; detector=D, name=fname)
end

function parse_unit(unitstr::AbstractString)
    tok = lowercase(unitstr)
    if tok === "angstrom" || tok === "aa"
        return u"angstrom"
    elseif tok === "nm" || tok === "nanometer"
        return u"nm"
    elseif tok === "um"
        return u"μm"
    else
        throw(ArgumentError("Could not parse Unitful unit from $unitstr"))
    end
end

###
### Code generation
###

const FILTER_NAMES = [
    "2MASS_H",
    "2MASS_J",
    "2MASS_Ks",
    "CFHT_CFH12K_CFH7406",
    "CFHT_CFH12K_CFH7504",
    "CFHT_MEGAPRIME_CFH7605",
    "CFHT_MEGAPRIME_CFH7701",
    "CFHT_MEGAPRIME_CFH7803",
    "CFHT_MEGAPRIME_CFH9301",
    "CFHT_MEGAPRIME_CFH9401",
    "CFHT_MEGAPRIME_CFH9601",
    "CFHT_MEGAPRIME_CFH9701",
    "CFHT_MEGAPRIME_CFH9702",
    "CFHT_MEGAPRIME_CFH9801",
    "CFHT_WIRCAM_CFH8002",
    "CFHT_WIRCAM_CFH8101",
    "CFHT_WIRCAM_CFH8102",
    "CFHT_WIRCAM_CFH8103",
    "CFHT_WIRCAM_CFH8104",
    "CFHT_WIRCAM_CFH8201",
    "CFHT_WIRCAM_CFH8202",
    "CFHT_WIRCAM_CFH8203",
    "CFHT_WIRCAM_CFH8204",
    "CFHT_WIRCAM_CFH8301",
    "CFHT_WIRCAM_CFH8302",
    "CFHT_WIRCAM_CFH8303",
    "CFHT_WIRCAM_CFH8304",
    "CFHT_WIRCAM_CFH8305",
    "GALEX_FUV",
    "GALEX_NUV",
    "GROUND_BESSELL_H",
    "GROUND_BESSELL_J",
    "GROUND_BESSELL_K",
    "GROUND_COUSINS_I",
    "GROUND_COUSINS_R",
    "GROUND_JOHNSON_B",
    "GROUND_JOHNSON_U",
    "GROUND_JOHNSON_V",
    "GaiaDR2_BP",
    "GaiaDR2_G",
    "GaiaDR2_RP",
    "GaiaDR2_weiler_BPbright",
    "GaiaDR2_weiler_BPfaint",
    "GaiaDR2_weiler_G",
    "GaiaDR2_weiler_RP",
    "GaiaDR2v2_BP",
    "GaiaDR2v2_G",
    "GaiaDR2v2_RP",
    "Gaia_BP",
    "Gaia_G",
    "Gaia_MAW_BP_bright",
    "Gaia_MAW_BP_faint",
    "Gaia_MAW_G",
    "Gaia_MAW_RP",
    "Gaia_RP",
    "Gaia_rvs",
    "HERSCHEL_PACS_BLUE",
    "HERSCHEL_PACS_GREEN",
    "HERSCHEL_PACS_RED",
    "HERSCHEL_SPIRE_PLW",
    "HERSCHEL_SPIRE_PLW_EXT",
    "HERSCHEL_SPIRE_PMW",
    "HERSCHEL_SPIRE_PSW",
    "HERSCHEL_SPIRE_PSW_EXT",
    "HST_ACS_HRC_F220W",
    "HST_ACS_HRC_F250W",
    "HST_ACS_HRC_F330W",
    "HST_ACS_HRC_F344N",
    "HST_ACS_HRC_F435W",
    "HST_ACS_HRC_F475W",
    "HST_ACS_HRC_F502N",
    "HST_ACS_HRC_F550M",
    "HST_ACS_HRC_F555W",
    "HST_ACS_HRC_F606W",
    "HST_ACS_HRC_F625W",
    "HST_ACS_HRC_F658N",
    "HST_ACS_HRC_F660N",
    "HST_ACS_HRC_F775W",
    "HST_ACS_HRC_F814W",
    "HST_ACS_HRC_F850LP",
    "HST_ACS_HRC_F892N",
    "HST_ACS_WFC_F435W",
    "HST_ACS_WFC_F475W",
    "HST_ACS_WFC_F502N",
    "HST_ACS_WFC_F550M",
    "HST_ACS_WFC_F555W",
    "HST_ACS_WFC_F606W",
    "HST_ACS_WFC_F625W",
    "HST_ACS_WFC_F658N",
    "HST_ACS_WFC_F660N",
    "HST_ACS_WFC_F775W",
    "HST_ACS_WFC_F814W",
    "HST_ACS_WFC_F850LP",
    "HST_ACS_WFC_F892N",
    "HST_NIC2_F110W",
    "HST_NIC2_F160W",
    "HST_NIC2_F205W",
    "HST_NIC3_F108N",
    "HST_NIC3_F110W",
    "HST_NIC3_F113N",
    "HST_NIC3_F150W",
    "HST_NIC3_F160W",
    "HST_NIC3_F164N",
    "HST_NIC3_F166N",
    "HST_NIC3_F175W",
    "HST_NIC3_F187N",
    "HST_NIC3_F190N",
    "HST_NIC3_F196N",
    "HST_NIC3_F200N",
    "HST_NIC3_F205M",
    "HST_NIC3_F212N",
    "HST_NIC3_F215N",
    "HST_NIC3_F222M",
    "HST_NIC3_F240M",
    "HST_WFC3_F098M",
    "HST_WFC3_F105W",
    "HST_WFC3_F110W",
    "HST_WFC3_F125W",
    "HST_WFC3_F126N",
    "HST_WFC3_F127M",
    "HST_WFC3_F128N",
    "HST_WFC3_F130N",
    "HST_WFC3_F132N",
    "HST_WFC3_F139M",
    "HST_WFC3_F140W",
    "HST_WFC3_F153M",
    "HST_WFC3_F160W",
    "HST_WFC3_F164N",
    "HST_WFC3_F167N",
    "HST_WFC3_F200LP",
    "HST_WFC3_F218W",
    "HST_WFC3_F225W",
    "HST_WFC3_F275W",
    "HST_WFC3_F280N",
    "HST_WFC3_F300X",
    "HST_WFC3_F336W",
    "HST_WFC3_F343N",
    "HST_WFC3_F350LP",
    "HST_WFC3_F373N",
    "HST_WFC3_F390M",
    "HST_WFC3_F390W",
    "HST_WFC3_F395N",
    "HST_WFC3_F410M",
    "HST_WFC3_F438W",
    "HST_WFC3_F467M",
    "HST_WFC3_F469N",
    "HST_WFC3_F475W",
    "HST_WFC3_F475X",
    "HST_WFC3_F487N",
    "HST_WFC3_F502N",
    "HST_WFC3_F547M",
    "HST_WFC3_F555W",
    "HST_WFC3_F600LP",
    "HST_WFC3_F606W",
    "HST_WFC3_F621M",
    "HST_WFC3_F625W",
    "HST_WFC3_F631N",
    "HST_WFC3_F645N",
    "HST_WFC3_F656N",
    "HST_WFC3_F657N",
    "HST_WFC3_F658N",
    "HST_WFC3_F665N",
    "HST_WFC3_F673N",
    "HST_WFC3_F680N",
    "HST_WFC3_F689M",
    "HST_WFC3_F763M",
    "HST_WFC3_F775W",
    "HST_WFC3_F814W",
    "HST_WFC3_F845M",
    "HST_WFC3_F850LP",
    "HST_WFC3_F953N",
    "HST_WFC3_FQ232N",
    "HST_WFC3_FQ243N",
    "HST_WFC3_FQ378N",
    "HST_WFC3_FQ387N",
    "HST_WFC3_FQ422M",
    "HST_WFC3_FQ436N",
    "HST_WFC3_FQ437N",
    "HST_WFC3_FQ492N",
    "HST_WFC3_FQ508N",
    "HST_WFC3_FQ575N",
    "HST_WFC3_FQ619N",
    "HST_WFC3_FQ634N",
    "HST_WFC3_FQ672N",
    "HST_WFC3_FQ674N",
    "HST_WFC3_FQ727N",
    "HST_WFC3_FQ750N",
    "HST_WFC3_FQ889N",
    "HST_WFC3_FQ906N",
    "HST_WFC3_FQ924N",
    "HST_WFC3_FQ937N",
    "HST_WFPC2_F170W",
    "HST_WFPC2_F218W",
    "HST_WFPC2_F255W",
    "HST_WFPC2_F300W",
    "HST_WFPC2_F336W",
    "HST_WFPC2_F439W",
    "HST_WFPC2_F450W",
    "HST_WFPC2_F555W",
    "HST_WFPC2_F606W",
    "HST_WFPC2_F622W",
    "HST_WFPC2_F675W",
    "HST_WFPC2_F791W",
    "HST_WFPC2_F814W",
    "HST_WFPC2_F850LP",
    "JWST_NIRCAM_F070W",
    "JWST_NIRCAM_F090W",
    "JWST_NIRCAM_F115W",
    "JWST_NIRCAM_F140M",
    "JWST_NIRCAM_F150W",
    "JWST_NIRCAM_F150W2",
    "JWST_NIRCAM_F162M",
    "JWST_NIRCAM_F164N",
    "JWST_NIRCAM_F182M",
    "JWST_NIRCAM_F187N",
    "JWST_NIRCAM_F200W",
    "JWST_NIRCAM_F210M",
    "JWST_NIRCAM_F212N",
    "JWST_NIRCAM_F250M",
    "JWST_NIRCAM_F277W",
    "JWST_NIRCAM_F300M",
    "JWST_NIRCAM_F322W2",
    "JWST_NIRCAM_F323N",
    "JWST_NIRCAM_F335M",
    "JWST_NIRCAM_F356W",
    "JWST_NIRCAM_F360M",
    "JWST_NIRCAM_F405N",
    "JWST_NIRCAM_F410M",
    "JWST_NIRCAM_F430M",
    "JWST_NIRCAM_F444W",
    "JWST_NIRCAM_F460M",
    "JWST_NIRCAM_F466N",
    "JWST_NIRCAM_F470N",
    "JWST_NIRCAM_F480M",
    "KEPLER_Kp",
    "NGTS_I",
    "PS1_g",
    "PS1_i",
    "PS1_r",
    "PS1_w",
    "PS1_y",
    "PS1_z",
    "SDSS_g",
    "SDSS_i",
    "SDSS_r",
    "SDSS_u",
    "SDSS_z",
    "SPITZER_IRAC_36",
    "SPITZER_IRAC_45",
    "SPITZER_IRAC_58",
    "SPITZER_IRAC_80",
    "STROMGREN_b",
    "STROMGREN_u",
    "STROMGREN_v",
    "STROMGREN_y",
    "TESS",
    "TYCHO_B_MvB",
    "TYCHO_V_MvB",
    "WISE_RSR_W1",
    "WISE_RSR_W2",
    "WISE_RSR_W3",
    "WISE_RSR_W4",
    "ZTF_g",
    "ZTF_i",
    "ZTF_r"
]

for filter_name in FILTER_NAMES
    F = Symbol(filter_name)
    @eval $F(; units=true) = read_filter($filter_name, units)
end