# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).
# Since these FMAs can increase the performance of many numerical algorithms,
# we need to opt-in explicitly.
# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.
@muladd begin

# `DGMulti` refers to both multiple DG types (polynomial/SBP, simplices/quads/hexes) as well as
# the use of multi-dimensional operators in the solver.
const DGMulti{NDIMS, ElemType, ApproxType, SurfaceIntegral, VolumeIntegral} =
  DG{<:RefElemData{NDIMS, ElemType, ApproxType}, Mortar, SurfaceIntegral, VolumeIntegral} where {Mortar}

# these are necessary for pretty printing
polydeg(dg::DGMulti) = dg.basis.N
Base.summary(io::IO, dg::DG) where {DG <: DGMulti} = print(io, "DGMulti(polydeg=$(polydeg(dg)))")
Base.real(rd::RefElemData{NDIMS, Elem, ApproxType, Nfaces, RealT}) where {NDIMS, Elem, ApproxType, Nfaces, RealT} = RealT

"""
    DGMulti(; polydeg::Integer,
              element_type::AbstractElemShape,
              approximation_type=Polynomial(),
              surface_flux=flux_central,
              surface_integral=SurfaceIntegralWeakForm(surface_flux),
              volume_integral=VolumeIntegralWeakForm(),
              RefElemData_kwargs...)

Create a discontinuous Galerkin method which uses
- approximations of polynomial degree `polydeg`
- element type `element_type` (`Tri()`, `Quad()`, `Tet()`, and `Hex()` currently supported)

Optional:
- `approximation_type` (default is `Polynomial()`; `SBP()` also supported for `Tri()`, `Quad()`,
  and `Hex()` element types).
- `RefElemData_kwargs` are additional keyword arguments for `RefElemData`, such as `quad_rule_vol`.
  For more info, see the [StartUpDG.jl docs](https://jlchan.github.io/StartUpDG.jl/dev/).
"""
function DGMulti(; polydeg::Integer,
                   element_type::AbstractElemShape,
                   approximation_type=Polynomial(),
                   surface_flux=flux_central,
                   surface_integral=SurfaceIntegralWeakForm(surface_flux),
                   volume_integral=VolumeIntegralWeakForm(),
                   kwargs...)
  rd = RefElemData(element_type, approximation_type, polydeg, kwargs...)
  return DG(rd, nothing #= mortar =#, surface_integral, volume_integral)
end

# Type aliases. The first parameter is `ApproxType` since it is more commonly used for dispatch.
const DGMultiWeakForm{ApproxType, ElemType} =
  DGMulti{NDIMS, ElemType, ApproxType, <:SurfaceIntegralWeakForm, <:VolumeIntegralWeakForm} where {NDIMS}

const DGMultiFluxDiff{ApproxType, ElemType} =
  DGMulti{NDIMS, ElemType, ApproxType, <:SurfaceIntegralWeakForm, <:VolumeIntegralFluxDifferencing} where {NDIMS}

# now that DGMulti is defined, we can define constructors for VertexMappedMesh which use dg::DGMulti
"""
    VertexMappedMesh(vertex_coordinates, EToV, dg::DGMulti;
                     is_on_boundary = nothing,
                     is_periodic::NTuple{NDIMS, Bool} = ntuple(_->false, NDIMS)) where {NDIMS, Tv}

Constructor which uses `dg::DGMulti` instead of `rd::RefElemData`.
"""
VertexMappedMesh(vertex_coordinates, EToV, dg::DGMulti; kwargs...) =
  VertexMappedMesh(vertex_coordinates, EToV, dg.basis; kwargs...)

"""
    VertexMappedMesh(triangulateIO, dg::DGMulti, boundary_dict::Dict{Symbol, Int})

Constructor which uses `dg::DGMulti` instead of `rd::RefElemData`.
"""
VertexMappedMesh(triangulateIO, dg::DGMulti, boundary_dict::Dict{Symbol, Int}) =
  VertexMappedMesh(triangulateIO, dg.basis, boundary_dict)

# Todo: simplices. Add traits for dispatch on affine/curved meshes here.

# Matrix type for lazy construction of physical differentiation matrices

# lazy linear combination of B = ∑_i coeffs[i] * A[i]
struct LazyMatrixLinearCombo{Tcoeffs, N, Tv, TA <: AbstractMatrix{Tv}} <: AbstractMatrix{Tv}
  matrices::NTuple{N, TA}
  coeffs::NTuple{N, Tcoeffs}
  function LazyMatrixLinearCombo(matrices, coeffs)
    @assert all(matrix -> size(matrix) == size(first(matrices)), matrices)
    new{typeof(first(coeffs)), length(matrices), eltype(first(matrices)), typeof(first(matrices))}(matrices, coeffs)
  end
end
Base.eltype(A::LazyMatrixLinearCombo) = eltype(first(A.matrices))
Base.IndexStyle(A::LazyMatrixLinearCombo) = IndexCartesian()
Base.size(A::LazyMatrixLinearCombo) = size(first(A.matrices))

@inline function Base.getindex(A::LazyMatrixLinearCombo{<:Real, N}, i, j) where {N}
  val = zero(eltype(A))
  for k in Base.OneTo(N)
    val = val + A.coeffs[k] * getindex(A.matrices[k], i, j)
  end
  return val
end

end # @muladd


#####
##### `AbstractDifferential`
#####

"""
The subtypes of `AbstractDifferential` define a custom \"algebra\" for chain
rule evaluation that attempts to factor various features like complex derivative
support, broadcast fusion, zero-elision, etc. into nicely separated parts.

All subtypes of `AbstractDifferential` implement the following operations:

`+(a, b)`: linearly combine differential `a` and differential `b`

`*(a, b)`: multiply the differential `a` by the differential `b`

`Base.conj(x)`: complex conjugate of the differential `x`

`extern(x)`: convert `x` into an appropriate non-`AbstractDifferential` type for
use outside of `ChainContext`.

Valid arguments to these operations are `T` where `T<:AbstractDifferential`, or
where `T` has proper `+` and `*` implementations.

Additionally, all subtypes of `AbstractDifferential` support `Base.iterate` and
`Base.Broadcast.broadcastable(x)`.
"""
abstract type AbstractDifferential end

Base.:+(x::AbstractDifferential) = x

"""
    extern(x)

Return `x` converted to an appropriate non-`AbstractDifferential` type, for use
with external packages that might not handle `AbstractDifferential` types.

Note that this function may return an alias (not necessarily a copy) to data
wrapped by `x`, such that mutating `extern(x)` might mutate `x` itself.
"""
@inline extern(x) = x

@inline Base.conj(x::AbstractDifferential) = x

#####
##### `Wirtinger`
#####

"""
    Wirtinger(primal::Union{Number,AbstractDifferential},
              conjugate::Union{Number,AbstractDifferential})

Returns a `Wirtinger` instance representing the complex differential:

```
df = ∂f/∂z * dz + ∂f/∂z̄ * dz̄
```

where `primal` corresponds to `∂f/∂z * dz` and `conjugate` corresponds to `∂f/∂z̄ * dz̄`.

The two fields of the returned instance can be accessed generically via the
[`wirtinger_primal`](@ref) and [`wirtinger_conjugate`](@ref) methods.
"""
struct Wirtinger{P,C} <: AbstractDifferential
    primal::P
    conjugate::C
    function Wirtinger(primal::Union{Number,AbstractDifferential},
                       conjugate::Union{Number,AbstractDifferential})
        return new{typeof(primal),typeof(conjugate)}(primal, conjugate)
    end
end

wirtinger_primal(x::Wirtinger) = x.primal
wirtinger_primal(x) = x

wirtinger_conjugate(x::Wirtinger) = x.conjugate
wirtinger_conjugate(::Any) = Zero()

extern(x::Wirtinger) = throw(ArgumentError("`Wirtinger` cannot be converted to an external type."))

Base.Broadcast.broadcastable(w::Wirtinger) = Wirtinger(broadcastable(w.primal),
                                                       broadcastable(w.conjugate))

Base.iterate(x::Wirtinger) = (x, nothing)
Base.iterate(::Wirtinger, ::Any) = nothing

# TODO: define `conj` for` `Wirtinger`
Base.conj(x::Wirtinger) = throw(MethodError(conj, x))

#####
##### `Zero`
#####

"""
    Zero()
The additive identity for differentials.
This is basically the same as `0`.
"""
struct Zero <: AbstractDifferential end

extern(x::Zero) = false  # false is a strong 0. E.g. `false * NaN = 0.0`

Base.Broadcast.broadcastable(::Zero) = Ref(Zero())

Base.iterate(x::Zero) = (x, nothing)
Base.iterate(::Zero, ::Any) = nothing

#####
##### `DoesNotExist`
#####

"""
    DoesNotExist()

This differential indicates that the derivative Does Not Exist (D.N.E).
This is not the cast that it is not implemented, but rather that it mathematically
is not defined.
"""
struct DoesNotExist <: AbstractDifferential end

function extern(x::DoesNotExist)
    throw(ArgumentError("Derivative does not exit. Cannot be converted to an external type."))
end

Base.Broadcast.broadcastable(::DoesNotExist) = Ref(DoesNotExist())

Base.iterate(x::DoesNotExist) = (x, nothing)
Base.iterate(::DoesNotExist, ::Any) = nothing

#####
##### `One`
#####

"""
     One()
The Differential which is the multiplicative identity.
Basically, this represents `1`.
"""
struct One <: AbstractDifferential end

extern(x::One) = true  # true is a strong 1.

Base.Broadcast.broadcastable(::One) = Ref(One())

Base.iterate(x::One) = (x, nothing)
Base.iterate(::One, ::Any) = nothing

#####
##### `AbstractThunk
#####
abstract type AbstractThunk <: AbstractDifferential end

Base.Broadcast.broadcastable(x::AbstractThunk) = broadcastable(extern(x))

@inline function Base.iterate(x::AbstractThunk)
    externed = extern(x)
    element, state = iterate(externed)
    return element, (externed, state)
end

@inline function Base.iterate(::AbstractThunk, (externed, state))
    element, new_state = iterate(externed, state)
    return element, (externed, new_state)
end

#####
##### `Thunk`
#####

"""
    Thunk(()->v)
A thunk is a deferred computation.
It wraps a zero argument closure that when invoked returns a differential.
`@thunk(v)` is a macro that expands into `Thunk(()->v)`.

Calling a thunk, calls the wrapped closure.
`extern`ing thunks applies recursively, it also externs the differial that the closure returns.
If you do not want that, then simply call the thunk

```
julia> t = @thunk(@thunk(3))
Thunk(var"##7#9"())

julia> extern(t)
3

julia> t()
Thunk(var"##8#10"())

julia> t()()
3
```

### When to `@thunk`?
When writing `rrule`s (and to a lesser exent `frule`s), it is important to `@thunk`
appropriately.
Propagation rule's that return multiple derivatives are not able to do all the computing themselves.
 By `@thunk`ing the work required for each, they then compute only what is needed.

#### So why not thunk everything?
`@thunk` creates a closure over the expression, which (effectively) creates a `struct`
with a field for each variable used in the expression, and call overloaded.

Do not use `@thunk` if this would be equal or more work than actually evaluating the expression itself. Examples being:
- The expression wrapping something in a `struct`, such as `Adjoint(x)` or `Diagonal(x)`
- The expression being a constant
- The expression being itself a `thunk`
- The expression being from another `rrule` or `frule` (it would be `@thunk`ed if required by the defining rule already)
"""
struct Thunk{F} <: AbstractThunk
    f::F
end

"""
    @thunk expr

Define a [`Thunk`](@ref) wrapping the `expr`, to lazily defer its evaluation.
"""
macro thunk(body)
    # Basically `:(Thunk(() -> $(esc(body))))` but use the location where it is defined.
    # so we get useful stack traces if it errors.
    func = Expr(:->, Expr(:tuple), Expr(:block, __source__, body))
    return :(Thunk($(esc(func))))
end

# have to define this here after `@thunk` and `Thunk` is defined
Base.conj(x::AbstractThunk) = @thunk(conj(extern(x)))

(x::Thunk)() = x.f()
@inline extern(x::Thunk) = extern(x())

Base.show(io::IO, x::Thunk) = println(io, "Thunk($(repr(x.f)))")

"""
    InplaceableThunk(val::Thunk, add!::Function)

A wrapper for a `Thunk`, that allows it to define an inplace `add!` function,
which is used internally in `accumulate!(Δ, ::InplaceableThunk)`.

`add!` should be defined such that: `ithunk.add!(Δ) = Δ .+= ithunk.val`
but it should do this more efficently than simply doing this directly.
(Otherwise one can just use a normal `Thunk`).

Most operations on an `InplaceableThunk` treat it just like a normal `Thunk`;
and destroy its inplacability.
"""
struct InplaceableThunk{T<:Thunk, F} <: AbstractThunk
    val::T
    add!::F
end

(x::InplaceableThunk)() = x.val()
@inline extern(x::InplaceableThunk) = extern(x.val)

function Base.show(io::IO, x::InplaceableThunk)
    println(io, "InplaceableThunk($(repr(x.val)), $(repr(x.add!)))")
end

# The real reason we have this:
accumulate!(Δ, ∂::InplaceableThunk) = ∂.add!(Δ)
store!(Δ, ∂::InplaceableThunk) = ∂.add!((Δ.*=false))  # zero it, then add to it.

"""
    NO_FIELDS

Constant for the reverse-mode derivative with respect to a structure that has no fields.
The most notable use for this is for the reverse-mode derivative with respect to the
function itself, when that function is not a closure.
"""
const NO_FIELDS = DoesNotExist()

"""
    refine_differential(𝒟::Type, der)

Converts, if required, a differential object `der`
(e.g. a `Number`, `AbstractDifferential`, `Matrix`, etc.),
to another  differential that is more suited for the domain given by the type 𝒟.
Often this will behave as the identity function on `der`.
"""
function refine_differential(::Type{<:Union{<:Real, AbstractArray{<:Real}}}, w::Wirtinger)
    return wirtinger_primal(w) + wirtinger_conjugate(w)
end
refine_differential(::Any, der) = der  # most of the time leave it alone.


################################################################################
##############################    Tuple Stuff    ###############################
################################################################################
for op in [:typemin,:typemax]
    @eval Base.$op(::Type{Tuple{A,B,C,D,E,F}}) where {A,B,C,D,E,F}    = map(i->$op(i),(A,B,C,D,E,F))
    @eval Base.$op(::Type{Tuple{A,B,C,D,E}}) where {A,B,C,D,E}        = map(i->$op(i),(A,B,C,D,E))
    @eval Base.$op(::Type{Tuple{A,B,C,D}}) where {A,B,C,D}            = map(i->$op(i),(A,B,C,D))
    @eval Base.$op(::Type{Tuple{A,B,C}}) where {A,B,C}                = map(i->$op(i),(A,B,C))
    @eval Base.$op(::Type{Tuple{A,B}}) where {A,B}                    = map(i->$op(i),(A,B))
    @eval Base.$op(::Type{Tuple{A}}) where {A}                        = map(i->$op(i),(A,))
end
Base.Tuple{A,B,C,D,E,F}() where {A,B,C,D,E,F}    = map(i->i(0),(A,B,C,D,E,F))
Base.Tuple{A,B,C,D,E}() where {A,B,C,D,E}        = map(i->i(0),(A,B,C,D,E))
Base.Tuple{A,B,C,D}() where {A,B,C,D}            = map(i->i(0),(A,B,C,D))
Base.Tuple{A,B,C}() where {A,B,C}                = map(i->i(0),(A,B,C))
Base.Tuple{A,B}() where {A,B}                    = map(i->i(0),(A,B))
Base.Tuple{A}() where {A}                        = map(i->i(0),(A))

Base.typemin(::Type{NTuple{N,R}}) where {N,R} = NTuple{N,R}(map(i->typemin(R),1:N))
Base.typemax(::Type{NTuple{N,R}}) where {N,R} = NTuple{N,R}(map(i->typemax(R),1:N))
Base.NTuple{N,R}() where {N,R} = NTuple{N,R}(map(i->R(0), 1:N))
Base.NTuple{N,R}(val::R) where {N,R} = NTuple{N,R}(map(i->val, 1:N))
Base.typemin(c::Tuple) = typemin(typeof(c))
Base.typemax(c::Tuple) = typemax(typeof(c))

for op = (:(>), :(<), :(>=), :(<=))
    eval(quote
        Base.$op(a::T,b::R) where {N,T<:Tuple,R<:Real} = $op(a[1],b)
        Base.$op(b::R,a::T) where {N,T<:Tuple,R<:Real} = $op(b,a[1])
    end)
end

################################################################################
############################### Cost Model Stuff ###############################
################################################################################
export
    get_initial_cost,
    get_infeasible_cost,
    compute_path_cost,
    # get_transition_cost,
    accumulate_cost,
    add_heuristic_cost,
    aggregate_costs,

    AggregationFunction,
    MaxCost,
    SumCost,

    FullCostModel,
    get_aggregation_function,
    get_cost_model,
    cost_type,

    MetaCostModel,
    MetaCost,
    CompositeCostModel,
    construct_composite_cost_model,

    LowLevelCostModel,

    TravelTime,
    TravelDistance,
    FinalTime,
    NullCost,
    MakeSpan,
    SumOfTravelDistance,
    SumOfTravelTime

"""
    get_initial_cost(model)

Part of cost model interface. Defaults to zero.
"""
get_initial_cost(model)     = cost_type(model)(0)
# get_initial_cost(model::AbstractCostModel)     = cost_type(model)(0)

"""
    get_infeasible_cost(model)

Part of cost model interface. Defaults to zero.
"""
get_infeasible_cost(model)  = typemax(cost_type(model))
# get_infeasible_cost(model::AbstractCostModel)  = typemax(cost_type(model))

"""
    compute_path_cost(model,env,path,i)

Compute the cost of a path from scratch.
"""
compute_path_cost(model,env,path,i) = cost_type(model)(0)

# """
#     get_transition_cost(model,env,s,a,sp)
#     get_transition_cost(env,s,a,sp)
#     get_transition_cost(env,s,a)
#
# Part of cost model interface. Defines the cost of transitioning from state `s`
# to state `sp` via action `a` in environment `env` under cost model `model`.
# """
# function get_transition_cost end

"""
    accumulate_cost(model,current_cost,transition_cost)

Defines the way that a `transition_cost` updates the `current_cost` of a `Path`.
"""
function accumulate_cost end

"""
    add_heuristic_cost(cost_model,heuristic_model,cost,h_cost)

Defines the output heuristic cost that results from combining the current path
cost `cost` with a heuristic "cost-to-go" `h_cost`. Gener
"""
function add_heuristic_cost end
add_heuristic_cost(::C, cost, h_cost) where {C<:AbstractCostModel} = cost + h_cost
add_heuristic_cost(m::C, ::E, cost, h_cost) where {C<:AbstractCostModel,E<:AbstractLowLevelEnv} = add_heuristic_cost(m,cost,h_cost)
add_heuristic_cost(m::C, ::H, cost, h_cost) where {C<:AbstractCostModel,H<:AbstractCostModel}   = add_heuristic_cost(m,cost,h_cost)
add_heuristic_cost(m::C, h::H, ::E, cost, h_cost) where {C<:AbstractCostModel,H<:AbstractCostModel,E<:AbstractLowLevelEnv} = add_heuristic_cost(m,h,cost,h_cost)
add_heuristic_cost(env::E, cost, h_cost) where {E<:AbstractLowLevelEnv} = add_heuristic_cost(get_cost_model(env),get_heuristic_model(env),env,cost,h_cost)

"""
    compute_heuristic_cost(env,cost,sp)

Defaults to `add_heuristic_cost(env,cost,get_heuristic_cost(env,sp))`
Can be overridden so that state info can inform the heuristic cost directly.
"""
compute_heuristic_cost(m::C,h::H,env::E,cost,sp) where {C<:AbstractCostModel,H<:AbstractCostModel,E<:AbstractLowLevelEnv} = add_heuristic_cost(m,h,env,cost,get_heuristic_cost(h,env,sp))
compute_heuristic_cost(m::C,env::E,cost,sp) where {C<:AbstractCostModel,E<:AbstractLowLevelEnv} = add_heuristic_cost(m,env,cost,get_heuristic_cost(env,sp))
compute_heuristic_cost(env::E,args...) where {E<:AbstractLowLevelEnv} = compute_heuristic_cost(get_cost_model(env),get_heuristic_model(env),env,args...)

get_initial_cost(env::E) where {E<:AbstractLowLevelEnv}     = get_initial_cost(get_cost_model(env))
get_infeasible_cost(env::E) where {E<:AbstractLowLevelEnv}  = get_infeasible_cost(get_cost_model(env))
accumulate_cost(env::E, cost, transition_cost) where {E<:AbstractLowLevelEnv} = accumulate_cost(get_cost_model(env), cost, transition_cost)
# add_heuristic_cost(env::E, cost, h_cost) where {E<:AbstractLowLevelEnv} = add_heuristic_cost(env,get_cost_model(env),cost,h_cost)
get_transition_cost(env::E,s,a) where {E<:AbstractLowLevelEnv} = get_transition_cost(env,s,a,get_next_state(env,s,a))
get_transition_cost(env::E,s,a,sp) where {E<:AbstractLowLevelEnv} = get_transition_cost(get_cost_model(env),env,s,a,sp)

"""
    aggregate_costs(model, costs::Vector{T}) where {T}

Defines how costs from multiple distinct paths are combined into a single cost
for the whole solution. For example, the aggregation function for a
`SumOfTravelTime` objective is the `sum` of individual cost.
"""
function aggregate_costs end

"""
    a special version of aggregate_costs for the meta_env
"""
aggregate_costs_meta(m::AbstractCostModel,args...) = aggregate_costs(m,args...)

abstract type AggregationFunction end
struct MaxCost <: AggregationFunction end
(f::MaxCost)(costs...) = maximum(costs...)
struct SumCost <: AggregationFunction end
(f::SumCost)(costs...) = sum(costs...)

"""
    FullCostModel{F,T,M<:AbstractCostModel{T}} <: AbstractCostModel{T}

The `FullCostModel` defines all the behavior required for running CBS-based
algorithms.

Elements:
- f::F must be callable, and defines how a vector of path costs (e.g., the
    paths of a solution) should be combined into a single cost that reflects
    the cost of the entire path group together
- model::M<:AbstractCostModel{T} defines how the cost is computed during low
level search (when individual paths are being compared against each other).
"""
struct FullCostModel{F,T,M<:AbstractCostModel{T}} <: AbstractCostModel{T}
    f::F        # the aggregation function
    model::M    # the low level cost model
end
get_aggregation_function(m::FullCostModel)  = m.f
aggregate_costs(m::FullCostModel, costs)    = m.f(costs)
get_cost_model(m::FullCostModel)            = m.model
for op in [:accumulate_cost,:get_initial_cost,:compute_path_cost,
    :get_infeasible_cost,:add_heuristic_cost,:get_transition_cost]
    @eval $op(model::FullCostModel,args...) = $op(model.model,args...)
end
for T in [:FullCostModel]
    @eval begin
        add_heuristic_cost(m::$T, h::H, env::E, args...) where {H<:AbstractCostModel,E<:AbstractLowLevelEnv} = add_heuristic_cost(m.model, h, env, args...)
    end
end

"""
    CompositeCostModel{T}

Combines multiple cost models in a specific order (i.e., to use for cascaded
tie-breaking).
"""
struct CompositeCostModel{M<:Tuple,T<:Tuple} <: AbstractCostModel{T}
    cost_models::M
end
function construct_composite_cost_model(args...)
    models = Tuple(args)
    for m in models
        @assert typeof(m) <: AbstractCostModel
    end
    cost_types = map(m->cost_type(m),models)
    CompositeCostModel{typeof(models),Tuple{cost_types...}}(models)
end
# function get_transition_cost(model::C,env,s,a,sp) where {C<:CompositeCostModel}
#     cost_type(model)(map(m->get_transition_cost(m,env,s,a,sp), model.cost_models))
# end
function accumulate_cost(model::C, cost::T, transition_cost::T) where {T,M,C<:CompositeCostModel{M,T}}
    new_cost = map(x->accumulate_cost(x[1],x[2],x[3]),
    zip(model.cost_models, cost, transition_cost))
    T(new_cost)
end
function aggregate_costs(model::C, costs::Vector{T}) where {T,M,C<:CompositeCostModel{M,T}}
    aggregated_costs = map(
        i->aggregate_costs(model.cost_models[i], map(c->c[i], costs)), 1:length(model.cost_models))
    T(aggregated_costs)
end
function aggregate_costs_meta(model::C, costs::Vector{T}) where {T,M,C<:CompositeCostModel{M,T}}
    aggregated_costs = map(
        i->aggregate_costs_meta(model.cost_models[i], map(c->c[i], costs)), 1:length(model.cost_models))
    T(aggregated_costs)
end
for op in [:get_initial_cost,:get_infeasible_cost,:get_transition_cost,
    :compute_path_cost]
    @eval $op(model::CompositeCostModel,args...) = cost_type(model)(map(m->$op(m,args...),model.cost_models))
end
function add_heuristic_cost(model::C, cost::T, h_cost) where {T,M,C<:CompositeCostModel{M,T}}
    T(map(
        i->add_heuristic_cost(model.cost_models[i], cost[i], h_cost[i]),
        1:length(cost)
        ))
end
function compute_heuristic_cost(m::C,h::H,env::E,cost,s) where {C<:CompositeCostModel,H<:AbstractCostModel,E<:AbstractLowLevelEnv}
    # T([compute_heuristic_cost(env,m,c,s) for (m,c) in zip(model.cost_models,cost)])
    cost_type(m)(map(
        i->compute_heuristic_cost(
            m.cost_models[i],
            h.cost_models[i],
            env,
            cost[i],
            s),
        1:length(cost)
        ))
end

"""
    MetaCost

`MetaCost` maintaining separate costs for individual agents that have been
combined into a MetaAgent.
- independent_costs::Vector{T} a vector of costs, 1 per agent
- total_cost::T the total cost, which reflects the combined cost (its
    interpretation depends on the `MetaCostModel` used to define cost-
    upating behavior)
"""
struct MetaCost{T}
    independent_costs::Vector{T}
    total_cost::T
end
Base.isless(m1::MetaCost,m2::MetaCost) = m1.total_cost < m2.total_cost

"""
    MetaCostModel

Defines the cost-updating behavior of `MetaCost` for MetaAgent applications.
"""
struct MetaCostModel{T,M<:AbstractCostModel{T}} <: AbstractCostModel{MetaCost{T}}
    model::M
    num_agents::Int
end
aggregate_costs(m::C, costs::Vector{T}) where {T,C<:MetaCostModel} = aggregate_costs(m.model, costs)
# function add_heuristic_cost(m::C, cost, h_cost) where {C<:MetaCostModel}
#     costs = map(i->add_heuristic_cost(
#         m.model,
#         cost.independent_costs[i],
#         h_cost[i]),1:m.num_agents)
#     MetaCost(costs, aggregate_costs_meta(m.model, costs))
# end
function accumulate_cost(model::M, cost::MetaCost{T}, transition_cost::Vector{T}) where {T,M<:MetaCostModel}
    new_costs = Vector{T}()
    for (i,(c1,c2)) in enumerate(zip(
        cost.independent_costs,
        transition_cost))
        push!(new_costs, accumulate_cost(model.model, c1, c2))
    end
    total_cost = aggregate_costs_meta(model.model,new_costs)
    new_cost = MetaCost{T}(new_costs,total_cost)
    return new_cost
end
function accumulate_cost(model::M, cost::MetaCost{T}, transition_cost::MetaCost{T}) where {T,M<:MetaCostModel}
    accumulate_cost(model, cost, transition_cost.independent_costs)
end
function get_initial_cost(model::C) where {C<:MetaCostModel}
    costs = map(a->get_initial_cost(model.model),1:model.num_agents)
    MetaCost(costs,aggregate_costs(model,costs))
end
function get_infeasible_cost(model::C) where {C<:MetaCostModel}
    costs = map(a->get_infeasible_cost(model.model),1:model.num_agents)
    MetaCost(costs,aggregate_costs(model,costs))
end

################################################################################
############################## Atomic Cost Models ##############################
################################################################################

"""
    LowLevelCostModel{C}

The low level cost model defines the objective to be optimized by the
solver at the low level. An optimal low level solver will return a path if a
feasible path exists) of minimal cost under the objective specified by the
associated LowLevelCostModel.
The parameter `C` defines the `cost_type` of the objective. The following
functions must be implemented for a `LowLevelCostModel` to be used:
* `get_initial_cost(model::LowLevelCostModel,env)` - returns
the initial_cost for a path
* `get_transition_cost(model::LowLevelCostModel{C},path::Path,s::S,a::A,
    sp::S) where {S,A,C}` - defines the cost associated with taking action
    `a` from state `s` to arrive in state `sp` according to the objective
    defined by `model` given that `s` is the "tip" of `path`.
* `accumulate_cost(model::LowLevelCostModel{C}, current_cost::C,
    transition_cost::C)` - defines how cost accumulates as new `PathNode`s
    are added to a Path.
"""
abstract type LowLevelCostModel{T} <: AbstractCostModel{T} end

export
    TransformCostModel

"""
    TransformCostModel{T,M<:LowLevelCostModel{T}} <: LowLevelCostModel{T}

Applies a transformation to an underlying cost model.
    e.g., `TransformCostModel(c->2*c, TravelTime())`
"""
struct TransformCostModel{T,M<:LowLevelCostModel{T}} <: LowLevelCostModel{T}
    f::Function
    model::M
end
# get_transition_cost(m::TransformCostModel,args...) = m.f(get_transition_cost(m.model,args...))
# accumulate_cost(m::TransformCostModel, args...)     = accumulate_cost(m.model,args...)
# get_initial_cost(m::TransformCostModel,args...)     = get_initial_cost(m.model,args...)
# get_infeasible_cost(m::TransformCostModel)          = get_infeasible_cost(m.model)
# add_heuristic_cost(m::TransformCostModel,args...)   = add_heuristic_cost(m.model,args...)
for op in [:accumulate_cost,:get_initial_cost,
    :get_infeasible_cost,:add_heuristic_cost,:get_transition_cost]
    @eval $op(model::TransformCostModel,args...) = model.f($op(model.model,args...))
end
# function compute_heuristic_cost(m::TransformCostModel,env::E,args...) where {E<:AbstractLowLevelEnv}
#     m.f(compute_heuristic_cost(m.model,env,args...))
# end
for op in [:add_heuristic_cost,:compute_heuristic_cost]
    @eval begin
        function $op(m::TransformCostModel, h::H, env::E, args...) where {H<:AbstractCostModel,E<:AbstractLowLevelEnv}
            m.f($op(m.model, h, env, args...))
        end
        function $op(m::TransformCostModel, env::E, args...) where {E<:AbstractLowLevelEnv}
            m.f($op(m.model, env, args...))
        end
    end
end
# add_heuristic_cost(m::TransformCostModel, h::H, env::E, args...) where {H<:AbstractCostModel,E<:AbstractLowLevelEnv} = m.f(add_heuristic_cost(m.model, h, env, args...))
# compute_heuristic_cost(m::TransformCostModel, h::H, env::E, args...) where {H<:AbstractCostModel,E<:AbstractLowLevelEnv} = m.f(add_heuristic_cost(m.model, h, env, args...))

"""
    TravelTime <: LowLevelCostModel{Float64}

Cost model that assigns cost equal to the duration of a path.
"""
struct TravelTime       <: LowLevelCostModel{Float64} end
accumulate_cost(model::TravelTime,      cost,transition_cost) = cost+transition_cost

"""
    TravelDistance <: LowLevelCostModel{Float64}

Cost model that assigns cost equal to the length (distance) of a path.
"""
struct TravelDistance   <: LowLevelCostModel{Float64} end
accumulate_cost(model::TravelDistance,  cost,transition_cost) = cost+transition_cost

struct FinalTime        <: LowLevelCostModel{Float64} end
accumulate_cost(model::FinalTime,       cost,transition_cost) = cost+transition_cost

"""
    NullCost <: LowLevelCostModel{Float64}

Cost equal to `0.0`.
"""
struct NullCost         <: LowLevelCostModel{Float64} end
get_transition_cost(model::F,env,s,a,sp) where {F<:NullCost} = 0.0
accumulate_cost(model::NullCost,        cost,transition_cost) = cost

export
    DeadlineCost,
    set_deadline!,
    FullDeadlineCost,
    # WeightedDeadlinesCost,
    MultiDeadlineCost,
    SumOfMakeSpans

abstract type AbstractDeadlineCost <: LowLevelCostModel{Float64} end
"""
    DeadlineCost

Identical to `TravelTime`, except for the behavior of
`add_heuristic_cost`.

add_heuristic_cost: `c = max(0.0, t + Δt - deadline)`
"""
mutable struct DeadlineCost     <: AbstractDeadlineCost
    deadline::Float64 # deadline
    m::TravelTime
end
DeadlineCost(deadline::R) where {R<:Real} = DeadlineCost(deadline,TravelTime())
function set_deadline!(m::DeadlineCost,t_max)
    m.deadline = minimum(t_max)
    return m
end
set_deadline!(m::C,args...) where {C<:AbstractCostModel} = nothing
set_deadline!(m::C,args...) where {C<:FullCostModel} = set_deadline!(m.model,args...)
set_deadline!(m::C,args...) where {C<:MetaCostModel} = set_deadline!(m.model,args...)
function set_deadline!(m::C,args...) where {C<:CompositeCostModel}
    for model in m.cost_models
        set_deadline!(model,args...)
    end
end
add_heuristic_cost(m::C, cost, h_cost) where {C<:DeadlineCost} = max(0.0, cost + h_cost - m.deadline) # assumes heuristic is PerfectHeuristic
for op in [:accumulate_cost,:get_initial_cost,:get_transition_cost,:compute_path_cost]
    @eval $op(model::AbstractDeadlineCost,args...) = $op(model.m,args...)
end
FullDeadlineCost(model::DeadlineCost) = FullCostModel(costs->max(0.0, maximum(costs)),model)

"""
    MultiDeadlineCost

Combines multiple deadlines according to some specified aggregation function.
"""
struct MultiDeadlineCost{F} <: AbstractDeadlineCost
    f::F # aggregation function
    tF::Vector{Float64}
    root_nodes::Vector{Int} # weights and deadlines correspond to root_nodes only
    weights::Vector{Float64}
    deadlines::Vector{Float64}
    m::TravelTime
end
const SumOfMakeSpans = MultiDeadlineCost{SumCost}
const MakeSpan = MultiDeadlineCost{MaxCost}
SumOfMakeSpans(tF,root_nodes,weights,deadlines) = MultiDeadlineCost(SumCost(),Float64.(tF),root_nodes,Float64.(weights),Float64.(deadlines),TravelTime())
SumOfMakeSpans() = MultiDeadlineCost(SumCost(),Float64[],Int[],Float64[],Float64[],TravelTime())
MakeSpan(tF,root_nodes,weights,deadlines) = MultiDeadlineCost(MaxCost(),Float64.(tF),root_nodes,Float64.(weights),Float64.(deadlines),TravelTime())
MakeSpan() = MultiDeadlineCost(MaxCost(),Float64[],Int[],Float64[],Float64[],TravelTime())
function set_deadline!(m::M,t_max) where {M<:MultiDeadlineCost}
    m.deadlines .= t_max
    return m
end
add_heuristic_cost(m::C, cost, h_cost) where {C<:MultiDeadlineCost} = m.f(m.weights .* max.(0.0, cost .+ h_cost .- m.deadlines)) # assumes heuristic is PerfectHeuristic
aggregate_costs(m::C, costs::Vector{T}) where {T,C<:MultiDeadlineCost}  = m.f(m.tF[m.root_nodes] .* m.weights)
aggregate_costs_meta(m::C, costs::Vector{T}) where {T,C<:MultiDeadlineCost}  = maximum(costs)

# MakeSpan(model::FinalTime=FinalTime()) = FullCostModel(maximum,model)
SumOfTravelDistance(model::TravelDistance=TravelDistance()) = FullCostModel(sum,model)
SumOfTravelTime(model::TravelTime=TravelTime()) = FullCostModel(sum, model)

export EnvDeadlineCost
"""
    EnvDeadlineCost

Deadlines come from an external environment
"""
@with_kw struct EnvDeadlineCost{E,F} <: AbstractDeadlineCost
    env::E = nothing
    f::F   = SumCost()          # aggregation function
    m::TravelTime = TravelTime()
end
# EnvDeadlineCost() = EnvDeadlineCost(SumCost(),nothing,TravelTime())
EnvDeadlineCost{E,F}() where {E,F} = EnvDeadlineCost(env=E(),f=F())
aggregate_costs(::EnvDeadlineCost,costs) = maximum(costs)

################################################################################
############################# HardConflictHeuristic ############################
################################################################################
export
    AbstractConflictTable,
    HardConflictTable,
    get_time_horizon,
    get_planned_vtx,
    reset_path!,
    set_path!,
    partially_set_path!,
    get_conflict_value,
    get_conflicting_paths,
    construct_empty_lookup_table

abstract type AbstractConflictTable end

"""
    HardConflictTable

Stores a lookup table of planned paths for all agents.
When agent `i` queries the table, `table.paths[i]` (the existing path plan
for agent `i`) must be subtracted so that the agent does not try to avoid
conflicts with itself.
"""
@with_kw struct HardConflictTable{V<:AbstractVector,M<:AbstractMatrix} <: AbstractConflictTable
    paths   ::Vector{V} = Vector{SparseVector{Int,Int}}()
    CAT     ::M         = SparseMatrixCSC(zeros(2,2)) # global table
    start_time::Int     = 0
end
get_time_horizon(h::T) where {T<:HardConflictTable} = size(h.CAT,2)
function get_planned_vtx(h::T,agent_idx::Int,t::Int) where {T<:HardConflictTable}
    t_idx = t + (1-h.start_time)
    if agent_idx == -1
        return 0
    else
        return get(h.paths[agent_idx], t_idx, -1)
    end
end
function reset_path!(h::T,path_idx::Int) where {V,M,T<:HardConflictTable{V,M}}
    # first remove the old path from the lookup table
    for (t,vtx) in enumerate(h.paths[path_idx])
        if vtx > 0
            h.CAT[vtx,t] = h.CAT[vtx,t] - 1
        end
    end
    # initialize an empty new path
    h.paths[path_idx] = V(zeros(Int,get_time_horizon(h)))
    return h
end
function set_path!(h::T,path_idx::Int,path::Vector{Int},start_time::Int=0) where {V,M,T<:HardConflictTable{V,M}}
    # println("Updating conflict table with path ", path, " for agent ",path_idx)
    reset_path!(h,path_idx)
    # now add new path vtxs to new path and lookup table
    for (i,vtx) in enumerate(path)
        t = (start_time-h.start_time) + i
        h.paths[path_idx][t] = vtx
        h.CAT[vtx,t] = h.CAT[vtx,t] + 1
    end
    h
end
"""
    partially_set_path!

Only replaces the cached path from start_time to length(path). Useful if you
want the remainder of the cached path to stay in the lookup table (i.e. for
repairing an existing plan).
"""
function partially_set_path!(h::T,path_idx::Int,path::Vector{Int},start_time::Int=0) where {V,M,T<:HardConflictTable{V,M}}
    # now add new path vtxs to new path and lookup table
    t0 = (start_time-h.start_time)
    for (i,vtx) in enumerate(path)
        t = t0 + i
        # try
        checkbounds(Bool,h.paths[path_idx],t) ? nothing : throw(SolverException("t = $(t) Out of bounds in partially_set_path! with start_time = $(h.start_time)"))
        old_vtx = h.paths[path_idx][t]
        if old_vtx != 0
            h.CAT[old_vtx,t] = h.CAT[old_vtx,t] - 1
        end
        h.paths[path_idx][t] = vtx
        h.CAT[vtx,t] = h.CAT[vtx,t] + 1
        # catch e
        #     throw(e)
        # end
    end
    h
end
function get_conflict_value(h::HardConflictTable,agent_idx::Int,vtx::Int,t::Int)
    t_idx = t + (1-h.start_time)
    try
        # c = h.CAT[vtx,t_idx]
        c = get(h.CAT, (vtx,t_idx), 0)
        if get_planned_vtx(h,agent_idx,t) == vtx # conflict with own trajectory
            c = c - 1
        end
        return c
    catch e
        @show vtx,t_idx,size(h.CAT)
        rethrow(e)
    end
end

"""
    get_conflicting_paths

Operates over a lookup table and returns a dictionary mapping path index to the
time index at which the conflict occurred
"""
function get_conflicting_paths(ct::T) where {T<:HardConflictTable}
    conflict_idxs = map(idx->Int.([idx.I...]), findall(ct.CAT .> 1))
    agent_id_to_time_idx = Dict{Int,Int}()
    for idx in conflict_idxs
        vtx = idx[1]
        t = idx[2]+ct.start_time-1
        for (agent_id,path) in enumerate(ct.paths)
            if get_planned_vtx(ct,agent_id,t) == vtx
                agent_id_to_time_idx[agent_id] = t
            end
        end
    end
    agent_id_to_time_idx
end

"""
    construct_empty_lookup_table(G,T::Int)

Returns an empty lookup table.
"""
construct_empty_lookup_table(V::Int,T::Int) = SparseMatrixCSC(zeros(V,T))
construct_empty_lookup_table(graph::G,T::Int) where {G<:AbstractGraph} = construct_empty_lookup_table(nv(graph),T)
function HardConflictTable(graph::G,T::Int,num_agents::Int) where {G<:AbstractGraph}
    HardConflictTable(
        paths = map(i->SparseVector(zeros(Int,T)),1:num_agents),
        CAT = construct_empty_lookup_table(graph,T)
        )
end
function HardConflictTable(graph::G,T::Float64,num_agents::Int) where {G<:AbstractGraph}
    @assert abs(T - Int(round(T))) < 0.01
    HardConflictTable(graph,Int(round(T)),num_agents)
end

################################################################################
############################### SoftConflictHeuristic ##############################
################################################################################
export
    SoftConflictTable,
    get_fat_path,
    add_fat_path_to_table!,
    populate_soft_lookup_table!

"""
    SoftConflictTable
"""
@with_kw struct SoftConflictTable{V,M<:AbstractMatrix} <: AbstractConflictTable
    paths   ::Vector{V} = Vector{SparseMatrixCSC{Float64,Int}}()
    CAT     ::M         = SparseMatrixCSC(zeros(2,2)) # global table
    start_time::Int     = 0
end
get_time_horizon(h::SoftConflictTable) = size(h.CAT,2)
get_conflict_value(h::SoftConflictTable,vtx::Int,t::Int) = h.CAT[vtx,t]
get_conflict_value(h::SoftConflictTable,agent_idx::Int,vtx::Int,t::Int) = h.CAT[vtx,t] - h.paths[agent_idx][vtx,t]

"""
    get_fat_path(G,D,start_vtx,goal_vtx)

returns a fat path through `G` from `start_vtx` to `goal_vtx`. Each set
of vertices in the fat path contains all vertices with distance d1 from
start_vtx and distance d2 to goal_vtx, where d1+d2 == the length of the
shortest path(s) from `start_vtx` to `goal_vtx`

G is a graph, D is the distance matrix
"""
function get_fat_path(G,D,start_vtx::Int,goal_vtx::Int)
    fat_path = Vector{Set{Int}}([Set{Int}(start_vtx)])
    for i in 1:D[start_vtx,goal_vtx]
        next_set = Set{Int}()
        for src_vtx in fat_path[end]
            for dst_vtx in outneighbors(G,src_vtx)
                if D[dst_vtx,goal_vtx] <= D[src_vtx,goal_vtx] - 1
                    push!(next_set, dst_vtx)
                end
            end
        end
        push!(fat_path, next_set)
    end
    fat_path
end

"""
    add_fat_path_to_table(CAT,fat_path)
"""
function add_fat_path_to_table!(CAT,fat_path,t0=0)
    for t in 1:length(fat_path)
        idxs = collect(fat_path[t])
        if t+t0 > 0
            CAT[idxs,t+t0] .+= 1.0/length(idxs)
        end
    end
end

"""
    populate_soft_lookup_table!(CAT,start_times,start_vtxs,goal_vtxs)
"""
function populate_soft_lookup_table!(CAT,G,D,start_vtxs,goal_vtxs,start_times=zeros(Int,length(start_vtxs)))
    for (s,t,g) in zip(start_vtxs,start_times,goal_vtxs)
        fat_path = get_fat_path(G,D,s,g)
        add_fat_path_to_table!(CAT,t0,fat_path)
    end
    CAT
end

"""
    construct_empty_lookup_table(graph,T::Int)

Returns a soft lookup table to encode possible paths for each agent through
`graph`. The argument `T` defines the time horizon of the lookup table.
"""
function SoftConflictTable(n_agents::Int,N::Int,T::Int,t0=0)
    SoftConflictTable(
        paths = map(i->spzeros(Float64,N,T),1:n_agents),
        CAT = spzeros(Float64,N,T),
        t0=t0
        )
end
function SoftConflictTable(n_agents::Int,N::Int,T::Float64,t0=0)
    @assert abs(T - Int(round(T))) < 0.01
    @assert abs(t0 - Int(round(t0))) < 0.01
    SoftConflictTable(n_agents,N,Int(round(T)),Int(round(t0)))
end

# """
#     `construct_empty_lookup_table(graph,T::Int)`
#
#     Returns a soft lookup table to encode possible paths for each agent through
#     `graph`. The argument `T` defines the time horizon of the lookup table.
# """
# function SoftConflictTable(graph,n_agents::Int,T::Int,t0=0)
#     SoftConflictTable(
#         paths = map(i->construct_empty_lookup_table(graph,T),1:n_agents),
#         CAT = construct_empty_lookup_table(graph,T),
#         t0=0
#         )
# end

# """
#     `construct_and_populate_soft_lookup_table!`
# """
# function SoftConflictTable(graph,
#         start_vtxs::Vector{Int},
#         goal_vtxs::Vector{Int},
#         start_times=zero(Int,length(start_vtxs),
#         T = Int(round(maximum(start_times) + nv(graph))),
#         )
#     )
#     CAT = construct_empty_lookup_table(graph,T)
#     D = get_dist_matrix(graph)
#     populate_soft_lookup_table!(CAT,graph,D,start_vtxs,goal_vtxs,start_times)
#     SoftConflictTable(CAT)
# end

################################################################################
############################# ConflictCostModel ############################
################################################################################
export
    ConflictCostModel,
    HardConflictCost,
    SoftConflictCost

struct ConflictCostModel{T<:Union{HardConflictTable,SoftConflictTable}} <: LowLevelCostModel{Float64}
    table::T
end
get_conflict_value(h::H, args...) where {H<:ConflictCostModel} = get_conflict_value(h.table, args...)
accumulate_cost(h::H,cost,transition_cost) where {H<:ConflictCostModel} = cost + transition_cost

HardConflictCost(args...) = FullCostModel(sum,ConflictCostModel(HardConflictTable(args...)))
SoftConflictCost(args...) = FullCostModel(sum,ConflictCostModel(SoftConflictTable(args...)))
SoftConflictCost(table::SoftConflictTable,args...) = FullCostModel(sum,ConflictCostModel(table))

get_time_horizon(h::H) where {H<:ConflictCostModel} = get_time_horizon(h.table)
get_planned_vtx(h::H,args...) where {T<:HardConflictTable,H<:ConflictCostModel}  = get_planned_vtx(h.table,args...)
reset_path!(h::H,args...) where {H<:ConflictCostModel}    = reset_path!(h.table,args...)
reset_path!(h::H,args...) where {H<:AbstractCostModel}    = nothing
reset_path!(h::FullCostModel{F,T,M},args...) where {F,T,M<:ConflictCostModel} = reset_path!(h.model,args...)
function reset_path!(h::H,args...) where {H<:CompositeCostModel}
    for m in h.cost_models
        reset_path!(m,args...)
    end
end
set_path!(h::H,args...) where {H<:ConflictCostModel}    = set_path!(h.table,args...)
set_path!(h::H,args...) where {H<:AbstractCostModel}    = nothing
set_path!(h::FullCostModel{F,T,M},args...) where {F,T,M<:ConflictCostModel} = set_path!(h.model,args...)
function set_path!(h::H,args...) where {H<:CompositeCostModel}
    for m in h.cost_models
        set_path!(m,args...)
    end
end
partially_set_path!(h::H,args...) where {H<:ConflictCostModel}    = partially_set_path!(h.table,args...)
partially_set_path!(h::H,args...) where {H<:AbstractCostModel}    = nothing
partially_set_path!(h::FullCostModel{F,T,M},args...) where {F,T,M<:ConflictCostModel} = partially_set_path!(h.model,args...)
function partially_set_path!(h::H,args...) where {H<:CompositeCostModel}
    for m in h.cost_models
        partially_set_path!(m,args...)
    end
end


"""
`module JAC.AngularCoefficientsRatip2013`
    ... a submodel of JAC that provides data types for angular (T and V) coefficients and methods to calculate the angular
        coefficients between pairs of relativistic configuration state functions via an external Fortran library.
"""
module AngularCoefficientsRatip2013

    using  ..Basics,  ..Defaults,  ..ManyElectron
    using  Libdl: dlopen, dlsym

    export AngularTcoeff, AngularVcoeff

    const ANCOPATH = joinpath(@__DIR__, "..", "deps", "bin", "libanco-ratip2013.so")

    """
    `AngularCoefficientsRatip2013.twice(j::AngularJ64)`  ... returns `2*j` for a given angular momentum quantum number `j`.
    """
    twice(j::AngularJ64) = Int(2*j.num//j.den)

    """
    `AngularCoefficientsRatip2013.getsubshells(csf::CsfR)`
        ... returns the array of subshells of a relativistic configuration state function.
    """
    getsubshells(csf::CsfR) = csf.useStandardSubshells ?   Defaults.GBL_STANDARD_SUBSHELL_LIST : csf.subshells

    """
    `AngularCoefficientsRatip2013.have_same_subshells(csfa::CsfR, csfb::CsfR)`
        ... checks whether two relativistic configurations state functions have the same subshells; true is returned if
            both CSF have the same subshell list, and false otherwise.
    """
    function have_same_subshells(csfa::CsfR, csfb::CsfR)
        return csfa.useStandardSubshells && csfb.useStandardSubshells || getsubshells(csfa)==getsubshells(csfb)
    end

    """
    `struct AngularCoefficientsRatip2013.Fnkappa`
        ... type that mirrors `nkappa` from the Fortran library. This type is used to communicate with the external Fortran
            library. It can be converted from and to a `Subshell` using the appropriate constructors.
    """
    struct Fnkappa      # nkappa
         n    ::Int32   # integer
         kappa::Int32   # integer
    end

    Fnkappa(s::Subshell) = Fnkappa(s.n, s.kappa)

    """
    `AngularCoefficientsRatip2013.Subshell(nk::Fnkappa)`
        ... constructor to define a Subshell (instance) from the quantum numbers n, kappa.
    """
    Subshell(nk::Fnkappa) = Subshell(nk.n, nk.kappa)

    """
    `AngularCoefficientsRatip2013.maxocc(nk::Fnkappa)`  ... return the maximal occupation number for a given relativistic subshell.
    """
    maxocc(nk::Fnkappa) = 2abs(nk.kappa)

    """
    `struct AngularCoefficientsRatip2013.FancoTcoeff`
        ... type that mirrors `anco_T_coeff` from the Fortran library. This type is used to communicate with the external
            Fortran library. It can be converted to a `AngularTcoeff` using the appropriate constructor.
    """
    struct FancoTcoeff # anco_T_coeff
        nu::Int32      # integer
        a::Fnkappa     # nkappa
        b::Fnkappa     # nkappa
        T::Float64     # real(kind=dp)
    end

    """
    `struct AngularCoefficientsRatip2013.FancoVcoeff`
        ... type that mirrors `anco_V_coeff` from the Fortran library. This type is used to communicate with the external
            Fortran library. It can be converted to an `AngularVcoeff` using the appropriate constructor.
    """
    struct FancoVcoeff # anco_V_coeff
        nu::Int32      # integer
        a::Fnkappa     # nkappa
        b::Fnkappa     # nkappa
        c::Fnkappa     # nkappa
        d::Fnkappa     # nkappa
        V::Float64     # real(kind=dp)
    end

    """
    `struct AngularCoefficientsRatip2013.Fmtcoefficient`
        ... type that mirrors `mct_coefficient` from the Fortran library. This type is used to communicate with the
            external Fortran library. It can be converted to an `AngularTcoeff` using the appropriate constructor.
    """
    struct Fmctcoefficient # mct_coefficient
        r::Int32           # integer
        s::Int32           # integer
        a::Int16           # integer(kind=i2b)
        b::Int16           # integer(kind=i2b)
        ν::Int16           # integer(kind=i2b)
        T::Float64         # real(kind=dp)
    end

    """
    `struct AngularTcoeff`  ... type for representing angular T coefficients.
    """
    struct AngularTcoeff
        nu::Int
        a::Subshell
        b::Subshell
        T::Float64
    end

    # `Base.show(io::IO, coeff::AngularTcoeff)`  ... prepares a proper printout of the variable coeff::AngularTcoeff.
    function Base.show(io::IO, coeff::AngularTcoeff)
        sa = " T^$(coeff.nu)[" * string(coeff.a) * "," * string(coeff.b) * "] = $(coeff.T)"
        print(io, sa )
    end

    """
    `AngularCoefficientsRatip2013.AngularTcoeff(t::FancoTcoeff)`
        ... constructor to define an AngularTcoeff (instance) from an internal FancoTcoeff coefficient.
    """
    AngularTcoeff(t::FancoTcoeff) = AngularTcoeff(t.nu, Subshell(t.a), Subshell(t.b), t.T)

    """
    `AngularCoefficientsRatip2013.AngularTcoeff(t::Fmctcoefficient)`
        ... constructor to define an AngularTcoeff (instance) from an internal Fmctcoefficient and the list of relativistic
            subshells the CSFs are based on.
    """
    AngularTcoeff(t::Fmctcoefficient, subshells::AbstractArray{Subshell}) = AngularTcoeff(t.ν, subshells[t.a], subshells[t.b], t.T)

    """
    `struct AngularCoefficientsRatip2013.AngularTcoeff`  ... type for representing angular V coefficients.
    """
    struct AngularVcoeff
        nu::Int
        a::Subshell
        b::Subshell
        c::Subshell
        d::Subshell
        V::Float64
    end

    # `Base.show(io::IO, coeff::AngularVcoeff)`  ... prepares a proper printout of the variable coeff::AngularVcoeff.
    function Base.show(io::IO, coeff::AngularVcoeff)
        sa = " V^$(coeff.nu)[" * string(coeff.a) * "," * string(coeff.b) * ";" * string(coeff.c) * "," * string(coeff.d) * "] = $(coeff.V)"
        print(io, sa )
    end

    """
    `AngularCoefficientsRatip2013.AngularVcoeff(t::FancoVcoeff)`
        ... constructor to define an AngularVcoeff (instance) from an internal FancoVcoeff coefficient.
    """
    AngularVcoeff(t::FancoVcoeff) = AngularVcoeff(t.nu, Subshell(t.a), Subshell(t.b), Subshell(t.c), Subshell(t.d), t.V)

    const NUMBER_OF_MCT_COEFFICIENTS_MAX = 1000

    # The following `Ref`s need to be initialized at runtime (via the __init__() function)
    # in order to enable precompilation

    const ANCOLIB  = Ref{Ptr{Nothing}}() # dlopen(ANCOPATH)

    const no_anco_t_list             = Ref{Ptr{Int32}}() # cglobal(dlsym(ANCOLIB[], :no_anco_t_list), Int32)
    const no_anco_v_list             = Ref{Ptr{Int32}}() # cglobal(dlsym(ANCOLIB[], :no_anco_v_list), Int32)
    const number_of_mct_coefficients = Ref{Ptr{Int32}}() # cglobal(dlsym(ANCOLIB[], :number_of_mct_coefficients), Int32)

    const anco_t_list = Ref{Ptr{FancoTcoeff}}()     # cglobal(dlsym(ANCOLIB[], :anco_t_list), FancoTcoeff)
    const anco_v_list = Ref{Ptr{FancoVcoeff}}()     # cglobal(dlsym(ANCOLIB[], :anco_v_list), FancoVcoeff)
    const mct_list    = Ref{Ptr{Fmctcoefficient}}() # cglobal(dlsym(ANCOLIB[], :mct_list), Fmctcoefficient)

    const t_vector   = Ref{Vector{FancoTcoeff}}()     # unsafe_wrap(Vector{FancoTcoeff}, anco_t_list, 1000)
    const v_vector   = Ref{Vector{FancoVcoeff}}()     # unsafe_wrap(Vector{FancoVcoeff}, anco_v_list, 2000)
    const mct_vector = Ref{Vector{Fmctcoefficient}}() # unsafe_wrap(Vector{Fmctcoefficient}, mct_list, NUMBER_OF_MCT_COEFFICIENTS_MAX)

    function __init__()
        ANCOLIB[] = dlopen(ANCOPATH)

        no_anco_t_list[]             = cglobal(dlsym(ANCOLIB[], :no_anco_t_list), Int32)
        no_anco_v_list[]             = cglobal(dlsym(ANCOLIB[], :no_anco_v_list), Int32)
        number_of_mct_coefficients[] = cglobal(dlsym(ANCOLIB[], :number_of_mct_coefficients), Int32)

        anco_t_list[] = cglobal(dlsym(ANCOLIB[], :anco_t_list), FancoTcoeff)
        anco_v_list[] = cglobal(dlsym(ANCOLIB[], :anco_v_list), FancoVcoeff)
        mct_list[]    = cglobal(dlsym(ANCOLIB[], :mct_list), Fmctcoefficient)

        t_vector[]   = unsafe_wrap(Vector{FancoTcoeff}, anco_t_list[], 1000)
        v_vector[]   = unsafe_wrap(Vector{FancoVcoeff}, anco_v_list[], 2000)
        mct_vector[] = unsafe_wrap(Vector{Fmctcoefficient}, mct_list[], NUMBER_OF_MCT_COEFFICIENTS_MAX)
    end

    """
    `AngularCoefficientsRatip2013.number_of_t_coeffs()`
        ... to read the number of calculated T coefficients from the external library.
    """
    number_of_t_coeffs() = unsafe_load(no_anco_t_list[])

    """
    `AngularCoefficientsRatip2013.number_of_v_coeffs()`
        ... to read the number of calculated V coefficients from the external library.
    """
    number_of_v_coeffs() = unsafe_load(no_anco_v_list[])

    """
    `AngularCoefficientsRatip2013.number_of_mct_coeffs()`
        ... to read the number of calculated MCT coefficients from the external library.
    """
    number_of_mct_coeffs() = unsafe_load(number_of_mct_coefficients[])

    """
    `AngularCoefficientsRatip2013.view_t_coeffs()`
        ... to return the array of T coefficients as a view into the memory of the external library.
    """
    view_t_coeffs() = view(t_vector[], 1:number_of_t_coeffs())

    """
    `AngularCoefficientsRatip2013.view_v_coeffs()`
        ... to return the array of V coefficients as a view into the memory of the external library.
    """
    view_v_coeffs() = view(v_vector[], 1:number_of_v_coeffs())

    """
    `AngularCoefficientsRatip2013.view_mct_coeffs()`
        ... to return the array of MCT coefficients as a view into the memory of the external library.
    """
    view_mct_coeffs() = view(mct_vector[], 1:number_of_mct_coeffs())

    """
    `AngularCoefficientsRatip2013.angular_coefficients_pair(r::Integer, s::Integer)`
        ... to calculate the angular coefficients between the `r`-th and `s`-th configuration state function of the
            CSF set that was loaded into the external library.
    """
    function angular_coefficients_pair(r::Integer, s::Integer)
        ccall(dlsym(ANCOLIB[], :jac_anco_calculate_csf_pair), Nothing, (Ref{Int32}, Ref{Int32}), r, s)
        return AngularTcoeff.(view_t_coeffs()), AngularVcoeff.(view_v_coeffs())
    end

    """
    `AngularCoefficientsRatip2013.angular_coefficients_pair_1p(ν::Integer, r::Integer, s::Integer)`
        ... to calculate the angular coefficients between the `r`-th and `s`-th configuration state function of the CSF
            set that was loaded into the external library, for a non-scalar one-particle operator of rank `ν`.
    """
    function angular_coefficients_pair_1p(ν::Integer, r::Integer, s::Integer)
        ccall(dlsym(ANCOLIB[], :jac_anco_calculate_csf_pair_1p), Nothing, (Ref{Int32}, Ref{Int32}, Ref{Int32}), ν, r, s)
        return AngularTcoeff.(view_t_coeffs())
    end

    """
    `AngularCoefficientsRatip2013.load_csl(nocsf, nwshells, nwcore, number_of_electrons, subshell,
                                           totalJs, parities, occupations, seniorities, subshellJs, subshellXs)`
        ... a wrapper function for the call to the subroutine `jac_anco_load_csl` of the external library.
    """
    load_csl(nocsf::Integer, nwshells::Integer, nwcore::Integer, number_of_electrons::Integer,
             subshell::AbstractVector{Fnkappa},
             totalJs::AbstractVector{Int8}, parities::AbstractVector{UInt8},
             occupations::AbstractMatrix{Int8}, seniorities::AbstractMatrix{Int8},
             subshellJs::AbstractMatrix{Int8},  subshellXs::AbstractMatrix{Int8}) =
        ccall(dlsym(ANCOLIB[], :jac_anco_load_csl), Nothing,
             (Ref{Int32}, Ref{Int32}, Ref{Int32}, Ref{Int32}, Ref{Fnkappa}, Ref{Int8},
              Ref{UInt8}, Ref{Int8}, Ref{Int8}, Ref{Int8}, Ref{Int8}),
              nocsf, nwshells, nwcore, number_of_electrons, subshell, totalJs, parities,
              occupations, seniorities, subshellJs, subshellXs)

    """
    `AngularCoefficientsRatip2013.load_csl(csf::CsfR)`
        ... to load a single relativistic configuration state function into the external library. Returns the list of
            subshells the CSF is based on.
    """
    function load_csl(csf::CsfR)
        number_of_electrons = sum(csf.occupation)
        subshells = getsubshells(csf)
        nkappas = Fnkappa.(subshells)
        nwshells = length(nkappas)
        occ = reshape(Int8.(csf.occupation), nwshells, 1)
        sen = reshape(Int8.(csf.seniorityNr), nwshells, 1)
        suJ = reshape(Int8.(twice.(csf.subshellJ)), nwshells, 1)
        suX = reshape(Int8.(twice.(csf.subshellX)), nwshells, 1)
        totalJs =  Int8[twice(csf.J)]
        # parities = UInt8[Char(csf.parity)]
        if   csf.parity == Basics.plus    parities = UInt8[Char('+')]   else    parities = UInt8[Char('-')]   end
        nwcore = 0
        for i in eachindex(nkappas)
            if  occ[i]!=maxocc(nkappas[i])
                nwcore = i-1; break
            end
        end
        load_csl(1, nwshells, nwcore, number_of_electrons, nkappas, totalJs, parities, occ, sen, suJ, suX)
        subshells
    end

    """
    `AngularCoefficientsRatip2013.load_csl(csfa::CsfR, csfb::CsfR)`
        ... to load two relativistic configuration state functions into the external library. Returns a common
            list of subshells for both CSFs.
    """
    function load_csl(csfa::CsfR, csfb::CsfR)
        number_of_electrons = sum(csfa.occupation)
        sum(csfb.occupation) == number_of_electrons || error("CSFs must have the same number of electrons")
        if have_same_subshells(csfa, csfb)
            subshells = getsubshells(csfa)
            nwshells = length(subshells)
            occ = Matrix{Int8}(hcat(csfa.occupation, csfb.occupation))
            sen = Matrix{Int8}(hcat(csfa.seniorityNr, csfb.seniorityNr))
            suJ = Matrix{Int8}(twice.(hcat(csfa.subshellJ, csfb.subshellJ)))
            suX = Matrix{Int8}(twice.(hcat(csfa.subshellX, csfb.subshellX)))
        else
            subshells = sort(unique(vcat(getsubshells(csfa), getsubshells(csfb))))
            nwshells = length(subshells)
            occ = zeros(Int8, nwshells, 2)
            sen = zeros(Int8, nwshells, 2)
            suJ = zeros(Int8, nwshells, 2)
            suX = zeros(Int8, nwshells, 2)
            aindex, bindex = 1, 1
            ashells, bshells = getsubshells.((csfa, csfb))
            for i in eachindex(subshells)
                if ashells[aindex] == subshells[i]
                    occ[i,1] = csfa.occupation[aindex]
                    sen[i,1] = csfa.seniorityNr[aindex]
                    suJ[i,1] = twice(csfa.subshellJ[aindex])
                    suX[i,1] = twice(csfa.subshellX[aindex])
                    endof(ashells)<aindex && (aindex+=1)
                else
                    suX[i,1] = i==1 ? 0 : suX[i-1,1]
                end
                if bshells[bindex] == subshells[i]
                    occ[i,2] = csfb.occupation[bindex]
                    sen[i,2] = csfb.seniorityNr[bindex]
                    suJ[i,2] = twice(csfb.subshellJ[bindex])
                    suX[i,2] = twice(csfb.subshellX[bindex])
                    endof(bshells)<bindex && (bindex+=1)
                else
                    suX[i,2] = i==1 ? 0 : suX[i-1,2]
                end
            end
            aindex==endof(ashells) && bindex==endof(bshells) || error("Currently, only standard subshell ordering is supported")
        end
        totalJs =  Int8[twice(csfa.J), twice(csfb.J)]
        ## parities = UInt8[Char(csfa.parity), Char(csfb.parity)]
        parities = UInt8['a', 'a']
        if csfa.parity == Basics.plus      parities[1] = '+'     else       parities[1] = '-'      end
        if csfb.parity == Basics.plus      parities[2] = '+'     else       parities[2] = '-'      end
        nkappas = Fnkappa.(subshells)
        nwcore = 0
        for i in eachindex(subshells)
            coreocc = maxocc(nkappas[i])
            if  occ[i,1]!=coreocc || occ[i,2]!=coreocc
                nwcore = i-1; break
            end
        end
        load_csl(2, nwshells, nwcore, number_of_electrons, nkappas, totalJs, parities, occ, sen, suJ, suX)
        subshells
    end

    """
    `AngularCoefficientsRatip2013.mct_generate_coefficients(rl::Integer, ru::Integer, sl::Integer, su::Integer, iopar::Integer, rank::Integer)`
        ... to calculate the MCT coefficients between the `rl`..`ru`-th and `sl`..`su`-th configuration state functions of the CSF set
            that was loaded into the external library, for a non-scalar one-particle operator of parity `iopar` and rank `rank`.
    """
    function mct_generate_coefficients(rl::Integer, ru::Integer, sl::Integer, su::Integer, iopar::Integer, rank::Integer)
        ccall(dlsym(ANCOLIB[], :jac_mct_generate_coefficients), Nothing,
              (Ref{Int32}, Ref{Int32}, Ref{Int32}, Ref{Int32}, Ref{Int32}, Ref{Int32}), rl, ru, sl, su, iopar, rank)
        return view_mct_coeffs()
    end

end # module


using StaticArrays: SA

const atomic_symbols = SA["H","He","Li","Be","B","C","N","O","F","Ne",
        "Na","Mg","Al","Si","P","S","Cl","Ar","K","Ca",
        "Sc","Ti","V","Cr","Mn","Fe","Co","Ni","Cu","Zn",
        "Ga","Ge","As","Se","Br","Kr","Rb","Sr","Y","Zr",
        "Nb","Mo","Tc","Ru","Rh","Pd","Ag","Cd","In","Sn",
        "Sb","Te","I","Xe","Cs","Ba","La","Ce","Pr","Nd",
        "Pm","Sm","Eu","Gd","Tb","Dy","Ho","Er","Tm","Yb",
        "Lu","Hf","Ta","W","Re","Os","Ir","Pt","Au","Hg",
        "Tl","Pb","Bi","Po","At","Rn","Fr","Ra","Ac","Th",
        "Pa","U"] #,"Np","Pu","Am"] # we don't have partition funcs for these last three

const Natoms = UInt8(length(atomic_symbols))
const atomic_numbers = Dict(atomic_symbols .=> UInt8.(1:Natoms))

#in grams
const atomic_masses = SA[
     1.008,4.003,6.941,9.012,10.81,12.01,14.01,16.00,19.00,20.18,
     22.99,24.31,26.98,28.08,30.97,32.06,35.45,39.95,39.10,40.08,
     44.96,47.90,50.94,52.00,54.94,55.85,58.93,58.71,63.55,65.37,
     69.72,72.59,74.92,78.96,79.90,83.80,85.47,87.62,88.91,91.22,
     92.91,95.94,98.91,101.1,102.9,106.4,107.9,112.4,114.8,118.7,
     121.8,127.6,126.9,131.3,132.9,137.3,138.9,140.1,140.9,144.2,
     145.0,150.4,152.0,157.3,158.9,162.5,164.9,167.3,168.9,173.0,
     175.0,178.5,181.0,183.9,186.2,190.2,192.2,195.1,197.0,200.6,
     204.4,207.2,209.0,210.0,210.0,222.0,223.0,226.0,227.0,232.0,
     231.0,238.0].* amu_cgs #,237.0,244.0,243.0]

#solar/meteoritic abundances per Asplund et al. (2009, Ann. Rev. Ast. Ap., 47, 481).
const solar_abundances = SA[
      12.00,10.93, 1.05, 1.38, 2.70, 8.43, 7.83, 8.69, 4.56, 7.93,
      6.24, 7.60, 6.45, 7.51, 5.41, 7.12, 5.50, 6.40, 5.03, 6.34,
      3.15, 4.95, 3.93, 5.64, 5.43, 7.50, 4.99, 6.22, 4.19, 4.56,
      3.04, 3.65, 2.30, 3.34, 2.54, 3.25, 2.52, 2.87, 2.21, 2.58,
      1.46, 1.88,-5.00, 1.75, 0.91, 1.57, 0.94, 1.71, 0.80, 2.04,
      1.01, 2.18, 1.55, 2.24, 1.08, 2.18, 1.10, 1.58, 0.72, 1.42,
     -5.00, 0.96, 0.52, 1.07, 0.30, 1.10, 0.48, 0.92, 0.10, 0.84,
      0.10, 0.85,-0.12, 0.85, 0.26, 1.40, 1.38, 1.62, 0.92, 1.17,
      0.90, 1.75, 0.65,-5.00,-5.00,-5.00,-5.00,-5.00,-5.00, 0.02,
      -5.00,-0.54]#,-5.00,-5.00,-5.00]


# Analyzing PowerModels' raw parser, it was concluded that b_to & b_fr data was present in DSHL section
function _handle_b(pwf_data::Dict, f_bus::Int, t_bus::Int, susceptance::Float64, circuit::Int, dict_dshl)
    b_fr, b_to = susceptance / 200, susceptance / 200
    if haskey(pwf_data, "DSHL") && haskey(dict_dshl, (f_bus, t_bus)) && haskey(dict_dshl[(f_bus, t_bus)], circuit)
        group = dict_dshl[(f_bus, t_bus)][circuit]
        b_fr += group["SHUNT FROM"] !== nothing && group["STATUS FROM"] == " L" ? group["SHUNT FROM"] / 100 : 0
        b_to += group["SHUNT TO"] !== nothing && group["STATUS TO"] == " L" ? group["SHUNT TO"] / 100 : 0
    end
    return b_fr, b_to
end

function _create_dict_dshl(data::Dict)
    dshl_dict = Dict{Tuple, Any}()

    for (k,v) in data
        sub_data = Dict{String, Any}()
        sub_data["SHUNT FROM"] = v["SHUNT FROM"]
        sub_data["SHUNT TO"] = v["SHUNT TO"]
        sub_data["STATUS FROM"] = v["STATUS FROM"]
        sub_data["STATUS TO"] = v["STATUS TO"]
        current_value = get(dshl_dict, (v["FROM BUS"], v["TO BUS"]), Dict{Int, Any}())
        current_value[v["CIRCUIT"]] = sub_data
        dshl_dict[(v["FROM BUS"], v["TO BUS"])] = current_value
    end
    return dshl_dict
end

function _pwf2pm_branch!(pm_data::Dict, pwf_data::Dict, branch::Dict; add_control_data::Bool=false)
    sub_data = Dict{String,Any}()

    sub_data["f_bus"] = pop!(branch, "FROM BUS")
    sub_data["t_bus"] = pop!(branch, "TO BUS")
    sub_data["br_r"] = pop!(branch, "RESISTANCE") / 100
    sub_data["br_x"] = pop!(branch, "REACTANCE") / 100

    dshl_dict = haskey(pwf_data, "DSHL") ? _create_dict_dshl(pwf_data["DSHL"]) : nothing
    b = _handle_b(pwf_data, sub_data["f_bus"], sub_data["t_bus"], branch["SHUNT SUSCEPTANCE"], branch["CIRCUIT"], dshl_dict)
    sub_data["g_fr"] = 0.0
    sub_data["b_fr"] = b[1]
    sub_data["g_to"] = 0.0
    sub_data["b_to"] = b[2]

    sub_data["tap"] = pop!(branch, "TAP")
    sub_data["shift"] = -pop!(branch, "LAG")
    sub_data["angmin"] = -60.0 # PowerModels.jl standard
    sub_data["angmax"] = 60.0 # PowerModels.jl standard
    sub_data["transformer"] = false

    if branch["STATUS"] == branch["OPENING FROM BUS"] == branch["OPENING TO BUS"] == 'L'
        sub_data["br_status"] = 1
    else
        sub_data["br_status"] = 0
    end

    if add_control_data
        sub_data["control_data"] = Dict{String,Any}()
        sub_data["control_data"]["tapmin"] = sub_data["tap"]
        sub_data["control_data"]["tapmax"] = sub_data["tap"]
        sub_data["control_data"]["circuit"] = branch["CIRCUIT"]

        # Transformer control_data fields are created and set to nothing
        sub_data["control_data"]["control_type"] = nothing
        sub_data["control_data"]["constraint_type"] = nothing
        sub_data["control_data"]["controlled_bus"] = nothing
        sub_data["control_data"]["vmsp"] = nothing
        sub_data["control_data"]["vmmin"] = nothing
        sub_data["control_data"]["vmmax"] = nothing
        sub_data["control_data"]["shift_control_variable"] = nothing
        sub_data["control_data"]["shiftmin"] = nothing
        sub_data["control_data"]["shiftmax"] = nothing
        sub_data["control_data"]["valsp"] = nothing
        sub_data["control_data"]["control"] = nothing
    end

    sub_data["source_id"] = ["branch", sub_data["f_bus"], sub_data["t_bus"], "01"]
    sub_data["index"] = length(pm_data["branch"]) + 1

    sub_data["rate_a"] = pop!(branch, "NORMAL CAPACITY")
    sub_data["rate_b"] = pop!(branch, "EMERGENCY CAPACITY")
    sub_data["rate_c"] = pop!(branch, "EQUIPAMENT CAPACITY")

    if sub_data["rate_a"] >= 9999
        delete!(sub_data, "rate_a")
    end
    if sub_data["rate_b"] >= 9999
        delete!(sub_data, "rate_b")
    end
    if sub_data["rate_c"] >= 9999
        delete!(sub_data, "rate_c")
    end

    idx = string(sub_data["index"])
    pm_data["branch"][idx] = sub_data

end

function _pwf2pm_DCSC_branch!(pm_data::Dict, pwf_data::Dict, branch::Dict; add_control_data::Bool=false)
    sub_data = Dict{String,Any}()

    sub_data["f_bus"] = pop!(branch, "FROM BUS")
    sub_data["t_bus"] = pop!(branch, "TO BUS")
    sub_data["br_r"] = 0
    sub_data["br_x"] = pop!(branch, "INITIAL VALUE") / 100

    sub_data["g_fr"] = 0.0
    sub_data["b_fr"] = 0.0
    sub_data["g_to"] = 0.0
    sub_data["b_to"] = 0.0

    sub_data["tap"] = 1.0
    sub_data["shift"] = 0
    sub_data["angmin"] = -60.0 # PowerModels.jl standard
    sub_data["angmax"] = 60.0 # PowerModels.jl standard
    sub_data["transformer"] = false

    if branch["STATUS"] == 'L'
        sub_data["br_status"] = 1
    else
        sub_data["br_status"] = 0
    end

    if add_control_data
        sub_data["control_data"] = Dict{String,Any}()
        sub_data["control_data"]["tapmin"] = 1.0
        sub_data["control_data"]["tapmax"] = 1.0
        sub_data["control_data"]["circuit"] = branch["CIRCUIT"]

        # Transformer control_data fields are created and set to nothing
        sub_data["control_data"]["control_type"] = nothing
        sub_data["control_data"]["constraint_type"] = nothing
        sub_data["control_data"]["controlled_bus"] = nothing
        sub_data["control_data"]["vmsp"] = nothing
        sub_data["control_data"]["vmmin"] = nothing
        sub_data["control_data"]["vmmax"] = nothing
        sub_data["control_data"]["shift_control_variable"] = nothing
        sub_data["control_data"]["shiftmin"] = nothing
        sub_data["control_data"]["shiftmax"] = nothing
        sub_data["control_data"]["valsp"] = nothing
        sub_data["control_data"]["control"] = nothing
    end

    sub_data["source_id"] = ["branch", sub_data["f_bus"], sub_data["t_bus"], "01"]
    sub_data["index"] = length(pm_data["branch"]) + 1

    sub_data["rate_a"] = pop!(branch, "NORMAL CAPACITY")
    sub_data["rate_b"] = pop!(branch, "EMERGENCY CAPACITY")
    sub_data["rate_c"] = pop!(branch, "EQUIPAMENT CAPACITY")

    if sub_data["rate_a"] >= 9999
        delete!(sub_data, "rate_a")
    end
    if sub_data["rate_b"] >= 9999
        delete!(sub_data, "rate_b")
    end
    if sub_data["rate_c"] >= 9999
        delete!(sub_data, "rate_c")
    end

    rep = findall(x -> x["f_bus"] == sub_data["f_bus"] && x["t_bus"] == sub_data["t_bus"] && x["circuit"] == sub_data["circuit"], pm_data["branch"])
    if length(rep) > 0
        @warn "Branch from $(sub_data["f_bus"]) to $(sub_data["t_bus"]) in circuit $(sub_data["circuit"]) is duplicated"
    end
    idx = string(sub_data["index"])
    pm_data["branch"][idx] = sub_data

end

function _pwf2pm_branch!(pm_data::Dict, pwf_data::Dict; add_control_data::Bool=false)
    pm_data["branch"] = Dict{String, Any}()
    if haskey(pwf_data, "DLIN")
        for (i,branch) in pwf_data["DLIN"]
            if !branch["TRANSFORMER"]
                _pwf2pm_branch!(pm_data, pwf_data, branch, add_control_data = add_control_data)
            end
        end
    end
    if haskey(pwf_data, "DCSC")
        for (i,csc) in pwf_data["DCSC"]
            _pwf2pm_DCSC_branch!(pm_data, pwf_data, csc, add_control_data = add_control_data)
        end
    end
end


using JuLIP.Chemistry: atomic_number
using JuLIP: Atoms
using StaticArrays: SVector

abstract type MSitePotential <: AbstractCalculator end

abstract type MPairPotential <: MSitePotential end

# Experimental Multi-component codes

# ----------------------------------------------------------------------
#    Managing a list of species
# ----------------------------------------------------------------------

abstract type AbstractZList end

"""
`ZList` and `SZList{NZ}` : simple data structures that store a list
of species and convert between atomic numbers and the index in the list.
Can be constructed via
* `ZList(zors)` : where `zors` is an Integer  or `Symbol` (single species)
* `ZList(zs1, zs2, ..., zsn)`
* `ZList([sz1, zs2, ..., zsn])`
* All of these take a kwarg `static = {true, false}`; if `true`, then `ZList`
will return a `SZList{NZ}` for (possibly) faster access.
"""
struct ZList <: AbstractZList
   list::Vector{Int16}
end

Base.length(zlist::AbstractZList) = length(zlist.list)

struct SZList{N} <: AbstractZList
   list::SVector{N, Int16}
end

ZList(zlist::AbstractVector{<: Integer}; static = false) = (
   static ? SZList(SVector( (Int16.(sort(zlist)))... ))
          :  ZList( convert(Vector{Int16}, sort(zlist)) ))

ZList(s::Symbol; kwargs...) =
      ZList( [ atomic_number(s) ]; kwargs... )

ZList(S::AbstractVector{Symbol}; kwargs...) =
      ZList( atomic_number.(S); kwargs... )

ZList(args...; kwargs) =
      ZList( [args...]; kwargs...)

i2z(Zs::AbstractZList, i::Integer) = Zs.list[i]

function z2i(Zs::AbstractZList, z::Integer)
   for j = 1:length(Zs.list)
      if Zs.list[j] == z
         return j
      end
   end
   error("z = $z not found in ZList $(Zs.list)")
end

i2z(V, i::Integer) = i2z(V.zlist, i)
z2i(V, z::Integer) = z2i(V.zlist, z)

Dict(zlist::ZList) = Dict("__id__" => "JuLIP_ZList",
                          "list" => zlist.list)
Base.convert(::Val{:JuLIP_ZList}, D::Dict) = ZList(D)
ZList(D::Dict) = ZList(D["list"])

Dict(zlist::SZList) = Dict("__id__" => "JuLIP_SZList",
                          "list" => zlist.list)
Base.convert(::Val{:JuLIP_SZList}, D::Dict) = SZList(D)
SZList(D::Dict) = SZList(SVector(Int16.(D["list"])...))

# ----------------------------------------------------------------------
#       Basic calculator extensions for multi-species
# ----------------------------------------------------------------------

"""
`neigsz!(tmp, nlist::PairList, at::Atoms, i::Integer) -> j, R Z`

requires a temporary storage array `tmp` with fields
`tmp.R, tmp.Z`.
"""
function neigsz!(tmp, nlist::PairList, at::Atoms, i::Integer)
   j, R = neigs!(tmp.R, nlist, i)
   Z = tmp.Z
   for n = 1:length(j)
      Z[n] = at.Z[j[n]]
   end
   return j, R, (@view Z[1:length(j)])
end

function neigsz(nlist::PairList, at::Atoms, i::Integer)
   j, R = NeighbourLists.neigs(nlist, i)
   return j, R, at.Z[j]
end

alloc_temp(V::MSitePotential, at::AbstractAtoms) =
      alloc_temp(V, maxneigs(neighbourlist(at, cutoff(V))))

alloc_temp(V::MSitePotential, N::Integer) =
      ( R = zeros(JVecF, N),
        Z = zeros(Int16, N), )

alloc_temp_d(V::MSitePotential, at::AbstractAtoms) =
      alloc_temp_d(V, maxneigs(neighbourlist(at, cutoff(V))))

alloc_temp_d(V::MSitePotential, N::Integer) =
      (dV = zeros(JVecF, N),
        R = zeros(JVecF, N),
        Z = zeros(Int16, N), )

energy(V::MSitePotential, at::AbstractAtoms; kwargs...) =
      energy!(alloc_temp(V, at), V, at; kwargs...)

virial(V::MSitePotential, at::AbstractAtoms; kwargs...) =
      virial!(alloc_temp_d(V, at), V, at; kwargs...)

forces(V::MSitePotential, at::AbstractAtoms{T}; kwargs...) where {T} =
      forces!(zeros(JVec{T}, length(at)), alloc_temp_d(V, at), V, at; kwargs...)

function energy!(tmp, calc::MSitePotential, at::Atoms{T};
                 domain=1:length(at)) where {T}
   E = zero(T)
   nlist = neighbourlist(at, cutoff(calc))
   for i in domain
      j, R, Z = neigsz!(tmp, nlist, at, i)
      E += evaluate!(tmp, calc, R, Z, Int16(at.Z[i]))
   end
   return E
end

function forces!(frc, tmp, calc::MSitePotential, at::Atoms{T};
                 domain=1:length(at), reset=true) where {T}
   if reset; fill!(frc, zero(JVec{T})); end
   nlist = neighbourlist(at, cutoff(calc))
   for i in domain
      j, R, Z = neigsz!(tmp, nlist, at, i)
      if length(j) > 0
         evaluate_d!(tmp.dV, tmp, calc, R, Z, Int16(at.Z[i]))
         for a = 1:length(j)
            frc[j[a]] -= tmp.dV[a]
            frc[i]    += tmp.dV[a]
         end
      end
   end
   return frc
end

function virial!(tmp, calc::MSitePotential, at::Atoms{T};
                 domain=1:length(at)) where {T}
   nlist = neighbourlist(at, cutoff(calc))
   vir = zero(JMat{T})
   for i in domain
      j, R, Z = neigsz!(tmp, nlist, at, i)
      if length(j) > 0
         evaluate_d!(tmp.dV, tmp, calc, R, Z, Int16(at.Z[i]))
         vir += site_virial(tmp.dV, R)
      end
   end
   return vir
end

evaluate(V::MSitePotential, R, Z, z) =
      evaluate!(alloc_temp(V, length(R)), V, R, Z, z)

evaluate_d(V::MSitePotential, R::AbstractVector{JVec{T}}, Z, z) where {T} =
      evaluate_d!(zeros(JVec{T}, length(R)),
                  alloc_temp_d(V, length(R)),
                  V, R, Z, z)

site_energy(V::MSitePotential, at::AbstractAtoms, i0::Integer) =
      energy(V, at; domain = (i0,))

site_energy_d(V::MSitePotential, at::AbstractAtoms, i0::Integer) =
      rmul!(forces(V, at; domain = (i0,)), -one(eltype(at)))

# -------- Dispatch for MPairPotentials ------------------------------------

evaluate!(tmp, V::MPairPotential,
          R::AbstractVector{<: JVec{T}}, Z::AbstractVector, z0) where {T} =
   length(R) == 0 ? zero(T) : T(0.5) * sum( evaluate!(tmp, V, norm(R[i]), Z[i], z0)
                                            for i = 1:length(R) )

function evaluate_d!(dV, tmp, V::MPairPotential, R, Z, z0)
   dV = tmp.dV
   for i = 1:length(R)
      r = norm(R[i])
      dV[i] = ((evaluate_d!(tmp, V, r, Z[i], z0)/r) * R[i]) / 2
   end
   return dV
end

evaluate(V::MPairPotential, r, z1, z0) =
      evaluate!(alloc_temp(V, 1), V, r, z1, z0)

evaluate_d(V::MPairPotential, r, z1, z0) =
      evaluate_d!(alloc_temp_d(V, 1), V, r, z1, z0)

# ------------------ Preconditioners for MPairPotentials ----------------------

function precon!(hEs, tmp, V::MPairPotential, R::AbstractVector{<: JVec},
                 Z::AbstractVector, z0, innerstab=T(0.0))
   n = length(R)
   for i = 1:n
      hEs[i,i] = precon!(tmp, V, norm(R[i]), R[i], Z[i], z0, innerstab)
   end
   return hEs
end

# # an FF preconditioner for pair potentials
# function precon!(tmp, V::PairPotential, r::T, R::JVec{T}, innerstab=T(0.1)
#                  ) where {T <: Number}
#    r = norm(R)
#    dV = evaluate_d!(tmp, V, r)
#    ddV = evaluate_dd!(tmp, V, r)
#    R̂ = R/r
#    return (1-innerstab) * (abs(ddV) * R̂ * R̂' + abs(dV / r) * (I - R̂ * R̂')) +
#              innerstab  * (abs(ddV) + abs(dV / r)) * I
# end

# struct MPairPotential <: MSitePotential
#    Z2V::Dict{Tuple{Int16,Int16}, WrappedPairPotential}
# end
#
# MPairPotential() =
#       MPairPotential(Dict{(Int16, Int16), WrappedPairPotential}())
#
# function MPairPotential(args...)
#    Vm = MPairPotential()
#    for p in args
#       @assert p isa Pair
#       z, V = _convert_multi_pp(sym::Symbol, p)
#       Vm.Z2V[z] = V
#    end
#    return Vm
# end
#
# F64fun(V::F64fun) = V
# _convert_multi_pp(sym::Tuple{Symbol, Symbol}, V) =
#       _convert_multi_pp(atomic_number.(sym), V)
# _convert_multi_pp(z::Tuple{<:Integer,<:Integer}, V) =
#       Int16.(z), V
#
# cutoff(V::MPairPotential) =
#    maximum( cutoff(V) for (key, V) in V.Z2V )


"""
    Code for convenience functions for calculating CCFs for each chunk of an AbstractChunkListTimeseries (see RvSpectMLBase)
Author: Eric Ford
Created: August 2020
"""

"""  `calc_order_ccf_chunklist_timeseries( chunklist_timeseries, ccf_plan )`
Convenience function to compute separate CCFs for each chunk (potentially an order or view around one or two lines) of each spectrum in a timeseries.
    CCF is evaluated using line list and mask_shape provided by the ccf plan for each chunk.
Uses multiple threads if avaliable.
# Inputs:
- chunklist_timeseries
# Optional Arguments:
- ccf_plan (BasicCCFPlan())
# Return:
A 3-d array containing the CCF at each (velocity, chunk, spectrum)
Note that the ccf_plan provided is used as a template for creating a custom ccf_plan for each chunk that
    only includes lines that reliably appear in that order for all spectra in the chunklist_timeseries.
"""
function calc_order_ccf_chunklist_timeseries(clt::AbstractChunkListTimeseries,
                plan::PlanT = BasicCCFPlan(); verbose::Bool = false ,
                Δfwhm::AbstractVector{T} = zeros(0)
                ) where { PlanT<:AbstractCCFPlan, T<:Real  }

  #@assert issorted( plan.line_list.λ )
  #nvs = length(calc_ccf_v_grid(plan))
  nvs = calc_length_ccf_v_grid(plan)
  norders = length(clt.chunk_list[1].data)
  nobs =  length(clt.chunk_list)
  order_ccfs = zeros(nvs, norders, nobs)
  num_lines = length(plan.line_list.λ)
  plan_for_chunk = Vector{BasicCCFPlan}(undef,num_chunks(clt))
  for chid in 1:num_chunks(clt)
      order = clt[1].order[chid]
      if typeof(plan.line_list) <: BasicLineList2D
          start_order_idx = searchsortedfirst(plan.line_list.order,order)
          stop_order_idx = searchsortedlast(view(plan.line_list.order,start_order_idx:num_lines),order) + start_order_idx-1
      else
          start_order_idx = 1
          stop_order_idx = num_lines
      end
      if (1<=start_order_idx<=stop_order_idx<=num_lines)
          # find the maximum lower wavelength for the chunk, and the minumum upper wavelength, over all observations
          λmin = maximum(map(obsid->first(clt.chunk_list[obsid].data[chid].λ), 1:length(clt) ))
          λmax = minimum(map(obsid->last( clt.chunk_list[obsid].data[chid].λ), 1:length(clt) ))
          # extend λmin/λmax by the velocity range over which we don't want the mask to change
          λmin  = λmin / (calc_doppler_factor(plan.v_center)*calc_doppler_factor(-plan.v_range_no_mask_change))
          λmax  = λmax / (calc_doppler_factor(plan.v_center)*calc_doppler_factor(plan.v_range_no_mask_change))
          # extend λmin/λmax by the mask width
          upper_edge_of_mask_for_line_at_λmin = λ_max(plan.mask_shape,λmin)
          lower_edge_of_mask_for_line_at_λmax = λ_min(plan.mask_shape,λmax)
          # find the first and last mask entries to use in each chunk
          start_line_idx = searchsortedfirst(view(plan.line_list.λ,start_order_idx:stop_order_idx),upper_edge_of_mask_for_line_at_λmin) + start_order_idx-1
          #stop_line_idx  = num_lines+1 - searchsortedfirst(view(plan.line_list.λ,num_lines:-1:1),lower_edge_of_mask_for_line_at_λmax,rev=true)
          stop_line_idx  = start_order_idx-1 + searchsortedlast(view(plan.line_list.λ,start_order_idx:stop_order_idx),lower_edge_of_mask_for_line_at_λmax)
          if verbose
              flush(stdout)
              println("extrema(plan.line_list.λ) = ",extrema(plan.line_list.λ) )
              if (1 <= start_line_idx <= length(plan.line_list.λ)) && (1 <= stop_line_idx <= length(plan.line_list.λ))
                  println("start_order_idx = ", start_order_idx, "  stop_order_idx = ", stop_order_idx)
                  println("start_line_idx = ", start_line_idx, " λ= ", plan.line_list.λ[start_line_idx]) #, " order= ", plan.line_list.order[start_line_idx])
                  println("stop_line_idx = ", stop_line_idx, " λ= ", plan.line_list.λ[stop_line_idx]) #, " order= ", plan.line_list.order[stop_line_idx])
              end
              println("upper_edge_of_mask_for_line_at_λmin = ", upper_edge_of_mask_for_line_at_λmin, " Lower_edge_of_mask_for_line_at_λmax = ", lower_edge_of_mask_for_line_at_λmax)
          end

          if (1 <= start_line_idx <= length(plan.line_list.λ)) && (1 <= stop_line_idx <= length(plan.line_list.λ))
              if verbose && typeof(plan.line_list) <: BasicLineList2D
                  println("start_line_idx = ", start_line_idx, " λ= ", plan.line_list.λ[start_line_idx], " order= ", plan.line_list.order[start_line_idx])
                  println("stop_line_idx = ", stop_line_idx, " λ= ", plan.line_list.λ[stop_line_idx], " order= ", plan.line_list.order[stop_line_idx])
                  println(" Using ", length(start_line_idx:stop_line_idx), " lines for chunk ", chid, " order ", order)
              end
              if ! issorted( view(plan.line_list.λ, start_line_idx:stop_line_idx ) )
                  println("# View is not sorted.  Why?")
                  for i in start_line_idx:stop_line_idx
                      println("i= ", i, " λ= ", plan.line_list.λ[i], " order= ", plan.line_list.order[i])
                  end
              end
              @assert issorted( view(plan.line_list.λ, start_line_idx:stop_line_idx ) )
              # line_list_for_chunk = BasicLineList2D(view(plan.line_list.λ,start_line_idx:stop_line_idx), view(plan.line_list.weight,start_line_idx:stop_line_idx), fill(order,length(start_line_idx:stop_line_idx)) )
              line_list_for_chunk = BasicLineList(view(plan.line_list.λ,start_line_idx:stop_line_idx), view(plan.line_list.weight,start_line_idx:stop_line_idx)  )
          else  # No lines in this chunk!
              line_list_for_chunk = EmptyBasicLineList()
          end
      else
        line_list_for_chunk = EmptyBasicLineList()
      end

      #create a plan for this chunk that only includes the mask entries we want for this chunk
      plan_for_chunk[chid] = BasicCCFPlan( line_list=line_list_for_chunk, midpoint=plan.v_center, step=plan.v_step, max=plan.v_max, mask_shape=plan.mask_shape )
  end
  #@threaded mapreduce(obsid->calc_ccf_chunklist(clt.chunk_list[obsid], plan_for_chunk),hcat, 1:length(clt) )
  #Threads.@threads for i in 1:nobs
    #  order_ccfs[:,:,i] .= calc_order_ccfs_chunklist(clt.chunk_list[i], plan)
  #end
  Threads.@threads for i in 1:nobs
      this_Δfwhm = length(Δfwhm) == nobs ? Δfwhm[i] : 0.0
      order_ccfs[:,:,i] .= calc_order_ccfs_chunklist(clt.chunk_list[i], plan_for_chunk, Δfwhm=this_Δfwhm, assume_sorted=true )
  end

  return order_ccfs
end

"""  `calc_order_ccf_and_var_chunklist_timeseries( chunklist_timeseries, ccf_plan )`
Convenience function to compute separate CCFs for each chunk (potentially an order or view around one or two lines) of each spectrum in a timeseries.
    CCF is evaluated using line list and mask_shape provided by the ccf plan for each chunk.
Uses multiple threads if avaliable.
# Inputs:
- chunklist_timeseries
# Optional Arguments:
- ccf_plan (BasicCCFPlan())
# Return:
A 3-d array containing the CCF at each (velocity, chunk, spectrum)
Note that the ccf_plan provided is used as a template for creating a custom ccf_plan for each chunk that
    only includes lines that reliably appear in that order for all spectra in the chunklist_timeseries.
"""
function calc_order_ccf_and_var_chunklist_timeseries(clt::AbstractChunkListTimeseries,
                plan::PlanT = BasicCCFPlan(); verbose::Bool = false,
                ccf_var_scale::Real = 1.0, Δfwhm::AbstractVector{T} = zeros(0)
                 ) where {
                    PlanT<:AbstractCCFPlan, T<:Real }
  #@assert issorted( plan.line_list.λ )
  #nvs = length(calc_ccf_v_grid(plan))
  nvs = calc_length_ccf_v_grid(plan)
  norders = length(clt.chunk_list[1].data)
  nobs =  length(clt.chunk_list)
  order_ccfs = zeros(nvs, norders, nobs)
  order_ccf_vars = zeros(nvs, norders, nobs)
  num_lines = length(plan.line_list.λ)
  plan_for_chunk = Vector{BasicCCFPlan}(undef,num_chunks(clt))
  for chid in 1:num_chunks(clt)
      order = clt[1].order[chid]
      if typeof(plan.line_list) <: BasicLineList2D
          start_order_idx = searchsortedfirst(plan.line_list.order,order)
          stop_order_idx = searchsortedlast(view(plan.line_list.order,start_order_idx:num_lines),order) + start_order_idx-1
      else
          start_order_idx = 1
          stop_order_idx = num_lines
      end
      if (1<=start_order_idx<=stop_order_idx<=num_lines)
          # find the maximum lower wavelength for the chunk, and the minumum upper wavelength, over all observations
          λmin = maximum(map(obsid->first(clt.chunk_list[obsid].data[chid].λ), 1:length(clt) ))
          λmax = minimum(map(obsid->last( clt.chunk_list[obsid].data[chid].λ), 1:length(clt) ))
          # extend λmin/λmax by the velocity range over which we don't want the mask to change
          λmin  = λmin / (calc_doppler_factor(plan.v_center)*calc_doppler_factor(-plan.v_range_no_mask_change))
          λmax  = λmax / (calc_doppler_factor(plan.v_center)*calc_doppler_factor(plan.v_range_no_mask_change))
          # extend λmin/λmax by the mask width
          upper_edge_of_mask_for_line_at_λmin = λ_max(plan.mask_shape,λmin)
          lower_edge_of_mask_for_line_at_λmax = λ_min(plan.mask_shape,λmax)
          # find the first and last mask entries to use in each chunk
          start_line_idx = searchsortedfirst(view(plan.line_list.λ,start_order_idx:stop_order_idx),upper_edge_of_mask_for_line_at_λmin) + start_order_idx-1
          stop_line_idx  = start_order_idx-1 + searchsortedlast(view(plan.line_list.λ,start_order_idx:stop_order_idx),lower_edge_of_mask_for_line_at_λmax)
          if verbose
              flush(stdout)
              println("extrema(plan.line_list.λ) = ",extrema(plan.line_list.λ) )
              if (1 <= start_line_idx <= length(plan.line_list.λ)) && (1 <= stop_line_idx <= length(plan.line_list.λ))
                  println("start_order_idx = ", start_order_idx, "  stop_order_idx = ", stop_order_idx)
                  println("start_line_idx = ", start_line_idx, " λ= ", plan.line_list.λ[start_line_idx]) #, " order= ", plan.line_list.order[start_line_idx])
                  println("stop_line_idx = ", stop_line_idx, " λ= ", plan.line_list.λ[stop_line_idx]) #, " order= ", plan.line_list.order[stop_line_idx])
              end
              println("upper_edge_of_mask_for_line_at_λmin = ", upper_edge_of_mask_for_line_at_λmin, " Lower_edge_of_mask_for_line_at_λmax = ", lower_edge_of_mask_for_line_at_λmax)
          end

          if (1 <= start_line_idx <= length(plan.line_list.λ)) && (1 <= stop_line_idx <= length(plan.line_list.λ))
              if verbose && typeof(plan.line_list) <: BasicLineList2D
                  println("start_line_idx = ", start_line_idx, " λ= ", plan.line_list.λ[start_line_idx], " order= ", plan.line_list.order[start_line_idx])
                  println("stop_line_idx = ", stop_line_idx, " λ= ", plan.line_list.λ[stop_line_idx], " order= ", plan.line_list.order[stop_line_idx])
                  println(" Using ", length(start_line_idx:stop_line_idx), " lines for chunk ", chid, " order ", order)
              end
              if ! issorted( view(plan.line_list.λ, start_line_idx:stop_line_idx ) )
                  println("# View is not sorted.  Why?")
                  for i in start_line_idx:stop_line_idx
                      println("i= ", i, " λ= ", plan.line_list.λ[i], " order= ", plan.line_list.order[i])
                  end
              end
              @assert issorted( view(plan.line_list.λ, start_line_idx:stop_line_idx ) )
              # line_list_for_chunk = BasicLineList(view(plan.line_list.λ,start_line_idx:stop_line_idx), view(plan.line_list.weight,start_line_idx:stop_line_idx) )
              line_list_for_chunk = BasicLineList(view(plan.line_list.λ,start_line_idx:stop_line_idx), view(plan.line_list.weight,start_line_idx:stop_line_idx)  )
          else  # No lines in this chunk!
              line_list_for_chunk = EmptyBasicLineList()
          end
      else
        line_list_for_chunk = EmptyBasicLineList()
      end
      #create a plan for this chunk that only includes the mask entries we want for this chunk
      plan_for_chunk[chid] = BasicCCFPlan( line_list=line_list_for_chunk, midpoint=plan.v_center, step=plan.v_step, max=plan.v_max, mask_shape=plan.mask_shape )
  end
  #@threaded mapreduce(obsid->calc_ccf_chunklist(clt.chunk_list[obsid], plan_for_chunk),hcat, 1:length(clt) )
  #Threads.@threads for i in 1:nobs
    #  order_ccfs[:,:,i] .= calc_order_ccfs_chunklist(clt.chunk_list[i], plan)
  #end
  Threads.@threads for i in 1:nobs
      this_Δfwhm = length(Δfwhm) == nobs ? Δfwhm[i] : 0.0
      for j in 1:num_chunks(clt)
          ( ccf_tmp, ccf_var_tmp ) = calc_ccf_and_var_chunk(clt.chunk_list[i][j], plan_for_chunk[j],
                                            ccf_var_scale=ccf_var_scale, Δfwhm=this_Δfwhm, assume_sorted=true )
          order_ccfs[:,j,i] .= ccf_tmp
          order_ccf_vars[:,j,i] .= ccf_var_tmp
      end
  end

  return (ccf=order_ccfs, ccf_var=order_ccf_vars)
end


# Fast sincos function for Julia 0.5
# (see https://github.com/JuliaLang/julia/pull/21589)
@inline function sincos(v::Float64)
    return Base.llvmcall("""
        %f = bitcast i8 *%1 to void (double, double *, double *)*
        %ps = alloca double
        %pc = alloca double
        call void %f(double %0, double *%ps, double *%pc)
        %s = load double, double* %ps
        %c = load double, double* %pc
        %res0 = insertvalue [2 x double] undef, double %s, 0
        %res = insertvalue [2 x double] %res0, double %c, 1
        ret [2 x double] %res
        """, Tuple{Float64,Float64}, Tuple{Float64,Ptr{Void}}, v,
        cglobal((:sincos, Base.Math.libm)))
end

sincos(v::Real) = ( (r, i) = reim(cis(v)); (i, r) )