"""
    AbstractVertex

Vertex base type.
"""
abstract type AbstractVertex end

Base.Broadcast.broadcastable(v::AbstractVertex) = Ref(v)

"""
    inputs(v)

Return an Array of vertices which are input to the given vertex.

# Examples
```jldoctest
julia> using NaiveNASlib, NaiveNASlib.Extend

julia> inputs(CompVertex(identity, InputVertex(1)))
1-element Vector{AbstractVertex}:
 InputVertex(1)
```
"""
function inputs(::AbstractVertex) end

"""
    outputs(v)

Return an Array of vertices for which the given vertex is input to.

# Examples
```jldoctest
julia> using NaiveNASlib

julia> iv = inputvertex("in", 3);

julia> cv = invariantvertex(identity, iv);

julia> outputs(iv)
1-element Vector{NaiveNASlib.AbstractVertex}:
 MutationVertex(CompVertex(identity, inputs=[in], outputs=[]), NaiveNASlib.SizeInvariant())
```
"""
function outputs(::AbstractVertex) end

"""
    InputVertex

Acts as a source of data to the graph and therefore does not need
any input vertices to feed it.

# Examples
```jldoctest
julia> using NaiveNASlib, NaiveNASlib.Extend

julia> InputVertex(1)
InputVertex(1)

julia> InputVertex("input")
InputVertex(input)
```
"""
struct InputVertex{N} <: AbstractVertex
    name::N
end
inputs(::InputVertex)::AbstractArray{AbstractVertex,1} = []
(v::InputVertex)(x...) = error("Missing input $(v.name) to graph!")

@functor InputVertex

"""
    CompVertex
    CompVertex(c, ins::AbstractVertex...)
    CompVertex(c, ins::AbstractArray{<:AbstractVertex}) =

Maps input from input vertices to output through `output = c(input...)`.

Must have at least one input vertex.

# Examples
```jldoctest
julia> using NaiveNASlib, NaiveNASlib.Extend

julia> CompVertex(+, InputVertex(1), InputVertex(2))
CompVertex(+, inputs=[InputVertex(1), InputVertex(2)])

julia> CompVertex(x -> 4x, InputVertex(1))(2)
8

julia> CompVertex(*, InputVertex(1), InputVertex(2))(2,3)
6
```
"""
struct CompVertex{F} <: AbstractVertex
    computation::F
    inputs::Vector{AbstractVertex} # Untyped because we might add other vertices to it
end
CompVertex(c, ins::AbstractArray{<:AbstractVertex}) = CompVertex(c, collect(AbstractVertex, ins))
CompVertex(c, ins::AbstractVertex...) = CompVertex(c, collect(AbstractVertex, ins))
inputs(v::CompVertex) = v.inputs
(v::CompVertex)(x...) = v.computation(x...)

@functor CompVertex

## Stuff for displaying information about vertices

# To avoid too verbose console output
function Base.show(io::IO, vs::AbstractVector{<:AbstractVertex})
    print(io, "[")
    for (i, v) in enumerate(vs)
        show_less(io, v)
        i != length(vs) && print(io, ", ")
    end
    print(io, "]")
end
show_less(io::IO, v::AbstractVertex; close=')') = summary(io, v)
show_less(io::IO, v::InputVertex; close=')') = show_less(io, v, v.name; close)
show_less(io::IO, ::InputVertex, name::String; close=')') = print(io, name)
show_less(io::IO, ::InputVertex, name; close=')') = print(io, "InputVertex(", name, close)

function show_less(io::IO, v::CompVertex; close=')')
    print(io, "CompVertex(")
    show(io, v.computation)
    print(io, close)
end

Base.show(io::IO, v::InputVertex; close=')') = print(io, "InputVertex(", v.name, close)
function Base.show(io::IO, v::CompVertex; close=')')
    print(io, "CompVertex(")
    show(io, v.computation)
    print(io, ", inputs=")
    show(io, inputs(v))
    print(io, close)
end

# Stuff for logging

"""
    name(v)

Return a the name of the vertex `v`.
Will return a generic string describing `v` if no name has been given to `v`.

Note that names in a graph don't have to be unique.
"""
name(v::AbstractVertex) = string(nameof(typeof(v)))
name(v::InputVertex) = v.name


# consume upto 50% of available memory, across all workers, and across inputs, U and P matrices
def_cache() = max(MAX_BLK_BYTES, floor(Int, Base.Sys.free_memory()/3/2/nworkers()))

zero{T<:AbstractString}(::Type{T}) = convert(T, "")

# TODO: optimize save/load instead of a blind serialize call
function save(model, filename::AbstractString)
    open(filename, "w") do f
        serialize(f, model)
    end
    nothing
end

function load(filename::AbstractString)
    open(filename, "r") do f
        deserialize(f)
    end
end

##
# Types of file specifications

# DLM/CSV file (input only)
type DlmFile <: FileSpec
    name::AbstractString
    dlm::Char
    header::Bool
    quotes::Bool

    function DlmFile(name::AbstractString; dlm::Char=Base.DataFmt.invalid_dlm(Char), header::Bool=false, quotes::Bool=true)
        new(name, dlm, header, quotes)
    end
end

function read_input(fspec::DlmFile)
    # read file and skip the header
    F = readdlm(fspec.name, fspec.dlm, header=fspec.header, quotes=fspec.quotes)
    fspec.header ? F[1] : F
end

# MAT file (input only)
type MatFile <: FileSpec
    filename::AbstractString
    entryname::AbstractString
end
read_input(fspec::MatFile) = read(matopen(fspec.filename), fspec.entryname)

# sparse matrix - in memory (input only)
type SparseMat <: FileSpec
    S::Union{RatingMatrix, SharedRatingMatrix}
end
read_input(fspec::SparseMat) = fspec.S

# sparse matrix blobs (input only)
type SparseBlobs <: FileSpec
    name::AbstractString
    maxcache::Int
    function SparseBlobs(name::AbstractString; maxcache::Int=def_cache())
        new(name, maxcache)
    end
end
read_input(fspec::SparseBlobs) = SparseMatBlobs(fspec.name; maxcache=fspec.maxcache)

# dense matrix blobs (input and output)
type DenseBlobs <: FileSpec
    name::AbstractString
    maxcache::Int
    function DenseBlobs(name::AbstractString; maxcache::Int=def_cache())
        new(name, maxcache)
    end
end
read_input(fspec::DenseBlobs) = DenseMatBlobs(fspec.name; maxcache=fspec.maxcache)

const MAX_BLK_BYTES = 128*1000*1000 #128MB
function _max_items(T::Type, sz::Tuple)
    m,n = sz
    ceil(Int, MAX_BLK_BYTES/sizeof(T)/m)
end
function create{T}(fspec::DenseBlobs, ::Type{T}, sz::Tuple, init::Function, max_items::Int=_max_items(T,sz))
    @logmsg("creating densematarray")
    isdir(fspec.name) || mkdir(fspec.dir)
    m,n = sz
    dm = DenseMatBlobs(T, fspec.name; maxcache=fspec.maxcache)

    startidx = 1
    while startidx <= n
        idxrange = startidx:min(n, startidx + max_items)
        blobsz = (m,length(idxrange))
        M = init(T, blobsz...)
        @logmsg("idxrange: $idxrange, sz: $(size(M))")
        append!(dm, M)
        startidx = last(idxrange) + 1
    end
    dm
end


# Setup

run(`git clone https://github.com/mohamed82008/TopOpt.jl TopOpt`)
cd(() -> run(`git checkout cuda_error`), "TopOpt")
using Pkg
Pkg.activate("./TopOpt")
Pkg.instantiate()

# Error example

using Revise, TopOpt, CuArrays, LinearAlgebra
CuArrays.allowscalar(false)

function gpu_testcase(s)
	problem = PointLoadCantilever(Val{:Linear}, s, (1.0, 1.0), 1.0, 0.3, 1.0);
	solver = FEASolver(Displacement, CG, MatrixFree, problem);
	cusolver = cu(solver);
	cusolver.vars .= 1;
	cuop = TopOpt.buildoperator(cusolver)
	cux = CuArray(ones(size(cuop, 1)))
	cuy = similar(cux)
	mul!(cuy, cuop, cux)
end

function cpu_testcase(s)
	problem = PointLoadCantilever(Val{:Linear}, s, (1.0, 1.0), 1.0, 0.3, 1.0);
	solver = FEASolver(Displacement, CG, MatrixFree, problem);
	solver.vars .= 1;
	op = TopOpt.buildoperator(solver)
	x = ones(size(op, 1))
	y = similar(x)
	mul!(y, op, x)
end

s = (4, 4)
cpu_testcase(s) # works
gpu_testcase(s) # works

s = (10, 4)
cpu_testcase(s) # works
gpu_testcase(s)
# Some times works and other times errors with:
# ERROR: CUDA error: unspecified launch failure (code #719, ERROR_LAUNCH_FAILED)

s = (100, 10)
gpu_testcase(s)
# Works the first time and then fails the second time with:
# ERROR: CUDA error: an illegal memory access was encountered (code #700, ERROR_ILLEGAL_ADDRESS)

#=
The block-thread config is found using https://github.com/mohamed82008/TopOpt.jl/blob/cuda_error/src/GPUUtils/GPUUtils.jl#L101.

The number of blocks and threads can be overwritten in https://github.com/mohamed82008/TopOpt.jl/blob/cuda_error/src/GPUUtils/GPUUtils.jl#L95.
=#


# libcublas_types.jl
#
# Initially generated with wrap_c from Clang.jl. Modified to remove anonymous
# enums and add cublasContext.
#
# Author: Nick Henderson <nwh@stanford.edu>
# Created: 2014-08-27
# License: MIT
#

# begin enum cublasStatus_t
typealias cublasStatus_t UInt32
const CUBLAS_STATUS_SUCCESS = 0
const CUBLAS_STATUS_NOT_INITIALIZED = 1
const CUBLAS_STATUS_ALLOC_FAILED = 3
const CUBLAS_STATUS_INVALID_VALUE = 7
const CUBLAS_STATUS_ARCH_MISMATCH = 8
const CUBLAS_STATUS_MAPPING_ERROR = 11
const CUBLAS_STATUS_EXECUTION_FAILED = 13
const CUBLAS_STATUS_INTERNAL_ERROR = 14
const CUBLAS_STATUS_NOT_SUPPORTED = 15
const CUBLAS_STATUS_LICENSE_ERROR = 16
# end enum cublasStatus_t
# begin enum cublasFillMode_t
typealias cublasFillMode_t UInt32
const CUBLAS_FILL_MODE_LOWER = 0
const CUBLAS_FILL_MODE_UPPER = 1
# end enum cublasFillMode_t
# begin enum cublasDiagType_t
typealias cublasDiagType_t UInt32
const CUBLAS_DIAG_NON_UNIT = 0
const CUBLAS_DIAG_UNIT = 1
# end enum cublasDiagType_t
# begin enum cublasSideMode_t
typealias cublasSideMode_t UInt32
const CUBLAS_SIDE_LEFT = 0
const CUBLAS_SIDE_RIGHT = 1
# end enum cublasSideMode_t
# begin enum cublasOperation_t
typealias cublasOperation_t UInt32
const CUBLAS_OP_N = 0
const CUBLAS_OP_T = 1
const CUBLAS_OP_C = 2
# end enum cublasOperation_t
# begin enum cublasPointerMode_t
typealias cublasPointerMode_t UInt32
const CUBLAS_POINTER_MODE_HOST = 0
const CUBLAS_POINTER_MODE_DEVICE = 1
# end enum cublasPointerMode_t
# begin enum cublasAtomicsMode_t
typealias cublasAtomicsMode_t UInt32
const CUBLAS_ATOMICS_NOT_ALLOWED = 0
const CUBLAS_ATOMICS_ALLOWED = 1
# end enum cublasAtomicsMode_t
typealias cublasContext Void
typealias cublasHandle_t Ptr{cublasContext}
typealias cudaStream Void
typealias cudaStream_t Ptr{cudaStream}
# complex numbers in cuda
typealias cuComplex Complex{Float32}
typealias cuDoubleComplex Complex{Float64}
# complex types from Base/linalg.jl
typealias CublasFloat Union{Float64,Float32,Complex128,Complex64}
typealias CublasReal Union{Float64,Float32}
typealias CublasComplex Union{Complex128,Complex64}


"""
Widgets are renderable elements which capture window events.
They are immutable, and should be re-created in a real-time loop.

A widget subtype should always extend the following methods:
- `GeometryExperiments.MeshVertexEncoding`
- `Base.in`
- `zindex`

If `Base.in` is not extended, then either of `GeometryExperiments.boundingelement` and `GeometryExperiments.PointSet` must be extended.

To allow the specification of callbacks, it may extend `callbacks`.
"""
abstract type Widget end

not_implemented_for(x) = error("Not implemented for $(typeof(x))")

GeometryExperiments.MeshVertexEncoding(w::Widget) = not_implemented_for(w)

"""
Test whether the point `p` is inside the widget `w`.
"""
Base.in(p::Point, w::Widget) = p in boundingelement(w)

GeometryExperiments.boundingelement(w::Widget) = boundingelement(PointSet(w))

GeometryExperiments.PointSet(w::Widget) = PointSet(boundingelement(w))

"""
Z-index of the widget. Used to determine whether a widget should be rendered and capture events.
Defaults to zero.
"""
zindex(w::Widget) = 0.


include("material.jl")

struct Particle
    A::Int  # Mass Number
    Z::Int  # Element number
    material::Material  # Target material
end

function stoppingpower(p::Particle, E)
    Erel = E + p.mev
end


#
# Wrapper for the paralel/structured PIPS-NLP interface
#

module PipsNlpSolver

using StructJuMPSolverInterface
import MPI

try
  sharedLib=ENV["PIPS_NLP_PAR_SHARED_LIB"]

  # Explicitly check if the file exists. dlopen sometimes does not throw an
  # error for invalid filenames, resulting in a seg fault)
  if(!isfile(sharedLib))
    error(string("The specified shared library ([", sharedLib, "]) does not exist"))
  end
  global const libparpipsnlp=Libdl.dlopen(get(ENV,"PIPS_NLP_PAR_SHARED_LIB",""))
catch
  warn("Could not load PIPS-NLP shared library. Make sure the ENV variable 'PIPS_NLP_PAR_SHARED_LIB' points to its location, usually in the PIPS repo at PIPS/build_pips/PIPS-NLP/libparpipsnlp.so")
  rethrow()
end

#######################
type FakeModel <: ModelInterface
    sense::Symbol
    status::Int
    nscen::Int
    rowmap::Dict{Int,Int}
    colmap::Dict{Int,Int}
    eq_rowmap::Dict{Int,Int}
    inq_rowmap::Dict{Int,Int}

    get_num_scen::Function
    get_sense::Function

    get_status::Function
    get_num_rows::Function
    get_num_cols::Function
    get_num_eq_cons::Function
    get_num_ineq_cons::Function

    set_status::Function
    set_num_rows::Function
    set_num_cols::Function
    set_num_eq_cons::Function
    set_num_ineq_cons::Function

    str_init_x0::Function
    str_prob_info::Function
    str_eval_f::Function
    str_eval_g::Function
    str_eval_grad_f::Function
    str_eval_jac_g::Function
    str_eval_h::Function
    str_write_solution::Function

    function FakeModel(sense::Symbol,status::Int,nscen::Int, str_init_x0, str_prob_info, str_eval_f, str_eval_g,str_eval_grad_f,str_eval_jac_g, str_eval_h)
        instance = new(sense,status,nscen,Dict{Int,Int}(),Dict{Int,Int}(),Dict{Int,Int}(),Dict{Int,Int}())
        instance.str_init_x0 = str_init_x0
        instance.str_prob_info = str_prob_info
        instance.str_eval_f = str_eval_f
        instance.str_eval_g = str_eval_g
        instance.str_eval_grad_f = str_eval_grad_f
        instance.str_eval_jac_g = str_eval_jac_g
        instance.str_eval_h = str_eval_h

        instance.str_write_solution = function(id::Integer, x::Vector{Float64}, y_eq::Vector{Float64}, y_ieq::Vector{Float64})
            @show id
            @show x, y_eq, y_ieq
        end

        instance.get_num_scen = function()
            return instance.nscen
        end
        instance.get_sense = function()
            return instance.sense
        end
        instance.get_status = function()
            return instance.status
        end
        instance.get_num_rows = function(id::Integer)
            return instance.rowmap[id]
        end
        instance.get_num_cols = function(id::Integer)
            return instance.colmap[id]
        end
        instance.get_num_eq_cons = function(id::Integer)
            return instance.eq_rowmap[id]
        end
        instance.get_num_ineq_cons = function(id::Integer)
            return instance.inq_rowmap[id]
        end
        instance.set_status = function(s::Integer)
            instance.status = s
        end
        instance.set_num_rows = function(id::Integer, v::Integer)
            return instance.rowmap[id] = v
        end
        instance.set_num_cols = function(id::Integer, v::Integer)
            return instance.colmap[id] = v
        end
        instance.set_num_eq_cons = function(id::Integer, v::Integer)
            return instance.eq_rowmap[id] = v
        end
        instance.set_num_ineq_cons = function(id::Integer, v::Integer)
            return instance.inq_rowmap[id] = v
        end
        return instance
    end
end

#######################

type PipsNlpProblemStruct
    ref::Ptr{Void}
    model::ModelInterface
    comm::MPI.Comm
    prof::Bool

    n_iter::Int
    t_jl_init_x0::Float64
    t_jl_str_prob_info::Float64
    t_jl_eval_f::Float64
    t_jl_eval_g::Float64
    t_jl_eval_grad_f::Float64

    t_jl_eval_jac_g::Float64
    t_jl_str_eval_jac_g::Float64
    t_jl_eval_h::Float64
    t_jl_str_eval_h::Float64
    t_jl_write_solution::Float64

    t_jl_str_total::Float64
    t_jl_eval_total::Float64

    function PipsNlpProblemStruct(comm, model, prof)
        prob = new(C_NULL, model, comm, prof,-3
            ,0.0,0.0,0.0,0.0,0.0
            ,0.0,0.0,0.0,0.0,0.0
            ,0.0,0.0
            )
        finalizer(prob, freeProblemStruct)

        return prob
    end
end

immutable CallBackData
	prob::Ptr{Void}
	row_node_id::Cint
    col_node_id::Cint
    flag::Cint
end

export  ModelInterface, FakeModel,
        createProblemStruct, solveProblemStruct

###########################################################################
# Callback wrappers
###########################################################################

function str_init_x0_wrapper(x0_ptr::Ptr{Float64}, cbd::Ptr{CallBackData})
	# @show " julia - str_init_x0_wrapper "
    # @show cbd
    data = unsafe_load(cbd)
    # @show data
    userdata = data.prob
    prob = unsafe_pointer_to_objref(userdata)::PipsNlpProblemStruct
    # @show prob
    # data = unsafe_pointer_to_objref(cbd)::CallBackData
    # out = Array(Ptr{CallBackData},1)
    rowid = Int(Int(data.row_node_id))
    colid = Int(Int(data.col_node_id))
    assert(rowid == colid)
    n0 = prob.model.get_num_cols(colid)
    x0 = unsafe_wrap(Array, x0_ptr, n0)

    @timing prob.prof tic()
    prob.model.str_init_x0(colid,x0)
    @timing prob.prof prob.t_jl_init_x0 += toq()

    return Int32(1)
end

# prob info (prob_info)
function str_prob_info_wrapper(n_ptr::Ptr{Cint}, col_lb_ptr::Ptr{Float64}, col_ub_ptr::Ptr{Float64}, m_ptr::Ptr{Cint}, row_lb_ptr::Ptr{Float64}, row_ub_ptr::Ptr{Float64}, cbd::Ptr{CallBackData})
    # @show " julia - str_prob_info_wrapper "
    # @show cbd
    data = unsafe_load(cbd)
    # @show data
    userdata = data.prob
    prob = unsafe_pointer_to_objref(userdata)::PipsNlpProblemStruct
    # @show prob
    # data = unsafe_pointer_to_objref(cbd)::CallBackData
    # out = Array(Ptr{CallBackData},1)
    rowid = Int(Int(data.row_node_id))
    colid = Int(data.col_node_id)
    flag = Int(data.flag)
    assert(rowid == colid)

	mode = (col_lb_ptr == C_NULL) ? (:Structure) : (:Values)
    # @show flag
    if flag == 0
        # @show mode
    	if(mode==:Structure)
            col_lb = unsafe_wrap(Array,col_lb_ptr,0)
    		col_ub = unsafe_wrap(Array,col_ub_ptr,0)
    		row_lb = unsafe_wrap(Array,row_lb_ptr,0)
    		row_ub = unsafe_wrap(Array,row_ub_ptr,0)
            @timing prob.prof tic()
    		(n,m) = prob.model.str_prob_info(colid,flag,mode,col_lb,col_ub,row_lb,row_ub)
            @timing prob.prof prob.t_jl_str_prob_info += toq()

    		unsafe_store!(n_ptr,convert(Cint,n)::Cint)
    		unsafe_store!(m_ptr,convert(Cint,m)::Cint)
            # @show typeof(colid), typeof(m)
    		prob.model.set_num_rows(colid, m)
    		prob.model.set_num_cols(colid, n)
    	else
    		n = unsafe_load(n_ptr)
    		m = unsafe_load(m_ptr)
    		col_lb = unsafe_wrap(Array,col_lb_ptr,n)
    		col_ub = unsafe_wrap(Array,col_ub_ptr,n)
    		row_lb = unsafe_wrap(Array,row_lb_ptr,m)
    		row_ub = unsafe_wrap(Array,row_ub_ptr,m)

            @timing prob.prof tic()
    		prob.model.str_prob_info(colid,flag,mode,col_lb,col_ub,row_lb,row_ub)
            @timing prob.prof prob.t_jl_str_prob_info += toq()

    		neq = 0
    		nineq = 0
    		for i = 1:length(row_lb)
    			if row_lb[i] == row_ub[i]
    				neq += 1
    			else
    				nineq += 1
    			end
    		end
    		assert(neq+nineq == length(row_lb) == m)
    		prob.model.set_num_eq_cons(colid,neq)
    		prob.model.set_num_ineq_cons(colid,nineq)
    	end
    else
        @assert flag ==1
        if mode == :Structure
            col_lb = unsafe_wrap(Array,col_lb_ptr,0)
            col_ub = unsafe_wrap(Array,col_ub_ptr,0)
            row_lb = unsafe_wrap(Array,row_lb_ptr,0)
            row_ub = unsafe_wrap(Array,row_ub_ptr,0)
            (n,m) = prob.model.str_prob_info(colid,flag,mode,col_lb,col_ub,row_lb,row_ub)
            # @show n,m
            unsafe_store!(m_ptr,convert(Cint,m)::Cint)
        else
            n = unsafe_load(n_ptr)
            m = unsafe_load(m_ptr)
            @assert m==0
        end
    end
	return Int32(1)
end
# Objective (eval_f)
function str_eval_f_wrapper(x0_ptr::Ptr{Float64}, x1_ptr::Ptr{Float64}, obj_ptr::Ptr{Float64}, cbd::Ptr{CallBackData})
    # @show " julia - eval_f_wrapper "
    data = unsafe_load(cbd)
    # @show data
    # @show data
    userdata = data.prob
    prob = unsafe_pointer_to_objref(userdata)::PipsNlpProblemStruct
    rowid = Int(Int(data.row_node_id))
    colid = Int(data.col_node_id)
    assert(rowid == colid)
    n0 = prob.model.get_num_cols(0)
    n1 = prob.model.get_num_cols(colid)
    # Calculate the new objective
    x0 = unsafe_wrap(Array, x0_ptr, n0)
    x1 = unsafe_wrap(Array, x1_ptr, n1)

    @timing prob.prof tic()
    new_obj = convert(Float64, prob.model.str_eval_f(colid,x0,x1))::Float64
    @timing prob.prof prob.t_jl_eval_f += toq()

    # Fill out the pointer
    unsafe_store!(obj_ptr, new_obj)
    # Done
    return Int32(1)
end

# Constraints (eval_g)
function str_eval_g_wrapper(x0_ptr::Ptr{Float64}, x1_ptr::Ptr{Float64}, eq_g_ptr::Ptr{Float64}, inq_g_ptr::Ptr{Float64}, cbd::Ptr{CallBackData})
    # @show " julia - eval_g_wrapper "
    data = unsafe_load(cbd)
    # @show data
    userdata = data.prob
    prob = unsafe_pointer_to_objref(userdata)::PipsNlpProblemStruct
    rowid = Int(data.row_node_id)
    colid = Int(data.col_node_id)
    assert(rowid == colid)
    n0 = prob.model.get_num_cols(0)
    n1 = prob.model.get_num_cols(colid)
    x0 = unsafe_wrap(Array, x0_ptr, n0)
    x1 = unsafe_wrap(Array, x1_ptr, n1)
    # Calculate the new constraint values
    neq = prob.model.get_num_eq_cons(rowid)
    nineq = prob.model.get_num_ineq_cons(rowid)
    new_eq_g = unsafe_wrap(Array, eq_g_ptr, neq)
    new_inq_g = unsafe_wrap(Array, inq_g_ptr, nineq)

    @timing prob.prof tic()
    prob.model.str_eval_g(colid,x0,x1,new_eq_g,new_inq_g)
    @timing prob.prof prob.t_jl_eval_g += toq()

    # Done
    return Int32(1)
end

# Objective gradient (eval_grad_f)
function str_eval_grad_f_wrapper(x0_ptr::Ptr{Float64}, x1_ptr::Ptr{Float64}, grad_f_ptr::Ptr{Float64}, cbd::Ptr{CallBackData})
    # @show " julia -  eval_grad_f_wrapper "
    # Extract Julia the problem from the pointer
    # @show cbd
    data = unsafe_load(cbd)
    # @show data
    userdata = data.prob
    prob = unsafe_pointer_to_objref(userdata)::PipsNlpProblemStruct
    rowid = Int(data.row_node_id)
    colid = Int(data.col_node_id)
    n0 = prob.model.get_num_cols(0)
    n1 = prob.model.get_num_cols(rowid)
    # @show n0,n1
    x0 = unsafe_wrap(Array, x0_ptr, n0)
    x1 = unsafe_wrap(Array, x1_ptr, n1)
    # Calculate the gradient
    grad_len = prob.model.get_num_cols(colid)
    new_grad_f = unsafe_wrap(Array, grad_f_ptr, grad_len)

    @timing prob.prof tic()
    prob.model.str_eval_grad_f(rowid,colid,x0,x1,new_grad_f)
    @timing prob.prof prob.t_jl_eval_grad_f += toq()

    if prob.model.get_sense() == :Max
        new_grad_f *= -1.0
    end
    # Done
    return Int32(1)
end

# Jacobian (eval_jac_g)
function str_eval_jac_g_wrapper(x0_ptr::Ptr{Float64}, x1_ptr::Ptr{Float64},
	e_nz_ptr::Ptr{Cint}, e_values_ptr::Ptr{Float64}, e_row_ptr::Ptr{Cint}, e_col_ptr::Ptr{Cint},
	i_nz_ptr::Ptr{Cint}, i_values_ptr::Ptr{Float64}, i_row_ptr::Ptr{Cint}, i_col_ptr::Ptr{Cint},
	cbd::Ptr{CallBackData}
	)
    # @show " julia -  eval_jac_g_wrapper "
    # Extract Julia the problem from the pointer
    data = unsafe_load(cbd)
    # @show data
    userdata = data.prob
    prob = unsafe_pointer_to_objref(userdata)::PipsNlpProblemStruct
    rowid = Int(Int(data.row_node_id))
    colid = Int(Int(data.col_node_id))
    flag = Int(data.flag)
    n0 = prob.model.get_num_cols(0)
    n1 = prob.model.get_num_cols(rowid) #we can do this because of 2-level and no linking constraint
    # @show n0, n1
    x0 = unsafe_wrap(Array, x0_ptr, n0)
    x1 = unsafe_wrap(Array, x1_ptr, n1)
    # @show x0
    # @show x1
    nrow = prob.model.get_num_rows(rowid)
    ncol = prob.model.get_num_cols(colid)
    #@show prob
    # Determine mode
    mode = (e_col_ptr == C_NULL && i_col_ptr == C_NULL) ? (:Structure) : (:Values)
    if flag != 1
        if(mode == :Structure)
        	e_values = unsafe_wrap(Array,e_values_ptr,0)
    		e_colptr = unsafe_wrap(Array,e_col_ptr,0)
    		e_rowidx = unsafe_wrap(Array,e_row_ptr,0)
    		i_values = unsafe_wrap(Array,i_values_ptr,0)
    		i_colptr = unsafe_wrap(Array,i_col_ptr,0)
    		i_rowidx = unsafe_wrap(Array,i_row_ptr,0)

            @timing prob.prof tic()
            (e_nz,i_nz) = prob.model.str_eval_jac_g(rowid,colid,flag,x0,x1,mode,e_rowidx,e_colptr,e_values,i_rowidx,i_colptr,i_values)
            @timing prob.prof prob.t_jl_str_eval_jac_g += toq()

            unsafe_store!(e_nz_ptr,convert(Cint,e_nz)::Cint)
    		unsafe_store!(i_nz_ptr,convert(Cint,i_nz)::Cint)
    		# @show "structure - ",(e_nz,i_nz)
        else
        	e_nz = unsafe_load(e_nz_ptr)
        	e_values = unsafe_wrap(Array,e_values_ptr,e_nz)
        	e_rowidx = unsafe_wrap(Array,e_row_ptr,e_nz)
        	e_colptr = unsafe_wrap(Array,e_col_ptr,ncol+1)
        	i_nz = unsafe_load(i_nz_ptr)
        	# @show "values - ",(e_nz,i_nz), ncol
        	i_values = unsafe_wrap(Array,i_values_ptr,i_nz)
        	i_rowidx = unsafe_wrap(Array,i_row_ptr,i_nz)
        	i_colptr = unsafe_wrap(Array,i_col_ptr,ncol+1)
            # @show x0
            # @show x1
            @timing prob.prof tic()
        	prob.model.str_eval_jac_g(rowid,colid,flag,x0,x1,mode,e_rowidx,e_colptr,e_values,i_rowidx,i_colptr,i_values)
            @timing prob.prof prob.t_jl_eval_jac_g += toq()
        end
    else
        @assert flag == 1
        if mode == :Structure
            e_nz = 0
            i_nz = 0
            unsafe_store!(e_nz_ptr,convert(Cint,e_nz)::Cint)
            unsafe_store!(i_nz_ptr,convert(Cint,i_nz)::Cint)
        else
            e_nz = unsafe_load(e_nz_ptr)
            i_nz = unsafe_load(i_nz_ptr)
            @assert e_nz == i_nz == 0
        end
    end
    # Done
    return Int32(1)
end

# Hessian
function str_eval_h_wrapper(x0_ptr::Ptr{Float64}, x1_ptr::Ptr{Float64}, lambda_ptr::Ptr{Float64}, nz_ptr::Ptr{Cint}, values_ptr::Ptr{Float64}, row_ptr::Ptr{Cint}, col_ptr::Ptr{Cint}, cbd::Ptr{CallBackData})
    # @show " julia - eval_h_wrapper "
    # Extract Julia the problem from the pointer
    data = unsafe_load(cbd)
    # @show data
    userdata = data.prob
    prob = unsafe_pointer_to_objref(userdata)::PipsNlpProblemStruct
    rowid = Int(data.row_node_id)
    colid = Int(data.col_node_id)
    flag = Int(data.flag)
    # @message @sprintf(" julia - eval_h_wrapper - %d %d %d", rowid, colid, flag)
    @timing prob.prof begin
        if rowid == colid ==0
            prob.n_iter += 1
            if prob.n_iter == 0
                prob.t_jl_str_total = t_reset(prob)
            end
        end
    end
    # @show prob.n_iter

    high = max(rowid,colid)
    low  = min(rowid,colid)
    n0 = prob.model.get_num_cols(0)
    n1 = prob.model.get_num_cols(high)
    x0 = unsafe_wrap(Array,x0_ptr,n0)
    x1 = unsafe_wrap(Array,x1_ptr,n1)
    # @show x0
    # @show x1
    ncol = prob.model.get_num_cols(low)
    g0 = prob.model.get_num_rows(high)
    # @show g0
    # @show ncol
    lambda = unsafe_wrap(Array,lambda_ptr,g0)
    obj_factor = 1.0
    if prob.model.get_sense() == :Max
        obj_factor *= -1.0
    end
    # Did the user specify a Hessian
    mode = (col_ptr == C_NULL) ? (:Structure) : (:Values)
    if(mode == :Structure)
    	values = unsafe_wrap(Array,values_ptr,0)
		colptr = unsafe_wrap(Array,col_ptr,0)
		rowidx = unsafe_wrap(Array,row_ptr,0)

        @timing prob.prof tic()
		nz = prob.model.str_eval_h(rowid,colid,flag, x0,x1,obj_factor,lambda,mode,rowidx,colptr,values)
        @timing prob.prof prob.t_jl_str_eval_h += toq()

		unsafe_store!(nz_ptr,convert(Cint,nz)::Cint)
		# @show "structure - ", nz
    else
    	nz = unsafe_load(nz_ptr)
    	values = unsafe_wrap(Array, values_ptr, nz)
    	rowidx = unsafe_wrap(Array, row_ptr, nz)
    	colptr = unsafe_wrap(Array, col_ptr, ncol+1)
    	# @show "value - ", nz
        @timing prob.prof tic()
    	prob.model.str_eval_h(rowid,colid,flag,x0,x1,obj_factor,lambda,mode,rowidx,colptr,values)
        @timing prob.prof prob.t_jl_eval_h += toq()
    end
    # Done

    return Int32(1)
end

#write solution
function str_write_solution_wrapper(x_ptr::Ptr{Float64}, y_eq_ptr::Ptr{Float64}, y_ieq_ptr::Ptr{Float64}, cbd::Ptr{CallBackData})
    data = unsafe_load(cbd)
    # @show data
    userdata = data.prob
    prob = unsafe_pointer_to_objref(userdata)::PipsNlpProblemStruct
    rowid = Int(data.row_node_id)
    colid = Int(data.col_node_id)
    @assert rowid == colid

    nx = prob.model.get_num_cols(rowid)
    neq = prob.model.get_num_eq_cons(rowid)
    nieq = prob.model.get_num_ineq_cons(rowid)
    x = unsafe_wrap(Array,x_ptr,nx)
    y_eq = unsafe_wrap(Array,y_eq_ptr,neq)
    y_ieq = unsafe_wrap(Array,y_ieq_ptr,nieq)
    @timing prob.prof tic()
    prob.model.str_write_solution(rowid,x,y_eq,y_ieq)
    @timing prob.prof prob.t_jl_write_solution += toq()

    return Int32(1)
end
###########################################################################
# C function wrappers
###########################################################################

function createProblemStruct(comm::MPI.Comm, model::ModelInterface, prof::Bool)
	# println(" createProblemStruct  -- julia")
	str_init_x0_cb = cfunction(str_init_x0_wrapper, Cint, (Ptr{Float64}, Ptr{CallBackData}) )
    str_prob_info_cb = cfunction(str_prob_info_wrapper, Cint, (Ptr{Cint}, Ptr{Float64}, Ptr{Float64}, Ptr{Cint}, Ptr{Float64}, Ptr{Float64}, Ptr{CallBackData}) )
    str_eval_f_cb = cfunction(str_eval_f_wrapper,Cint, (Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{CallBackData}) )
    str_eval_g_cb = cfunction(str_eval_g_wrapper,Cint, (Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{CallBackData}) )
    str_eval_grad_f_cb = cfunction(str_eval_grad_f_wrapper, Cint, (Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{CallBackData}) )
    str_eval_jac_g_cb = cfunction(str_eval_jac_g_wrapper, Cint, (Ptr{Float64}, Ptr{Float64},
    	Ptr{Cint}, Ptr{Float64}, Ptr{Cint}, Ptr{Cint},
    	Ptr{Cint}, Ptr{Float64}, Ptr{Cint}, Ptr{Cint},
    	Ptr{CallBackData}))
    str_eval_h_cb = cfunction(str_eval_h_wrapper, Cint, (Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{Cint}, Ptr{Float64}, Ptr{Cint}, Ptr{Cint}, Ptr{CallBackData}))
    str_write_solution_cb = cfunction(str_write_solution_wrapper, Cint, (Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{CallBackData}))

    # println(" callback created ")
    prob = PipsNlpProblemStruct(comm, model, prof)
    # @show prob
    ret = ccall(Libdl.dlsym(libparpipsnlp,:CreatePipsNlpProblemStruct),Ptr{Void},
            (MPI.Comm,
            Cint, Ptr{Void}, Ptr{Void},
	    Ptr{Void}, Ptr{Void}, Ptr{Void},
	    Ptr{Void}, Ptr{Void}, Ptr{Void},Any
            ),
            comm,
            model.get_num_scen(),
            str_init_x0_cb,
            str_prob_info_cb,
            str_eval_f_cb,
            str_eval_g_cb,
            str_eval_grad_f_cb,
            str_eval_jac_g_cb,
            str_eval_h_cb,
            str_write_solution_cb,
            prob
            )
    # println(" ccall CreatePipsNlpProblemStruct done ")
    # @show ret

    if ret == C_NULL
        error("PIPS-NLP: Failed to construct problem.")
    else
        prob.ref = ret
    end
    # @show prob
    # println("end createProblemStruct - julia")
    return prob
end

function solveProblemStruct(prob::PipsNlpProblemStruct)
    # println("solveProblemStruct - julia")
    # @show prob

    ret = ccall(Libdl.dlsym(libparpipsnlp,:PipsNlpSolveStruct), Cint,
            (Ptr{Void},),
            prob.ref)
    # @show ret
    prob.model.set_status(Int(ret))

    prob.t_jl_eval_total = report_total_now(prob)
    # @show prob
    return prob.model.get_status()
end

function freeProblemStruct(prob::PipsNlpProblemStruct)
    # @show "freeProblemStruct"
    ret = ccall(Libdl.dlsym(libparpipsnlp,:FreePipsNlpProblemStruct),
            Void, (Ptr{Void},),
            prob.ref)
    # @show ret
    return ret
end

function report_total_now(prob::PipsNlpProblemStruct)
    total = 0.0
    total += prob.t_jl_init_x0
    total += prob.t_jl_str_prob_info
    total += prob.t_jl_eval_f
    total += prob.t_jl_eval_g
    total += prob.t_jl_eval_grad_f
    total += prob.t_jl_eval_jac_g
    total += prob.t_jl_str_eval_jac_g
    total += prob.t_jl_eval_h
    total += prob.t_jl_str_eval_h
    total += prob.t_jl_write_solution
    return total
end

function t_reset(prob::PipsNlpProblemStruct)
    total = report_total_now(prob)
    prob.t_jl_init_x0  = 0.0
    prob.t_jl_str_prob_info  = 0.0
    prob.t_jl_eval_f = 0.0
    prob.t_jl_eval_g = 0.0
    prob.t_jl_eval_grad_f = 0.0
    prob.t_jl_eval_jac_g = 0.0
    prob.t_jl_str_eval_jac_g = 0.0
    prob.t_jl_eval_h = 0.0
    prob.t_jl_str_eval_h = 0.0
    prob.t_jl_write_solution = 0.0
    return total
end

end


"""
Device has two channels: 1 & 2
# Supported functions
- `initialize()`
- `terminate()`
- `enable_output()`
- `disable_output()`
- `get_output()`
- `get_frequency()`
- `set_frequency()`
- `get_amplitude()`
- `set_amplitude()`
- `get_burst_num_cycles()`
- `set_burst_num_cycles()`
- `get_time_offset()`: Not implemented
- `set_time_offset()`: Not implemented
- `get_voltage_offset()`
- `set_voltage_offset()`
- `get_burst_period()`
- `set_burst_period()`
- `get_mode()`
- `set_mode_burst(; mode=:trigger, trig_src=:timer)`
- `set_mode_cw()`

Creating a Sin Wave Example:
```
wave = initialize(Keysight33612A, "10.1.30.36")
set_mode_cw(wave) # Set to continuous waveform mode
set_function(wave, "SIN")
set_frequency(wave, "1e3") # Frequency val can be string or number
set_amplitude(wave, 0.1)
set_voltage_offset(wave, 0)
enable_output(wave) # Starts wave
```
"""
struct Keysight33612A <: WaveformGenerator end

enable_output(obj::Instr{Keysight33612A}; chan=1)  = write(obj, "OUTPUT$chan ON")
disable_output(obj::Instr{Keysight33612A}; chan=1) = write(obj, "OUTPUT$chan OFF")
get_output(obj::Instr{Keysight33612A}; chan=1)     = write(obj, "OUTPUT$chan?")

get_voltage_offset(obj::Instr{Keysight33612A}; chan=1) =
    f_query(obj, "SOURCE$chan:VOLTAGE:OFFSET?") * V
set_voltage_offset(obj::Instr{Keysight33612A}, num::Voltage; chan=1) =
    write(obj, "SOURCE$chan:VOLTAGE:OFFSET $(raw(num))")

get_amplitude(obj::Instr{Keysight33612A}; chan=1) =
    f_query(obj, "SOURCE$chan:VOLTAGE?") * V
set_amplitude(obj::Instr{Keysight33612A}, num::Voltage; chan=1) =
    write(obj, "SOURCE$chan:VOLTAGE $(raw(num))")

get_frequency(obj::Instr{Keysight33612A}; chan=1) =
    f_query(obj, "SOURCE$chan:FREQUENCY?") # +4.0E+05
set_frequency(obj::Instr{Keysight33612A}, num::Frequency; chan=1) =
    write(obj, "SOURCE$chan:FREQUENCY $(raw(num)))") # +4.0E+05

"""
    get_function(instr)
    get_function(instr; chan=2)

# Keywords
- `chan`: Specify channel: Default is 1
# Returns
- `String`: Will return one of these shortened forms:
{SINusoid|SQUare|TRIangle|RAMP|PULSe|PRBS|NOISe|ARB|DC}
"""
get_function(obj::Instr{Keysight33612A}; chan=1) = query(obj, "SOURCE$chan:FUNCTION?") # +4.0E+05

"""
set_function(instr, func; chan=1)

# Arguments
- `func::String`: Acceptable inputs:
{SINusoid|SQUare|TRIangle|RAMP|PULSe|PRBS|NOISe|ARB|DC}

# Keywords
- `chan`: Specify channel: Default is 1
"""
set_function(obj::Instr{Keysight33612A}, func; chan=1) = write(obj, "SOURCE$chan:FUNCTION $func") # +4.0E+05

"""
    Sets the burst mode of a device to Triggered Mode

"""
set_burst_mode_trigger(obj::Instr{Keysight33612A}; chan=1) = write(obj, "SOURCE$chan:BURST:MODE TRIG")
"""
Sets the burst mode of a device to Gated Mode
"""
set_burst_mode_gated(obj::Instr{Keysight33612A}; chan=1) = write(obj, "SOURCE$chan:BURST:MODE GATED")

"""
    get_burst_mode(instr)
    get_burst_mode(instr; chan=1)

Returns the burst mode of a device:
    "TRIG" ~ If the device is in Triggered Mode
    "GAT" ~ If the device is in Gated Mode
"""
get_burst_mode(obj::Instr{Keysight33612A}; chan=1) = query(obj, "SOURCE$chan:BURST:MODE?")

"""
```
    set_mode_burst(instr)
```
Changes Waveform Generator mode from continous waveform to burst

By default it sets the type of burst mode to triggered mode. Gated
mode can also be set by using the optional flag:

```
    set_mode_burst(instr; mode=:gated)
```

The optional trig_src flag sets the trigger source for burst
triggered mode. Right now the default is Timer. To implement
more trigger sources see page 130 of the manual for 33612A

"""
function set_mode_burst(obj::Instr{Keysight33612A};
                         chan=1,
                         mode=:trigger,
                         trig_src=:timer)
    if mode == :trigger
        set_burst_mode_trigger(obj)
    elseif mode == :gated
        set_burst_mode_gated(obj)
    else
        error("""mode flag cannot be set to: $mode\n
              available modes are [:trigger, :gated]""")
    end
    if trig_src == :timer
        set_trigger_source_timer(obj; chan=chan)
    else
        error("""trig_src flag cannot be set to: $trig_src\n
              available modes are [:timer]""")
    end
    write(obj, "SOURCE$chan:BURST:STATE ON")
end

"""
set_mode_cw(instr)
set_mode_cw(instr; chan=2)

Puts the device in continuous waveform/turns off burst mode

# Keywords
- `chan`: Specify channel: Default is 1
"""
set_mode_cw(obj::Instr{Keysight33612A}; chan=1) = write(obj, "SOURCE$chan:BURST:STATE OFF")

"""
    get_mode(instr)
    get_mode(instr; chan=2)

# Keywords
- `chan`: Specify channel: Default is 1
Returns:
    "CW" ~ if device is in continous wavefrom mode
    "BURST" ~ if device is in BURST mode
"""
get_mode(obj::Instr{Keysight33612A}; chan=1) = query(obj, "SOURCE$chan:BURST:STATE?") == "1" ? "BURST" : "CW"

"""
    get_burst_num_of_cycles(instr)
    get_burst_num_of_cycles(instr; chan=2)

# Keywords
- `chan`: Specify channel: Default is 1

# Returns
- `Float64`: number of cycles burst mode is set to
"""
get_burst_num_cycles(obj::Instr{Keysight33612A}; chan=1) = f_query(obj, "SOURCE$chan:BURST:NCYCLES?")

"""
    set_burst_num_of_cycles(instr, cycles)
    set_burst_num_of_cycles(instr, cycles; chan=2)

Sets the number of cycles for burst mode

# Arguments
- `cycles`
# Keywords
- `chan`: Specify channel: Default is 1
"""
set_burst_num_cycles(obj::Instr{Keysight33612A}, num; chan=1) = write(obj, "SOURCE$chan:BURST:NCYCLES $(Float64(num))")

"""
    set_burst_period(obj, num; chan=1)

# Arguments
- `num`: The number of seconds (This value can also be "MIN" or "MAX")
# Keywords
- `chan`: Specify channel: Default is 1
"""
set_burst_period(obj, num; chan::Integer=1) = write(obj, "SOURCE$chan:BURST:INTERNAL:PERIOD $(Float64(num))")

"""
    get_burst_period(instr)
    get_burst_period(instr; chan=2)

# Keywords
- `chan`: Specify channel: Default is 1
# Returns
- `Float64`: number of cycles burst mode is set to
"""
get_burst_period(obj; chan::Integer=1) = f_query(obj, "SOURCE$chan:BURST:INTERNAL:PERIOD?")

set_trigger_source_timer(obj; chan::Integer=1) = write(obj, "TRIGGER$chan:SOURCE TIMER")

status(obj::Instr{Keysight33612A}) = query(obj, "APPLY?")


module PowerModelsPrivacyPreserving
import PowerModels
import PowerModelsDistribution
import InfrastructureModels
import JuMP
import Random
import Distributions

const _PM = PowerModels
const _PMD = PowerModelsDistribution
const _IM = InfrastructureModels

"Work around bug in PowerModels by short-circuiting export_extra_data"
function _PM._export_extra_data(io::IO, data::Dict{String,<:Any}, component, excluded_fields=Set(["index", "source_id"]); postfix="")
    #do nothing
end

include("core/data.jl")
include("core/variable.jl")
include("core/constraint.jl")
include("core/constraint_template.jl")
include("core/objective.jl")

include("prob/opf.jl")
include("prob/opf_bf.jl")
include("prob/opf_bf_mc.jl")
include("prob/opf_impedance.jl")

include("core/export.jl")

end # module


#######################################################################
# SystemInfo
#######################################################################

struct SystemInfo{Tv,S}
    sampledterms::S
    namesdict::Dict{NameType,Int}
    names::Vector{NameType}
    nsites::Vector{Int}
    norbitals::Vector{Int}
    dims::Vector{Int}       # Hamiltonian block dimensions for each sublattice
    offsets::Vector{Int}    # Hamiltonian block offset for each sublattice
end

SystemInfo{Tv}(terms::S, args...) where {Tv,S} = SystemInfo{Tv,S}(terms, args...)

function SystemInfo(lat::Lattice{E,L,T}, model::Model{Tv}, prevsamples...) where {E,L,T,Tv}
    ns = length(lat.sublats)
    nsites = Vector{Int}(undef, ns)
    namesdict = Dict{NameType,Int}()
    names = Vector{NameType}(undef, ns)
    zeropos = zero(SVector{E,T})
    for (i, sublat) in enumerate(lat.sublats)
        nsites[i] = length(sublat.sites)
        namesdict[sublat.name] = i
        names[i] = sublat.name
    end
    sampledterms = getsamples(namesdict, zeropos, model.terms...)
    allterms = (prevsamples..., sampledterms...)
    norbitals = zeros(Int, ns)
    _fillorbitals!(norbitals, allterms...)
    dims = zeros(Int, ns)
    offsets = zeros(Int, ns + 1)
    for i in eachindex(lat.sublats)
        dims[i] = norbitals[i] * nsites[i]
        offsets[i + 1] = offsets[i] + dims[i]
    end
    return SystemInfo{Tv}(allterms, namesdict, names, nsites, norbitals, dims, offsets)
end

getsamples(namesdict, zeropos) = ()
getsamples(namesdict, zeropos, term, terms...) =
    (_getsamples(namesdict, zeropos, term), getsamples(namesdict, zeropos, terms...)...)

_getsamples(namesdict, zeropos, term) =
    (term, term(zeropos, zeropos), _getsublats(namesdict, term.sublats))

_getsublats(namesdict, s::Missing) = s
_getsublats(namesdict, s::NTuple{N,Any}) where {N} =
    ntuple(i -> (sublatindex(namesdict, first(s[i])),
                 sublatindex(namesdict, last(s[i]))), Val(N))

_fillorbitals!(norbitals) = nothing

function _fillorbitals!(norbitals, sample::Tuple, samples...)
    _fillorbitals!(norbitals, sample...)
    _fillorbitals!(norbitals, samples...)
end

function _fillorbitals!(norbitals, term, ::SMatrix{N,M}, ::Missing) where {N,M}
    N == M ? fill!(norbitals, N) : throw(DimensionMismatch("Inconsitent model orbital dimensions"))
    return nothing
end

_fillorbitals!(norbitals, term, sm::SMatrix, ss::NTuple{N,Tuple{Int,Int}}) where {N} =
    foreach(s -> _fillorbitals!(norbitals, sm, s), ss)
_fillorbitals!(norbitals, ::SMatrix{N,M}, (s1, s2)::Tuple{Int,Int}) where {N,M} =
    (_fillorbitals!(norbitals, Val(N), s1); _fillorbitals!(norbitals, Val(M), s2))

function _fillorbitals!(norbitals, ::Val{N}, s) where {N}
    0 < s <= length(norbitals) || return nothing
    if norbitals[s] == 0
        norbitals[s] = N
    elseif norbitals[s] != N
        throw(DimensionMismatch("Inconsitent model orbital dimensions"))
    end
    return nothing
end

function tosite(row, sysinfo)
    s = findsublat(row, sysinfo.offsets)
    offset = sysinfo.offsets[s]
    norbs = sysinfo.norbitals[s]
    deltaoffset = row - offset - 1
    site = div(deltaoffset, norbs) + 1
    orboffset = rem(deltaoffset, norbs) + 1
    return site, orboffset, s
end

torow(siteindex, sublat, sysinfo) =
    sysinfo.offsets[sublat] + (siteindex - 1) * sysinfo.norbitals[sublat] + 1

function findsublat(row, offsets)
    for n in eachindex(offsets)
        offsets[n] >= row && return n - 1
    end
    return 0
end

#######################################################################
# Operator
#######################################################################

mutable struct Block{Tv,L}
    ndist::SVector{L,Int}
    matrix::SparseMatrixCSC{Tv,Int}
    nlinks::Int
end

function Block(ndist, matrix, sysinfo::SystemInfo)
    b = Block(ndist, matrix, 0)
    isempty(matrix) || updatenlinks!(b, sysinfo)
    return b
end

Base.isempty(b::Block) = isempty(b.matrix)

Base.zero(b::Block{Tv,L}) where {Tv,L} =
    Block(zero(SVector{L,Int}), spzeros(Tv, size(b.matrix)...), 0)

function Base.show(io::IO, b::Block{Tv,L}) where {Tv,L}
    print(io, "Block{$Tv,$L}: Bloch harmonic $(b.ndist) of dimensions $(size(b.matrix)) with $(nnz(b.matrix)) elements")
end

struct Operator{Tv,L}
    matrix::SparseMatrixCSC{Tv,Int}
    intra::Block{Tv,L}
    inters::Vector{Block{Tv,L}}
    boundary::Vector{Tuple{Int,Int}} # ptrs to matrix and intra that contain terms in any inters
end

function Operator{Tv,L}(n::Int) where {Tv,L}
    return Operator(
        spzeros(n,n),
        Block{Tv,L}(zero(SVector{L,Int}), n),
        Block{Tv,L}[],
        Tuple{Int,Int}[])
end

function Base.show(io::IO, op::Operator{L,Tv}) where {L,Tv}
    print(io, "Operator{$L,$Tv}: Bloch operator of dimensions $(size(op.matrix)) with $(nnz(op.matrix)) elements in main matrix")
end

#######################################################################
# Operator API
#######################################################################

nlinks(o::Operator) = nlinks(o.intra) + (isempty(o.inters) ? 0 : sum(nlinks, o.inters))
nlinks(b::Block) = b.nlinks

nsublats(b::Block) = nsublats(b.sysinfo)

function updatenlinks!(b::Block, sysinfo)
    n = 0
    zeron = iszero(b.ndist)
    for (_, (target, source), (row, col), _) in BlockIterator(b, sysinfo)
        (!zeron || row != col) && (n += 1)
    end
    b.nlinks = n
    return nothing
end

insertblochphases!(o::Operator{Tv,L}, kn) where {Tv<:AbstractFloat,L} =
    throw(DomainError(Tv, "Cannot apply Bloch phases to a real Hamiltonian."))

function insertblochphases!(op::Operator{Tv,L}, ϕn, dϕaxis = missing) where {Tv,L}
    length(ϕn) == L || throw(DimensionMismatch(
        "The dimension of the normalized Bloch phases should match the lattice dimension $L"))
    rows = rowvals(op.matrix)
    vals = nonzeros(op.matrix)
    valsintra = nonzeros(op.intra.matrix)
    for (ptrmatrix, ptrintra) in op.boundary
        vals[ptrmatrix] = iszero(ptrintra) ? zero(Tv) : valsintra[ptrintra]
    end
    for inter in op.inters
        # dϕaxis != missing produces derivatives of Bloch phases respect a given axis
        dexp = dϕaxis === missing ? 1.0 : 2pi * im * dot(inter.ndist, dϕaxis)
        phase = dexp * exp(2pi * im * dot(inter.ndist, ϕn))
        for (i,j,v,ptr) in SparseMatrixReader(inter.matrix)
            for ptr in nzrange(op.matrix,j)
                rows[ptr] == i && (vals[ptr] += phase * v; break)
            end
        end
    end
    return op
end

function boundaryoperator(op::Operator{Tv}) where {Tv}
    n = length(op.boundary)
    sb = SparseMatrixBuilder{Tv}(size(op.matrix)...)
    rows = rowvals(op.matrix)
    vals = nonzeros(op.matrix)
    sofar = 1
    for col in 1:size(op.matrix, 2)
        colrange = nzrange(op.matrix, col)
        for bidx in sofar:n
            ptr = first(op.boundary[bidx])
            if ptr in colrange
                sofar = bidx + 1
                pushtocolumn!(sb, rows[ptr], vals[ptr])
            else
                break
            end
        end
        finalizecolumn!(sb)
    end
    matrix = sparse(sb)
    intra = zero(op.intra)
    inters = op.inters
    boundary = extractboundary(matrix, intra, inters)
    return Operator(matrix, intra, inters, boundary)
end


using YaoExperiment
using Yao, Yao.ConstGate, YaoExtensions
using Test

# x, y, z, h, s, sdag, t, tdag
@testset "basic gates" begin
    for G in [:X, :Y, :Z, :H, :S, :Sdag, :T, :Tdag]
        g = string(G)
        @eval @test quil($G) == "$($(g))"
        @eval @test quil(put(3,1=>$G)) == "$($g) 0"
    end

    for G in [:SWAP, :CNOT]
        g = string(G)
        @eval @test quil($G) == "$($(g))"
        @eval @test quil(put(3,(2,1)=>$G)) == "$($g) 1 0"
    end
end
# id
@test quil(put(4, 2=>I2)) == "I 1"

# cx, cy, cz, ch, ct
# ccx, ccy, ccz, cch, cct
@testset "basic controled gates" begin
    for G in [:X, :Y, :Z, :H, :T]
        g = G == :X ? "NOT" : string(G)
        @eval @test quil(control(3, 1, 2=>$G)) == "C$($g) 0 1"
        @eval @test quil(control(3, -1, 2=>$G)) == "¬C$($g) 0 1"
        @eval @test quil(control(7, (1,3), 2=>$G)) == "CC$($g) 0 2 1"
    end
end

# rx, ry, rz
# crz, crx, cry
@testset "(controled) rotation gates" begin
    for G in [:X, :Y, :Z]
        g = string(G)
        @eval @test quil(control(3, 1, 2=>rot($G, 0.3))) == "CR$($g)(0.3) 0 1"
        @eval @test quil(rot($G, 0.3)) == "R$($g)(0.3)"
        @eval @test quil(put(3, 2=>rot($G, 0.3))) == "R$($g)(0.3) 1"
    end
    @test quil(rot(SWAP, 0.3)) == "PSWAP(0.3)"
    @test quil(put(3, (3,1)=>rot(SWAP, 0.3))) == "PSWAP(0.3) 2 0"
end

# measure
@testset "measure" begin
    @test quil(Measure(3), YaoExperiment.QuilAddress([1,4,3])) == "MEASURE 0 3 2"
end

@testset "composite" begin
    # kron
    @test quil(kron(5, 2=>X, 4=>T)) == "X 1\nT 3"
    @test quil(PauliString(I2, I2, X, X)) == "X 2\nX 3"
    # chain
    @test quil(chain(put(3,1=>Z), put(3, 2=>X))) == "Z 0\nX 1"
    # Concentrator
    @test quil(subroutine(5, put(3,1=>Z), [3,4,1])) == "Z 2"
    # cache and diff
    @test quil(kron(5, 2=>X, 4=>T) |> cache) == "X 1\nT 3"
    @test quil(rot(X, 0.3)) == "RX(0.3)"
end

@testset "show" begin
    # show
    @test repr(MIME("quil"), control(3, 2, 1=>X)) |> String == "CNOT 1 0\n"
end

@testset "system test" begin
    circuit = chain(qft_circuit(4), Measure())
    println(quil(circuit))
end


module ElboMaximize

using Optim
using Optim: Options, NewtonTrustRegion
using ..Model
using ..SensitiveFloats
using ..DeterministicVI
using ..DeterministicVI: init_thread_pool!, ElboIntermediateVariables
using ..DeterministicVI.ConstraintTransforms: TransformDerivatives,
                                              VariationalParams,
                                              ConstraintBatch,
                                              ParameterConstraint,
                                              BoxConstraint,
                                              SimplexConstraint,
                                              allocate_free_params,
                                              to_free!,
                                              to_bound!,
                                              enforce!,
                                              propagate_derivatives!

##################################################
# defaults for optional arguments to `maximize!` #
##################################################

struct ElboConfig{N,T}
    bound_params::VariationalParams{T}
    free_params::VariationalParams{T}
    free_initial_input::Vector{T}
    free_previous_input::Vector{T}
    free_result::SensitiveFloat{T}
    bvn_bundle::Model.BvnBundle{T}
    constraints::ConstraintBatch
    derivs::TransformDerivatives{N,T}
    optim_options::Options
    trust_region::NewtonTrustRegion{T}
end

function ElboConfig{T}(ea::ElboArgs,
                   vp::VariationalParams{T},
                   bound_params::VariationalParams{T} = vp[ea.active_sources];
                   termination_callback = nothing,
                   loc_width::Float64 = 1e-4,
                   loc_scale::Float64 = 1.0,
                   max_iters::Int = 50,
                   constraints::ConstraintBatch = elbo_constraints(bound_params, loc_width, loc_scale),
                   optim_options::Options = elbo_optim_options(max_iters=max_iters,
                                                               termination_callback=termination_callback),
                   trust_region::NewtonTrustRegion = elbo_trust_region())
    free_params = allocate_free_params(bound_params, constraints)
    free_initial_input = to_flat_vector(free_params)
    free_previous_input = similar(free_initial_input)
    free_result = SensitiveFloat{Float64}(length(free_params[1]), length(bound_params), true, true)
    bvn_bundle = Model.BvnBundle{T}(ea.psf_K, ea.S)
    derivs = TransformDerivatives(bound_params, free_params)
    return ElboConfig(bound_params, free_params, free_initial_input,
                  free_previous_input, free_result, bvn_bundle,
                  constraints, derivs, optim_options, trust_region)
end

function elbo_constraints{T}(bound::VariationalParams{T},
                             loc_width::Real = 1.0e-4,
                             loc_scale::Real = 1.0)
    n_sources = length(bound)
    boxes = Vector{Vector{ParameterConstraint{BoxConstraint}}}(n_sources)
    simplexes = Vector{Vector{ParameterConstraint{SimplexConstraint}}}(n_sources)
    for src in 1:n_sources
        i1, i2 = ids.pos[1], ids.pos[2]
        u1, u2 = bound[src][i1], bound[src][i2]
        boxes[src] = [
            ParameterConstraint(BoxConstraint(u1 - loc_width, u1 + loc_width, loc_scale), i1),
            ParameterConstraint(BoxConstraint(u2 - loc_width, u2 + loc_width, loc_scale), i2),
            ParameterConstraint(BoxConstraint(1e-2, 0.99, 1.0), ids.gal_fracdev),
            ParameterConstraint(BoxConstraint(1e-2, 0.99, 1.0), ids.gal_ab),
            ParameterConstraint(BoxConstraint(-10.0, 10.0, 1.0), ids.gal_angle),
            ParameterConstraint(BoxConstraint(0.10, 70.0, 1.0), ids.gal_scale),
            ParameterConstraint(BoxConstraint(-1.0, 10.0, 1.0), ids.flux_loc),
            ParameterConstraint(BoxConstraint(1e-4, 0.10, 1.0), ids.flux_scale),
            ParameterConstraint(BoxConstraint(-10.0, 10.0, 1.0), ids.color_mean[:, 1]),
            ParameterConstraint(BoxConstraint(-10.0, 10.0, 1.0), ids.color_mean[:, 2]),
            ParameterConstraint(BoxConstraint(1e-4, 1.0, 1.0), ids.color_var[:, 1]),
            ParameterConstraint(BoxConstraint(1e-4, 1.0, 1.0), ids.color_var[:, 2])
        ]
        simplexes[src] = [
            ParameterConstraint(SimplexConstraint(0.005, 1.0, 2), ids.is_star),
            ParameterConstraint(SimplexConstraint(0.01/NUM_COLOR_COMPONENTS, 1.0, NUM_COLOR_COMPONENTS), ids.k[:, 1]),
            ParameterConstraint(SimplexConstraint(0.01/NUM_COLOR_COMPONENTS, 1.0, NUM_COLOR_COMPONENTS), ids.k[:, 2])
        ]
    end
    return ConstraintBatch(boxes, simplexes)
end

function elbo_optim_options(args...; xtol_abs = 1e-7, ftol_rel = 1e-6, max_iters = 50,
                            termination_callback = nothing, kwargs...)
    return Optim.Options(args...; x_tol = xtol_abs, f_tol = ftol_rel,
                         g_tol = 1e-8, iterations = max_iters,
                         store_trace = false, show_trace = false,
                         extended_trace = false,
                         callback = termination_callback,
                         kwargs...)
end

function elbo_trust_region(; initial_delta = 1.0, delta_hat = 1e9)
    return Optim.NewtonTrustRegion(initial_delta = initial_delta,
                                   delta_hat = delta_hat)
end

function enforce_references!{T}(ea::ElboArgs, vp::VariationalParams{T}, cfg::ElboConfig)
    @assert length(cfg.bound_params) == length(ea.active_sources)
    for i in 1:length(ea.active_sources)
        cfg.bound_params[i] = vp[ea.active_sources[i]]
    end
    return nothing
end

to_flat_vector{T}(sources::VariationalParams{T}) = vcat(sources...)::Vector{T}

function to_flat_vector!{T}(x::Vector{T}, sources::VariationalParams{T})
    i = 1
    for src in sources
        for j in eachindex(src)
            x[i] = src[j]
            i += 1
        end
    end
    return sources
end

function to_variational_params!{T}(sources::VariationalParams{T}, x::Vector{T})
    i = 1
    for src in sources
        for j in eachindex(src)
            src[j] = x[i]
            i += 1
        end
    end
    return sources
end

##########################################
# Preallocated ElboIntermediateVariables #
##########################################

const ELBO_VARS_POOL = Vector{ElboIntermediateVariables{Float64}}()

get_elbo_vars() = ELBO_VARS_POOL[Base.Threads.threadid()]

function __init__()
    init_thread_pool!(ELBO_VARS_POOL, () -> ElboIntermediateVariables(Float64, 1, true, true))
end

# explicitly call this for use with compiled system image
__init__()

##################################
# Callable Types Passed to Optim #
##################################

function evaluate!{T}(ea::ElboArgs, vp::VariationalParams{T}, cfg::ElboConfig, x::Vector)
    if x != cfg.free_previous_input
        copy!(cfg.free_previous_input, x)
        to_variational_params!(cfg.free_params, x)
        to_bound!(cfg.bound_params, cfg.free_params, cfg.constraints)
        bound_result = elbo(ea, vp, get_elbo_vars(), cfg.bvn_bundle)
        propagate_derivatives!(to_bound!, bound_result,
                               cfg.free_result, cfg.free_params,
                               cfg.constraints, cfg.derivs)
    end
    return nothing
end

# Objective #
#-----------#

struct Objective{N,T} <: Function
    ea::ElboArgs
    vp::VariationalParams{T}
    cfg::ElboConfig{N,T}
end

function (f::Objective)(x::Vector)
    evaluate!(f.ea, f.vp, f.cfg, x)
    return -(f.cfg.free_result.v[])
end

# Gradient #
#----------#

struct Gradient{N,T} <: Function
    ea::ElboArgs
    vp::VariationalParams{T}
    cfg::ElboConfig{N,T}
end

function (f::Gradient)(x::Vector, result::Vector)
    evaluate!(f.ea, f.vp, f.cfg, x)
    free_gradient = f.cfg.free_result.d
    for i in eachindex(result)
        result[i] = -(free_gradient[i])
    end
    return result
end

# Hessian #
#---------#

struct Hessian{N,T} <: Function
    ea::ElboArgs
    vp::VariationalParams{T}
    cfg::ElboConfig{N,T}
end

function (f::Hessian)(x::Vector, result::Matrix)
    evaluate!(f.ea, f.vp, f.cfg, x)
    free_hessian = f.cfg.free_result.h
    for i in eachindex(result)
        result[i] = -(free_hessian[i])
    end
    return result
end

#############
# maximize! #
#############

function maximize!(ea::ElboArgs, vp::VariationalParams{Float64}, cfg::ElboConfig = ElboConfig(ea, vp))
    enforce_references!(ea, vp, cfg)
    enforce!(cfg.bound_params, cfg.constraints)
    to_free!(cfg.free_params, cfg.bound_params, cfg.constraints)
    to_flat_vector!(cfg.free_initial_input, cfg.free_params)
    fill!(cfg.free_previous_input, NaN)
    R = Optim.MultivariateOptimizationResults{Float64,1,Optim.NewtonTrustRegion{Float64}}
    result::R = Optim.optimize(Objective(ea, vp, cfg), Gradient(ea, vp, cfg), Hessian(ea, vp, cfg),
                               cfg.free_initial_input, cfg.trust_region, cfg.optim_options)
    min_value::Float64 = -(Optim.minimum(result))
    min_solution::Vector{Float64} = Optim.minimizer(result)
    to_variational_params!(cfg.free_params, min_solution)
    to_bound!(cfg.bound_params, cfg.free_params, cfg.constraints)
    return result.f_calls, min_value, min_solution, result
end

end # module


export get_pha!

"""
    T = get_pha(Δ::Float64, z::Float64)

Command-line interface to IRIS online travel time calculator, which calls TauP [1-2].
Returns a matrix of strings.

Specify Δ in decimal degrees, z in km treating down as +.

Standard keywords: pha, to, v

Additional keywords:
* model: velocity model ("iasp91")

### References
[1] TauP manual: http://www.seis.sc.edu/downloads/TauP/taup.pdf
[2] Crotwell, H. P., Owens, T. J., & Ritsema, J. (1999). The TauP Toolkit:
Flexible seismic travel-time and ray-path utilities, SRL 70(2), 154-160.
"""
function get_pha!(Ev::SeisEvent;
                  pha::String   = KW.pha,
                  model::String = "iasp91",
                  to::Int64     = KW.to,
                  v::Int64      = KW.v
                  )

  # Check that distaz has been done
  TD = getfield(Ev, :data)
  N = getfield(TD, :n)
  z = zeros(Float64, N)
  if (TD.az == z) && (TD.baz == z) && (TD.dist == z)
    v > 0 && println(stdout, "az, baz, and dist are unset; calling distaz!...")
    distaz!(Ev)
  end

  # Generate URL and do web query
  src_dep = getfield(getfield(getfield(Ev, :hdr), :loc), :dep)
  if isempty(pha) || pha == "all"
    pq = "&phases=ttall"
  else
    pq = string("&phases=", pha)
  end
  url_tail = string("&evdepth=", src_dep, pq, "&model=", model, "&mintimeonly=true&noheader=true")

  # Loop begins
  dist = getfield(TD, :dist)
  PC = getfield(TD, :pha)
  for i = 1:N
    Δ = getindex(dist, i)
    pcat = getindex(PC, i)

    url = string("http://service.iris.edu/irisws/traveltime/1/query?", "distdeg=", Δ, url_tail)
    v > 1 && println(stdout, "url = ", url)

    req_info_str = string("\nIRIS travel time request:\nΔ = ", Δ, "\nDepth = ", z, "\nPhases = ", pq, "\nmodel = ", model)
    (R, parsable) = get_http_req(url, req_info_str, to)

    # Parse results
    if parsable
      req = String(take!(copy(IOBuffer(R))))
      pdat = split(req, '\n')
      deleteat!(pdat, findall(isempty, pdat))   # can have trailing blank line
      npha = length(pdat)
      for j = 1:npha
        pha = split(pdat[j], keepempty=false)
        pcat[pha[10]] = SeisPha(0.0,                              # a
                                parse(Float64, pha[8]),           # d
                                parse(Float64, pha[7]),           # ia
                                0.0,                              # res
                                parse(Float64, pha[5]),           # rp
                                parse(Float64, pha[6]),           # ta
                                parse(Float64, pha[4]),           # tt
                                0.0,                              # unc
                                ' ',                              # pol
                                ' ',                              # qual
                                )
      end
    end
  end
  return nothing
end
# "Model: iasp91"
# "Distance   Depth   Phase        Travel    Ray Param  Takeoff  Incident  Purist    Purist    "
# "  (deg)     (km)   Name         Time (s)  p (s/deg)   (deg)    (deg)   Distance   Name      "
# "--------------------------------------------------------------------------------------------"
# "   66.83    19.7   P             650.37     6.375     19.49    19.42    66.83   = P"
# "   66.83    19.7   pP            656.77     6.385    160.48    19.45    66.83   = pP"
# "   66.83    19.7   sP            659.32     6.383    168.84    19.45    66.83   = sP"
# "   66.83    19.7   PcP           678.82     4.167     12.59    12.55    66.83   = PcP"
# "   66.83    19.7   PP            797.49     8.699     27.08    26.98    66.83   = PP"
# "   66.83    19.7   PKiKP        1038.70     1.352      4.06     4.04    66.83   = PKiKP"
# "   66.83    19.7   pPKiKP       1045.47     1.352    175.94     4.04    66.83   = pPKiKP"
# "   66.83    19.7   sPKiKP       1047.95     1.352    177.65     4.04    66.83   = sPKiKP"
# "   66.83    19.7   S            1182.56    12.085     21.49    21.42    66.83   = S"
# "   66.83    19.7   pS           1190.65    12.109    140.68    21.46    66.83   = pS"
# "   66.83    19.7   sS           1193.48    12.101    158.48    21.45    66.83   = sS"
# "   66.83    19.7   SP           1201.90    13.645     24.43    45.38    66.83   = SP"
# "   66.83    19.7   PS           1204.86    13.644     45.55    24.35    66.83   = PS"
# "   66.83    19.7   SKS          1246.03     7.572     13.27    13.23    66.83   = SKS"
# "   66.83    19.7   SKKS         1246.05     7.585     13.29    13.25    66.83   = SKKS"
# "   66.83    19.7   ScS          1246.40     7.761     13.61    13.56    66.83   = ScS"
# "   66.83    19.7   SKiKP        1250.70     1.408      2.45     4.21    66.83   = SKiKP"
# "   66.83    19.7   pSKS         1254.86     7.573    156.66    13.23    66.83   = pSKS"
# "   66.83    19.7   sSKS         1257.44     7.573    166.73    13.23    66.83   = sSKS"
# "   66.83    19.7   SS           1441.81    15.478     27.98    27.88    66.83   = SS"
# "   66.83    19.7   PKIKKIKP     1860.33     1.411      4.23     4.22   293.17   = PKIKKIKP"
# "   66.83    19.7   SKIKKIKP     2072.34     1.352      2.35     4.05   293.17   = SKIKKIKP"
# "   66.83    19.7   PKIKKIKS     2074.81     1.352      4.06     2.34   293.17   = PKIKKIKS"
# "   66.83    19.7   SKIKKIKS     2286.62     1.297      2.25     2.25   293.17   = SKIKKIKS"
# "   66.83    19.7   PKIKPPKIKP   2359.03     1.666      5.00     4.98   293.17   = PKIKPPKIKP"
# "   66.83    19.7   PKPPKP       2361.68     2.976      8.96     8.93   293.17   = PKPPKP"
# "   66.83    19.7   PKPPKP       2364.24     3.928     11.86    11.82   293.17   = PKPPKP"
# "   66.83    19.7   SKIKSSKIKS   3216.94     1.426      2.48     2.47   293.17   = SKIKSSKIKS"


#=
New DA structure that allows minimal manipulation
=#

struct EdgeTreeRates2
    tree::Tree                          # the grafted tree
    stem_rate::Array{Float64,1}         # λ_i : every other rates are given divided by λ_i
    tip_rate::Float64                   # the tip rate
    tip_id::Int64                       # tip_id
    tip_rates::Array{Float64,1}         # rates at t_i (for internal branches)
    effective_bl::Float64               # t_j * λ_j / λ_i
    tip_number::Int64                   # extant tips
    scale::Float64
    #relative_rates::Array{Float64,1}
    depth::Float64
    stem_depth::Float64
    parent_edge::Int64
end

function EdgeTreeRates2(tree::Tree, stem_rate::Float64, tip_rate::Float64,
    tip_id::Int64, tip_rates::Array{Float64,1}, tip_number::Int64, depth::Float64,stem_depth::Float64, parent_edge::Int64)
    effective_bl = sum(extract_branch_lengths(tree) .* extract_rates(tree)) / stem_rate
    return EdgeTreeRates2(tree, [stem_rate], tip_rate, tip_id, tip_rates,
        effective_bl, tip_number, depth, stem_depth,parent_edge)
end

function EdgeTreeRates2(tree::Tree, stem_rate::Float64, tip_rate::Float64,
    tip_id::Int64, tip_rates::Array{Float64,1},
    effective_bl::Float64, tip_number::Int64,depth::Float64,stem_depth::Float64,
    parent_edge::Int64)
    return EdgeTreeRates2(tree, [stem_rate], tip_rate, tip_id, tip_rates, effective_bl, tip_number, depth,stem_depth, parent_edge)
end

function EdgeTreeRates2(tree::Tree, stem_rate::Array{Float64,1}, tip_rate::Float64,
    tip_id::Int64, tip_rates::Array{Float64,1},
    effective_bl::Float64, tip_number::Int64,
    depth::Float64,stem_depth::Float64,parent_edge::Int64)

    return EdgeTreeRates2(tree, stem_rate, tip_rate, tip_id, tip_rates, effective_bl,
        tip_number, stem_rate[1], depth,stem_depth, parent_edge)
end

#=
A few functions to manipulate it
=#

function change_bl(tree::Tree, scale::Float64)
    function aux(sub_tree)
        if sub_tree.n_nodes < 2
            return Tree(sub_tree.offsprings,
                sub_tree.branch_length / scale,
                sub_tree.attributes.*scale,
                sub_tree.n_nodes,
                sub_tree.extant)
        else
            left = aux(sub_tree.offsprings[1])
            right = aux(sub_tree.offsprings[2])
            return Tree([left,right],
                sub_tree.branch_length / scale,
                sub_tree.attributes.*scale,
                sub_tree.n_nodes,
                sub_tree.extant)
        end
    end

    aux(tree)
end

function change_bl(tree::Tree, scale::Float64, stem_rate::Float64)
    function aux(sub_tree)
        if sub_tree.n_nodes < 2
            return Tree(sub_tree.offsprings,
                sub_tree.branch_length / scale,
                sub_tree.attributes.*stem_rate,
                sub_tree.n_nodes,
                sub_tree.extant)
        else
            left = aux(sub_tree.offsprings[1])
            right = aux(sub_tree.offsprings[2])
            return Tree([left,right],
                sub_tree.branch_length / scale,
                sub_tree.attributes.*stem_rate,
                sub_tree.n_nodes,
                sub_tree.extant)
        end
    end

    aux(tree)
end

function graft_edge_trees(tree, edge_trees::Array{EdgeTreeRates2,1})

    function aux(subtree, sub_edge_trees)
        if subtree.n_nodes < 2
            return change_bl(sub_edge_trees[1].tree, sub_edge_trees[1].scale, sub_edge_trees[1].stem_rate[1])
        else
            n_edges_left = subtree.offsprings[1].n_nodes + 1
            sub_left = deepcopy(sub_edge_trees[2:n_edges_left])
            sub_right = deepcopy(sub_edge_trees[(n_edges_left+1):subtree.n_nodes])
            subtree_left = aux(subtree.offsprings[1], sub_left)
            subtree_right = aux(subtree.offsprings[2], sub_right)
            return graft_to_edge(Tree([subtree_left, subtree_right], 0., subtree.attributes),
                change_bl(sub_edge_trees[1].tree, sub_edge_trees[1].scale, sub_edge_trees[1].stem_rate[1]), 0, sub_edge_trees[1].tip_id)
        end
    end

    n_edges_left = tree.offsprings[1].n_nodes
    left = deepcopy(edge_trees[1:n_edges_left])
    right = deepcopy(edge_trees[(n_edges_left+1):(tree.n_nodes - 1)])
    tree_left = aux(tree.offsprings[1], left)
    tree_right = aux(tree.offsprings[2], right)
    return Tree([tree_left, tree_right], tree.branch_length, tree.attributes)
end

function node_depths_base(tree::Tree, edge_trees::Array{EdgeTreeRates2,1})
    function aux(subtree, node_times, current_depth, att)
        if subtree.n_nodes < 2
            pushfirst!(node_times, current_depth)
        else
            aux(subtree.offsprings[2],node_times, current_depth + subtree.branch_length/att, att)
            aux(subtree.offsprings[1],node_times, current_depth + subtree.branch_length/att, att)
            pushfirst!(node_times,current_depth)
        end
    end

    node_times = node_depths_base(tree)[2:end]
    node_times_full = Array{Float64,1}(undef,0)
    for i in length(edge_trees):-1:1
        aux(edge_trees[i].tree, node_times_full, node_times[i], edge_trees[i].scale)
    end
    #pushfirst!(node_times,0.)
    return node_times_full
end

function node_depths(tree::Tree, edge_trees::Array{EdgeTreeRates2,1})
    function aux(subtree, node_times, current_depth, sa)
        if subtree.n_nodes < 2
            pushfirst!(node_times, current_depth + subtree.branch_length/sa)
        else
            aux(subtree.offsprings[2],node_times, current_depth + subtree.branch_length/sa, sa)
            aux(subtree.offsprings[1],node_times, current_depth + subtree.branch_length/sa, sa)
            pushfirst!(node_times,current_depth+ subtree.branch_length/sa)
        end
    end

    node_times = node_depths_base(tree)[2:end]
    node_times_full = Array{Float64,1}(undef,0)
    for i in length(edge_trees):-1:1
        aux(edge_trees[i].tree, node_times_full, node_times[i], edge_trees[i].scale)
    end
    #pushfirst!(node_times,0.)
    return node_times_full
end

function node_depths_base(tree::Tree)
    function aux(subtree, node_times, current_depth)
        if subtree.n_nodes < 2
            pushfirst!(node_times, current_depth)
        else
            aux(subtree.offsprings[2],node_times, current_depth + subtree.branch_length)
            aux(subtree.offsprings[1],node_times, current_depth + subtree.branch_length)
            pushfirst!(node_times,current_depth)
        end
    end

    node_times = Array{Float64,1}(undef,0)
    aux(tree, node_times, 0.)
    #pushfirst!(node_times,0.)
    return node_times
end

function node_depths(tree::Tree)
    function aux(subtree, node_times, current_depth)
        if subtree.n_nodes < 2
            pushfirst!(node_times, current_depth + subtree.branch_length)
        else
            aux(subtree.offsprings[2],node_times, current_depth + subtree.branch_length)
            aux(subtree.offsprings[1],node_times, current_depth + subtree.branch_length)
            pushfirst!(node_times,current_depth+ subtree.branch_length)
        end
    end

    node_times = Array{Float64,1}(undef,0)
    aux(tree, node_times, 0.)
    #pushfirst!(node_times,0.)
    return node_times
end

function graft_to_tip(tree,subtree, tip_id)

    function aux(t1, t2, tip)
        if t1.n_nodes == 1
            return Tree(t2.offsprings, t2.branch_length + t1.branch_length, t1.attributes, t2.n_nodes, t2.extant)
        else
            t11 = t1.offsprings[1]
            t12 = t1.offsprings[2]
            t11_tips = (t11.n_nodes + 1) / 2
            if t11_tips < tip
                t12 = aux(t12, t2, tip - t11_tips)
                return Tree([t11, t12], t1.branch_length, t1.attributes)
            else
                t11 = aux(t11, t2, tip)
                return Tree([t11, t12], t1.branch_length, t1.attributes)
            end
        end
    end

    aux(tree,subtree, tip_id)
end

function graft_to_edge(tree, sub_tree, edge, tip)

    if sub_tree.n_nodes < 2
        return Tree(tree.offsprings, tree.branch_length + sub_tree.branch_length, tree.attributes, tree.n_nodes, sub_tree.extant)
    end

    function aux(t1, t2, edge_id)
        if t1.n_nodes == 1
            return t2
        elseif edge_id == 1
            new_t1 = Tree(t1.offsprings, 0., t1.attributes, t1.n_nodes, t1.extant)
            return graft_to_tip(t2, new_t1, tip)
        else
            t11 = t1.offsprings[1]
            t12 = t1.offsprings[2]
            if t11.n_nodes < (edge_id-1)
                t12 = aux(t12, t2, edge_id-1-t11.n_nodes)
                return Tree([t11, t12], t1.branch_length, t1.attributes)
            else
                t11 = aux(t11, t2, edge_id-1)
                return Tree([t11, t12], t1.branch_length, t1.attributes)
            end
        end
    end

    return aux(tree, sub_tree, edge+1)
end

function graft_to_tips(tree, subtree_list, tips)
    if length(tips) == 0
        return tree
    end

    ntips = Int64((tree.n_nodes + 1) / 2)
    subtrees = fill(Tree(),ntips)
    #println("$tips, $ntips")

    for i in 1:ntips
        for j in 1:length(tips)
            if i==tips[j]
                subtrees[i] = subtree_list[j]
                break
            end
        end
    end

    function aux(t1, t2)
        if t1.n_nodes <= 1
            #=if ! t2[1].extant
                println("is it ok ? $(t2[1]) ; $(t1.extant)")
            end=#
            return Tree(t2[1].offsprings, t2[1].branch_length + t1.branch_length, t1.attributes, t2[1].extant)
        else
            t11 = t1.offsprings[1]
            t12 = t1.offsprings[2]
            t11_tips = Int64((t11.n_nodes + 1) / 2)

            t11 = aux(t11, t2[1:t11_tips])
            t12 = aux(t12, t2[(t11_tips+1):end])

            return Tree([t11, t12], t1.branch_length, t1.attributes)
        end
    end

    new_tree = aux(tree,subtrees)
    return new_tree
end

function init_edge_tree_rates2(tree, rates)
    bl = extract_branch_lengths(tree)
    edge_trees = Array{EdgeTreeRates2,1}(undef,0)
    lefts = n_left(tree)
    for i in 2:tree.n_nodes
        nd = get_node_depth(tree, i-1)
        parent_edge = get_parent_edge(tree, i-1)

        ne = 0
        if lefts[i]==0
            ne = 1
        end

        push!(edge_trees,
            EdgeTreeRates2(Tree(Array{Tree,1}(undef,0), bl[i] * rates[i], 1.),       # tree
                rates[i],                                                           # stem_rate
                1.,                                                                 # tip_rate
                1,                                                                  # tip_id
                [1.],                                                               # tip_rates
                bl[i],                                                              # effective_bl
                ne,                                                                  # tip_number
                nd,
                nd + bl[i],
                parent_edge))
    end
    return edge_trees
end

function sim_ClaDS2_time(root_age,σ,α,ε,λ0,u,sf,lf ; return_if_extinct = true, max_node_number = Inf, make_tree = true, prune_extinct = false, return_if_max = true)
    # accesory functions that will be called latter
    bernouilli=Bernoulli(ε/(1+ε))
    function dead()
        return (rand(bernouilli)[1] == 1)
    end

    lambda_law = LogNormal(log(α),σ)
    function new_rates(λ)
        λ * rand(lambda_law, 2)
    end
    #=function new_rates(λ::Float64)
        X = (rand(lambda_law, 2))
        λ * X#* sort2!(X)
    end=#
    #println()
    function aux_ext(time, rate, n_max, s)
        #println(n_max)
        if n_max == 0
            #println("max")
            return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0, -Inf
        end
        node_time = randexp()/(rate*(1+ε))
        if node_time <= 0
            return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0, -Inf
        end
        if node_time > time
            s += lf
            #print(" $s $u ;")
            if u<s
                return Tree(Array{Tree,1}(undef,0), time, [rate], 1, true), n_max + 1, s
            else
                #print(" !")
                return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0, s
            end
        else
                is_dead = dead()
                left_time = time - node_time
                if time == left_time
                    return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0
                end
                if is_dead
                    #print("dead")
                    return Tree(Array{Tree,1}(undef,0), node_time, [rate], 1, false), n_max + 1, s
                else
                    offspring_rates = new_rates(rate)
                    if offspring_rates[1] > offspring_rates[2]
                        offspring_rates = [offspring_rates[2],offspring_rates[1]]
                    end
                    #print("$depth ;")
                    left_tree = aux_ext(left_time, offspring_rates[1], n_max - 1, s)
                    #print(left_tree[2])
                    if left_tree[1].n_nodes == -1
                        return left_tree
                    end
                    right_tree = aux_ext(left_time, offspring_rates[2], left_tree[2], left_tree[3])
                    if right_tree[1].n_nodes == -1
                        return right_tree
                    end
                    return Tree([left_tree[1], right_tree[1]], node_time, [rate]), right_tree[2], right_tree[3]
                end
        end
    end
    tree = aux_ext(root_age, λ0, max_node_number, sf)

    if prune_extinct
        if (tree[1].n_nodes == -1) & return_if_max
            return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), -Inf
        else
            return prune_extinct_lineages(tree[1]), tree[3]
        end
    else
        if (tree[1].n_nodes == -1) & return_if_max
            return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), -Inf
        else
            return tree[1], tree[3]
        end
    end
end

function sim_ClaDS2_time_rates(root_age,σ,α,ε,λ0 ; return_if_extinct = true, max_node_number = 1_000,
    make_tree = true, prune_extinct = false, return_if_max = true, return_na = false)

    # accesory functions that will be called latter
    bernouilli=Bernoulli(ε/(1+ε))
    function dead()
        return (rand(bernouilli)[1] == 1)
    end

    lambda_law = LogNormal(log(α),σ)
    function new_rates(λ)
        λ * rand(lambda_law, 2)
    end
    #=function new_rates(λ::Float64)
        X = (rand(lambda_law, 2))
        λ * X#* sort2!(X)
    end=#
    if return_na
        function aux_na(time, rate, n_max, rates, live)
            #println(n_max)
            if n_max == 0
                return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0
            end
            node_time = randexp()/(rate*(1+ε))
            if node_time <= 0
                return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0
            end
            if node_time > time
                #offsprings, branch_length, attributes, n_nodes
                push!(rates, rate)
                push!(live, true)
                return Tree(Array{Tree,1}(undef,0), time, [rate], 1, true), n_max
            else
                is_dead = dead()
                if is_dead
                    #print("dead")
                    push!(live, false)
                    return Tree(Array{Tree,1}(undef,0), node_time, [rate], 1, false), n_max + 1
                else
                    offspring_rates = new_rates(rate)
                    left_time = time - node_time
                    if time == left_time
                        return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0
                    end
                    #print("$depth ;")
                    left_tree = aux_na(left_time, offspring_rates[1], n_max - 1, rates, live)
                    #print(left_tree[2])
                    if left_tree[1].n_nodes == -1
                        return left_tree
                    end
                    right_tree = aux_na(left_time, offspring_rates[2], left_tree[2], rates, live)
                    if right_tree[1].n_nodes == -1
                        return right_tree
                    end
                    return Tree([left_tree[1], right_tree[1]], node_time, [rate]), right_tree[2]
                end
            end
        end

        rates = Array{Float64,1}(undef,0)
        alive = Array{Bool,1}(undef,0)

        tree = aux_na(root_age, λ0, max_node_number, rates, alive)[1]
        if (tree.n_nodes == -1) & return_if_max
            return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), Array{Float64,1}(undef,0), Array{Bool,1}(undef,0)
        else
            return tree, rates, alive
        end
    else
        function aux_ext(time, rate, n_max, rates)
            #println(n_max)
            if n_max == 0
                return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0
            end
            node_time = randexp()/(rate*(1+ε))
            if node_time > time
                #offsprings, branch_length, attributes, n_nodes
                push!(rates, rate)
                return Tree(Array{Tree,1}(undef,0), time, [rate], 1, true), n_max
            else
                is_dead = dead()
                if is_dead
                    #print("dead")
                    return Tree(Array{Tree,1}(undef,0), node_time, [rate], 1, false), n_max + 1
                else
                    offspring_rates = new_rates(rate)
                    left_time = time - node_time
                    #print("$depth ;")
                    left_tree = aux_ext(left_time, offspring_rates[1], n_max - 1, rates)
                    #print(left_tree[2])
                    if left_tree[1].n_nodes == -1
                        return left_tree
                    end
                    right_tree = aux_ext(left_time, offspring_rates[2], left_tree[2], rates)
                    if right_tree[1].n_nodes == -1
                        return right_tree
                    end
                    return Tree([left_tree[1], right_tree[1]], node_time, [rate]), right_tree[2]
                end
            end
        end

        rates = Array{Float64,1}(undef,0)

        tree = aux_ext(root_age, λ0, max_node_number, rates)[1]
        if (tree.n_nodes == -1) & return_if_max
            return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), Array{Float64,1}(undef,0)
        else
            return tree, rates
        end
    end

end

function sim_ClaDS2_time_unsampled_rates(root_age,σ,α,ε,λ0, u,fl ;
    sampling_proba = 1., return_if_extinct = true, max_node_number = 1_000,
    make_tree = false, prune_extinct = false)

    while true
        tree, rates, s = sim_ClaDS2_time_rates_tip(root_age,σ,α,ε,λ0,u,sampling_proba,fl,
            return_if_extinct = true, max_node_number = max_node_number,
            prune_extinct = false)
        if tree.n_nodes == -1
            return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), false, Inf, rates
        else
            n_alive = length(rates)
            if s < u
                return tree, false, n_alive, rates
            else
                return tree, true, n_alive, rates
            end
        end
    end
end

function sim_ClaDS2_time_rates_tip(root_age,σ,α,ε,λ0,u,sf,fl ; return_if_extinct = true, max_node_number = 1_000,
    make_tree = true, prune_extinct = false, return_if_max = true, return_na = false)

    # accesory functions that will be called latter
    #print(" $(ε/(1+ε))")
    bernouilli=Bernoulli(ε/(1+ε))
    function dead()
        return (rand(bernouilli)[1] == 1)
    end

    lambda_law = LogNormal(log(α),σ)
    function new_rates(λ)
        λ * rand(lambda_law, 2)
    end
    #=function new_rates(λ::Float64)
        X = (rand(lambda_law, 2))
        λ * X#* sort2!(X)
    end=#
    if return_na
    else
        function aux_ext(time, rate, n_max, rates, s ,n)
            if n_max == 0
                return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0,0,0
            end
            node_time = randexp()/(rate*(1+ε))
            #println(node_time)
            if node_time <= 0
                return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0,0,0
            end#print(" $s $n $u;")# $time $node_time $rate;")
            if node_time > time
                #print("zzzz")
                new_s = s
                #offsprings, branch_length, attributes, n_nodes
                push!(rates, rate)
                if n>0
                    new_s = s + log(1-sf)+log(n+1)-log(n)
                else
                    new_s = s + log(sf)
                end
                if new_s >= s
                    #print("a")
                    return Tree(Array{Tree,1}(undef,0), time, [rate], 1, true), n_max, new_s , n+1
                elseif u < new_s
                    #print("b")
                    return Tree(Array{Tree,1}(undef,0), time, [rate], 1, true), n_max, new_s , n+1
                else
                    #print("c")
                    return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0,new_s ,n+1
                end
            else
                is_dead = dead()
                if is_dead
                    return Tree(Array{Tree,1}(undef,0), node_time, [rate], 1, false), n_max + 1, s, n
                else
                    offspring_rates = new_rates(rate)
                    left_time = time - node_time
                    if time == left_time
                        return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), 0,0,0
                    end
                    #print("$depth ;")
                    left_tree = aux_ext(left_time, offspring_rates[1], n_max - 1, rates, s, n)
                    #print(left_tree[2])
                    if left_tree[1].n_nodes == -1
                        return left_tree
                    end
                    right_tree = aux_ext(left_time, offspring_rates[2], left_tree[2], rates, left_tree[3], left_tree[4])
                    if right_tree[1].n_nodes == -1
                        return right_tree
                    end
                    return Tree([left_tree[1], right_tree[1]], node_time, [rate]), right_tree[2], right_tree[3], right_tree[4]
                end
            end
        end

        rates = Array{Float64,1}(undef,0)

        tree = aux_ext(root_age, λ0, max_node_number, rates,fl,0)
        #if ((tree[1].n_nodes == -1) & return_if_max) | (tree[4] == 0) | (u > tree[3])
        #    return Tree(Array{Tree,1}(undef,0), -1., [-1], -1, false), Array{Float64,1}(undef,0)
        #else
        return tree[1], rates, tree[3]
        #end
    end

end

function update_edges_ETR2!(tree::Tree, edge_trees::Array{EdgeTreeRates2,1}, σ::Float64, α::Float64, ε::Float64, rates::Array{Float64,1},
    lefts::Array{Int64,1}; it_rates = 1, prior_ε = "lognormal", logε0 = 0., sd = 0.5)

    if prior_ε == "uniform"
        ε = draw_ε_crown_priorUnif(tree, edge_trees, lefts)
    elseif prior_ε == "uniformInf"
        ε = draw_ε_crown(tree, edge_trees, lefts)
    elseif prior_ε == "ClaDS0"
        ε = 0.
    else
        ε = draw_ε_crown_priorln(tree, edge_trees, lefts, logε0 = logε0, sd = sd)
    end

    for j in 1:it_rates

        draw_λi_quad!(rates, edge_trees, σ, α, ε, tree, lefts)
        relative_rates = extract_relative_rates(tree, edge_trees, rates)

        σ = draw_σ(relative_rates, α, β0 = 0.05, α0 = 0.5)
        α = draw_α(relative_rates, σ,  α_0 = -0.05, σ_0 = 0.1)

        if prior_ε == "uniform"
            ε = draw_ε_crown_priorUnif(tree, edge_trees, lefts)
        elseif prior_ε == "uniformInf"
            ε = draw_ε_crown(tree, edge_trees, lefts)
        elseif prior_ε == "ClaDS0"
            ε = 0.
        else
            ε = draw_ε_crown_priorln(tree, edge_trees, lefts, logε0 = logε0, sd = sd)
        end
    end

    return ε, σ, α
end


export isless

function isless(x :: JAC.ManyElectron.Level, y :: JAC.ManyElectron.Level)
    return x.energy < y.energy
end

function isless(x :: JAC.Hfs.IJF_Level, y :: JAC.Hfs.IJF_Level)
    return x.energy < y.energy
end

function isless(x :: JAC.Basics.AngularJ64, y :: JAC.Basics.AngularJ64)
    return x.num < y.num
end

"""
`Basics.isSimilar()`  ... returns true if two instances are similar to each other, and false otherwise.

  + `(keya::LevelKey, keyb::LevelKey, relAcc::Float64)`
    ... returns true if two level keys refer to the same level, i.e. level with the same symmetry and
        if the relative energy abs( (E_a - E_b)/E_a ) < relAcc. It returns false otherwise.
"""
function Basics.isSimilar(keya::LevelKey, keyb::LevelKey, relAcc::Float64)
    ##x println("** isSimilar abs() = $(abs( (keya.energy - keyb.energy)/keya.energy ))")
    if  keya.sym == keyb.sym   &&   abs( (keya.energy - keyb.energy)/keya.energy ) < relAcc    return(true)
    else                                                                                return(false)
    end
end

"""
`Basics.isStandardSubshellList(basis::Basis)`
    ... returns true if the subshell list basis.subshells is in standard order, and false otherwise.
        It requests especially that both subshells of the same shell (n,l) occur in the sequence j = l-1/2, j = l+1/2
        and that l increases before n increases.
"""
function Basics.isStandardSubshellList(basis::Basis)
    function  decimal(n,l,jnum)
        return( 10000n + 100l + jnum)
    end
    subshells = basis.subshells
    na = subshells[1].n;    la = Basics.subshell_l(subshells[1]);    ja = Basics.subshell_j(subshells[1])

    for  i = 2:length(subshells)
        nb = subshells[i].n;    lb = Basics.subshell_l(subshells[i]);    jb = Basics.subshell_j(subshells[1])
        if  decimal(nb,lb,jb.num) <= decimal(na,la,ja.num)    return( false )   end
    end

    return( true )
end