# Copyright (c) 2018: Matthew Wilhelm & Matthew Stuber.
# This code is licensed under MIT license (see LICENSE.md for full details)
#############################################################################
# McCormick.jl
# A McCormick operator library in Julia
# See https://github.com/PSORLab/McCormick.jl
#############################################################################
# src/forward_operators/arithmetic.jl
# Contains definitions of +, -, /, *, promotions, conversion, one, zero.
#############################################################################

# Defines functions required for linear algebra packages
@inline nan(::Type{MC{N,T}}) where {N, T <: RelaxTag} = MC{N,T}(NaN, NaN, Interval{Float64}(NaN),
                                                                fill(NaN, SVector{N,Float64}),
																fill(NaN, SVector{N,Float64}), true)
@inline nan(x::MC{N,T}) where {N, T <: RelaxTag} = MC{N,T}(NaN, NaN, Interval{Float64}(NaN),
                                                           fill(NaN, SVector{N,Float64}),
														   fill(NaN, SVector{N,Float64}), true)

@inline one(::Type{MC{N,T}}) where {N, T <: RelaxTag} = MC{N,T}(1.0, 1.0, one(Interval{Float64}),
                                                                zero(SVector{N,Float64}),
																zero(SVector{N,Float64}), true)
@inline one(x::MC{N,T}) where {N, T <: RelaxTag} = MC{N,T}(1.0, 1.0, one(Interval{Float64}),
                                                           zero(SVector{N,Float64}),
														   zero(SVector{N,Float64}), true)

@inline zero(::Type{MC{N,T}}) where {N, T <: RelaxTag} = MC{N,T}(0.0, 0.0, zero(Interval{Float64}),
                                                                 zero(SVector{N,Float64}),
																 zero(SVector{N,Float64}), true)
@inline zero(x::MC{N,T}) where {N, T <: RelaxTag} = zero(MC{N,T})

@inline real(x::MC) = x
@inline dist(x1::MC, x2::MC) = max(abs(x1.cc - x2.cc), abs(x1.cv - x2.cv))
@inline eps(x::MC) = max(eps(x.cc), eps(x.cv))
@inline mid(x::MC) = mid(x.Intv)

# Unsafe addition
@inline function plus_kernel(x::MC{N,T}, y::MC{N,T}, z::Interval{Float64}) where {N, T <: Union{NS, MV, Diff}}
	MC{N,T}(x.cv + y.cv, x.cc + y.cc, z, x.cv_grad + y.cv_grad, x.cc_grad + y.cc_grad, (x.cnst && y.cnst))
end
@inline +(x::MC, y::MC) = plus_kernel(x, y, x.Intv + y.Intv)
@inline plus_kernel(x::MC, y::Interval{Float64}) = x
@inline +(x::MC) = x

@inline minus_kernel(x::MC{N,T}, z::Interval{Float64}) where {N, T <: RelaxTag} = MC{N,T}(-x.cc, -x.cv, z, -x.cc_grad, -x.cv_grad, x.cnst)
@inline -(x::MC) = minus_kernel(x, -x.Intv)
@inline -(x::MC{N,T}, y::MC{N,T}) where {N, T <: RelaxTag} = minus_kernel(x, y, x.Intv - y.Intv)

# Unsafe subtraction
@inline function minus_kernel(x::MC{N,T}, y::MC{N,T}, z::Interval{Float64}) where {N, T <: Union{NS, MV, Diff}}
	MC{N,T}(x.cv - y.cc, x.cc - y.cv, z, x.cv_grad - y.cc_grad, x.cc_grad - y.cv_grad, (x.cnst && y.cnst))
end

################## CONVERT THROUGH BINARY DEFINITIONS #########################
# Unsafe scalar addition
@inline function plus_kernel(x::MC{N,T}, y::Float64, z::Interval{Float64}) where {N, T <: Union{NS, MV, Diff}}
	MC{N,T}(x.cv + y, x.cc + y, z, x.cv_grad, x.cc_grad, x.cnst)
end
@inline +(x::MC, y::Float64) = plus_kernel(x, y, x.Intv + y)
@inline +(y::Float64, x::MC) = plus_kernel(x, y, x.Intv + y)
@inline +(x::MC{N,T}, y::Interval{Float64}) where {N, T<:RelaxTag} = x + MC{N,T}(y)
@inline +(y::Interval{Float64}, x::MC{N,T}) where {N, T<:RelaxTag} = x + MC{N,T}(y)

@inline plus_kernel(x::MC, y::C, z::Interval{Float64}) where {C <: NumberNotRelax} = plus_kernel(x, convert(Float64, y), z)
@inline plus_kernel(x::C, y::MC, z::Interval{Float64}) where {C <: NumberNotRelax} = plus_kernel(y, convert(Float64, x), z)
@inline +(x::MC, y::C) where {C <: NumberNotRelax} = x + convert(Float64, y)
@inline +(y::C, x::MC) where {C <: NumberNotRelax} = x + convert(Float64, y)

# Unsafe scalar subtraction
@inline function minus_kernel(x::MC{N,T}, c::Float64, z::Interval{Float64}) where {N, T <: Union{NS, MV, Diff}}
	MC{N,T}(x.cv - c, x.cc - c, z, x.cv_grad, x.cc_grad, x.cnst)
end
@inline function minus_kernel(c::Float64, x::MC{N,T}, z::Interval{Float64}) where {N, T <: Union{NS, MV, Diff}}
	MC{N,T}(c - x.cc, c - x.cv, z, -x.cc_grad, -x.cv_grad, x.cnst)
end
@inline -(x::MC, c::Float64) = minus_kernel(x, c, x.Intv - c)
@inline -(c::Float64, x::MC) = minus_kernel(c, x, c - x.Intv)
@inline -(x::MC{N,T}, y::Interval{Float64}) where {N, T<:RelaxTag} = x - MC{N,T}(y)
@inline -(y::Interval{Float64}, x::MC{N,T}) where {N, T<:RelaxTag} = MC{N,T}(y) - x

@inline minus_kernel(x::MC, y::C, z::Interval{Float64}) where {C <: NumberNotRelax} = minus_kernel(x, convert(Float64, y), z)
@inline minus_kernel(y::C, x::MC, z::Interval{Float64}) where {C <: NumberNotRelax} = minus_kernel(convert(Float64, y), x, z)
@inline -(x::MC, c::C) where {C <: NumberNotRelax} = x - convert(Float64,c)
@inline -(c::C, x::MC) where {C <: NumberNotRelax} = convert(Float64,c) - x

# Unsafe Scalar Multiplication
@inline function mult_kernel(x::MC{N,T}, c::Float64, z::Interval{Float64}) where {N, T <: Union{NS, MV, Diff}}
	if c >= 0.0
		zMC = MC{N,T}(c*x.cv, c*x.cc, z, c*x.cv_grad, c*x.cc_grad, x.cnst)
	else
		zMC = MC{N,T}(c*x.cc, c*x.cv, z, c*x.cc_grad, c*x.cv_grad, x.cnst)
	end
	return zMC
end
@inline *(x::MC, c::Float64) = mult_kernel(x, c, c*x.Intv)
@inline *(c::Float64, x::MC) = mult_kernel(x, c, c*x.Intv)
@inline *(x::MC{N,T}, y::Interval{Float64}) where {N, T<:RelaxTag} = x*MC{N,T}(y)
@inline *(y::Interval{Float64}, x::MC{N,T}) where {N, T<:RelaxTag} = MC{N,T}(y)*x

@inline mult_kernel(x::MC, c::C, z::Interval{Float64}) where {C <: NumberNotRelax} = mult_kernel(x, convert(Float64, c), z)
@inline mult_kernel(c::C, x::MC, z::Interval{Float64}) where {C <: NumberNotRelax} =  mult_kernel(x, convert(Float64, c), z)
@inline *(c::C, x::MC) where {C <: NumberNotRelax}  = x*Float64(c)
@inline *(x::MC, c::C) where {C <: NumberNotRelax}  = x*Float64(c)

# Unsafe scalar division
@inline div_kernel(x::MC, y::Float64, z::Interval{Float64}) = mult_kernel(x, inv(y), z)
@inline div_kernel(x::Float64, y::MC, z::Interval{Float64}) = mult_kernel(inv(y), x, z)
@inline div_kernel(x::MC, y::C, z::Interval{Float64}) where {C <: NumberNotRelax}  = mult_kernel(x, inv(y), z)
@inline div_kernel(x::C, y::MC, z::Interval{Float64}) where {C <: NumberNotRelax}  = mult_kernel(inv(y), x, z)
@inline /(x::MC, y::Float64) = x*inv(y)
@inline /(x::Float64, y::MC) = x*inv(y)
@inline /(x::MC, y::C) where {C <: NumberNotRelax} = x*inv(convert(Float64,y))
@inline /(x::C, y::MC) where {C <: NumberNotRelax} = convert(Float64,x)*inv(y)
@inline /(x::MC{N,T}, y::Interval{Float64}) where {N, T<:RelaxTag} = x/MC{N,T}(y)
@inline /(y::Interval{Float64}, x::MC{N,T}) where {N, T<:RelaxTag} = MC{N,T}(y)/x

# Maximization
@inline max_kernel(c::Float64, x::MC, z::Interval{Float64}) = max_kernel(x, c, z)
@inline max_kernel(x::MC, c::C, z::Interval{Float64}) where {C <: NumberNotRelax} = max_kernel(x, convert(Float64, c), z)
@inline max_kernel(c::C, x::MC, z::Interval{Float64}) where {C <: NumberNotRelax} = max_kernel(x, convert(Float64, c), z)

@inline max(c::Float64, x::MC) = max_kernel(x, c, max(x.Intv, c))
@inline max(x::MC, c::C) where {C <: NumberNotRelax} = max_kernel(x, convert(Float64, c), max(x.Intv, c))
@inline max(c::C, x::MC) where {C <: NumberNotRelax} = max_kernel(x, convert(Float64, c), max(x.Intv, c))
@inline max(x::MC{N,T}, y::Interval{Float64}) where {N, T<:RelaxTag} = max(x, MC{N,T}(y))
@inline max(y::Interval{Float64}, x::MC{N,T}) where {N, T<:RelaxTag} = max(MC{N,T}(y), x)

# Minimization
@inline min_kernel(x::MC, c::C, z::Interval{Float64}) where {C <: NumberNotRelax} = min_kernel(x, convert(Float64, c), z)
@inline min_kernel(c::C, x::MC, z::Interval{Float64}) where {C <: NumberNotRelax} = min_kernel(x, convert(Float64, c), z)

@inline min(c::Float64, x::MC) = min_kernel(x, c, min(x.Intv, c))
@inline min(x::MC, c::C) where {C <: NumberNotRelax} = min_kernel(x, convert(Float64, c), min(x.Intv, c))
@inline min(c::C, x::MC) where {C <: NumberNotRelax} = min_kernel(x, convert(Float64, c), min(x.Intv, c))
@inline min(x::MC{N,T}, y::Interval{Float64}) where {N, T<:RelaxTag} = min(x, MC{N,T}(y))
@inline min(y::Interval{Float64}, x::MC{N,T}) where {N, T<:RelaxTag} = min(MC{N,T}(y), x)

# Add fma function
@inline fma(x::MC, y::Float64, z::Float64) = x*y + z
@inline fma(x::MC, y::MC, z::Float64) = x*y + z
@inline fma(x::MC, y::Float64, z::MC) = x*y + z
@inline fma(x::MC, y::MC, z::MC) = x*y + z
@inline fma(x::Float64, y::MC, z::Float64) = x*y + z
@inline fma(x::Float64, y::MC, z::MC) = x*y + z
@inline fma(x::Float64, y::Float64, z::MC) = x*y + z

@inline fma(x::MC, y::Float64, z::Float64, q::Interval{Float64}) = x*y + z
@inline fma(x::MC, y::MC, z::Float64, q::Interval{Float64}) = x*y + z
@inline fma(x::MC, y::Float64, z::MC, q::Interval{Float64}) = x*y + z
@inline fma(x::MC, y::MC, z::MC, q::Interval{Float64}) = x*y + z
@inline fma(x::Float64, y::MC, z::Float64, q::Interval{Float64}) = x*y + z
@inline fma(x::Float64, y::MC, z::MC, q::Interval{Float64}) = x*y + z
@inline fma(x::Float64, y::Float64, z::MC, q::Interval{Float64}) = x*y + z

# Promote and Convert
promote_rule(::Type{MC{N,T}}, ::Type{S}) where {S<:NumberNotRelax, N, T <: RelaxTag} = MC{N,T}
promote_rule(::Type{MC{N,T}}, ::Type{S}) where {S<:Real, N, T <: RelaxTag} = MC{N,T}

convert(::Type{MC{N,T}}, x::S) where {S<:NumberNotRelax, N, T <: RelaxTag} = MC{N,T}(Interval{Float64}(x))
convert(::Type{MC{N,T}}, x::S) where {S<:AbstractInterval, N, T <: RelaxTag} = MC{N,T}(Interval{Float64}(x.lo, x.hi))
Interval(x::MC{N,T}) where {N,T<:RelaxTag} = x.Intv
Interval{Float64}(x::MC{N,T}) where {N,T<:RelaxTag} = x.Intv


"""
collecting all needed  functions required to calculate Housdorff distance
"""
module Housdorff
using CUDA
using ..CUDAGpuUtils ,..IterationUtils,..ReductionUtils , ..MemoryUtils,..CUDAAtomicUtils
using ..BitWiseUtils,..MetadataAnalyzePass,..MetaDataUtils,..WorkQueueUtils,..ProcessMainDataVerB,..HFUtils,..ResultListUtils,..PrepareArrtoBool, ..MainLoopKernel,..ScanForDuplicates
export get_shmemMainKernel, getHousedorffDistance,boolKernelLoad,mainKernelLoad,get_shmemMainKernel,get_shmemBoolKernel,preparehousedorfKernel
"""
calculate housedorff distance of given arrays with given robustness percentage

"""
function getHousedorffDistance(goldGPUa,segmGPUa,boolKernelArgs,mainKernelArgs,threadsBoolKern,blocksBoolKern ,threadsMainKern,blocksMainKern,shmemSizeBool,shmemSizeMain)
    # boolKernelArgs[1]= goldGPU
    # boolKernelArgs[2]= segmGPU
    mainArrDims,dataBdim,metaData,metaDataDims,reducedGoldA,reducedSegmA,loopXinPlane,loopYinPlane,minxRes,maxxRes,minyRes,maxyRes,minzRes,maxzRes,fn,fp ,numberToLooFor,inBlockLoopXZIterWithPadding,shmemblockDataLoop,shmemblockDataLenght,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength = boolKernelArgs

    @cuda threads=threadsBoolKern blocks=blocksBoolKern shmem=shmemSizeBool  cooperative=true boolKernelLoad(goldGPUa,segmGPUa,boolKernelArgs...)
    #now time to get data structures dependent on bool kernel like for example loading subsections of meta data, creating work queue ...
    #some arrays needs to be instantiated only after we know the number of the false and true positives
    metaData,reducedGoldA  ,reducedSegmA ,paddingStore,resList,workQueue,workQueueCounter= getBigGPUForHousedorffAfterBoolKernel(metaData,minxRes,maxxRes,minyRes,maxyRes,minzRes,maxzRes,fn,fp,reducedGoldA,reducedSegmA,dataBdim)
    referenceArrs,dilatationArrs, mainArrDims,dataBdim,numberToLooFor,metaDataDims,metaData,iterThrougWarNumb,robustnessPercent,shmemSumLengthMaxDiv4,globalFpResOffsetCounter,globalFnResOffsetCounter    ,globalIterationNumber,globalCurrentFnCount,globalCurrentFpCount    ,globalIterationNumb,workQueue,workQueueCounter,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed    ,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop    ,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength, fn,fp,resList,inBlockLoopXZIterWithPadding,paddingStore,shmemblockDataLenght,shmemblockDataLoop= mainKernelArgs

    dilatationArrs= (reducedGoldA,reducedSegmA)
    #reverse order as when dilatating gold we want to establish do we cover segm voxels
    referenceArrs=(segmGPUa,goldGPUa )

    #main calculations
    @cuda threads=threadsMainKern blocks=blocksMainKern shmem=shmemSizeMain cooperative=true mainKernelLoadB( referenceArrs,dilatationArrs, mainArrDims,dataBdim
    ,numberToLooFor,metaDataDims,metaData,iterThrougWarNumb,robustnessPercent
    ,shmemSumLengthMaxDiv4,globalFpResOffsetCounter,globalFnResOffsetCounter
    ,globalIterationNumber,globalCurrentFnCount,globalCurrentFpCount
    ,globalIterationNumb,workQueue,workQueueCounter
    ,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed
    ,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY
    ,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop
    ,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength, fn,fp,resList,inBlockLoopXZIterWithPadding,paddingStore,shmemblockDataLenght,shmemblockDataLoop)
        #@cuda threads=threadsMainKern blocks=blocksMainKern shmem=shmemSizeMain cooperative=true mainKernelLoad(dilatationArrs,referenceArrs, mainArrDims,dataBdim,numberToLooFor,metaDataDims,metaData,iterThrougWarNumb,robustnessPercent,shmemSumLengthMaxDiv4,globalFpResOffsetCounter,globalFnResOffsetCounter,globalIterationNumber,globalCurrentFnCount,globalCurrentFpCount,globalIterationNumb,workQueaue,resList,resListIndicies,maxResListIndex,inBlockLoopXZIterWithPadding,shmemblockDataLoop,shmemblockDataLenght,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength, fn,fp)
    return globalIterationNumb

    end

"""
clearing data before next execution
"""
function housClearForExecution()

end

"""
for invoking getBoolCubeKernel
"""
function boolKernelLoad(goldGPU,segmGPU,mainArrDims,dataBdim,metaData,metaDataDims,reducedGoldA,reducedSegmA,loopXinPlane,loopYinPlane,minxRes,maxxRes,minyRes,maxyRes,minzRes,maxzRes,fn,fp ,numberToLooFor,inBlockLoopXZIterWithPadding,shmemblockDataLoop,shmemblockDataLenght,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength)
    @getBoolCubeKernel()
    return
end
"""
main function responsible for calculations of Housedorff distance
"""
function mainKernelLoadB(referenceArrs,dilatationArrs, mainArrDims,dataBdim
    ,numberToLooFor,metaDataDims,metaData,iterThrougWarNumb,robustnessPercent
    ,shmemSumLengthMaxDiv4,globalFpResOffsetCounter,globalFnResOffsetCounter
    ,globalIterationNumber,globalCurrentFnCount,globalCurrentFpCount
    ,globalIterationNumb,workQueue,workQueueCounter
    ,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed
    ,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY
    ,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop
    ,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength, fn,fp,resList,inBlockLoopXZIterWithPadding,paddingStore,shmemblockDataLenght,shmemblockDataLoop)

   #@mainLoopKernel()
    return
end

function mainKernelLoad(referenceArrs,dilatationArrs, mainArrDims,dataBdim
    ,numberToLooFor,metaDataDims,metaData,iterThrougWarNumb,robustnessPercent
    ,shmemSumLengthMaxDiv4,globalFpResOffsetCounter,globalFnResOffsetCounter
    ,globalIterationNumber,globalCurrentFnCount,globalCurrentFpCount
    ,globalIterationNumb,workQueue,workQueueCounter
    ,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed
    ,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY
    ,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop
    ,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength, fn,fp,resList,inBlockLoopXZIterWithPadding,paddingStore,shmemblockDataLenght,shmemblockDataLoop)
    @mainLoopKernel()
    return
end

function get_shmemMainKernel(dataBdim)

    shmemblockData= sizeof(UInt32)*32*32
    # shmemblockData= sizeof(UInt32)*dataBdim[1]* dataBdim[2]
    resShmemblockData= sizeof(UInt32)*32*32
    # resShmemblockData= sizeof(UInt32)*dataBdim[1]* dataBdim[2]
    shmemPaddings= sizeof(Bool)*(  max(dataBdim[1], dataBdim[2]))*(  max(dataBdim[1], dataBdim[2]))*6
    shmemSum= sizeof(UInt32)*36*16
    areToBeValidated= sizeof(Bool)*14
    isAnythingInPadding= sizeof(Bool)*7
    alreadyCoveredInQueues= sizeof(UInt32)*14
    someBools = sizeof(Bool)*4
    someInt16 = sizeof(UInt16)*6
    workCountersInshmem = sizeof(UInt16)*2
return workCountersInshmem+shmemSum+areToBeValidated+isAnythingInPadding+alreadyCoveredInQueues+someBools+shmemblockData+someInt16+resShmemblockData+shmemPaddings
end

function get_shmemBoolKernel(dataBdim)
    # shmemSum= sizeof(Float32)*32*2
    shmemblockData= sizeof(Int32)*(dataBdim[1])*(dataBdim[2])
    localQuesValues = sizeof(UInt32)*14
    minMaxes = sizeof(Float32)*6
return minMaxes+localQuesValues+shmemblockData
end

"""
creates required cu arrays , calculates some kernel constants and uses occupancy API
to calculate optimal number of threads and blocks to run a kernel
robustnessPercent - frequently we do not want to analyze all of the fap and fn in order to reduce the impact of the outliers
numberToLooFor - what we will look for in main arrays
"""
function preparehousedorfKernel(goldGPU,segmGPU,robustnessPercent,numberToLooFor)
    mainArrDims = size(goldGPU)
    dataBdim = (32,32,32) # will be modified after number of threads gets calculated by occupancy API

    metaData = MetaDataUtils.allocateMetadata(mainArrDims,dataBdim);
    metaDataDims= size(metaData);
    #for bool cube kernel
    threadsBoolKern= (30,32); blocksBoolKern = 10#just some dummy will be modified after invoking occupancy API
    #for main kernel
    threadsMainKern= (30,32); blocksMainKern = 10#just some dummy will be modified after invoking occupancy API
    iterThrougWarNumb = cld(14,threadsMainKern[2])

    inBlockLoopXZIterWithPadding,shmemblockDataLoop,shmemblockDataLenght,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength=calculateLoopsIter(dataBdim,threadsBoolKern[1],threadsBoolKern[2],metaDataDims,blocksBoolKern)
        minxRes,maxxRes,minyRes,maxyRes,minzRes,maxzRes,fn,fp  =getSmallForBoolKernel();
        reducedGoldA,reducedSegmA=  getLargeForBoolKernel(mainArrDims,dataBdim);

    loopXinPlane,loopYinPlane = 1,1
    boolKernelArgs = (mainArrDims,dataBdim,metaData,metaDataDims,reducedGoldA,reducedSegmA,loopXinPlane,loopYinPlane,minxRes,maxxRes,minyRes,maxyRes,minzRes,maxzRes,fn,fp ,numberToLooFor,inBlockLoopXZIterWithPadding,shmemblockDataLoop,shmemblockDataLenght,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength)
    #needed also in bool kernel

    #### main kernel
    fpLoc = 10
    fnLoc = 10
    resList= allocateResultLists(fpLoc,fnLoc)
    globalFpResOffsetCounter,globalFnResOffsetCounter,globalIterationNumber,globalCurrentFnCount,globalCurrentFpCount,globalIterationNumb= getSmallGPUForHousedorff()

    referenceArrs=(goldGPU,segmGPU)

    inBlockLoopXZIterWithPadding,shmemblockDataLoop,shmemblockDataLenght,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength=calculateLoopsIter(dataBdim,threadsBoolKern[1],threadsBoolKern[2],metaDataDims,blocksBoolKern)
    shmemSumLengthMaxDiv4= fld((36*14),4)*4 # subject to futre changes

    metaData,reducedGoldA  ,reducedSegmA ,paddingStore,resList,workQueue,workQueueCounter = getBigGPUForHousedorffAfterBoolKernel(metaData,minxRes,maxxRes,minyRes,maxyRes,minzRes,maxzRes,fn,fp,reducedGoldA,reducedSegmA,dataBdim)

    reducedGoldA,reducedSegmA=  getLargeForBoolKernel(mainArrDims,dataBdim);

    dilatationArrs= (reducedGoldA,reducedSegmA)

    mainKernelArgs= (referenceArrs,dilatationArrs, mainArrDims,dataBdim
    ,numberToLooFor,metaDataDims,metaData,iterThrougWarNumb,robustnessPercent
    ,shmemSumLengthMaxDiv4,globalFpResOffsetCounter,globalFnResOffsetCounter
    ,globalIterationNumber,globalCurrentFnCount,globalCurrentFpCount
    ,globalIterationNumb,workQueue,workQueueCounter
    ,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed
    ,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY
    ,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop
    ,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength, fn,fp,resList,inBlockLoopXZIterWithPadding,paddingStore,shmemblockDataLenght,shmemblockDataLoop)

    function get_shmemMainKernelLoc(threads)
        dataBdim = (threads[1],threads[2],32)
        get_shmemMainKernel(dataBdim)
    end

    threadsMainKern,blocksMainKern = getThreadsAndBlocksNumbForKernel(get_shmemMainKernel,mainKernelLoad,(mainKernelArgs))
    function get_shmemBoolKernelLoc(threads)
        dataBdim = (threadsMainKern[1],threadsMainKern[2],32)
        get_shmemBoolKernel(dataBdim)
    end

    boolKernelArgs = (mainArrDims,dataBdim,metaData,metaDataDims,reducedGoldA,reducedSegmA,loopXinPlane,loopYinPlane,minxRes,maxxRes,minyRes,maxyRes,minzRes,maxzRes,fn,fp ,numberToLooFor,inBlockLoopXZIterWithPadding,shmemblockDataLoop,shmemblockDataLenght,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength)

     ## now we need to make use of occupancy API to get optimal number of threads and blocks fo each kernel
    threadsBoolKern,blocksBoolKern = getThreadsAndBlocksNumbForKernel(get_shmemBoolKernelLoc,boolKernelLoad,(goldGPU,segmGPU,boolKernelArgs...))

    loopXinPlane,loopYinPlane = fld(threadsMainKern[1],threadsBoolKern[1] ), fld(threadsMainKern[2],threadsBoolKern[2] )
#now we get defoult values of data b dim  set on the basis of the threadsMainHKernel; and generally recalculating loops constants
    dataBdim = (threadsMainKern[1],threadsMainKern[2],32)
    metaData = MetaDataUtils.allocateMetadata(mainArrDims,dataBdim);
    metaDataDims= size(metaData);
    iterThrougWarNumb = cld(14,threadsMainKern[2])

    boolKernelArgs = (mainArrDims,dataBdim,metaData,metaDataDims,reducedGoldA,reducedSegmA,loopXinPlane,loopYinPlane,minxRes,maxxRes,minyRes,maxyRes,minzRes,maxzRes,fn,fp ,numberToLooFor,inBlockLoopXZIterWithPadding,shmemblockDataLoop,shmemblockDataLenght,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength)

    inBlockLoopXZIterWithPadding,shmemblockDataLoop,shmemblockDataLenght,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength=calculateLoopsIter(dataBdim,threadsBoolKern[1],threadsBoolKern[2],metaDataDims,blocksBoolKern)

    boolKernelArgs = (mainArrDims,dataBdim,metaData,metaDataDims,reducedGoldA,reducedSegmA,loopXinPlane,loopYinPlane,minxRes,maxxRes,minyRes,maxyRes,minzRes,maxzRes,fn,fp ,numberToLooFor,inBlockLoopXZIterWithPadding,shmemblockDataLoop,shmemblockDataLenght,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength)
    metaData,reducedGoldA  ,reducedSegmA ,paddingStore,resList,workQueue,workQueueCounter= getBigGPUForHousedorffAfterBoolKernel(metaData,minxRes,maxxRes,minyRes,maxyRes,minzRes,maxzRes,fn,fp,reducedGoldA,reducedSegmA,dataBdim)

    reducedGoldA,reducedSegmA=  getLargeForBoolKernel(mainArrDims,dataBdim);

    mainKernelArgs= (referenceArrs,dilatationArrs, mainArrDims,dataBdim
    ,numberToLooFor,metaDataDims,metaData,iterThrougWarNumb,robustnessPercent
    ,shmemSumLengthMaxDiv4,globalFpResOffsetCounter,globalFnResOffsetCounter
    ,globalIterationNumber,globalCurrentFnCount,globalCurrentFpCount
    ,globalIterationNumb,workQueue,workQueueCounter
    ,loopAXFixed,loopBXfixed,loopAYFixed,loopBYfixed,loopAZFixed,loopBZfixed
    ,loopdataDimMainX,loopdataDimMainY,loopdataDimMainZ,inBlockLoopX,inBlockLoopY
    ,inBlockLoopZ,metaDataLength,loopMeta,loopWarpMeta,clearIterResShmemLoop
    ,clearIterSourceShmemLoop,resShmemTotalLength,sourceShmemTotalLength, fn,fp,resList,inBlockLoopXZIterWithPadding,paddingStore,shmemblockDataLenght,shmemblockDataLoop)
    shmemSizeBool=get_shmemBoolKernelLoc(threadsBoolKern)
    shmemSizeMain=get_shmemMainKernelLoc(threadsMainKern)

    # CUDA.unsafe_free!(goldGPU)
    # CUDA.unsafe_free!(segmGPU)
    #CUDA.reclaim()
return (boolKernelArgs, mainKernelArgs,threadsBoolKern,blocksBoolKern ,threadsMainKern,blocksMainKern ,shmemSizeBool,shmemSizeMain)

end

end# module


"""
GenX: An Configurable Capacity Expansion Model
Copyright (C) 2021,  Massachusetts Institute of Technology
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
A complete copy of the GNU General Public License v2 (GPLv2) is available
in LICENSE.txt.  Users uncompressing this from an archive may not have
received this license file.  If not, see <http://www.gnu.org/licenses/>.
"""

@doc raw"""
	emissions(EP::Model, inputs::Dict, UCommit::Int)

This function creates expression to add the CO2 emissions by plants in each zone, which is subsequently added to the total emissions
"""
function emissions(EP::Model, inputs::Dict)

	println("Emissions Module (for CO2 Policy modularization")

	dfGen = inputs["dfGen"]

	G = inputs["G"]     # Number of resources (generators, storage, DR, and DERs)
	T = inputs["T"]     # Number of time steps (hours)
	Z = inputs["Z"]     # Number of zones
	COMMIT = inputs["COMMIT"] # For not, thermal resources are the only ones eligible for Unit Committment

	@expression(EP, eEmissionsByPlant[y=1:G,t=1:T],
	 	if y in inputs["COMMIT"]
		 	dfGen[!,:CO2_per_MWh][y]*EP[:vP][y,t]+dfGen[!,:CO2_per_Start][y]*EP[:vSTART][y,t]
	 	else
		 	dfGen[!,:CO2_per_MWh][y]*EP[:vP][y,t]
	 	end
 	)

 	@expression(EP, eEmissionsByZone[z=1:Z, t=1:T], sum(eEmissionsByPlant[y,t] for y in dfGen[(dfGen[!,:Zone].==z),:R_ID]))

	return EP
end


################################################################################
#
#   IMPLEMENTATIONS OF DIFFERENT LATTICE MODIFICATION FUNCTIONS
#   (MOST OF THEM ALSO WORK FOR UNITCELLS)
#
#   STRUCTURE OF THE FILE
#
#   1) CONNECTIONS ADDING REMOVING
#       - Add a new connection
#       - remove connections (based on indices)
#       - remove connections (based on strength)
#
#   2) SITES ADDING REMOVING
#       - add a new site
#       - remove a site (based on index)
#
#   3) CONNECTION STRENGTH MODIFICATION
#       - all connections
#       - mapping of strengths
#       - evaluate strengths
#       - optimize connections
#
#   4) REAL SPACE ROTATION / SCALING / SHIFTING
#       - Rotation around some axis
#       - Scaling along some axis
#       - Global (isotropic) scaling
#       - Global (isotropic) scaling to mean / minimum bond length
#       - Shifting along some axis
#       - Shifting along some lattice vector
#       - Shifting center of lattice to origin
#
#   5) LATTICE VECTOR OPTIMIZATION
#
################################################################################

################################################################################
#
#   1) CONNECTIONS ADDING REMOVING
#       - Add a new connection
#       - remove connections (based on indices)
#       - remove connections (based on strength)
#
################################################################################

#-------------------------------------------------------------------------------
#
#  Add connection to given unitcell / lattice objects
#
#-------------------------------------------------------------------------------
"""
    addConnection!(unitcell::Unitcell, index_from::Int64, index_to::Int64, strength [, wrap=-1; overwrite::Bool=false])
    addConnection!(lattice::Lattice,   index_from::Int64, index_to::Int64, strength [, wrap=-1; overwrite::Bool=false])

Function to add a connection to either a `Unitcell` object or a `Lattice` object.
Necessary parameters include the indices of sites between which the connection takes place as well as the strength of the connection.
Additionally (in case of a unitcell or periodic boundaries), a wrap can be specified in terms of lattice vectors.
If no wrap is specified, the wrap will be chosen automatically as without wrapping around periodic boundaries.

The additional option `overwrite` specifies if one wants to simply add the connection
or if one wants to first search if there is a connection that already satisfies the new connection (in which case nothing would be added).

NOTE 1: This function always tries to add two connections, one going forward, one going backward.

NOTE 2: This function modifies the given object and does not create a copy.

# Examples

```julia-repl
julia> addConnection!(unitcell, 1, 3, "tx", (0,1))

julia> addConnection!(lattice, 31, 27, "tx")

```
"""
function addConnection!(unitcell::Unitcell, index_from::Int64, index_to::Int64, strength, wrap=-1; overwrite::Bool=false)

    # maybe default wrap
    if wrap == -1
        if length(unitcell.lattice_vectors) == 3
            wrap = (0,0,0)
        elseif length(unitcell.lattice_vectors) == 2
            wrap = (0,0)
        else
            wrap = (0)
        end
    end

    # construct the returning wrap
    if length(wrap) == 3
        wrap_return = (-wrap[1],-wrap[2],-wrap[3])
    elseif length(wrap) == 2
        wrap_return = (-wrap[1],-wrap[2])
    else
        wrap_return = (-wrap)
    end
    # construct the returning strength
    if typeof(strength) == Complex
        strength_return = deepcopy(conj(strength))
    else
        strength_return = deepcopy(strength)
    end

    # construct the connection as an array
    connection_1 = Any[index_from; index_to; strength; wrap]
    # construct the returning connection as an array
    connection_2 = Any[index_to; index_from; strength_return; wrap_return]

    # if not overwrite, check if the connection exists
    if !overwrite
        # report if found
        found_c1 = false
        found_c2 = false
        # check in all connections
        for c in unitcell.connections
            if  Int(c[1]) == Int(connection_1[1]) && Int(c[2]) == Int(connection_1[2]) && c[3] == connection_1[3] && c[4] == connection_1[4]
                found_c1 = true
            end
            if  Int(c[1]) == Int(connection_2[1]) && Int(c[2]) == Int(connection_2[2]) && c[3] == connection_2[3] && c[4] == connection_2[4]
                found_c2 = true
            end
        end
        # only add if not added already
        if !found_c1
            push!(unitcell.connections, connection_1);
        end
        if !found_c2
            push!(unitcell.connections, connection_2);
        end
    # otherwise, just add the connections
    else
        push!(unitcell.connections, connection_1);
        push!(unitcell.connections, connection_2);
    end

    # return nothing
    return nothing
end
function addConnection!(lattice::Lattice, index_from::Int64, index_to::Int64, strength, wrap=-1; overwrite::Bool=false)

    # maybe default wrap
    if wrap == -1
        if length(lattice.lattice_vectors) == 3
            wrap = (0,0,0)
        elseif length(lattice.lattice_vectors) == 2
            wrap = (0,0)
        else
            wrap = (0)
        end
    end

    # construct the returning wrap
    if length(wrap) == 3
        wrap_return = (-wrap[1],-wrap[2],-wrap[3])
    elseif length(wrap) == 2
        wrap_return = (-wrap[1],-wrap[2])
    else
        wrap_return = (-wrap)
    end
    # construct the returning strength
    if typeof(strength) == Complex
        strength_return = deepcopy(conj(strength))
    else
        strength_return = deepcopy(strength)
    end

    # construct the connection as an array
    connection_1 = Any[index_from; index_to; strength; wrap]
    # construct the returning connection as an array
    connection_2 = Any[index_to; index_from; strength_return; wrap_return]

    # if not overwrite, check if the connection exists
    if !overwrite
        # report if found
        found_c1 = false
        found_c2 = false
        # check in all connections
        for c in lattice.connections
            if  Int(c[1]) == Int(connection_1[1]) && Int(c[2]) == Int(connection_1[2]) && c[3] == connection_1[3] && c[4] == connection_1[4]
                found_c1 = true
            end
            if  Int(c[1]) == Int(connection_2[1]) && Int(c[2]) == Int(connection_2[2]) && c[3] == connection_2[3] && c[4] == connection_2[4]
                found_c2 = true
            end
        end
        # only add if not added already
        if !found_c1
            push!(lattice.connections, connection_1);
        end
        if !found_c2
            push!(lattice.connections, connection_2);
        end
    # otherwise, just add the connections
    else
        push!(lattice.connections, connection_1);
        push!(lattice.connections, connection_2);
    end

    # return nothing
    return nothing
end
export addConnection!

#-------------------------------------------------------------------------------
#
#   Remove interactions with given strengths of the unitcell / lattice
#
#-------------------------------------------------------------------------------
"""
    removeConnections!(unitcell::Unitcell, index_from::Int64, index_to::Int64 [, strength ])
    removeConnections!(lattice::Lattice,   index_from::Int64, index_to::Int64 [, strength ])

Function to remove connections from either a `Unitcell` object or a `Lattice` object based on the sites that they connect.
Optionally, one can pass a connection strength to also filter based on this strength. If no strength is passed, all connnections
between the two sites will be removed.

    removeConnections!(unitcell::Unitcell  [, strength])
    removeConnections!(lattice::Lattice    [, strength])

Function to remove connections from either a `Unitcell` object or a `Lattice` object only based on the strength of connections.
If no strength is passed, all connnections with Float64 strength `0.0` will be removed.

NOTE 1: This function always tries to delte both connections, one going forward, one going backward.

NOTE 2: This function modifies the given object and does not create a copy.

# Examples

```julia-repl
julia> removeConnections!(unitcell, 1, 3, "tx")

julia> removeConnections!(unitcell, 1, 3)

julia> removeConnections!(lattice, 23, 73)

julia> removeConnections!(lattice, "tx")

```
"""
function removeConnections!(lattice::Lattice,   index_from::Int64, index_to::Int64, strength)
    # use the julia filter function to filter out all elements which have the strength passed and the fitting indices
    filter!(x->!(x[3]==strength && Int(x[1])==index_from && Int(x[2])==index_to) && !(x[3]==strength && Int(x[2])==index_from && Int(x[1])==index_to), lattice.connections)
    # return nothing
    return nothing
end
function removeConnections!(unitcell::Unitcell, index_from::Int64, index_to::Int64, strength)
    # use the julia filter function to filter out all elements which have the strength passed and the fitting indices
    filter!(x->!(x[3]==strength && Int(x[1])==index_from && Int(x[2])==index_to) && !(x[3]==strength && Int(x[2])==index_from && Int(x[1])==index_to), unitcell.connections)
    # return nothing
    return nothing
end
function removeConnections!(lattice::Lattice,   index_from::Int64, index_to::Int64)
    # use the julia filter function to filter out all elements which have the fitting indices
    filter!(x->!(Int(x[1])==index_from && Int(x[2])==index_to) && !(Int(x[2])==index_from && Int(x[1])==index_to), lattice.connections)
    # return nothing
    return nothing
end
function removeConnections!(unitcell::Unitcell, index_from::Int64, index_to::Int64)
    # use the julia filter function to filter out all elements which have the fitting indices
    filter!(x->!(Int(x[1])==index_from && Int(x[2])==index_to) && !(Int(x[2])==index_from && Int(x[1])==index_to), unitcell.connections)
    # return nothing
    return nothing
end
function removeConnections!(lattice::Lattice,   strength=0.0)
    # use the julia filter function to filter out all elements which have the strength passed
    filter!(x->x[3]!=strength, lattice.connections)
    # return nothing
    return nothing
end
function removeConnections!(unitcell::Unitcell, strength=0.0)
    # use the julia filter function to filter out all elements which have the strength passed
    filter!(x->x[3]!=strength, unitcell.connections)
    # return nothing
    return nothing
end

export removeConnections!

################################################################################
#
#   2) SITES ADDING REMOVING
#       - add a new site
#       - remove a site (based on index)
#
################################################################################

"""
    addSite!(unitcell::Unitcell, position::Array{Float64,1})
    addSite!(lattice::Lattice,   position::Array{Float64,1})

Function to add a site to either a `Unitcell` object or a `Lattice` object which is located at
the real space position `position`. The function returns the index of the newly added site for further use.

NOTE 1: The newly added site is not connected to any other sites yet.

NOTE 2: This function modifies the given object and does not create a copy.

# Examples

```julia-repl
julia> addSite!(unitcell, Float64[0.0, 0.5])
5

julia> addSite!(lattice, Float64[0.0, 0.5, 1.0])
187
```
"""
function addSite!(lattice::Lattice, position::Array{Float64,1})
    # push the site into the positions list
    push!(lattice.positions, position)
    # return the site index
    return length(lattice.positions)
end
function addSite!(unitcell::Unitcell, position::Array{Float64,1})
    # push the site into the positions list
    push!(unitcell.basis, position)
    # return the site index
    return length(unitcell.basis)
end
export addSite!

"""
    removeSite!(unitcell::Unitcell, index::Int64)
    removeSite!(lattice::Lattice,   index::Int64)

Function to remove a site from either a `Unitcell` object or a `Lattice` object with index `index`.
The function furhtermore removes all connections that are connected to the site and relabels all
other connections as well to account for the index shift introduced by the removal.

NOTE: This function modifies the given object and does not create a copy.

# Examples

```julia-repl
julia> removeSite!(unitcell, 2)

julia> removeSite!(lattice, 128)

```
"""
function removeSite!(lattice::Lattice, index::Int64)
    # delete the element from the positions list
    deleteat!(lattice.positions, index)
    # delete all connections that touch the removed site
    deleteat!(lattice.connections, [c[1]==index || c[2]==index for c in lattice.connections])
    # now shift all indices in the connections array by 1
    for (i,c) in enumerate(lattice.connections)
        if Int(c[1]) > index
            c[1] = Int(c[1]) - 1
        end
        if Int(c[2]) > index
            c[2] = Int(c[2]) - 1
        end
    end
    # return nothing
    return nothing
end
function removeSite!(unitcell::Unitcell, index::Int64)
    # delete the element from the positions list
    deleteat!(unitcell.basis, index)
    # delete all connections that touch the removed site
    deleteat!(unitcell.connections, [c[1]==index || c[2]==index for c in unitcell.connections])
    # now shift all indices in the connections array by 1
    for (i,c) in enumerate(unitcell.connections)
        if Int(c[1]) > index
            c[1] = Int(c[1]) - 1
        end
        if Int(c[2]) > index
            c[2] = Int(c[2]) - 1
        end
    end
    # return nothing
    return nothing
end
export removeSite!

################################################################################
#
#   3) CONNECTION STRENGTH MODIFICATION
#       - all connections
#       - mapping of strengths
#       - evaluate strengths
#       - optimize connections
#
################################################################################

#-----------------------------------------------------------------------------------------------------------------------------
#
#   Set / Map interaction strengths of the unitcell / lattice
#
#-----------------------------------------------------------------------------------------------------------------------------
"""
    setAllInteractionStrengths!(unitcell::Unitcell, strength)
    setAllInteractionStrengths!(lattice::Lattice,   strength)

Function to overwrite ALL connection strength values of either a `Unitcell` object or a `Lattice` object
with the value `strength`.

NOTE: This function modifies the given object and does not create a copy.

# Examples

```julia-repl
julia> setAllInteractionStrengths!(unitcell, "tx")

julia> setAllInteractionStrengths!(lattice, 1.0)

```
"""
function setAllInteractionStrengths!(unitcell::Unitcell, strength)
    # go through all connections and modify their strength
    for c in unitcell.connections
        c[3] = strength
    end
    # return nothing
    return nothing
end
function setAllInteractionStrengths!(lattice::Lattice,   strength)
    # go through all connections and modify their strength
    for c in lattice.connections
        c[3] = strength
    end
    # return nothing
    return nothing
end
export setAllInteractionStrengths!

"""
    mapInteractionStrengths!(unitcell::Unitcell, mapping::Dict [; replace_in_strings::Bool=false, evaluate::Bool=false])
    mapInteractionStrengths!(lattice::Lattice,   mapping::Dict [; replace_in_strings::Bool=false, evaluate::Bool=false])

Function to map connection strength values of either a `Unitcell` object or a `Lattice` object
by replacing them along keys of a dictonary `mapping`.

Furthermore, to be able to resolve strengths like `"tx*ty"` (`String` valued) to a `Float64` value one can use the following two steps:
1) chosing `replace_in_strings=true` to also search for mapping keys in `String` connection strengths.
2) chosing `evaluate=true` to evaluate every `String` connection strength by `eval(parse(.))` after all replacement has taken place.

NOTE: This function modifies the given object and does not create a copy.

# Examples

Example mapping could read

    mapping = Dict("tx"=>1.0, "ty"=>1.0, "tz"=>2.0)

which modifies all `String` valued connection strengths `"tx"`, `"ty"` and `"tz"` to be `Float64` with values `1.0` and `2.0`

```julia-repl
julia> mapInteractionStrengths!(unitcell, mapping)

julia> mapInteractionStrengths!(lattice, mapping, replace_in_strings=true)

```
"""
function mapInteractionStrengths!(unitcell::Unitcell, mapping::Dict; replace_in_strings::Bool=false, evaluate::Bool=false)
    # get the old strengths
    old_strengths = keys(mapping)
    # iterate for replacement
    for old_strength in old_strengths
        # new strength is given by mapping
        new_strength = mapping[old_strength]
        # check all connections that are identical
        for c in unitcell.connections
            if c[3] == old_strength
                c[3] = new_strength
            end
        end
        # check for string replacement
        if replace_in_strings
            for c in unitcell.connections
                if typeof(c[3]) == String && contains(c[3], old_strength)
                    c[3] = replace(c[3], old_strength, new_strength)
                end
            end
        end
    end
    # maybe even evaluate
    if evaluate
        for c in unitcell.connections
            if typeof(c[3]) == String
                c[3] = eval(parse(c[3]))
            end
        end
    end
    # return nothing
    return nothing
end
function mapInteractionStrengths!(lattice::Lattice,   mapping::Dict; replace_in_strings::Bool=false, evaluate::Bool=false)
    # get the old strengths
    old_strengths = keys(mapping)
    # iterate for replacement
    for old_strength in old_strengths
        # new strength is given by mapping
        new_strength = mapping[old_strength]
        # check all connections that are identical
        for c in lattice.connections
            if c[3] == old_strength
                c[3] = new_strength
            end
        end
        # check for string replacement
        if replace_in_strings
            for c in lattice.connections
                if typeof(c[3]) == String && contains(c[3], old_strength)
                    c[3] = replace(c[3], old_strength, new_strength)
                end
            end
        end
    end
    # maybe even evaluate
    if evaluate
        for c in lattice.connections
            if typeof(c[3]) == String
                c[3] = eval(parse(c[3]))
            end
        end
    end
    # return nothing
    return nothing
end
export mapInteractionStrengths!

"""
    evaluateInteractionStrengths!(unitcell::Unitcell [, parameters::Dict])
    evaluateInteractionStrengths!(lattice::Lattice   [, parameters::Dict])

Function to evaluate connection strength values of either a `Unitcell` object or a `Lattice` object
by first optionally replacing them along keys of a dictonary `parameters` and then calling `eval(parse(.))`.

NOTE: This function modifies the given object and does not create a copy.

# Examples

Example parameters could read

    parameters = Dict("tx"=>1.0, "ty"=>1.0, "tz"=>2.0)

which modifies all `String` valued connection strengths `"tx"`, `"ty"` and `"tz"` to be `Float64` with values `1.0` and `2.0`

```julia-repl
julia> evaluateInteractionStrengths!(unitcell)

julia> evaluateInteractionStrengths!(lattice, parameters)

```
"""
function evaluateInteractionStrengths!(unitcell::Unitcell, parameters::Dict=Dict())
    # just call the mapping functions
    mapInteractionStrengths!(unitcell, parameters, replace_in_strings=true, evaluate=true)
end
function evaluateInteractionStrengths!(lattice::Lattice,   parameters::Dict=Dict())
    # just call the mapping functions
    mapInteractionStrengths!(lattice, parameters, replace_in_strings=true, evaluate=true)
end
export evaluateInteractionStrengths!

#-----------------------------------------------------------------------------------------------------------------------------
#
#   Optimize the lattice / unitcell by collapsing redundant connections into fewer connections and modify given object
#
#-----------------------------------------------------------------------------------------------------------------------------

"""
    optimizeConnections!(unitcell::Unitcell)
    optimizeConnections!(lattice::Lattice)

Function to optimize connections of either a `Unitcell` object or a `Lattice` object
by checking which ones can be compressed and put together.

NOTE: This function modifies the given object and does not create a copy.

# Examples

```julia-repl
julia> optimizeConnections!(unitcell)

```
"""
function optimizeConnections!(lattice::Lattice)
    # build up new connections
    connections_new = Array{Any,1}[]
    # go through all old connections to check for redundant entries
    for c in lattice.connections
        # check if the connection already is present in the new list
        found = false
        for c_new in connections_new
            if c[1] == c_new[1] && c[2] == c_new[2] && c[4] == c_new[4]
                # found a redundent connection
                found = true
                # add connection strength
                if typeof(c[3]) == String || typeof(c_new[3]) == String
                    c_new[3] = "$(c_new[3])+$(c[3])"
                else
                    c_new[3] = c_new[3] + c[3]
                end
                # break the inner loop
                break
            end
        end
        # if not, add the current connection
        if !found
            push!(connections_new, c)
        end
    end
    # check if the connection strength is 0
    connections_new_2 = Array{Any,1}[]
    for c in connections_new
        # check if close to 0 connection strength
        if typeof(c[3]) == Float64 || typeof(c[3]) == Int64
            if abs(c[3]) > 1e-18
                push!(connections_new_2, c)
            else
                # do not push! c[3] too small!
            end
        elseif isnull(tryparse(Float64,c[3]))
            # not a float and cannot be parsed to float, just push in list as it is
            push!(connections_new_2, c)
        else
            # check if the strength is non-zero
            if abs(parse(Float64,c[3])) > 1e-18
                push!(connections_new_2, c)
            else
                # do not push! c[3] too small!
            end
        end
    end
    # overwrite the connections in the given lattice
    lattice.connections = connections_new_2
    # return nothing
    return nothing
end
function optimizeConnections!(unitcell::Unitcell)
    # build up new connections
    connections_new = Array{Any,1}[]
    # go through all old connections to check for redundant entries
    for c in unitcell.connections
        # check if the connection already is present in the new list
        found = false
        for c_new in connections_new
            if c[1] == c_new[1] && c[2] == c_new[2] && c[4] == c_new[4]
                # found a redundent connection
                found = true
                # add connection strength
                if typeof(c[3]) == String || typeof(c_new[3]) == String
                    c_new[3] = "$(c_new[3])+$(c[3])"
                else
                    c_new[3] = c_new[3] + c[3]
                end
                # break the inner loop
                break
            end
        end
        # if not, add the current connection
        if !found
            push!(connections_new, c)
        end
    end
    # check if the connection strength is 0
    connections_new_2 = Array{Any,1}[]
    for c in connections_new
        # check if close to 0 connection strength
        if typeof(c[3]) == Float64 || typeof(c[3]) == Int64
            if abs(c[3]) > 1e-18
                push!(connections_new_2, c)
            else
                # do not push! c[3] too small!
            end
        elseif isnull(tryparse(Float64,c[3]))
            # not a float and cannot be parsed to float, just push in list as it is
            push!(connections_new_2, c)
        else
            # check if the strength is non-zero
            if abs(parse(Float64,c[3])) > 1e-18
                push!(connections_new_2, c)
            else
                # do not push! c[3] too small!
            end
        end
    end
    # overwrite the connections in the given lattice
    unitcell.connections = connections_new_2
    # return nothing
    return nothing
end
export optimizeConnections!

################################################################################
#
#   4) REAL SPACE ROTATION / SCALING / SHIFTING
#       - Rotation around some axis
#       - Scaling along some axis
#       - Global (isotropic) scaling
#       - Global (isotropic) scaling to mean / minimum bond length
#       - Shifting along some axis
#       - Shifting along some lattice vector
#       - Shifting center of lattice to origin
#
################################################################################

###################
#   ROTATION
###################

# ROTATION AROUND X AXIS
function rotateAroundXAxis!(unitcell::Unitcell, angle::Float64)
    # rotate the lattice vectors
    for lv in unitcell.lattice_vectors
        # rotate and overwrite simulateously all components
        lv[2], lv[3] = cos(angle)*lv[2] + sin(angle)*lv[3]  ,   -sin(angle)*lv[2] + cos(angle)*lv[3]
    end
    # rotate the basis sites
    for bs in unitcell.basis
        # rotate and overwrite simulateously all components
        bs[2], bs[3] = cos(angle)*bs[2] + sin(angle)*bs[3]  ,   -sin(angle)*bs[2] + cos(angle)*bs[3]
    end
end
function rotateAroundXAxis!(lattice::Lattice, angle::Float64)
    # rotate the lattice vectors
    for lv in lattice.lattice_vectors
        # rotate and overwrite simulateously all components
        lv[2], lv[3] = cos(angle)*lv[2] + sin(angle)*lv[3]  ,   -sin(angle)*lv[2] + cos(angle)*lv[3]
    end
    # rotate the basis sites
    for p in lattice.positions
        # rotate and overwrite simulateously all components
        p[2], p[3] = cos(angle)*p[2] + sin(angle)*p[3]  ,   -sin(angle)*p[2] + cos(angle)*p[3]
    end
end
export rotateAroundXAxis!

function rotateAroundXAxisDeg!(unitcell::Unitcell, angle::Float64)
    rotateAroundXAxis!(unitcell, angle*pi/180.0)
end
function rotateAroundXAxisDeg!(lattice::Lattice, angle::Float64)
    rotateAroundXAxis!(lattice, angle*pi/180.0)
end
export rotateAroundXAxisDeg!

# ROTATION AROUND Y AXIS
function rotateAroundYAxis!(unitcell::Unitcell, angle::Float64)
    # rotate the lattice vectors
    for lv in unitcell.lattice_vectors
        # rotate and overwrite simulateously all components
        lv[3], lv[1] = cos(angle)*lv[3] + sin(angle)*lv[1]  ,   -sin(angle)*lv[3] + cos(angle)*lv[1]
    end
    # rotate the basis sites
    for bs in unitcell.basis
        # rotate and overwrite simulateously all components
        bs[3], bs[1] = cos(angle)*bs[3] + sin(angle)*bs[1]  ,   -sin(angle)*bs[3] + cos(angle)*bs[1]
    end
end
function rotateAroundYAxis!(lattice::Lattice, angle::Float64)
    # rotate the lattice vectors
    for lv in lattice.lattice_vectors
        # rotate and overwrite simulateously all components
        lv[3], lv[1] = cos(angle)*lv[3] + sin(angle)*lv[1]  ,   -sin(angle)*lv[3] + cos(angle)*lv[1]
    end
    # rotate the basis sites
    for p in lattice.positions
        # rotate and overwrite simulateously all components
        p[3], p[1] = cos(angle)*p[3] + sin(angle)*p[1]  ,   -sin(angle)*p[3] + cos(angle)*p[1]
    end
end
export rotateAroundYAxis!

function rotateAroundYAxisDeg!(unitcell::Unitcell, angle::Float64)
    rotateAroundYAxis!(unitcell, angle*pi/180.0)
end
function rotateAroundYAxisDeg!(lattice::Lattice, angle::Float64)
    rotateAroundYAxis!(lattice, angle*pi/180.0)
end
export rotateAroundYAxisDeg!

# ROTATION AROUND Z AXIS
function rotateAroundZAxis!(unitcell::Unitcell, angle::Float64)
    # rotate the lattice vectors
    for lv in unitcell.lattice_vectors
        # rotate and overwrite simulateously all components
        lv[1], lv[2] = cos(angle)*lv[1] + sin(angle)*lv[2]  ,   -sin(angle)*lv[1] + cos(angle)*lv[2]
    end
    # rotate the basis sites
    for bs in unitcell.basis
        # rotate and overwrite simulateously all components
        bs[1], bs[2] = cos(angle)*bs[1] + sin(angle)*bs[2]  ,   -sin(angle)*bs[1] + cos(angle)*bs[2]
    end
end
function rotateAroundZAxis!(lattice::Lattice, anglTODOe::Float64)
    # rotate the lattice vectors
    for lv in lattice.lattice_vectors
        # rotate and overwrite simulateously all components
        lv[1], lv[2] = cos(angle)*lv[1] + sin(angle)*lv[2]  ,   -sin(angle)*lv[1] + cos(angle)*lv[2]
    end
    # rotate the basis sites
    for p in lattice.positions
        # rotate and overwrite simulateously all components
        p[1], p[2] = cos(angle)*p[1] + sin(angle)*p[2]  ,   -sin(angle)*p[1] + cos(angle)*p[2]
    end
end
export rotateAroundZAxis!

function rotateAroundZAxisDeg!(unitcell::Unitcell, angle::Float64)
    rotateAroundZAxis!(unitcell, angle*pi/180.0)
end
function rotateAroundZAxisDeg!(lattice::Lattice, angle::Float64)
    rotateAroundZAxis!(lattice, angle*pi/180.0)
end
export rotateAroundZAxisDeg!

###################
#   SCALING
###################

# SCALING ALONG X AXIS
function scaleAlongXAxis!(unitcell::Unitcell, factor::Float64)
    # scale all lattice vectors
    for l in unitcell.lattice_vectors
        l[1] *= factor
    end
    # scale all positions
    for p in unitcell.basis
        p[1] *= factor
    end
end
function scaleAlongXAxis!(lattice::Lattice, factor::Float64)
    # scale all lattice vectors
    for l in lattice.lattice_vectors
        l[1] *= factor
    end
    # scale all positions
    for p in lattice.positions
        p[1] *= factor
    end
end
export scaleAlongXAxis!

# SCALING ALONG Y AXIS
function scaleAlongYAxis!(unitcell::Unitcell, factor::Float64)
    # scale all lattice vectors
    for l in unitcell.lattice_vectors
        l[2] *= factor
    end
    # scale all positions
    for p in unitcell.basis
        p[2] *= factor
    end
end
function scaleAlongYAxis!(lattice::Lattice, factor::Float64)
    # scale all lattice vectors
    for l in lattice.lattice_vectors
        l[2] *= factor
    end
    # scale all positions
    for p in lattice.positions
        p[2] *= factor
    end
end
export scaleAlongYAxis!

# SCALING ALONG Z AXIS
function scaleAlongZAxis!(unitcell::Unitcell, factor::Float64)
    # scale all lattice vectors
    for l in unitcell.lattice_vectors
        l[3] *= factor
    end
    # scale all positions
    for p in unitcell.basis
        p[3] *= factor
    end
end
function scaleAlongZAxis!(lattice::Lattice, factor::Float64)
    # scale all lattice vectors
    for l in lattice.lattice_vectors
        l[3] *= factor
    end
    # scale all positions
    for p in lattice.positions
        p[3] *= factor
    end
end
export scaleAlongZAxis!

# SCALING ISOTROPIC
function scaleIsotropic!(unitcell::Unitcell, factor::Float64)
    # scale all lattice vectors
    for l in unitcell.lattice_vectors
        l .*= factor
    end
    # scale all positions
    for p in unitcell.basis
        p .*= factor
    end
end
function scaleIsotropic!(lattice::Lattice, factor::Float64)
    # scale all lattice vectors
    for l in lattice.lattice_vectors
        l .*= factor
    end
    # scale all positions
    for p in lattice.positions
        p .*= factor
    end
end
export scaleIsotropic!

# SCALING TO MEAN BOND LENGTH
function scaleToMeanBondLength!(unitcell::Unitcell, bond_length::Float64=1.0)
    # get all bond lengths
    bond_lengths = zeros(length(unitcell.connections))
    for i in 1:length(bond_lengths)
        # get the connection
        c = unitcell.connections[i]
        # get the connecting vector
        c_vector = unitcell.basis[Int(c[2])] .- unitcell.basis[Int(c[1])]
        for (j,l) in enumerate(unitcell.lattice_vectors)
            c_vector .+= l.*c[4][j]
        end
        # get how long it is
        bond_lengths[i] = sqrt(sum(c_vector.*c_vector))
    end
    # calculate the mean bond length
    mean_bond_length = mean(bond_lengths)
    # calculate how the scale factor looks like
    factor = bond_length / mean_bond_length
    # scale the object
    scaleIsotropic!(unitcell, factor)
end
function scaleToMeanBondLength!(lattice::Lattice, bond_length::Float64=1.0)
    # get all bond lengths
    bond_lengths = zeros(length(lattice.connections))
    for i in 1:length(bond_lengths)
        # get the connection
        c = lattice.connections[i]
        # get the connecting vector
        c_vector = lattice.positions[Int(c[2])] .- lattice.positions[Int(c[1])]
        for (j,l) in enumerate(lattice.lattice_vectors)
            c_vector .+= l.*c[4][j]
        end
        # get how long it is
        bond_lengths[i] = sqrt(sum(c_vector.*c_vector))
    end
    # calculate the mean bond length
    mean_bond_length = mean(bond_lengths)
    # calculate how the scale factor looks like
    factor = bond_length / mean_bond_length
    # scale the object
    scaleIsotropic!(lattice, factor)
end
export scaleToMeanBondLength!

# SCALING TO MINIMUM BOND LENGTH
function scaleToMinimumBondLength!(unitcell::Unitcell, bond_length::Float64=1.0)
    # get all bond lengths
    bond_lengths = zeros(length(unitcell.connections))
    for i in 1:length(bond_lengths)
        # get the connection
        c = unitcell.connections[i]
        # get the connecting vector
        c_vector = unitcell.basis[Int(c[2])] .- unitcell.basis[Int(c[1])]
        for (j,l) in enumerate(unitcell.lattice_vectors)
            c_vector .+= l.*c[4][j]
        end
        # get how long it is
        bond_lengths[i] = sqrt(sum(c_vector.*c_vector))
    end
    # calculate the minimum bond length
    minimum_bond_length = minimum(bond_lengths)
    # calculate how the scale factor looks like
    factor = bond_length / minimum_bond_length
    # scale the object
    scaleIsotropic!(unitcell, factor)
end
function scaleToMinimumBondLength!(lattice::Lattice, bond_length::Float64=1.0)
    # get all bond lengths
    bond_lengths = zeros(length(lattice.connections))
    for i in 1:length(bond_lengths)
        # get the connection
        c = lattice.connections[i]
        # get the connecting vector
        c_vector = lattice.positions[Int(c[2])] .- lattice.positions[Int(c[1])]
        for (j,l) in enumerate(lattice.lattice_vectors)
            c_vector .+= l.*c[4][j]
        end
        # get how long it is
        bond_lengths[i] = sqrt(sum(c_vector.*c_vector))
    end
    # calculate the minimum bond length
    minimum_bond_length = minimum(bond_lengths)
    # calculate how the scale factor looks like
    factor = bond_length / minimum_bond_length
    # scale the object
    scaleIsotropic!(lattice, factor)
end
export scaleToMinimumBondLength!

###################
#   SHIFTING
###################

# SHIFT ALONG X AXIS
function shiftAlongXAxis!(unitcell::Unitcell, offset::Float64)
    # shift all positions
    for p in unitcell.basis
        p[1] += offset
    end
end
function shiftAlongXAxis!(lattice::Lattice, offset::Float64)
    # shift all positions
    for p in lattice.positions
        p[1] += offset
    end
end
export shiftAlongXAxis!

# SHIFT ALONG Y AXIS
function shiftAlongYAxis!(unitcell::Unitcell, offset::Float64)
    # shift all positions
    for p in unitcell.basis
        p[2] += offset
    end
end
function shiftAlongYAxis!(lattice::Lattice, offset::Float64)
    # shift all positions
    for p in lattice.positions
        p[2] += offset
    end
end
export shiftAlongYAxis!

# SHIFT ALONG Z AXIS
function shiftAlongZAxis!(unitcell::Unitcell, offset::Float64)
    # shift all positions
    for p in unitcell.basis
        p[3] += offset
    end
end
function shiftAlongZAxis!(lattice::Lattice, offset::Float64)
    # shift all positions
    for p in lattice.positions
        p[3] += offset
    end
end
export shiftAlongZAxis!

# SHIFT BY VECTOR
function shiftByVector!(unitcell::Unitcell, vector::Array{Float64,1})
    # shift all positions
    for p in unitcell.basis
        p .+= vector
    end
end
function shiftByVector!(lattice::Lattice, vector::Array{Float64,1})
    # shift all positions
    for p in lattice.positions
        p .+= vector
    end
end
export shiftByVector!

# SHIFT ALONG ANY LATTICE VECTOR
function shiftAlongLatticeVector!(unitcell::Unitcell, lattice_vector::Int64, offset::Float64)
    # define a vector for shifting
    vector = unitcell.lattice_vectors[lattice_vector] .* (offset / sqrt(sum(unitcell.lattice_vectors[lattice_vector].*unitcell.lattice_vectors[lattice_vector])))
    # shift by that vector
    shiftByVector!(unitcell, vector)
end
function shiftAlongLatticeVector!(lattice::Lattice, lattice_vector::Int64, offset::Float64)
    # define a vector for shifting
    vector = lattice.lattice_vectors[lattice_vector] .* (offset / sqrt(sum(lattice.lattice_vectors[lattice_vector].*lattice.lattice_vectors[lattice_vector])))
    # shift by that vector
    shiftByVector!(lattice, vector)
end
export shiftAlongLatticeVector!

# SHIFT ALONG A1
function shiftAlongA1!(unitcell::Unitcell, offset::Float64)
    # shift along the first lattice vector
    shiftAlongLatticeVector!(unitcell, 1, offset)
end
function shiftAlongA1!(lattice::Lattice, offset::Float64)
    # shift along the first lattice vector
    shiftAlongLatticeVector!(lattice, 1, offset)
end
export shiftAlongA1!

# SHIFT ALONG A2
function shiftAlongA2!(unitcell::Unitcell, offset::Float64)
    # shift along the second lattice vector
    shiftAlongLatticeVector!(unitcell, 2, offset)
end
function shiftAlongA2!(lattice::Lattice, offset::Float64)
    # shift along the second lattice vector
    shiftAlongLatticeVector!(lattice, 2, offset)
end
export shiftAlongA2!

# SHIFT ALONG A3
function shiftAlongA3!(unitcell::Unitcell, offset::Float64)
    # shift along the third lattice vector
    shiftAlongLatticeVector!(unitcell, 3, offset)
end
function shiftAlongA3!(lattice::Lattice, offset::Float64)
    # shift along the third lattice vector
    shiftAlongLatticeVector!(lattice, 3, offset)
end
export shiftAlongA3!

# SHIFT ALONG ANY LATTICE VECTOR (Relative)
function shiftAlongLatticeVectorRelative!(unitcell::Unitcell, lattice_vector::Int64, offset::Float64)
    # define a vector for shifting
    vector = unitcell.lattice_vectors[lattice_vector] .* offset
    # shift by that vector
    shiftByVector!(unitcell, vector)
end
function shiftAlongLatticeVectorRelative!(lattice::Lattice, lattice_vector::Int64, offset::Float64)
    # define a vector for shifting
    vector = lattice.lattice_vectors[lattice_vector] .* offset
    # shift by that vector
    shiftByVector!(lattice, vector)
end
export shiftAlongLatticeVectorRelative!

# SHIFT ALONG A1 (Relative)
function shiftAlongA1Relative!(unitcell::Unitcell, offset::Float64)
    # shift along the first lattice vector
    shiftAlongLatticeVectorRelative!(unitcell, 1, offset)
end
function shiftAlongA1Relative!(lattice::Lattice, offset::Float64)
    # shift along the first lattice vector
    shiftAlongLatticeVectorRelative!(lattice, 1, offset)
end
export shiftAlongA1Relative!

# SHIFT ALONG A2 (Relative)
function shiftAlongA2Relative!(unitcell::Unitcell, offset::Float64)
    # shift along the second lattice vector
    shiftAlongLatticeVectorRelative!(unitcell, 2, offset)
end
function shiftAlongA2Relative!(lattice::Lattice, offset::Float64)
    # shift along the second lattice vector
    shiftAlongLatticeVectorRelative!(lattice, 2, offset)
end
export shiftAlongA2Relative!

# SHIFT ALONG A3 (Relative)
function shiftAlongA3Relative!(unitcell::Unitcell, offset::Float64)
    # shift along the third lattice vector
    shiftAlongLatticeVectorRelative!(unitcell, 3, offset)
end
function shiftAlongA3Relative!(lattice::Lattice, offset::Float64)
    # shift along the third lattice vector
    shiftAlongLatticeVectorRelative!(lattice, 3, offset)
end
export shiftAlongA3Relative!

# SHIFT CENTER TO ORIGIN
function shiftCenterToOrigin!(unitcell::Unitcell)
    # look for the central point
    center = unitcell.basis .* 0.0
    for p in unitcell.basis
        center .+= p
    end
    center ./= length(unitcell.basis)
    # shift by negative of that vector
    shiftByVector!(unitcell, center.*(-1))
end
function shiftCenterToOrigin!(lattice::Lattice)
    # look for the central point
    center = lattice.positions .* 0.0
    for p in lattice.positions
        center .+= p
    end
    center ./= length(lattice.positions)
    # shift by negative of that vector
    shiftByVector!(lattice, center.*(-1))
end
export shiftCenterToOrigin!

################################################################################
#
#   5) LATTICE VECTOR OPTIMIZATION
#
################################################################################

# Function to optimize lattice vectors
function optimizeLatticeVectors2D!(unitcell::Unitcell; verbose::Bool=false)
    # init the loop by assuming one refolded a vector
    refolded_one_vector = true
    # as long as one vector was refolded, repeat this loop
    while refolded_one_vector
        # init inside the loop by assuming one did not refold a vector
        refolded_one_vector = false
        # try a1
        if dot(unitcell.lattice_vectors[1],unitcell.lattice_vectors[1]) > dot(unitcell.lattice_vectors[1].-unitcell.lattice_vectors[2], unitcell.lattice_vectors[1].-unitcell.lattice_vectors[2])
            # print
            if verbose
                println("a1 --> a1 - a2")
            end
            # change the lattice vector
            unitcell.lattice_vectors[1] = unitcell.lattice_vectors[1] .- unitcell.lattice_vectors[2]
            # change all connections
            for c in unitcell.connections
                if c[4][1] != 0
                    c[4] = (c[4][1], c[4][2]+c[4][1])
                end
            end
            # did refold
            refolded_one_vector = true
        end
        if dot(unitcell.lattice_vectors[1],unitcell.lattice_vectors[1]) > dot(unitcell.lattice_vectors[1].+unitcell.lattice_vectors[2], unitcell.lattice_vectors[1].+unitcell.lattice_vectors[2])
            # print
            if verbose
                println("a1 --> a1 + a2")
            end
            # change the lattice vector
            unitcell.lattice_vectors[1] = unitcell.lattice_vectors[1] .+ unitcell.lattice_vectors[2]
            # change all connections
            for c in unitcell.connections
                if c[4][1] != 0
                    c[4] = (c[4][1], c[4][2]-c[4][1])
                end
            end
            # did refold
            refolded_one_vector = true
        end
        # try a2
        if dot(unitcell.lattice_vectors[2],unitcell.lattice_vectors[2]) > dot(unitcell.lattice_vectors[2].-unitcell.lattice_vectors[1], unitcell.lattice_vectors[2].-unitcell.lattice_vectors[1])
            # print
            if verbose
                println("a2 --> a2 - a1")
            end
            # change the lattice vector
            unitcell.lattice_vectors[2] = unitcell.lattice_vectors[2] .- unitcell.lattice_vectors[1]
            # change all connections
            for c in unitcell.connections
                if c[4][2] != 0
                    c[4] = (c[4][1]+c[4][2], c[4][2])
                end
            end
            # did refold
            refolded_one_vector = true
        end
        if dot(unitcell.lattice_vectors[2],unitcell.lattice_vectors[2]) > dot(unitcell.lattice_vectors[2].+unitcell.lattice_vectors[1], unitcell.lattice_vectors[2].+unitcell.lattice_vectors[1])
            # print
            if verbose
                println("a2 --> a2 + a1")
            end
            # change the lattice vector
            unitcell.lattice_vectors[2] = unitcell.lattice_vectors[2] .+ unitcell.lattice_vectors[1]
            # change all connections
            for c in unitcell.connections
                if c[4][2] != 0
                    c[4] = (c[4][1]-c[4][2], c[4][2])
                end
            end
            # did refold
            refolded_one_vector = true
        end
    end
end

# Function to optimize lattice vectors
function optimizeLatticeVectors3D!(unitcell::Unitcell; verbose::Bool=false)
    # init the loop by assuming one refolded a vector
    refolded_one_vector = true
    # as long as one vector was refolded, repeat this loop
    while refolded_one_vector
        # init inside the loop by assuming one did not refold a vector
        refolded_one_vector = false
        # try all combinations of the remaining two lattice vectors
        for comb in Array{Int64,1}[[1,0], [-1,0], [0,1], [0,-1], [1,1], [1,-1], [-1,1], [-1,-1]]
            # try a1
            a1p = unitcell.lattice_vectors[1] .+ comb[1].*unitcell.lattice_vectors[2] .+ comb[2].*unitcell.lattice_vectors[3]
            # check if this is closer
            if dot(unitcell.lattice_vectors[1],unitcell.lattice_vectors[1]) > dot(a1p, a1p)
                # print
                if verbose
                    println("a1 --> a1 + $(comb[1])*a2 + $(comb[2])*a3")
                end
                # change the lattice vector
                unitcell.lattice_vectors[1] = a1p
                # change all connections
                for c in unitcell.connections
                    c[4] = (c[4][1], c[4][2]-c[4][1]*comb[1], c[4][3]-c[4][1]*comb[2])
                end
                # did refold
                refolded_one_vector = true
            end
            # try a2
            a2p = unitcell.lattice_vectors[2] .+ comb[1].*unitcell.lattice_vectors[1] .+ comb[2].*unitcell.lattice_vectors[3]
            # check if this is closer
            if dot(unitcell.lattice_vectors[2],unitcell.lattice_vectors[2]) > dot(a2p, a2p)
                # print
                if verbose
                    println("a2 --> a2 + $(comb[1])*a1 + $(comb[2])*a3")
                end
                # change the lattice vector
                unitcell.lattice_vectors[2] = a2p
                # change all connections
                for c in unitcell.connections
                    c[4] = (c[4][1]-c[4][2]*comb[1], c[4][2], c[4][3]-c[4][2]*comb[2])
                end
                # did refold
                refolded_one_vector = true
            end
            # try a3
            a3p = unitcell.lattice_vectors[3] .+ comb[1].*unitcell.lattice_vectors[1] .+ comb[2].*unitcell.lattice_vectors[2]
            # check if this is closer
            if dot(unitcell.lattice_vectors[3],unitcell.lattice_vectors[3]) > dot(a3p, a3p)
                # print
                if verbose
                    println("a3 --> a3 + $(comb[1])*a1 + $(comb[2])*a2")
                end
                # change the lattice vector
                unitcell.lattice_vectors[3] = a3p
                # change all connections
                for c in unitcell.connections
                    c[4] = (c[4][1]-c[4][3]*comb[1], c[4][2]-c[4][3]*comb[2], c[4][3])
                end
                # did refold
                refolded_one_vector = true
            end
        end
    end
end

# general function
function optimizeLatticeVectors!(unitcell::Unitcell; verbose::Bool=false)
    if length(unitcell.lattice_vectors) == 2
        return optimizeLatticeVectors2D!(unitcell, verbose=verbose)
    elseif length(unitcell.lattice_vectors) == 3
        return optimizeLatticeVectors3D!(unitcell, verbose=verbose)
    else
        # DO NOTHING
    end
end
export optimizeLatticeVectors!


# A Using Case:
"""
yao> nbits(4)

yao> cc = begin
    2 => X
    3 => C, 1=>C, 4=>X
end

yao> nbits(6)

yao> dc = begin
    subroutine(1,5,2,3) cc
    3=>Rx(:θ)   # symbolic input
end

yao> reg = bit"000000"

yao> reg |> d

yao> p(rintln) reg

yao> p(rintln) c  # print summary

yao> s(how) reg

yao> c(ode) s reg

yao> m(easure!) [op] reg

yao> reset!(reg, 0)

yao> e(xpect) dc reg

yao> e(xpect)' dc reg

yao> f(idelity)' reg reg

yao> i(nspect) c     # show matrix

yao> l(istvars)    # list symbol table

yao> q(uit)
"""

CmdTable = Dict(
    # query
    'p'=>"print",
    'i'=>"inspect",
    # compute
    'e'=>"expect",
    'f'=>"fidelity",
    'r'=>"reset",
    'm'=>"measure",
    # interactive command
    'l'=>"listvars",
    'h'=>"help_repl",
)

struct YaoREPLState
    regs::Dict{Symbol,Any}
    blocks::Dict{Symbol,Any}
    parseinfo::YaoBlocks.ParseInfo
end
YaoREPLState() = YaoREPLState(Dict{Symbol,Any}(), Dict{Symbol,Any}(), YaoBlocks.ParseInfo(-1, ""))

function _checkfunc(f)
    sf = string(f)
    if sf in values(CmdTable)
        return f
    elseif sf[end] == '!' && sf[1:end-1] in values(CmdTable)
        return f
    else
        error("Expression `$f` can not be parsed to a known function!")
    end
end

"""
expand shortcuts.
"""
function yaorepl_parse(str)
    length(str) == 0 && return nothing
    str = strip(str, [' ', '\n'])
    if haskey(CmdTable, str[1]) && (length(str) == 1 || str[2] in [' ', '!', '\''])
        args = filter(x-> x!="", split(str, ' '))
        func = get(CmdTable, args[1][1], "noshortcut")
        str = func*args[1][2:end]*'('*join(args[2:end], ", ")*')'
    end
    Meta.parse(str)
end

yaorepl_trans(ex, state) = @match ex begin
    :($x = zero_state($nbit, nbatch=$nbatch)) => :($x = zero_state($(_checkbit(nbit)), nbatch=$(_checkbatch(nbatch))))
    :($x = zero_state($nbit)) => :($x = zero_state($(_checkbit(nbit))))
    :($x = rand_state($nbit, nbatch=$nbatch)) => :($x = rand_state($(_checkbit(nbit)), nbatch=$(_checkbatch(nbatch))))
    :($x = rand_state($nbit)) => :($x = rand_state($(_checkbit(nbit))))
    :($x = [$(args...)]) => :($x = ArrayReg(ComplexF64[$(Float64.(args)...)]))
    :($x = @bit_str $line $val) => :($x = ArrayReg(@bit_str $(_checkbit(val))))
    :($x = $ex) => begin
        blk = YaoBlocks.parse_ex(ex, state.parseinfo)
        :($x = $blk)
    end
    :(expect($a, $b=>$c)) => :(expect($(_checkop(a)), $(_checkreg(b))=>$(_checkop(c))))
    :(expect'($a, $b=>$c)) => :(expect'($(_checkop(a)), $(_checkreg(b))=>$(_checkop(c))))
    :(expect($a, $b)) => :(expect($(_checkop(a)), $(_checkreg(b))))
    :(expect'($a, $b)) => :(expect'($(_checkop(a)), $(_checkreg(b))))
    :(fidelity($a=>$oa, $b=>$ob)) => :(fidelity($(_checkreg(a))=>$(_checkop(oa)), $(_checkreg(b))=>$(_checkop(ob))))
    :(fidelity'($a=>$oa, $b=>$ob)) => :(fidelity'($(_checkreg(a))=>$(_checkop(oa)), $(_checkreg(b))=>$(_checkop(ob))))
    :(fidelity($a, $b)) => :(fidelity($(_checkreg(a)), $(_checkreg(b))))
    :(fidelity'($a, $b)) => :(fidelity'($(_checkreg(a)), $(_checkreg(b))))
    :(measure($a)) => :(measure($(_checkreg(a))))
    :(measure($a, nshots=$nshots)) => :(measure($(_checkreg(a)), nshots=$(_checkshots(nshots))))
    :(measure($op, $a)) => :(measure($(_checkop(op)), $(_checkreg(a))))
    :(measure($op, $a, nshots=$nshots)) => :(measure($(_checkop(op)), $(_checkreg(a)), nshots=$(_checkshots(nshots))))
    :(measure!($a)) => :(measure!($(_checkreg(a))))
    :(measure!($op, $a)) => :(measure!($(_checkop(op)), $(_checkreg(a))))
    :(print($a)) => :(print2str($(_checkreg(a))))
    :(inspect($a)) => :(inspect2str($(_checkreg(a))))
    :($x |> $y) => :(apply!($x, $y))

    :(help_repl()) => :(help_repl())
    :(listvars()) => :(listvars($state))
    :(nbits($n)) => :(setnbits!($n, $state))
    :(nbits()) => :(getnbits($state))
    _ => error("Expression `$ex` can not be translated!")
end

function yaorepl_execute(ex, state)
    @match ex begin
        :($x = $expr) => begin
            var = eval(expr)
            if var isa AbstractRegister
                state.regs[Symbol(x)] = var
            elseif var isa AbstractBlock
                state.blocks[Symbol(x)] = var
            else
                error("incorrect assignment with return value $var.")
            end
            "done"
        end
        :($f($(args...))) => begin
            nargs = [query_state(state, arg) for arg in args]
            res = eval(:($(_checkfunc(f))($(nargs...))))
            if res isa AbstractString
                res
            elseif res isa Nothing
                "done"
            else
                io = IOBuffer()
                Base.show(io, "text/plain", res)
                String(take!(io))
            end
        end
    end
end

function query_state(state::YaoREPLState, arg)
    @match arg begin
        :($a => $b) => :($(query_state(state, a)) => $(query_state(state, b)))
        ::Symbol => begin
            if haskey(state.regs, arg)
                state.regs[arg]
            elseif haskey(state.blocks, arg)
                state.blocks[arg]
            else
                arg
            end
        end
        _ => arg
    end
end

function listvars(state)
    regstr = ["$k ($(nqubits(v)))" for (k,v) in state.regs]
    blkstr = ["$k ($(nqubits(v)))" for (k,v) in state.blocks]
    join(["current nbits = $(state.parseinfo.nbit)\n", "=== registers ===", regstr..., "\n=== blocks ===", blkstr...], "\n")
end

function help_repl()
"""Guide:
=== Basic ===
q: quit
help_repl() | h: print this manual
c cmd: show the julia code of an expression
listvars() | l: list defined register/block table
nbits(n): set the number of qubits to `n` (used in defining a block)
nbits(): show the number of qubits

=== Define a Register ===
reg = rand_state(n): random `n`-qubit state
reg = zero_state(n): zero state
reg = bit"01011": product state

=== Operations ===
expect(op, reg): compute <reg|op|reg>
expect'(op, reg[=>c]): compute ∂<reg|c'*op*c|reg>/∂reg and ∂<reg|c'*op*c|reg>/∂c
fidelity(reg1, reg2): compute <reg1|reg2>
fidelity'(reg1[=>c1], reg2[=>c2]): compute ∂<reg1|c1'*c2|reg2>/∂reg1, ∂<reg1|c1*c2|reg2>/∂c1 ...
reg |> op: apply `op` on `reg`
measure!([op, ]reg) | m! [op] reg: measure `op` inplace.
measure([op, ]reg, [nshots=n]) | m! [op] reg [nshots=n]: measure `op` without collapsing `reg`.

* Note: `n` is integer, `cmd` is command, `reg` is register, `op` is operator or gate (block)."""
end

function yaorepl_handler(str::AbstractString, state)
    if length(strip(str)) >=2 && strip(str)[1:2] == "c "
        string(yaorepl_trans(yaorepl_parse(str[3:end]), state))
    else
        yaorepl_execute(yaorepl_trans(yaorepl_parse(str), state), state)
    end
end


"""
GenX: An Configurable Capacity Expansion Model
Copyright (C) 2021,  Massachusetts Institute of Technology
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
A complete copy of the GNU General Public License v2 (GPLv2) is available
in LICENSE.txt.  Users uncompressing this from an archive may not have
received this license file.  If not, see <http://www.gnu.org/licenses/>.
"""

@doc raw"""
	configure_cplex(solver_settings_path::String)

Reads user-specified solver settings from cplex\_settings.yml in the directory specified by the string solver\_settings\_path.

Returns a MathOptInterface OptimizerWithAttributes CPLEX optimizer instance to be used in the GenX.generate_model() method.

The CPLEX optimizer instance is configured with the following default parameters if a user-specified parameter for each respective field is not provided:

 - CPX\_PARAM\_EPRHS = 1e-6 (Constraint (primal) feasibility tolerances. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/EpRHS.html)
 - CPX\_PARAM\_EPOPT = 1e-6 (Dual feasibility tolerances. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/EpOpt.html)
 - CPX\_PARAM\_AGGFILL = 10 (Allowed fill during presolve aggregation. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/AggFill.html)
 - CPX\_PARAM\_PREDUAL = 0 (Decides whether presolve should pass the primal or dual linear programming problem to the LP optimization algorithm. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/PreDual.html)
 - CPX\_PARAM\_TILIM = 1e+75	(Limits total time solver. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/TiLim.html)
 - CPX\_PARAM\_EPGAP = 1e-4	(Relative (p.u. of optimal) mixed integer optimality tolerance for MIP problems (ignored otherwise). See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/EpGap.html)
 - CPX\_PARAM\_LPMETHOD = 0 (Algorithm used to solve continuous models (including MIP root relaxation). See https://www.ibm.com/support/knowledgecenter/de/SSSA5P_12.7.0/ilog.odms.cplex.help/CPLEX/Parameters/topics/LPMETHOD.html)
 - CPX\_PARAM\_BAREPCOMP = 1e-8 (Barrier convergence tolerance (determines when barrier terminates). See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/BarEpComp.html)
 - CPX\_PARAM\_NUMERICALEMPHASIS = 0 (Numerical precision emphasis. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/NumericalEmphasis.html)
 - CPX\_PARAM\_BAROBJRNG = 1e+75 (Sets the maximum absolute value of the objective function. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/BarObjRng.html)
 - CPX\_PARAM\_SOLUTIONTYPE = 2 (Solution type for LP or QP. See https://www.ibm.com/support/knowledgecenter/hr/SSSA5P_12.8.0/ilog.odms.cplex.help/CPLEX/Parameters/topics/SolutionType.html)

"""
function configure_cplex(solver_settings_path::String)

    solver_settings = YAML.load(open(solver_settings_path))

    # Set solve to use CPLEX for MIP or LP problems
    # Optional setup parameters ############################################
    # Constraint (primal) feasibility tolerances. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/EpRHS.html
    FeasibilityTol = 1e-6
    if (haskey(solver_settings, "Feasib_Tol"))
        FeasibilityTol = solver_settings["Feasib_Tol"]
    end

    # Dual feasibility tolerances. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/EpOpt.html
    OptimalityTol = 1e-6
    if (haskey(solver_settings, "Optimal_Tol"))
        OptimalityTol = solver_settings["Optimal_Tol"]
    end

    # Allowed fill during presolve aggregation. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/AggFill.html
    AggFill = 10
    if (haskey(solver_settings, "AggFill"))
        AggFill = solver_settings["AggFill"]
    end

    # Decides whether presolve should pass the primal or dual linear programming problem to the LP optimization algorithm. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/PreDual.html
    PreDual = 0
    if (haskey(solver_settings, "PreDual"))
        PreDual = solver_settings["PreDual"]
    end

    # Limits total time solver. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/TiLim.html
    TimeLimit = 1e+75
    if (haskey(solver_settings, "TimeLimit"))
        TimeLimit = solver_settings["TimeLimit"]
    end

    # Relative (p.u. of optimal) mixed integer optimality tolerance for MIP problems (ignored otherwise). See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/EpGap.html
    MIPGap = 1e-4
    if (haskey(solver_settings, "MIPGap"))
        MIPGap = solver_settings["MIPGap"]
    end

    # Algorithm used to solve continuous models (including MIP root relaxation). See https://www.ibm.com/support/knowledgecenter/de/SSSA5P_12.7.0/ilog.odms.cplex.help/CPLEX/Parameters/topics/LPMETHOD.html
    Method = 0
    if (haskey(solver_settings, "Method"))
        Method = solver_settings["Method"]
    end

    # Barrier convergence tolerance (determines when barrier terminates). See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/BarEpComp.html
    BarConvTol = 1e-8
    if (haskey(solver_settings, "BarConvTol"))
        BarConvTol = solver_settings["BarConvTol"]
    end

    # Numerical precision emphasis. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/NumericalEmphasis.html
    NumericFocus = 0
    if (haskey(solver_settings, "NumericFocus"))
        NumericFocus = solver_settings["NumericFocus"]
    end

    # Sets the maximum absolute value of the objective function. See https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.5.1/ilog.odms.cplex.help/CPLEX/Parameters/topics/BarObjRng.html
    BarObjRng = 1e+75
    if (haskey(solver_settings, "BarObjRng"))
        BarObjRng = solver_settings["BarObjRng"]
    end

    # Solution type for LP or QP. See https://www.ibm.com/support/knowledgecenter/hr/SSSA5P_12.8.0/ilog.odms.cplex.help/CPLEX/Parameters/topics/SolutionType.html
    SolutionType = 2
    if (haskey(solver_settings, "SolutionType"))
        SolutionType = solver_settings["SolutionType"]
    end
    ########################################################################

    OPTIMIZER = optimizer_with_attributes(
        CPLEX.Optimizer,
        "CPX_PARAM_EPRHS" => FeasibilityTol,
        "CPX_PARAM_EPOPT" => OptimalityTol,
        "CPX_PARAM_AGGFILL" => AggFill,
        "CPX_PARAM_PREDUAL" => PreDual,
        "CPX_PARAM_TILIM" => TimeLimit,
        "CPX_PARAM_EPGAP" => MIPGap,
        "CPX_PARAM_LPMETHOD" => Method,
        "CPX_PARAM_BAREPCOMP" => BarConvTol,
        "CPX_PARAM_NUMERICALEMPHASIS" => NumericFocus,
        "CPX_PARAM_BAROBJRNG" => BarObjRng,
        "CPX_PARAM_SOLUTIONTYPE" => SolutionType,
    )
    return OPTIMIZER
end


function add_flows(m::JuMP.AbstractModel, netinjection::BalanceNamedTuple, system_formulation::Type{S}, sys::PowerSystems.PowerSystem) where {S <: PM.AbstractDCPForm}

    fbr = m[:fbr]
    branch_name_index = m[:fbr].axes[1]
    time_index = m[:fbr].axes[2]

    for t in time_index, (ix,branch) in enumerate(branch_name_index)

        !isassigned(netinjection.var_active,sys.branches[ix].connectionpoints.from.number,t) ? netinjection.var_active[sys.branches[ix].connectionpoints.from.number,t] = -fbr[branch,t] : JuMP.add_to_expression!(netinjection.var_active[sys.branches[ix].connectionpoints.from.number,t],-fbr[branch,t])
        !isassigned(netinjection.var_active,sys.branches[ix].connectionpoints.to.number,t) ? netinjection.var_active[sys.branches[ix].connectionpoints.to.number,t] = fbr[branch,t] : JuMP.add_to_expression!(netinjection.var_active[sys.branches[ix].connectionpoints.to.number,t],fbr[branch,t])

    end

end

function add_flows(m::JuMP.AbstractModel, netinjection::BalanceNamedTuple, system_formulation::Type{S}, sys::PowerSystems.PowerSystem) where {S <: PM.AbstractDCPLLForm}

    fbr_fr = m[:fbr_fr]
    fbr_to = m[:fbr_to]
    time_index = m[:fbr_to].axes[2]
    branch_name_index = m[:fbr_fr].axes[1]

    for t in time_index, (ix,branch) in enumerate(branch_name_index)

        !isassigned(netinjection.var_active,sys.branches[ix].connectionpoints.from.number,t) ? netinjection.var_active[sys.branches[ix].connectionpoints.from.number,t] = -fbr_fr[branch,t] : JuMP.add_to_expression!(netinjection.var_active[sys.branches[ix].connectionpoints.from.number,t],-fbr_fr[branch,t])
        !isassigned(netinjection.var_active,sys.branches[ix].connectionpoints.to.number,t) ? netinjection.var_active[sys.branches[ix].connectionpoints.to.number,t] = fbr_to[branch,t] : JuMP.add_to_expression!(netinjection.var_active[sys.branches[ix].connectionpoints.to.number,t],fbr_to[branch,t])

    end

end

function nodalflowbalance(m::JuMP.AbstractModel, netinjection::BalanceNamedTuple, system_formulation::Type{S}, sys::PowerSystems.PowerSystem) where {S <: AbstractFlowForm}

    time_index = 1:sys.time_periods
    bus_name_index = [b.name for b in sys.buses]

    add_flows(m, netinjection, system_formulation, sys)

    pf_balance = JuMP.JuMPArray(Array{ConstraintRef}(undef,length(bus_name_index), sys.time_periods), bus_name_index, time_index)

        for t in time_index, (ix,bus) in enumerate(bus_name_index)

            isassigned(netinjection.var_active,ix, t) ? true : @error "Islanded Bus in the system"

            pf_balance[bus,t] = @constraint(m, netinjection.var_active[ix, t] == netinjection.timeseries_active[ix, t])

        end

        JuMP.register_object(m, :NodalFlowBalance_active, pf_balance)

end

function nodalflowbalance(m::JuMP.AbstractModel, netinjection::BalanceNamedTuple, system_formulation::Type{S}, sys::PowerSystems.PowerSystem) where {S <: AbstractDCPowerModel}

    time_index = 1:sys.time_periods
    bus_name_index = [b.name for b in sys.buses]

    PM_dict = pass_to_pm(sys, netinjection)

        for t in time_index, bus in sys.buses

            !isassigned(netinjection.var_active,bus.number,t) ? PM_dict["nw"]["$(t)"]["bus"]["$(bus.number)"]["pni"] = -(netinjection.timeseries_active[bus.number, t]) : PM_dict["nw"]["$(t)"]["bus"]["$(bus.number)"]["pni"] = JuMP.add_to_expression!(netinjection.var_active[bus.number,t],-(netinjection.timeseries_active[bus.number, t]))

        end

        m.ext[:PM_object] = PM_dict

end

function nodalflowbalance(m::JuMP.AbstractModel, netinjection::BalanceNamedTuple, system_formulation::Type{S}, sys::PowerSystems.PowerSystem) where {S <: AbstractACPowerModel}

    nodalflowbalance(m, netinjection, AbstractDCPowerModel, sys)

    PM_dict = m.ext[:PM_object]

    time_index = 1:sys.time_periods
    bus_name_index = [b.name for b in sys.buses]

        for t in time_index, bus in sys.buses

            !isassigned(netinjection.var_reactive,bus.number,t) ? PM_dict["nw"]["$(t)"]["bus"]["$(bus.number)"]["qni"] = -(netinjection.timeseries_reactive[bus.number, t]) : PM_dict["nw"]["$(t)"]["bus"]["$(bus.number)"]["qni"] = JuMP.add_to_expression!(netinjection.var_reactive[bus.number,t],-(netinjection.timeseries_reactive[bus.number, t]))

        end

        m.ext[:PM_object] = PM_dict

end