export Positions, GridPositions, RegularGridPositions, ChebyshevGridPositions,
       MeanderingGridPositions, UniformRandomPositions, ArbitraryPositions,
       SphericalTDesign, BreakpointGridPositions
export SpatialDomain, AxisAlignedBox, Ball
export loadTDesign, getPermutation
export fieldOfView, fieldOfViewCenter, shape
export idxToPos, posToIdx, posToLinIdx, spacing, isSubgrid, deriveSubgrid, toDict

abstract type Positions end
abstract type GridPositions<:Positions end

function Positions(file::HDF5.File)
  if haskey(file, "/positionsBreakpoint")
    return BreakpointGridPositions(file)
  end

  typ = read(file, "/positionsType")
  if typ == "RegularGridPositions"
    positions = RegularGridPositions(file)
  elseif typ == "ChebyshevGridPositions"
    positions = ChebyshevGridPositions(file)
  elseif typ == "SphericalTDesign"
    positions = SphericalTDesign(file)
  elseif typ == "UniformRandomPositions"
    positions = UniformRandomPositions(file)
  elseif typ == "ArbitraryPositions"
    positions = ArbitraryPositions(file)
  else
    throw(ErrorException("No grid found to load from $file"))
  end

  if haskey(file, "/positionsMeandering") && typ in ["RegularGridPositions","ChebyshevGridPositions"] && read(file, "/positionsMeandering") == Int8(1)
    positions = MeanderingGridPositions(positions)
  end

  return positions
end

function Positions(params::Dict)
  if haskey(params, "positionsBreakpoint")
    return BreakpointGridPositions(params)
  end

  typ = params["positionsType"]
  if typ == "RegularGridPositions"
    positions = RegularGridPositions(params)
  elseif typ == "ChebyshevGridPositions"
    positions = ChebyshevGridPositions(params)
  elseif typ == "SphericalTDesign"
    positions = SphericalTDesign(params)
  elseif typ == "UniformRandomPositions"
    positions = UniformRandomPositions(params)
  elseif typ == "ArbitraryPositions"
    positions = ArbitraryPositions(params)
  else
    throw(ErrorException("No grid found to load from dict $params"))
  end

  if haskey(params, "positionsMeandering") && typ in ["RegularGridPositions","ChebyshevGridPositions"] && params["positionsMeandering"]
    positions = MeanderingGridPositions(positions)
  end

  return positions
end

# Cartesian grid
mutable struct RegularGridPositions{T} <: GridPositions where {T<:Unitful.Length}
  shape::Vector{Int}
  fov::Vector{T}
  center::Vector{T}
  sign::Vector{Int}
end

function range(grid::RegularGridPositions, dim::Int)
  if grid.shape[dim] > 1
    sp = spacing(grid)
    return range(grid.center[dim] - grid.fov[dim]/2 + sp[dim]/2,
                 step=sp[dim], length=grid.shape[dim])
  else
    return 1:1
  end
end

RegularGridPositions(shape, fov, center) = RegularGridPositions(shape, fov, center, ones(Int,length(shape)))

function RegularGridPositions(file::HDF5.File)
  shape = read(file, "/positionsShape")
  fov = read(file, "/positionsFov")*Unitful.m
  center = read(file, "/positionsCenter")*Unitful.m

  return RegularGridPositions(shape, fov, center)
end

function RegularGridPositions(params::Dict)
  shape = params["positionsShape"]

  fov = params["positionsFov"]
  if !(eltype(fov) <: Quantity)
    fov = fov.*Unitful.m
  end

  center = params["positionsCenter"]
  if !(eltype(center) <: Quantity)
    center = center.*Unitful.m
  end

  return RegularGridPositions(shape, fov, center)
end

# Find a joint grid
function RegularGridPositions(positions::Vector{T}) where T<:RegularGridPositions
  posMin = positions[1].center .- 0.5*positions[1].fov
  posMax = positions[1].center .+ 0.5*positions[1].fov
  minSpacing = spacing(positions[1])
  for position in positions
    sp = spacing(position)
    for d=1:length(posMin)
      posMin[d] = min(posMin[d], position.center[d] - 0.5*position.fov[d])
      posMax[d] = max(posMax[d], position.center[d] + 0.5*position.fov[d])
      minSpacing[d] = min(minSpacing[d],sp[d])
    end
  end
  center = (posMin .+ posMax)/2
  fov = posMax .- posMin
  shape = round.(Int64,fov./minSpacing)
  fov = shape .* minSpacing
  return RegularGridPositions(shape, fov, center)
end

function isSubgrid(grid::RegularGridPositions, subgrid::RegularGridPositions)
  if any(fieldOfView(grid) .- fieldOfView(subgrid) .< 0) ||
     any(spacing(grid) .!= spacing(subgrid))
    return false
  else
    centerPosIdx = posToIdxFloat(grid,subgrid[ones(Int,length(subgrid.shape))])
    return all(isapprox.(centerPosIdx, round.(Int,centerPosIdx) ,rtol=1e-5))
  end
end

function deriveSubgrid(grid::RegularGridPositions, subgrid::RegularGridPositions)
  minI = ones(Int,length(subgrid.shape))
  maxI = copy(subgrid.shape)
  for d=1:length(minI)
    if subgrid.sign[d] == -1
      minI[d] = subgrid.shape[d]-minI[d]+1
      maxI[d] = subgrid.shape[d]-maxI[d]+1
    end
  end
  minPos = subgrid[ minI ]
  maxPos = subgrid[ maxI ]

  minIdx = posToIdx(grid,minPos)
  maxIdx = posToIdx(grid,maxPos)
  #shp = maxIdx-minIdx+ones(Int,length(subgrid.shape))
  shp = shape(subgrid)
  #center = (grid[minIdx].+grid[maxIdx])/2
  # TODO round properly
  center = (grid[minIdx].+grid[minIdx.+shp.-1])/2
  fov = shp.*spacing(grid)
  return RegularGridPositions(shp,fov,center,subgrid.sign)
end

function write(file::HDF5.File, positions::RegularGridPositions)
  write(file,"/positionsType", "RegularGridPositions")
  write(file, "/positionsShape", positions.shape)
  write(file, "/positionsFov", Float64.(ustrip.(uconvert.(Unitful.m, positions.fov))) )
  write(file, "/positionsCenter", Float64.(ustrip.(uconvert.(Unitful.m, positions.center))) )
end

function toDict(positions::RegularGridPositions)
  params = Dict{String,Any}()
  params["positionsType"] = "RegularGridPositions"
  params["positionsShape"] = positions.shape
  params["positionsFov"] = Float64.(ustrip.(uconvert.(Unitful.m, positions.fov)))
  params["positionsCenter"] = Float64.(ustrip.(uconvert.(Unitful.m, positions.center)))
  return params
end

function getindex(grid::RegularGridPositions, i::Integer)
  if i>length(grid) || i<1
     throw(BoundsError(grid,i))
  end

  #idx = collect(ind2sub(tuple(shape(grid)...), i))
  if length(grid.shape) == 1 #Very ugly but improves compile time
    idx = [i]
  elseif length(grid.shape) == 2
    idx = collect(Tuple((CartesianIndices(tuple(grid.shape[1],grid.shape[2])))[i]))
  elseif length(grid.shape) == 3
    idx = collect(Tuple((CartesianIndices(tuple(grid.shape[1],grid.shape[2],grid.shape[3])))[i]))
  else
    idx = collect(Tuple((CartesianIndices(tuple(grid.shape...)))[i]))
  end

  for d=1:length(idx)
    if grid.sign[d] == -1
      idx[d] = grid.shape[d]-idx[d]+1
    end
  end
  return ((-shape(grid).+(2 .*idx.-1))./shape(grid)).*fieldOfView(grid)./2 + fieldOfViewCenter(grid)
end

function getindex(grid::RegularGridPositions, idx::Vector{T}) where T<:Number
  for d=1:length(idx)
    if grid.sign[d] == -1
      idx[d] = grid.shape[d]-idx[d]+1
    end
  end
  return 0.5.*fieldOfView(grid) .* (-1 .+ (2 .* idx .- 1) ./ shape(grid)) .+ fieldOfViewCenter(grid)
end

function posToIdxFloat(grid::RegularGridPositions,pos::Vector)
  idx = 0.5 .* (shape(grid) .* ((pos .- fieldOfViewCenter(grid)) ./
              ( 0.5 .* fieldOfView(grid) ) .+ 1) .+ 1)
  return idx
end

function posToIdx(grid::RegularGridPositions,pos::Vector)
  idx = round.(Int64, posToIdxFloat(grid,pos))
  for d=1:length(idx)
    if grid.sign[d] == -1
      idx[d] = grid.shape[d]-idx[d]+1
    end
  end
  return idx
end

function posToLinIdx(grid::RegularGridPositions,pos::Vector)
  return (LinearIndices(tuple(shape(grid)...)))[posToIdx(grid,pos)...]
end

# Chebyshev Grid
mutable struct ChebyshevGridPositions{S,T} <: GridPositions where {S,T<:Unitful.Length}
  shape::Vector{Int}
  fov::Vector{S}
  center::Vector{T}
end

function write(file::HDF5.File, positions::ChebyshevGridPositions)
  write(file,"/positionsType", "ChebyshevGridPositions")
  write(file, "/positionsShape", positions.shape)
  write(file, "/positionsFov", Float64.(ustrip.(uconvert.(Unitful.m, positions.fov))) )
  write(file, "/positionsCenter", Float64.(ustrip.(uconvert.(Unitful.m, positions.center))) )
end

function toDict(positions::ChebyshevGridPositions)
  params = Dict{String,Any}()
  params["positionsType"] = "ChebyshevGridPositions"
  params["positionsShape"] = positions.shape
  params["positionsFov"] = Float64.(ustrip.(uconvert.(Unitful.m, positions.fov)))
  params["positionsCenter"] = Float64.(ustrip.(uconvert.(Unitful.m, positions.center)))
  return params
end

function ChebyshevGridPositions(file::HDF5.File)
  shape = read(file, "/positionsShape")
  fov = read(file, "/positionsFov")*Unitful.m
  center = read(file, "/positionsCenter")*Unitful.m
  return ChebyshevGridPositions(shape,fov,center)
end

function ChebyshevGridPositions(params::Dict)
  shape = params["positionsShape"]

  fov = params["positionsFov"]
  if !(eltype(fov) <: Quantity)
    fov = fov.*Unitful.m
  end

  center = params["positionsCenter"]
  if !(eltype(center) <: Quantity)
    center = center.*Unitful.m
  end

  return ChebyshevGridPositions(shape,fov,center)
end

function getindex(grid::ChebyshevGridPositions, i::Integer)
  if i>length(grid) || i<1
    throw(BoundsError(grid,i))
  else
    idx = collect(Tuple(CartesianIndices(tuple(shape(grid)...))[i]))
    return -cos.((idx .- 0.5) .* pi ./ shape(grid)) .* fieldOfView(grid) ./ 2 .+ fieldOfViewCenter(grid)
  end
end

# Meander regular grid positions
mutable struct MeanderingGridPositions{T} <: GridPositions where {T<:GridPositions}
  grid::T
end

function MeanderingGridPositions(file::HDF5.File)
  typ = read(file, "/positionsType")
  if typ == "RegularGridPositions"
    grid = RegularGridPositions(file)
    return MeanderingGridPositions(grid)
  elseif typ == "ChebyshevGridPositions"
    grid = ChebyshevGridPositions(file)
    return MeanderingGridPositions(grid)
  end
end

function MeanderingGridPositions(params::Dict)
  typ = params["positionsType"]
  if typ == "RegularGridPositions"
    grid = RegularGridPositions(params)
    return MeanderingGridPositions(grid)
  elseif typ == "ChebyshevGridPositions"
    grid = ChebyshevGridPositions(params)
    return MeanderingGridPositions(grid)
  end
end

function write(file::HDF5.File, positions::MeanderingGridPositions)
  write(file,"/positionsMeandering", Int8(1))
  write(file, positions.grid)
end

function toDict(positions::MeanderingGridPositions)
  params = toDict(positions.grid)
  params["positionsMeandering"] = true
  return params
end

function indexPermutation(grid::MeanderingGridPositions, i::Integer)
  dims = tuple(shape(grid)...)
  ndims = length(dims)
  idx = collect(Tuple(CartesianIndices(dims)[i]))
    for d=1:ndims-1
      if isodd(sum(idx[d+1:end])-(ndims-d))
      idx[d] = -idx[d] + shape(grid)[d] + 1
    end
  end
  linidx = (LinearIndices(dims))[idx...]
end

function getindex(grid::MeanderingGridPositions, i::Integer)
  iperm = indexPermutation(grid,i)
  return grid.grid[iperm]
end

function getPermutation(grid::MeanderingGridPositions)
  N = length(grid)
  perm = Array{Int}(undef,N)

  for i in eachindex(perm)
    perm[i] = indexPermutation(grid,i)
  end
  return vec(perm)
end

mutable struct BreakpointGridPositions{T,S} <: GridPositions where {T<:GridPositions}
  grid::T
  breakpointIndices::Vector{Int64}
  breakpointPosition::Vector{S}
end

function BreakpointGridPositions(file::HDF5.File)
  typ = read(file, "/positionsType")
  breakpointPosition = read(file, "/positionsBreakpoint") * Unitful.m
  breakpointIndices = read(file, "/indicesBreakpoint")

  if typ == "MeanderingGridPositions"
    grid = MeanderingGridPositions(file)
    return BreakpointGridPositions(grid,breakpointIndices, breakpointPosition)
  elseif typ == "RegularGridPositions"
    grid = RegularGridPositions(file)
    return BreakpointGridPositions(grid, breakpointIndices, breakpointPosition)
  elseif typ == "ChebyshevGridPositions"
    grid = ChebyshevGridPositions(file)
    return BreakpointGridPositions(grid, breakpointIndices, breakpointPosition)
  end
end

function BreakpointGridPositions(params::Dict)
  typ = params["positionsType"]

  breakpointPosition = params["positionsBreakpoint"]
  if !(eltype(breakpointPosition) <: Quantity)
    breakpointPosition = breakpointPosition.*Unitful.m
  end

  breakpointIndices = params["indicesBreakpoint"]

  if typ == "MeanderingGridPositions"
    grid = MeanderingGridPositions(params)
    return BreakpointGridPositions(grid, breakpointIndices, breakpointPosition)
  elseif typ == "RegularGridPositions"
    grid = RegularGridPositions(params)
    return BreakpointGridPositions(grid, breakpointIndices, breakpointPosition)
  elseif typ == "ChebyshevGridPositions"
    grid = ChebyshevGridPositions(params)
    return BreakpointGridPositions(grid, breakpointIndices, breakpointPosition)
  end
end

function write(file::HDF5.File, positions::BreakpointGridPositions)
  write(file,"/positionsBreakpoint", Float64.(ustrip.(uconvert.(Unitful.m, positions.breakpointPosition))))
  write(file,"/indicesBreakpoint", positions.breakpointIndices)
  write(file, positions.grid)
end

function toDict(positions::BreakpointGridPositions)
  params = toDict(positions.grid)
  params["positionsBreakpoint"] = Float64.(ustrip.(uconvert.(Unitful.m, positions.breakpointPosition)))
  params["indicesBreakpoint"] = positions.breakpointIndices
  return params
end

function getmask(grid::BreakpointGridPositions)
  bgind=grid.breakpointIndices
  mask = zeros(Bool, length(grid.grid)+length(bgind))
  mask[bgind] .= true
  return mask
end

function getindex(grid::BreakpointGridPositions, i::Integer)

  bgind=grid.breakpointIndices

  if i>(length(grid.grid)+length(bgind)) || i<1
    return throw(BoundsError(grid,i))
  elseif any(i .== bgind)
    return grid.breakpointPosition
  else
    pastBgind = sum(i .> bgind)
    return grid.grid[i-pastBgind]
  end
end

# Uniform random distributed positions
abstract type SpatialDomain end

struct AxisAlignedBox <: SpatialDomain
  fov::Vector{S} where {S<:Unitful.Length}
  center::Vector{T} where {T<:Unitful.Length}
end

function write(file::HDF5.File, domain::AxisAlignedBox)
  write(file, "/positionsDomain", "AxisAlignedBox")
  write(file, "/positionsDomainFieldOfView", Float64.(ustrip.(uconvert.(Unitful.m, domain.fov))) )
  write(file, "/positionsDomainCenter", Float64.(ustrip.(uconvert.(Unitful.m, domain.center))) )
end

function AxisAlignedBox(file::HDF5.File)
  fov = read(file, "/positionsDomainFieldOfView")*Unitful.m
  center = read(file, "/positionsDomainCenter")*Unitful.m
  return AxisAlignedBox(fov,center)
end

struct Ball <: SpatialDomain
  radius::S where {S<:Unitful.Length}
  center::Vector{T} where {T<:Unitful.Length}
end

function write(file::HDF5.File, domain::Ball)
  write(file, "/positionsDomain", "Ball")
  write(file, "/positionsDomainRadius", Float64.(ustrip.(uconvert.(Unitful.m, domain.radius))) )
  write(file, "/positionsDomainCenter", Float64.(ustrip.(uconvert.(Unitful.m, domain.center))) )
end

function Ball(file::HDF5.File)
  radius = read(file, "/positionsDomainRadius")*Unitful.m
  center = read(file, "/positionsDomainCenter")*Unitful.m
  return Ball(radius,center)
end

mutable struct UniformRandomPositions{T} <: Positions where {T<:SpatialDomain}
  N::UInt
  seed::UInt32
  domain::T
end

radius(rpos::UniformRandomPositions{Ball}) = rpos.domain.radius
seed(rpos::UniformRandomPositions) = rpos.seed

function getindex(rpos::UniformRandomPositions{AxisAlignedBox}, i::Integer)
  if i>length(rpos) || i<1
    throw(BoundsError(rpos,i))
  else
    # make sure Positions are randomly generated from given seed
    mersenneTwister = MersenneTwister(seed(rpos))
    rP = rand(mersenneTwister, 3, i)[:,i]
    return (rP.-0.5).*fieldOfView(rpos)+fieldOfViewCenter(rpos)
  end
end

function getindex(rpos::UniformRandomPositions{Ball}, i::Integer)
  if i>length(rpos) || i<1
    throw(BoundsError(rpos,i))
  else
    # make sure Positions are randomly generated from given seed
    mersenneTwister = MersenneTwister(seed(rpos))
    D = rand(mersenneTwister, i)[i]
    P = randn(mersenneTwister, 3, i)[:,i]
    return radius(rpos)*D^(1/3)*normalize(P)+fieldOfViewCenter(rpos)
  end
end

function write(file::HDF5.File, positions::UniformRandomPositions{T}) where {T<:SpatialDomain}
  write(file, "/positionsType", "UniformRandomPositions")
  write(file, "/positionsN", positions.N)
  write(file, "/positionsSeed", positions.seed)
  write(file, positions.domain)
end

function UniformRandomPositions(file::HDF5.File)
  N = read(file, "/positionsN")
  seed = read(file, "/positionsSeed")
  dom = read(file,"/positionsDomain")
  if dom=="Ball"
    domain = Ball(file)
    return UniformRandomPositions(N,seed,domain)
  elseif dom=="AxisAlignedBox"
    domain = AxisAlignedBox(file)
    return UniformRandomPositions(N,seed,domain)
  else
    throw(ErrorException("No method to read domain $domain"))
  end
end

# TODO fix conversion methods
#=
function convert(::Type{UniformRandomPositions}, N::Integer,seed::UInt32,fov::Vector{S},center::Vector{T}) where {S,T<:Unitful.Length}
  if N<1
    throw(DomainError())
  else
    uN = convert(UInt,N)
    return UniformRandomPositions(uN,seed,fov,center)
  end
end

function convert(::Type{UniformRandomPositions}, N::Integer,fov::Vector,center::Vector)
  return UniformRandomPositions(N,rand(UInt32),fov,center)
end
=#

# General functions for handling grids
fieldOfView(grid::GridPositions) = grid.fov
fieldOfView(grid::UniformRandomPositions{AxisAlignedBox}) = grid.domain.fov
fieldOfView(mgrid::MeanderingGridPositions) = fieldOfView(mgrid.grid)
fieldOfView(bgrid::BreakpointGridPositions) = fieldOfView(bgrid.grid)
shape(grid::GridPositions) = grid.shape
shape(mgrid::MeanderingGridPositions) = shape(mgrid.grid)
shape(bgrid::BreakpointGridPositions) = shape(bgrid.grid)
fieldOfViewCenter(grid::GridPositions) = grid.center
fieldOfViewCenter(grid::UniformRandomPositions) = grid.domain.center
fieldOfViewCenter(mgrid::MeanderingGridPositions) = fieldOfViewCenter(mgrid.grid)
fieldOfViewCenter(bgrid::BreakpointGridPositions) = fieldOfViewCenter(bgrid.grid)

spacing(grid::GridPositions) = grid.fov ./ grid.shape

mutable struct SphericalTDesign{S,V} <: Positions where {S,V<:Unitful.Length}
  T::Unsigned
  radius::S
  positions::Matrix
  center::Vector{V}
end

function SphericalTDesign(file::HDF5.File)
  T = read(file, "/positionsTDesignT")
  N = read(file, "/positionsTDesignN")
  radius = read(file, "/positionsTDesignRadius")*Unitful.m
  center = read(file, "/positionsCenter")*Unitful.m
  return loadTDesign(Int64(T),N,radius,center)
end

function write(file::HDF5.File, positions::SphericalTDesign)
  write(file,"/positionsType", "SphericalTDesign")
  write(file, "/positionsTDesignT", positions.T)
  write(file, "/positionsTDesignN", size(positions.positions,2))
  write(file, "/positionsTDesignRadius", Float64.(ustrip.(uconvert.(Unitful.m, positions.radius))) )
  write(file, "/positionsCenter", Float64.(ustrip.(uconvert.(Unitful.m, positions.center))) )
end

function toDict(positions::SphericalTDesign)
  params = Dict{String,Any}()
  params["positionsType"] = "SphericalTDesign"
  params["positionsTDesignT"] = positions.T
  params["positionsTDesignN"] = size(positions.positions,2)
  params["positionsTDesignRadius"] = Float64.(ustrip.(uconvert.(Unitful.m, positions.radius)))
  params["positionsCenter"] = Float64.(ustrip.(uconvert.(Unitful.m, positions.center)))
  return params
end

getindex(tdes::SphericalTDesign, i::Integer) = tdes.radius.*tdes.positions[:,i] + tdes.center

"""
    loadTDesign(t::Int64, N::Int64, radius::S=10Unitful.mm, center::Vector{V}=[0.0,0.0,0.0]Unitful.mm, filename::String=joinpath(@__DIR__, "TDesigns.hd5")) where {S,V<:Unitful.Length}
*Description:* Returns the t-design array for chosen degree t and number of points N\\
\\
*Input:*
- `t` - degree
- `N` - number of points
- `radius` - radius of the sphere (default: 10mm)
- `center` - center of the sphere (default: [0.0,0.0,0.0]mm)
- `filename` - name of the file containing the t-designs (default loads TDesign.hd5)

*Output:*
- t-design of type SphericalTDesign in Cartesian coordinates containing t, radius, center and positions (which are located on the unit sphere unless `getindex(tdes,i)` is used)
"""
function loadTDesign(t::Int64, N::Int64, radius::S=10Unitful.mm, center::Vector{V}=[0.0,0.0,0.0]Unitful.mm, filename::String=joinpath(@__DIR__, "TDesigns.hd5")) where {S,V<:Unitful.Length}
  h5file = h5open(filename, "r")
  address = "/$t-Design/$N"

  if haskey(h5file, address)
    positions = copy(transpose(read(h5file, address)))
    return SphericalTDesign(UInt(t),radius,positions, center)
  else
    if haskey(h5file, "/$t-Design/")
      Ns = Int[]
      for N in keys(read(h5file, string("/$t-Design")))
	push!(Ns,parse(Int,N))
      end
      sort!(Ns)
      @info "No spherical $t-Design with $N points availible!\nThere are spherical $t-Designs with following N:" Ns
      throw(DomainError(1))
    else
      ts = Int[]
      for d in keys(read(h5file))
	m = match(r"(\d{1,})-(Design)",d)
	if m != nothing
	  push!(ts,parse(Int,m[1]))
        end
      end
      sort!(ts)
      @info "No spherical $t-Design availible!\n Choose another t."
      throw(DomainError(1))
    end
  end
end

# Unstructured collection of positions
mutable struct ArbitraryPositions{T} <: Positions where {T<:Unitful.Length}
  positions::Matrix{T}
end

getindex(apos::ArbitraryPositions, i::Integer) = apos.positions[:,i]

function ArbitraryPositions(grid::GridPositions)
  T = eltype(grid.fov)
  positions = zeros(T,3,length(grid))
  for i=1:length(grid)
    positions[:,i] = grid[i]
  end
  return ArbitraryPositions(positions)
end

function write(file::HDF5.File, apos::ArbitraryPositions,)
  write(file,"/positionsType", "ArbitraryPositions")
  write(file, "/positionsPositions", Float64.(ustrip.(uconvert.(Unitful.m, apos.positions))) )
end

function ArbitraryPositions(file::HDF5.File)
  pos = read(file, "/positionsPositions")*Unitful.m
  return ArbitraryPositions(pos)
end

# TODO: Specialize to make it faster
function Base.:(==)(val1::Positions, val2::Positions)
  for (x, y) in zip(val1, val2)
    if !(upreferred.(x) == upreferred.(y))
      return false
    end
  end

  return true
end

# fuction related to looping
length(tdes::SphericalTDesign) = size(tdes.positions,2)
length(apos::ArbitraryPositions) = size(apos.positions,2)
length(grid::GridPositions) = prod(grid.shape)
length(rpos::UniformRandomPositions) = rpos.N
length(mgrid::MeanderingGridPositions) = length(mgrid.grid)
length(bgrid::BreakpointGridPositions) = length(bgrid.grid)+length(bgrid.breakpointIndices)

start_(grid::Positions) = 1
next_(grid::Positions,state) = (grid[state],state+1)
done_(grid::Positions,state) = state > length(grid)
iterate(grid::Positions, s=start_(grid)) = done_(grid, s) ? nothing : next_(grid, s)

include("Interpolation.jl")


using PowerSimulations
using PowerSystems
using PowerModels
using DataFrames
using Dates
using JuMP
using Test
using Ipopt
using GLPK
using Cbc
using OSQP
using TimeSeries
using ParameterJuMP

import PowerSystems.UtilsData: TestData
download(TestData; branch = "master")

const PM = PowerModels
const PSY = PowerSystems
const PSI = PowerSimulations
const PJ = ParameterJuMP
abstract type TestOpProblem<:PSI.AbstractOperationsProblem end

ipopt_optimizer = JuMP.with_optimizer(Ipopt.Optimizer, print_level = 0)
ipopt_ws_solver = JuMP.with_optimizer(Ipopt.Optimizer, tol=1e-6, mu_init=1e-4, print_level=0)
GLPK_optimizer = JuMP.with_optimizer(GLPK.Optimizer)
Cbc_optimizer = JuMP.with_optimizer(Cbc.Optimizer, logLevel=0)
OSQP_optimizer = JuMP.with_optimizer(OSQP.Optimizer, verbose = false)

include("test_utils/get_test_data.jl")
include("test_utils/model_checks.jl")

if !Sys.iswindows()

    @testset "Common Functionalities" begin
        include("test_base_structs.jl")
        include("test_PowerModels_interface.jl")
    end

    @testset "Device Constructors" begin
        include("test_thermal_generation_constructors.jl")
        include("test_renewable_generation_constructors.jl")
        include("test_load_constructors.jl")
        include("test_storage_constructors.jl")
        include("test_hydro_generation_constructors.jl")
    end

    @testset "Network Constructors" begin
        include("test_network_constructors.jl")
    end

    @testset "Services Constructors" begin
        #include("test_services_constructor.jl")
    end

end

@testset "Operation Models" begin
    !Sys.iswindows() && include("test_operation_model_constructor.jl")
    include("test_operation_model_solve.jl")
    include("test_operation_model_modifications.jl")
end

@testset "Simulation Models" begin
    include("test_simulation_models.jl")
    include("test_load_simulation.jl")
    include("test_chronology.jl")
end


#=
Copyright (c) 2018-2022 Chris Coey, Lea Kapelevich, and contributors

This Julia package Hypatia.jl is released under the MIT license; see LICENSE
file in the root directory or at https://github.com/chriscoey/Hypatia.jl
=#

insts = OrderedDict()
insts["minimal"] = [
    ((1, 2, 2, 2, true),),
    ((1, 2, 2, 2, false),),
    ]
insts["fast"] = [
    ((2, 2, 3, 2, true),),
    ((2, 2, 3, 2, false),),
    ((3, 3, 3, 3, true),),
    ((3, 3, 3, 3, false),),
    ((3, 3, 5, 4, true),),
    ((5, 2, 5, 3, true),),
    ((1, 30, 2, 30, true),),
    ((1, 30, 2, 30, false),),
    ((10, 1, 3, 1, true),),
    ((10, 1, 3, 1, false),),
    ]
return (PolyEnvelopeNative, insts)


# provides compatibility with Distributions.jl
# maintained by @zsunberg

import Distributions
import Distributions: Distribution, UnivariateDistribution, MultivariateDistribution, Matrixvariate
import Distributions: Categorical, MvNormal

@generated function rand(rng::AbstractRNG, d::Distribution)
    Core.println("""
         WARNING: You are using a $d distribution from Distributions.jl. This will work, but simulations will not be repeatable because Distributions.jl does not support non-global random number generators (i.e. there is no `rand(rng::AbstractRNG, d::$d)`)

         For more information, or help Distributions.jl to support this, see https://github.com/JuliaStats/Distributions.jl/issues/436

         To disable this warning, use `rand(rng::AbstractRNG, d::$d) = rand(d)` (you will still not be able to repeat simulations).
         """)
    return quote
        rand(d)
    end
end

rand(rng::AbstractRNG, d::UnivariateDistribution) = Distributions.quantile(d, rand(rng)) # fallback

function Base.iterate(d::Categorical)
    iterate(1:Distributions.ncategories(d))
end
function Base.iterate(d::Categorical, state)
    iterate(1:Distributions.ncategories(d), state)
end

support(d::Categorical) = 1:Distributions.ncategories(d)

# sampletype(d::UnivariateDistribution) = eltype(d)
# sampletype(d::MultivariateDistribution) = Vector{eltype(d)}
# sampletype(d::Distribution{Matrixvariate}) = Matrix{eltype(d)}

# for MvNormal - this should be removed once Distributions.jl PR #597 is in a tagged Distributions.jl release
rand(rng::AbstractRNG, d::MvNormal) = _rand!(rng, d, Vector{eltype(d)}(undef, length(d)))

function _rand!(rng::AbstractRNG, d::MvNormal, x::VecOrMat)
    Distributions.add!(Distributions.unwhiten!(d.Σ, randn!(rng, x)), d.μ)
end


abstract type AbstractStructure{T, LT} end

mutable struct Structure{T <: AbstractFloat, AA<:AbstractAtom{T}, LT <: Length{T}} <: AbstractStructure{T, LT}
    name ::AbstractString
    cell ::Mat3{LT}
    atoms::Vector{AA}
    data ::Dict{Symbol, Any}
end
Structure() =
	Structure("NoName", eye(3), Atom[], Dict{Symbol, Any}())

Structure(name, cell::Mat3{LT}, atoms::Vector{Atom{T, LT}}) where {T<:AbstractFloat,LT<:Length{T}} =
	Structure{T, Atom{T, LT}, LT}(name, cell, atoms, Dict{Symbol, Any}())

Structure(str::AbstractStructure{T}, atoms::Vector{AT}) where {T<:AbstractFloat, AT<:AbstractAtom{T}} =
	Structure{T, AT}(name(str), cell(str), atoms, data(str))

Structure(cell::Matrix{LT}, atoms::Vector{Atom{T, LT}}) where {T<:AbstractFloat,LT<:Length{T}} =
	Structure{T, Atom{T, LT}, LT}("NoName", cell, atoms, Dict{Symbol, Any}())

Structure(cif_file::String; name="NoName") =
	cif2structure(cif_file, structure_name = name)

structure(str::Structure) = str
"""
Returns all the atoms inside the structure with the specified symbol
"""
function atoms(str::AbstractStructure, atsym::Symbol)
    out = AbstractAtom[]
    for at in str.atoms
        name(at) == atsym && push!(out, at)
    end
    return out
end
atoms(str::AbstractStructure) = structure(str).atoms
name(str::AbstractStructure) = structure(str).name
data(str::AbstractStructure) = structure(str).data

Base.length(str::AbstractStructure) = length(atoms(str))
cell(str::AbstractStructure) = structure(str).cell

projections(str::AbstractStructure) = projections.(atoms(str))
hasprojections(str::AbstractStructure) = !all(isempty, projections(str))
hasprojections_assert(str::AbstractStructure) =
    @assert hasprojections(str) "No projections found in structure $(str.name).
    Please set the projections for the atoms inside the structure first using `setprojections!`."

reciprocal(cell::AbstractMatrix) = inv(cell)

"""
    setprojections!(str::Structure, projs::Pair...)

Sets the projections of the specified atoms. `projs` has to be of form `:atsym => [:proj]`,
where proj = :s, :p, :d, :f, etc.
"""
function setprojections!(str::Structure, projs::Pair...)
    projdict = Dict(projs)
    for at in unique(str.atoms)
        if !haskey(projdict, name(at))
            projdict[name(at)] = [proj.orb for proj in projections(at)]
        end
    end
    emptyprojections!(str)
    addprojections!(atoms(str), projdict)
end

function emptyprojections!(str::Structure)
    for at in str.atoms
        empty!(projections(at))
    end
end

function nprojections(structure)
    n = 0
    for at in atoms(structure)
        projs = projections(at)
        if !isempty(projs)
            n += sum(orbsize.(projs))
        end
    end
    return n
end

#TODO extend base.merge
"Takes a vector of structures and merges all the attributes of the atoms."
function mergestructures(structures::Vector{<:AbstractStructure})
    nonvoid = filter(x -> x != nothing, structures)
    out = nonvoid[1]
    for structure in nonvoid[2:end]
        for at1 in atoms(out), at2 in atoms(structure)
            if at1 == at2
                for fname in fieldnames(typeof(at1))
                    if fname in [:name, :element, :position_cart, :position_cryst]
                        continue
                    end
                    field = getfield(at2, fname)
                    if field == nothing || isdefault(field)
                        continue
                    else
                        setfield!(at1, fname, field)
                    end
                end
            end
        end
    end
    return out
end

"Uses cif2cell to Meta.parse a cif file, then returns the parsed structure."
function cif2structure(cif_file::String; structure_name="NoName")
    tmpdir = dirname(cif_file)
    tmpfile = joinpath(tmpdir, "tmp.in")
    @assert splitext(cif_file)[2] == ".cif" error("Please specify a valid cif input file")
    run(`$pythonpath $cif2cellpath $cif_file -p quantum-espresso -o $tmpfile`)

    bla, structure = qe_read_input(tmpfile, structure_name = structure_name)
    rm(tmpfile)
    return structure
end

function setpseudos!(structure::AbstractStructure, atoms::Vector{<:AbstractAtom}, set::Symbol, specifier::String=""; kwargs...)
	dir = getdefault_pseudodir(set)
	if dir == nothing
		@warn "No pseudos found for set $set."
		return
	end
    for (i, at) in enumerate(atoms)
        pseudo = getdefault_pseudo(name(at), set, specifier=specifier)
        if pseudo == nothing
            @warn "Pseudo for $(name(at)) at index $i not found in set $set."
        else
            setpseudo!(at, pseudo; kwargs...)
        end
    end
end

setpseudos!(structure::AbstractStructure, atname::Symbol, set::Symbol, specifier::String=""; kwargs...) =
	setpseudos!(structure, atoms(structure, atname), set, specifier; kwargs...)

setpseudos!(structure::AbstractStructure, set::Symbol, specifier::String=""; kwargs...) =
	setpseudos!(structure, atoms(structure), set, specifier; kwargs...)

function setpseudos!(structure::AbstractStructure, at_pseudos::Pair{Symbol, Pseudo}...; kwargs...)
    for (atsym, pseudo) in at_pseudos
        for at in atoms(structure, atsym)
            setpseudo!(at, pseudo; kwargs...)
        end
    end
end

"""
    create_supercell(structure::AbstractStructure, na::Int, nb::Int, nc::Int;make_afm=false)

Takes a structure and creates a supercell from it with the given amount of extra cells (`na, nb, nc`) along the a, b, c direction.
If `make_afm` is set to `true` all the labels and magnetizations of the magnetic atoms will be reversed.
"""
function create_supercell(structure::AbstractStructure, na::Int, nb::Int, nc::Int; make_afm=false)
    orig_ats   = atoms(structure)
    orig_cell  = cell(structure)
    scale_mat  = diagm(0 => 1 .+ [na, nb, nc])

    orig_uats = unique(orig_ats)

    new_cell   = orig_cell * scale_mat
    new_atoms  = eltype(orig_ats)[]
    for ia=0:na, ib=0:nb, ic=0:nc
        transl_vec = orig_cell * [ia, ib, ic]
        factor = isodd(ia + ib + ic) ? -1 : 1
        for at in orig_ats
	        cart_pos = position_cart(at) + transl_vec
	        cryst_pos = inv(new_cell) * cart_pos
            if make_afm && ismagnetic(at)
                new_magnetization = factor*magnetization(at)
                push!(new_atoms, Atom(at, projections=deepcopy(projections(at)), magnetization=new_magnetization, position_cart = cart_pos, position_cryst = Point3(cryst_pos)))
            else
                push!(new_atoms, Atom(at, projections=deepcopy(projections(at)), position_cart=cart_pos, position_cryst=Point3(cryst_pos)))
            end
        end
    end
    return Structure(name(structure), Mat3(new_cell), new_atoms, data(structure))
end

"Rescales the cell of the structure."
function scale_cell!(structure::Structure, v)
	scalemat = [v 0 0; 0 v 0; 0 0 v]
	structure.cell *= scalemat
	for at in atoms(structure)
		at.position_cart = structure.cell * at.position_cryst
	end
	structure.cell
end

function set_magnetization!(str::Structure, atsym_mag::Pair{Symbol, <:AbstractVector}...)
	for (atsym, mag) in atsym_mag
		for at in atoms(str, atsym)
			set_magnetization!(at, mag)
		end
	end
end

"""
    volume(cell::Mat3)
	volume(str::Structure)

Calculates the volume for the unit cell.
"""
volume(cell::Mat3) = det(cell)

volume(str::Structure) = cell(str)

# const DEFAULT_TOLERANCE = 1e-5

# struct SPGStructure
#     lattice::Matrix{Cdouble}
#     positions::Matrix{Cdouble}
#     species_indices::Vector{Cint}
# end

# function SPGStructure(s::Structure)
#     clattice = convert(Matrix{Cdouble}, ustrip.(cell(s)'))
#     cpositions = convert(Matrix{Cdouble}, hcat(position_cryst.(atoms(s))...))
#     uats         = unique(atoms(s))
#     species_indices = Cint[findfirst(x -> isequal_species(x, at), uats) for at in atoms(s)]
#     return SPGStructure(clattice, cpositions, species_indices)
# end

# function symmetry_operators(s::SPGStructure; maxsize=52, tolerance = DEFAULT_TOLERANCE)
#     rotations    = Array{Cint}(undef, 3, 3, maxsize)
#     translations = Array{Cdouble}(undef, 3, maxsize)

#     num_ops = ccall((:spg_get_symmetry, SPGLIB), Cint,
#       (Ptr{Cint}, Ptr{Cdouble}, Cint, Ptr{Cdouble}, Ptr{Cdouble}, Ptr{Cint}, Cint, Cdouble),
#        rotations, translations, maxsize, s.lattice, s.positions, s.species_indices, length(s.species_indices), tolerance)
#     return (rotations=[Mat3{Int}(rotations[:, :, i]) for i=1:num_ops], translations=[Vec3(translations[:, i]) for i=1:num_ops])
# end
# symmetry_operators(s::Structure; kwargs...) = symmetry_operators(SPGStructure(s); kwargs...)

# function international(s::SPGStructure; tolerance = DEFAULT_TOLERANCE)
#     res = zeros(Cchar, 11)

#     num = ccall((:spg_get_international, SPGLIB), Cint,
#                    (Ptr{Cchar}, Ptr{Cdouble}, Ptr{Cdouble}, Ptr{Cint}, Cint, Cdouble),
#                    res, s.lattice, s.positions, s.species_indices, length(s.species_indices), tolerance)
#     num == 0 && error("Could not determine the international symbol.")

#     return num, join(convert(Vector{Char}, res[1:findfirst(iszero, res) - 1]))
# end
# international(s::Structure; kwargs...) = international(SPGStructure(s); kwargs...)

# function niggli_reduce(c::Mat3{Float64}; tolerance = DEFAULT_TOLERANCE)
#     ccell = convert(Matrix{Cdouble}, c)
#     numops = ccall((:spg_niggli_reduce, SPGLIB), Cint,
#                    (Ptr{Cdouble}, Cdouble),
#                    ccell, tolerance)
#     return Mat3{Float64}(ccell)
# end

# function niggli_reduce!(s::SPGStructure; tolerance = DEFAULT_TOLERANCE)

#     numops = ccall((:spg_niggli_reduce, SPGLIB), Cint,
#                    (Ptr{Cdouble}, Cdouble),
#                    s.lattice, tolerance)
#     numops == 0 && error("Could not determine the niggli reduced cell.")

#     return s
# end
# niggli_reduce(s::Structure; kwargs...) = niggli_reduce!(SPGStructure(s); kwargs...).*unit(eltype(cell(s)))

# function find_primitive!(s::SPGStructure; tolerance = DEFAULT_TOLERANCE)
#     numats = ccall((:spg_find_primitive, SPGLIB), Cint,
#                    (Ptr{Cdouble}, Ptr{Cdouble}, Ptr{Cint}, Cint, Cdouble),
#                    s.lattice, s.positions, s.species_indices, length(s.species_indices), tolerance)
#     numats == 0 && error("Could not find the primitive of the supplied structure.")
#     return SPGStructure(s.lattice, s.positions[:, 1:numats], s.species_indices[1:numats])
# end

# function find_primitive(s::Structure; kwargs...)
#     uats = unique(atoms(s))
#     spg = find_primitive!(SPGStructure(s))
#     new_cell = Mat3{Float64}(spg.lattice)*unit(eltype(cell(s)))
#     newats = eltype(uats)[]
#     for i = 1:length(spg.species_indices)
#         tat = deepcopy(uats[spg.species_indices[i]])
#         set_position!(tat, Point3(spg.positions[:, i]), new_cell)
#         push!(newats, tat)
#     end

#     return Structure(s.name, new_cell,newats, s.data)
# end

# """
#     cell_parameters(cell::Mat3)
#     cell_parameters(str::Structure)

# Parameters (a, b, c, α, β, γ) of the input cell returned in a named tuple.
# """
# function cell_parameters(cell::Mat3)
#     G = transpose(cell) * cell
#     a, b, c = sqrt.(diag(G))
#     α = acos(0.5(G[2, 3] + G[3, 2])/(c * b))
#     β = acos(0.5(G[3, 1] + G[1, 3])/(a * c))
#     γ = acos(0.5(G[1, 2] + G[2, 1])/(a * b))
#     a, b, c, α, β, γ
# end

# cell_parameters(s::Structure) = cell_parameters(ustrip.(cell(s)))

# function crystal_kind(s::Structure; tolerance=DEFAULT_TOLERANCE)
#     n, sym = international(s)
#     f = (i, j) -> i <= n <= j
#     cs = [:triclinic => (1, 2), :monoclinic => (3, 15),
#           :orthorhombic => (16, 74), :tetragonal => (75, 142),
#           :trigonal => (143, 167), :hexagonal => (168, 194),
#           :cubic => (195, 230)]
#     for (k, v) in cs
#         if f(v...)
#             return k
#         end
#     end
#     error("Crystal kind could not be determined")
# end

# function lattice_kind(s::Structure; tolerance=DEFAULT_TOLERANCE)
#     n, sym = international(s)
#     kind = crystal_kind(s; tolerance=tolerance)
#     if n ∈ (146, 148, 155, 160, 161, 166, 167)
#         return :rhombohedral
#     elseif kind == :trigonal
#         return :hexagonal
#     else
#         return kind
#     end
# end

# function high_symmetry_kpoints(s::Structure; tolerance=DEFAULT_TOLERANCE)
#     n, sym       = international(s)
#     kind = lattice_kind(s)
#     primitive    = find_primitive(s)

#     primcell = ustrip.(cell(primitive))

#     a, b, c, α, β, γ = cell_parameters(ustrip.(cell(s)))

#     symerror = () -> error("Unexpected value for international symbol: $sym")
#     if kind == :cubic
#         if occursin("P", sym)
#             return cubic()
#         elseif occursin("F", sym)
#             return fcc()
#         elseif occursin("I", sym)
#             return bcc()
#         else
#             symerror()
#         end
#     elseif kind == :tetragonal
#         if occursin("P", sym)
#             return tet()
#         elseif occursin("I", sym)
#             if c < a
#                 return bctet1(c, a)
#             else
#                 return bctet2(c, a)
#             end
#         else
#             symerror()
#         end
#     elseif kind == :orthorhombic
#         if occursin("P", sym)
#             return orc()
#         elseif occursin("F", sym)
#             if 1 / a^2 > 1 / b^2 + 1 / c^2
#                 return orcf1(a, b, c)
#             elseif 1 / a^2 < 1 / b^2 + 1 / c^2
#                 return orcf2(a, b, c)
#             else
#                 return orcf3(a, b, c)
#             end

#         elseif occursin("I", sym)
#             return orci(a, b, c)

#         elseif occursin("C", sym) || occursin("A", sym)
#             return orcc(a, b, c)
#         else
#             symerror()
#         end

#     elseif kind == :hexagonal
#         return hex()

#     elseif kind == :rhombohedral
#         if α < π/2
#             return rhl1(α)
#         else
#             return rhl2(α)
#         end

#     elseif kind == :monoclinic
#         prim_rec = inv(primcell)
#         ka, kb, kc, kα, kβ, kγ = cell_parameters(prim_rec)
#         if occursin("P", sym)
#             return mcl(b, c, α)

#         elseif occursin("C", sym)
#             if kγ > π/2
#                 return mclc1(a, b, c, α)
#             elseif kγ == π/2
#                 return mclc2(a, b, c, α)
#             elseif kγ < π/2
#                 if b * cos(α) / c + b^2 * sin(α)^2 / a^2 < 1
#                     return mclc3(a, b, c, α)
#                 elseif b * cos(α) / c + b^2 * sin(α)^2 / a^2 == 1
#                     return mclc4(a, b, c, α)
#                 elseif b * cos(α) / c + b^2 * sin(α)^2 / a^2 > 1
#                     return mclc5(a, b, c, α)
#                 end
#             end
#         else
#             symerror()
#         end

#     elseif kind == :triclinic
#         prim_rec = inv(primcell)
#         ka, kb, kc, kα, kβ, kγ = cell_parameters(prim_rec)
#         if kα > π/2 && kβ > π/2 && kγ > π/2
#             return tria()
#         elseif kα < π/2 && kβ < π/2 && kγ < π/2
#             return trib()
#         elseif kα > π/2 && kβ > π/2 && kγ == π/2
#             return tria()
#         elseif kα < π/2 && kβ < π/2 && kγ == π/2
#             return trib()
#         end

#     else
#         error("Unknown lattice type $kind")
#     end
# end

# function cubic()
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                    :X => Vec3([0.0, 0.5, 0.0]),
#                    :R => Vec3([0.5, 0.5, 0.5]),
#                    :M => Vec3([0.5, 0.5, 0.0]))
#     path = [[:Γ, :X, :M, :Γ, :R, :X], [:M, :R]]
#     return (kpoints=kpoints, path=path)
# end

# function fcc()
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                    :K => Vec3([3.0 / 8.0, 3.0 / 8.0, 3.0 / 4.0]),
#                    :L => Vec3([0.5, 0.5, 0.5]),
#                    :U => Vec3([5.0 / 8.0, 1.0 / 4.0, 5.0 / 8.0]),
#                    :W => Vec3([0.5, 1.0 / 4.0, 3.0 / 4.0]),
#                    :X => Vec3([0.5, 0.0, 0.5]))

#     path = [[:Γ, :X, :W, :K,
#                  :Γ, :L, :U, :W, :L, :K], [:U, :X]]
#     return (kpoints=kpoints, path=path)
# end

# function bcc()
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                    :H => Vec3([0.5, -0.5, 0.5]),
#                    :P => Vec3([0.25, 0.25, 0.25]),
#                    :N => Vec3([0.0, 0.0, 0.5]))
#     path = [[:Γ, :H, :N, :Γ, :P, :H], [:P, :N]]
#     return (kpoints=kpoints, path=path)
# end

# function tet()
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                    :A => Vec3([0.5, 0.5, 0.5]),
#                    :M => Vec3([0.5, 0.5, 0.0]),
#                    :R => Vec3([0.0, 0.5, 0.5]),
#                    :X => Vec3([0.0, 0.5, 0.0]),
#                    :Z => Vec3([0.0, 0.0, 0.5]))
#         path = [[:Γ, :X, :M, :Γ, :Z, :R, :A, :Z], [:X, :R],
#                 [:M, :A]]

#     return (kpoints=kpoints, path=path)
# end

# function bctet1(c, a)
#     eta = (1 + c^2 / a^2) / 4.0
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :M => Vec3([-0.5, 0.5, 0.5]),
#                :N => Vec3([0.0, 0.5, 0.0]),
#                :P => Vec3([0.25, 0.25, 0.25]),
#                :X => Vec3([0.0, 0.0, 0.5]),
#                :Z => Vec3([eta, eta, -eta]),
#                :Z_1 => Vec3([-eta, 1 - eta, eta]))
#     path = [[:Γ, :X, :M, :Γ, :Z, :P, :N, :Z_1, :M],
#             [:X, :P]]
#     return (kpoints=kpoints, path=path)
# end

# function bctet2(c, a)
#     eta = (1 + a^2 / c^2) / 4.0
#     zeta = a^2 / (2 * c^2)
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :N => Vec3([0.0, 0.5, 0.0]),
#                :P => Vec3([0.25, 0.25, 0.25]),
#                :Sigma => Vec3([-eta, eta, eta]),
#                :Sigma_1 => Vec3([eta, 1 - eta, -eta]),
#                :X => Vec3([0.0, 0.0, 0.5]),
#                :Y => Vec3([-zeta, zeta, 0.5]),
#                :Y_1 => Vec3([0.5, 0.5, -zeta]),
#                :Z => Vec3([0.5, 0.5, -0.5]))
#     path = [[:Γ, :X, :Y, :Sigma, :Γ, :Z,
#              :Sigma_1, :N, :P, :Y_1, :Z], [:X, :P]]
#     return (kpoints=kpoints, path=path)
# end

# function orc()
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :R => Vec3([0.5, 0.5, 0.5]),
#                :S => Vec3([0.5, 0.5, 0.0]),
#                :T => Vec3([0.0, 0.5, 0.5]),
#                :U => Vec3([0.5, 0.0, 0.5]),
#                :X => Vec3([0.5, 0.0, 0.0]),
#                :Y => Vec3([0.0, 0.5, 0.0]),
#                :Z => Vec3([0.0, 0.0, 0.5]))
#     path = [[:Γ, :X, :S, :Y, :Γ,
#              :Z, :U, :R, :T, :Z], [:Y, :T], [:U, :X], [:S, :R]]
#     return (kpoints=kpoints, path=path)
# end

# function orcf1(a, b, c)
#     zeta = (1 + a^2 / b^2 - a^2 / c^2) / 4
#     eta = (1 + a^2 / b^2 + a^2 / c^2) / 4

#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :A => Vec3([0.5, 0.5 + zeta, zeta]),
#                :A_1 => Vec3([0.5, 0.5 - zeta, 1 - zeta]),
#                :L => Vec3([0.5, 0.5, 0.5]),
#                :T => Vec3([1, 0.5, 0.5]),
#                :X => Vec3([0.0, eta, eta]),
#                :X_1 => Vec3([1, 1 - eta, 1 - eta]),
#                :Y => Vec3([0.5, 0.0, 0.5]),
#                :Z => Vec3([0.5, 0.5, 0.0]))
#     path = [[:Γ, :Y, :T, :Z, :Γ, :X, :A_1, :Y],
#             [:T, :X_1], [:X, :A, :Z], [:L, :Γ]]
#     return (kpoints=kpoints, path=path)
# end

# function orcf2(a, b, c)
#     phi = (1 + c^2 / b^2 - c^2 / a^2) / 4
#     eta = (1 + a^2 / b^2 - a^2 / c^2) / 4
#     delta = (1 + b^2 / a^2 - b^2 / c^2) / 4
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :C => Vec3([0.5, 0.5 - eta, 1 - eta]),
#                :C_1 => Vec3([0.5, 0.5 + eta, eta]),
#                :D => Vec3([0.5 - delta, 0.5, 1 - delta]),
#                :D_1 => Vec3([0.5 + delta, 0.5, delta]),
#                :L => Vec3([0.5, 0.5, 0.5]),
#                :H => Vec3([1 - phi, 0.5 - phi, 0.5]),
#                :H_1 => Vec3([phi, 0.5 + phi, 0.5]),
#                :X => Vec3([0.0, 0.5, 0.5]),
#                :Y => Vec3([0.5, 0.0, 0.5]),
#                :Z => Vec3([0.5, 0.5, 0.0]))
#     path = [[:Γ, :Y, :C, :D, :X, :Γ,
#              :Z, :D_1, :H, :C], [:C_1, :Z], [:X, :H_1], [:H, :Y],
#             [:L, :Γ]]
#     return (kpoints=kpoints, path=path)
# end

# function orcf3(a, b, c)
#     zeta = (1 + a^2 / b^2 - a^2 / c^2) / 4
#     eta = (1 + a^2 / b^2 + a^2 / c^2) / 4
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :A => Vec3([0.5, 0.5 + zeta, zeta]),
#                :A_1 => Vec3([0.5, 0.5 - zeta, 1 - zeta]),
#                :L => Vec3([0.5, 0.5, 0.5]),
#                :T => Vec3([1, 0.5, 0.5]),
#                :X => Vec3([0.0, eta, eta]),
#                :X_1 => Vec3([1, 1 - eta, 1 - eta]),
#                :Y => Vec3([0.5, 0.0, 0.5]),
#                :Z => Vec3([0.5, 0.5, 0.0]))
#     path = [[:Γ, :Y, :T, :Z, :Γ, :X, :A_1, :Y],
#             [:X, :A, :Z], [:L, :Γ]]
#     return (kpoints=kpoints, path=path)
# end

# function orci(a, b, c)
#     zeta = (1 + a^2 / c^2) / 4
#     eta = (1 + b^2 / c^2) / 4
#     delta = (b^2 - a^2) / (4 * c^2)
#     mu = (a^2 + b^2) / (4 * c^2)
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :L => Vec3([-mu, mu, 0.5 - delta]),
#                :L_1 => Vec3([mu, -mu, 0.5 + delta]),
#                :L_2 => Vec3([0.5 - delta, 0.5 + delta, -mu]),
#                :R => Vec3([0.0, 0.5, 0.0]),
#                :S => Vec3([0.5, 0.0, 0.0]),
#                :T => Vec3([0.0, 0.0, 0.5]),
#                :W => Vec3([0.25, 0.25, 0.25]),
#                :X => Vec3([-zeta, zeta, zeta]),
#                :X_1 => Vec3([zeta, 1 - zeta, -zeta]),
#                :Y => Vec3([eta, -eta, eta]),
#                :Y_1 => Vec3([1 - eta, eta, -eta]),
#                :Z => Vec3([0.5, 0.5, -0.5]))
#     path = [[:Γ, :X, :L, :T, :W, :R, :X_1, :Z,
#              :Γ, :Y, :S, :W], [:L_1, :Y], [:Y_1, :Z]]
#     return (kpoints=kpoints, path=path)
# end

# function orcc(a, b, c)
#     zeta = (1 + a^2 / b^2) / 4
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :A => Vec3([zeta, zeta, 0.5]),
#                :A_1 => Vec3([-zeta, 1 - zeta, 0.5]),
#                :R => Vec3([0.0, 0.5, 0.5]),
#                :S => Vec3([0.0, 0.5, 0.0]),
#                :T => Vec3([-0.5, 0.5, 0.5]),
#                :X => Vec3([zeta, zeta, 0.0]),
#                :X_1 => Vec3([-zeta, 1 - zeta, 0.0]),
#                :Y => Vec3([-0.5, 0.5, 0]),
#                :Z => Vec3([0.0, 0.0, 0.5]))
#     path = [[:Γ, :X, :S, :R, :A, :Z,
#              :Γ, :Y, :X_1, :A_1, :T, :Y], [:Z, :T]]
#     return (kpoints=kpoints, path=path)
# end

# function hex()
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :A => Vec3([0.0, 0.0, 0.5]),
#                :H => Vec3([1.0 / 3.0, 1.0 / 3.0, 0.5]),
#                :K => Vec3([1.0 / 3.0, 1.0 / 3.0, 0.0]),
#                :L => Vec3([0.5, 0.0, 0.5]),
#                :M => Vec3([0.5, 0.0, 0.0]))
#     path = [[:Γ, :M, :K, :Γ, :A, :L, :H, :A], [:L, :M],
#             [:K, :H]]
#     return (kpoints=kpoints, path=path)
# end

# function rhl1(alpha)
#     eta = (1 + 4 * cos(alpha)) / (2 + 4 * cos(alpha))
#     nu = 3.0 / 4.0 - eta / 2.0
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :B => Vec3([eta, 0.5, 1.0 - eta]),
#                :B_1 => Vec3([1.0 / 2.0, 1.0 - eta, eta - 1.0]),
#                :F => Vec3([0.5, 0.5, 0.0]),
#                :L => Vec3([0.5, 0.0, 0.0]),
#                :L_1 => Vec3([0.0, 0.0, -0.5]),
#                :P => Vec3([eta, nu, nu]),
#                :P_1 => Vec3([1.0 - nu, 1.0 - nu, 1.0 - eta]),
#                :P_2 => Vec3([nu, nu, eta - 1.0]),
#                :Q => Vec3([1.0 - nu, nu, 0.0]),
#                :X => Vec3([nu, 0.0, -nu]),
#                :Z => Vec3([0.5, 0.5, 0.5]))
#     path = [[:Γ, :L, :B_1], [:B, :Z, :Γ, :X],
#             [:Q, :F, :P_1, :Z], [:L, :P]]
#     return (kpoints=kpoints, path=path)
# end

# function rhl2(alpha)
#     eta = 1 / (2 * tan(alpha / 2.0)^2)
#     nu = 3.0 / 4.0 - eta / 2.0
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :F => Vec3([0.5, -0.5, 0.0]),
#                :L => Vec3([0.5, 0.0, 0.0]),
#                :P => Vec3([1 - nu, -nu, 1 - nu]),
#                :P_1 => Vec3([nu, nu - 1.0, nu - 1.0]),
#                :Q => Vec3([eta, eta, eta]),
#                :Q_1 => Vec3([1.0 - eta, -eta, -eta]),
#                :Z => Vec3([0.5, -0.5, 0.5]))
#     path = [[:Γ, :P, :Z, :Q, :Γ,
#              :F, :P_1, :Q_1, :L, :Z]]
#     return (kpoints=kpoints, path=path)
# end

# function mcl(b, c, beta)
#     eta = (1 - b * cos(beta) / c) / (2 * sin(beta)^2)
#     nu = 0.5 - eta * c * cos(beta) / b
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :A => Vec3([0.5, 0.5, 0.0]),
#                :C => Vec3([0.0, 0.5, 0.5]),
#                :D => Vec3([0.5, 0.0, 0.5]),
#                :D_1 => Vec3([0.5, 0.5, -0.5]),
#                :E => Vec3([0.5, 0.5, 0.5]),
#                :H => Vec3([0.0, eta, 1.0 - nu]),
#                :H_1 => Vec3([0.0, 1.0 - eta, nu]),
#                :H_2 => Vec3([0.0, eta, -nu]),
#                :M => Vec3([0.5, eta, 1.0 - nu]),
#                :M_1 => Vec3([0.5, 1 - eta, nu]),
#                :M_2 => Vec3([0.5, 1 - eta, nu]),
#                :X => Vec3([0.0, 0.5, 0.0]),
#                :Y => Vec3([0.0, 0.0, 0.5]),
#                :Y_1 => Vec3([0.0, 0.0, -0.5]),
#                :Z => Vec3([0.5, 0.0, 0.0]))
#     path = [[:Γ, :Y, :H, :C, :E, :M_1, :A, :X, :H_1],
#             [:M, :D, :Z], [:Y, :D]]
#     return (kpoints=kpoints, path=path)
# end

# function mclc1(a, b, c, alpha)
#     zeta = (2 - b * cos(alpha) / c) / (4 * sin(alpha)^2)
#     eta = 0.5 + 2 * zeta * c * cos(alpha) / b
#     psi = 0.75 - a^2 / (4 * b^2 * sin(alpha)^2)
#     phi = psi + (0.75 - psi) * b * cos(alpha) / c
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :N => Vec3([0.5, 0.0, 0.0]),
#                :N_1 => Vec3([0.0, -0.5, 0.0]),
#                :F => Vec3([1 - zeta, 1 - zeta, 1 - eta]),
#                :F_1 => Vec3([zeta, zeta, eta]),
#                :F_2 => Vec3([-zeta, -zeta, 1 - eta]),
#                # :F_3 => Vec3([1 - zeta, -zeta, 1 - eta]),
#                :I => Vec3([phi, 1 - phi, 0.5]),
#                :I_1 => Vec3([1 - phi, phi - 1, 0.5]),
#                :L => Vec3([0.5, 0.5, 0.5]),
#                :M => Vec3([0.5, 0.0, 0.5]),
#                :X => Vec3([1 - psi, psi - 1, 0.0]),
#                :X_1 => Vec3([psi, 1 - psi, 0.0]),
#                :X_2 => Vec3([psi - 1, -psi, 0.0]),
#                :Y => Vec3([0.5, 0.5, 0.0]),
#                :Y_1 => Vec3([-0.5, -0.5, 0.0]),
#                :Z => Vec3([0.0, 0.0, 0.5]))
#     path = [[:Γ, :Y, :F, :L, :I], [:I_1, :Z, :F_1],
#             [:Y, :X_1], [:X, :Γ, :N], [:M, :Γ]]
#     return (kpoints=kpoints, path=path)
# end

# function mclc2(a, b, c, alpha)
#     zeta = (2 - b * cos(alpha) / c) / (4 * sin(alpha)^2)
#     eta = 0.5 + 2 * zeta * c * cos(alpha) / b
#     psi = 0.75 - a^2 / (4 * b^2 * sin(alpha)^2)
#     phi = psi + (0.75 - psi) * b * cos(alpha) / c
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :N => Vec3([0.5, 0.0, 0.0]),
#                :N_1 => Vec3([0.0, -0.5, 0.0]),
#                :F => Vec3([1 - zeta, 1 - zeta, 1 - eta]),
#                :F_1 => Vec3([zeta, zeta, eta]),
#                :F_2 => Vec3([-zeta, -zeta, 1 - eta]),
#                :F_3 => Vec3([1 - zeta, -zeta, 1 - eta]),
#                :I => Vec3([phi, 1 - phi, 0.5]),
#                :I_1 => Vec3([1 - phi, phi - 1, 0.5]),
#                :L => Vec3([0.5, 0.5, 0.5]),
#                :M => Vec3([0.5, 0.0, 0.5]),
#                :X => Vec3([1 - psi, psi - 1, 0.0]),
#                :X_1 => Vec3([psi, 1 - psi, 0.0]),
#                :X_2 => Vec3([psi - 1, -psi, 0.0]),
#                :Y => Vec3([0.5, 0.5, 0.0]),
#                :Y_1 => Vec3([-0.5, -0.5, 0.0]),
#                :Z => Vec3([0.0, 0.0, 0.5]))
#     path = [[:Γ, :Y, :F, :L, :I], [:I_1, :Z, :F_1],
#             [:N, :Γ, :M]]
#     return (kpoints=kpoints, path=path)
# end

# function mclc3(a, b, c, alpha)
#     mu = (1 + b^2 / a^2) / 4.0
#     delta = b * c * cos(alpha) / (2 * a^2)
#     zeta = mu - 0.25 + (1 - b * cos(alpha) / c) / (4 * sin(alpha)^2)
#     eta = 0.5 + 2 * zeta * c * cos(alpha) / b
#     phi = 1 + zeta - 2 * mu
#     psi = eta - 2 * delta
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :F => Vec3([1 - phi, 1 - phi, 1 - psi]),
#                :F_1 => Vec3([phi, phi - 1, psi]),
#                :F_2 => Vec3([1 - phi, -phi, 1 - psi]),
#                :H => Vec3([zeta, zeta, eta]),
#                :H_1 => Vec3([1 - zeta, -zeta, 1 - eta]),
#                :H_2 => Vec3([-zeta, -zeta, 1 - eta]),
#                :I => Vec3([0.5, -0.5, 0.5]),
#                :M => Vec3([0.5, 0.0, 0.5]),
#                :N => Vec3([0.5, 0.0, 0.0]),
#                :N_1 => Vec3([0.0, -0.5, 0.0]),
#                :X => Vec3([0.5, -0.5, 0.0]),
#                :Y => Vec3([mu, mu, delta]),
#                :Y_1 => Vec3([1 - mu, -mu, -delta]),
#                :Y_2 => Vec3([-mu, -mu, -delta]),
#                :Y_3 => Vec3([mu, mu - 1, delta]),
#                :Z => Vec3([0.0, 0.0, 0.5]))
#     path = [[:Γ, :Y, :F, :H, :Z, :I, :F_1],
#             [:H_1, :Y_1, :X, :Γ, :N], [:M, :Γ]]
#     return (kpoints=kpoints, path=path)
# end

# function mclc4(a, b, c, alpha)
#     mu = (1 + b^2 / a^2) / 4.0
#     delta = b * c * cos(alpha) / (2 * a^2)
#     zeta = mu - 0.25 + (1 - b * cos(alpha) / c) / (4 * sin(alpha)^2)
#     eta = 0.5 + 2 * zeta * c * cos(alpha) / b
#     phi = 1 + zeta - 2 * mu
#     psi = eta - 2 * delta
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :F => Vec3([1 - phi, 1 - phi, 1 - psi]),
#                :F_1 => Vec3([phi, phi - 1, psi]),
#                :F_2 => Vec3([1 - phi, -phi, 1 - psi]),
#                :H => Vec3([zeta, zeta, eta]),
#                :H_1 => Vec3([1 - zeta, -zeta, 1 - eta]),
#                :H_2 => Vec3([-zeta, -zeta, 1 - eta]),
#                :I => Vec3([0.5, -0.5, 0.5]),
#                :M => Vec3([0.5, 0.0, 0.5]),
#                :N => Vec3([0.5, 0.0, 0.0]),
#                :N_1 => Vec3([0.0, -0.5, 0.0]),
#                :X => Vec3([0.5, -0.5, 0.0]),
#                :Y => Vec3([mu, mu, delta]),
#                :Y_1 => Vec3([1 - mu, -mu, -delta]),
#                :Y_2 => Vec3([-mu, -mu, -delta]),
#                :Y_3 => Vec3([mu, mu - 1, delta]),
#                :Z => Vec3([0.0, 0.0, 0.5]))
#     path = [[:Γ, :Y, :F, :H, :Z, :I],
#             [:H_1, :Y_1, :X, :Γ, :N], [:M, :Γ]]
#     return (kpoints=kpoints, path=path)
# end

# function mclc5(a, b, c, alpha)
#     zeta = (b^2 / a^2 + (1 - b * cos(alpha) / c)
#             / sin(alpha)^2) / 4
#     eta = 0.5 + 2 * zeta * c * cos(alpha) / b
#     mu = eta / 2 + b^2 / (4 * a^2) - b * c * cos(alpha) / (2 * a^2)
#     nu = 2 * mu - zeta
#     rho = 1 - zeta * a^2 / b^2
#     omega = (4 * nu - 1 - b^2 * sin(alpha)^2 / a^2) * c / (2 * b * cos(alpha))
#     delta = zeta * c * cos(alpha) / b + omega / 2 - 0.25
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :F => Vec3([nu, nu, omega]),
#                :F_1 => Vec3([1 - nu, 1 - nu, 1 - omega]),
#                :F_2 => Vec3([nu, nu - 1, omega]),
#                :H => Vec3([zeta, zeta, eta]),
#                :H_1 => Vec3([1 - zeta, -zeta, 1 - eta]),
#                :H_2 => Vec3([-zeta, -zeta, 1 - eta]),
#                :I => Vec3([rho, 1 - rho, 0.5]),
#                :I_1 => Vec3([1 - rho, rho - 1, 0.5]),
#                :L => Vec3([0.5, 0.5, 0.5]),
#                :M => Vec3([0.5, 0.0, 0.5]),
#                :N => Vec3([0.5, 0.0, 0.0]),
#                :N_1 => Vec3([0.0, -0.5, 0.0]),
#                :X => Vec3([0.5, -0.5, 0.0]),
#                :Y => Vec3([mu, mu, delta]),
#                :Y_1 => Vec3([1 - mu, -mu, -delta]),
#                :Y_2 => Vec3([-mu, -mu, -delta]),
#                :Y_3 => Vec3([mu, mu - 1, delta]),
#                :Z => Vec3([0.0, 0.0, 0.5]))
#     path = [[:Γ, :Y, :F, :L, :I], [:I_1, :Z, :H, :F_1],
#             [:H_1, :Y_1, :X, :Γ, :N], [:M, :Γ]]
#     return (kpoints=kpoints, path=path)
# end

# function tria()
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :L => Vec3([0.5, 0.5, 0.0]),
#                :M => Vec3([0.0, 0.5, 0.5]),
#                :N => Vec3([0.5, 0.0, 0.5]),
#                :R => Vec3([0.5, 0.5, 0.5]),
#                :X => Vec3([0.5, 0.0, 0.0]),
#                :Y => Vec3([0.0, 0.5, 0.0]),
#                :Z => Vec3([0.0, 0.0, 0.5]))
#     path = [[:X, :Γ, :Y], [:L, :Γ, :Z],
#             [:N, :Γ, :M], [:R, :Γ]]
#     return (kpoints=kpoints, path=path)
# end

# function trib()
#     kpoints = Dict(:Γ => Vec3([0.0, 0.0, 0.0]),
#                :L => Vec3([0.5, -0.5, 0.0]),
#                :M => Vec3([0.0, 0.0, 0.5]),
#                :N => Vec3([-0.5, -0.5, 0.5]),
#                :R => Vec3([0.0, -0.5, 0.5]),
#                :X => Vec3([0.0, -0.5, 0.0]),
#                :Y => Vec3([0.5, 0.0, 0.0]),
#                :Z => Vec3([-0.5, 0.0, 0.5]))
#     path = [[:X, :Γ, :Y], [:L, :Γ, :Z],
#             [:N, :Γ, :M], [:R, :Γ]]
#     return (kpoints=kpoints, path=path)
# end


"""
    AdiabaticASEModel{A} <: AdiabaticModel

Wrapper for an `ase.Atoms` object that has a calculator attached.
This will synchronise the positions with the `ase` object and handle the unit conversions.

Implements both `potential` and `derivative!`.
"""
struct AdiabaticASEModel{A} <: AdiabaticModel
    atoms::A
end

NQCModels.ndofs(::AdiabaticASEModel) = 3

function NQCModels.potential(model::AdiabaticASEModel, R::AbstractMatrix)
    set_coordinates!(model, R)
    V = model.atoms.get_potential_energy()
    return austrip(V * u"eV")
end

function NQCModels.derivative!(model::AdiabaticASEModel, D::AbstractMatrix, R::AbstractMatrix)
    set_coordinates!(model, R)
    D .= -model.atoms.get_forces()'
    @. D = austrip(D * u"eV/Å")
    return D
end

function set_coordinates!(model::AdiabaticASEModel, R)
    model.atoms.set_positions(ustrip.(auconvert.(u"Å", R')))
end


module MLJEnsembles

using MLJModelInterface
import MLJModelInterface: predict, fit
import MLJBase # still needed for aggregating measures in oob-estimates of error
using Random
using CategoricalArrays
using CategoricalDistributions
using ComputationalResources
using Distributed
import Distributions
using ProgressMeter
import StatsBase

export EnsembleModel

const MMI = MLJModelInterface

include("ensembles.jl")

end # module