#### PH Types Used in Interface ####

"""
Struct containing all information needed by PH about a variable (along with the variable id) to run the PH algorithm.

**Fields**

* `name::String` : User specified name of the variable
* `is_integer::Bool` : True if the variable is an integer or binary
"""
struct VariableInfo
    name::String
    is_integer::Bool # true if variable is integer or binary
end

#### PH Subproblem Interface ####

"""
Abstract type for ProgressiveHedging subproblem types.

A concrete subtype handles all details of creating, solving and updating subproblems for ProgressiveHedging (PH).  In particular, it handles all interaction with any modeling language that the subproblem is written in.  See `JuMPSubproblem` for an implementation that uses JuMP as the optimization modeling language.

Variables and their values are identified and exchanged between PH and a Subproblem type using the `VariableID` type.  A unique `VariableID` is associated with each variable in the subproblem by the Subproblem implementation by using the `ScenarioID` of the subproblem as well as the `StageID` to which this variable belongs.  This combination uniquely identifies a node in the scenario tree to which the variable can be associated.  Variables associated with the same node in a scenario tree and sharing the same name are assumed to be consensus variables whose optimal value is determined by PH.  The final component of a `VariableID` is an `Index` which is just a counter assigned to a variable to differentiate it from other variables at the same node.  See `VariableID` type for more details.

Any concrete subtype **must** implement the following functions:
* [`add_ph_objective_terms`](@ref)
* [`objective_value`](@ref)
* [`report_values`](@ref)
* [`report_variable_info`](@ref)
* [`solve_subproblem`](@ref)
* [`update_ph_terms`](@ref)
See help strings on each function for details on arguments, returned objects and expected performance of each function. See JuMPSubproblem for an example using JuMP.

To use `warm_start=true` the concrete subtype must also implement
* [`warm_start`](@ref)
See help on `warm_start` for more information.  See JuMPSubproblem for an example using JuMP.

To use the extensive form functionality, the concrete subtype must implement
* [`ef_copy_model`](@ref)
* [`ef_node_dict_constructor`](@ref)
See the help on the functions for more details. See JuMPSubproblem for an example using JuMP. Note that the extensive form model is always constructed as a `JuMP.Model` object.

To use penalty parameter types other than `ScalarPenaltyParameter`, concrete subproblem types also need to implement
* [`report_penalty_info`](@ref)
See the help on individual penalty parameter types and `report_penalty_info` for more details.

To use lower bound computation functionality, the concrete subtype must also implement
* [`add_lagrange_terms`](@ref)
* [`update_lagrange_terms`](@ref)
See the help on these functions for more details.
"""
abstract type AbstractSubproblem end

## Required Interface Functions ##

# The below functions must be implemented by any new subtype of AbstractSubproblem.
# Note they all default to throwing an `UnimplementedError`.

"""
    add_ph_objective_terms(as::AbstractSubproblem,
                           vids::Vector{VariableID},
                           r::Union{Float64,Dict{VariableID,Float64}}
                           )::Dict{VariableID,Float64}

Create model variables for Lagrange multipliers and hat variables and add Lagrange and proximal terms to the objective function.

Returns mapping of the variable to any information needed from the subproblem to compute the penalty parameter. Only used if `is_subproblem_dependent(typeof(r))` returns `true`.

**Arguments**

* `as::AbstractSubproblem` : subproblem object (replace with appropriate type)
* `vids::Vector{VariableID}` : list of `VariableIDs` which need ph terms created
* `r::AbstractPenaltyParameter` : penalty parameter on quadratic term
"""
function add_ph_objective_terms(as::AbstractSubproblem,
                                vids::Vector{VariableID},
                                r::Union{Float64,Dict{VariableID,Float64}},
                                )::Nothing
    throw(UnimplementedError("add_ph_objective_terms is unimplemented"))
end

"""
    objective_value(as::AbstractSubproblem)::Float64

Return the objective value of the solved subproblem.

**Arguments**

* `as::AbstractSubproblem` : subproblem object (replace with appropriate type)
"""
function objective_value(as::AbstractSubproblem)::Float64
    throw(UnimplementedError("objective_value is unimplemented"))
end

"""
    report_penalty_info(as::AbstractSubproblem,
                        pp<:AbstractPenaltyParameter,
                        )::Dict{VariableID,Float64}

Returns mapping of variable to a value used to compute the penalty parameter.

This function must be implemented for any concrete penalty parameter type for which `is_subproblem_dependent` returns true and any concrete subproblem type wishing to use that penalty parameter. The mapping must contain all subproblem values needed for processing by `process_penalty_subproblem`. The returned mapping is handed unaltered to `process_penalty_subproblem` as the `subproblem_dict` argument.

**Arguments**

*`as::AbstractSubproblem` : subproblem object (replace with appropriate type)
*`pp<:AbstractPenaltyParameter` : penalty parameter type
"""
function report_penalty_info(as::AbstractSubproblem,
                             pp::Type{P},
                             )::Dict{VariableID,Float64} where P <: AbstractPenaltyParameter
    throw(UnimplementedError("report_penalty_info is unimplmented for subproblem of type $(typeof(as)) and penalty parameter of type $(pp)."))
end

"""
    report_variable_info(as::AbstractSubproblem,
                         st::ScenarioTree
                         )::Dict{VariableID, VariableInfo}

Assign `VariableID`s to all model variables and build a map from those ids to the required variable information (e.g., variable name).  See `VariableInfo` help for details on required variable information.

**Arguments**

* `as::AbstractSubproblem` : subproblem object (replace with appropriate type)
* `st::ScenarioTree` : scenario tree for the entire PH problem
"""
function report_variable_info(as::AbstractSubproblem,
                              st::ScenarioTree
                              )::Dict{VariableID, VariableInfo}
    throw(UnimplementedError("report_variable_info is unimplemented"))
end

"""
    report_values(as::AbstractSubproblem,
                  vars::Vector{VariableID}
                  )::Dict{VariableID, Float64}

Return the variable values specified by `vars`.

**Arguments**

* `as::AbstractSubproblem` : subproblem object (replace with appropriate type)
* `vars::Vector{VariableID}` : collection of `VariableID`s to gather values for
"""
function report_values(as::AbstractSubproblem,
                       vars::Vector{VariableID}
                       )::Dict{VariableID, Float64}
    throw(UnimplementedError("report_values is unimplemented"))
end

"""
    solve_subproblem(as::AbstractSubproblem)::MOI.TerminationStatusCode

Solve the subproblem specified by `as` and return the status code.

**Arguments**

* `as::AbstractSubproblem` : subproblem object (replace with appropriate type)
"""
function solve_subproblem(as::AbstractSubproblem)::MOI.TerminationStatusCode
    throw(UnimplementedError("solve_subproblem is unimplemented"))
end

"""
    update_ph_terms(as::AbstractSubproblem,
                    w_vals::Dict{VariableID,Float64},
                    xhat_vals::Dict{VariableID,Float64}
                    )::Nothing

Update the values of the PH variables in this subproblem with those given by `w_vals` and `xhat_vals`.

**Arguments**

* `as::AbstractSubproblem` : subproblem object (replace with appropriate type)
* `w_vals::Dict{VariableID,Float64}` : Values to update Lagrangian variables with
* `xhat_vals::Dict{VariableID,Float64}` : Values to update proximal variables with
"""
function update_ph_terms(as::AbstractSubproblem,
                         w_vals::Dict{VariableID,Float64},
                         xhat_vals::Dict{VariableID,Float64}
                         )::Nothing
    throw(UnimplementedError("update_ph_terms is unimplemented"))
end

"""
    warm_start(as::AbstractSubproblem)::Nothing

Use the values of previous solves non-PH variables as starting points of the next solve.

**Arguments**

* `as::AbstractSubproblem` : subproblem object (replace with appropriate type)
"""
function warm_start(as::AbstractSubproblem)::Nothing
    throw(UnimplementedError("warm_start is unimplemented"))
end

## Optional Interface Functions ##

"""
    add_lagrange_terms(as::AbstractSubproblem,
                       vids::Vector{VariableID}
                       )::Nothing

Create model variables for Lagrange multipliers and adds the corresponding terms to the objective function.

**Arguments**

* `as::AbstractSubproblem` : subproblem object (replace with appropriate type)
* `vids::Vector{VariableID}` : list of `VariableIDs` which need Lagrange terms

See also [`add_ph_objective_terms`](@ref).
"""
function add_lagrange_terms(as::AbstractSubproblem,
                            vids::Vector{VariableID}
                            )::Nothing
    throw(UnimplementedError("add_lagrange_terms is unimplemented"))
end

"""
    update_lagrange_terms(as::AbstractSubproblem,
                          w_vals::Dict{VariableID,Float64}
                          )::Nothing

Update the values of the dual variables in this subproblem with those given by `w_vals`.

**Arguments**

* `as::AbstractSubproblem` : subproblem object (replace with appropriate type)
* `w_vals::Dict{VariableID,Float64}` : values to update Lagrange dual variables with

See also [`update_ph_terms`](@ref).
"""
function update_lagrange_terms(as::AbstractSubproblem,
                               w_vals::Dict{VariableID,Float64}
                               )::Nothing
    throw(UnimplementedError("update_lagrange_terms is unimplemented"))
end

# These functions are required only if an extensive form construction is desired

"""
    ef_copy_model(destination::JuMP.Model,
                  original::AbstractSubproblem,
                  scid::ScenarioID,
                  scen_tree::ScenarioTree,
                  node_dict::Dict{NodeID, Any}
                  )

Copy the subproblem described by `original` to the extensive form model `destination`.

**Arguments**

* `destination::JuMP.Model` : extensive form of the model that is being built
* `original::AbstractSubproblem` : subproblem object (replace with appropriate type)
* `scid::ScenarioID` : `ScenarioID` corresponding to this subproblem
* `scen_tree::ScenarioTree` : scenario tree for the entire PH problem
* `node_dict::Dict{NodeID, Any}` : dictionary for transferring nodal information from one submodel to another
"""
function ef_copy_model(destination::JuMP.Model,
                       original::AbstractSubproblem,
                       scid::ScenarioID,
                       scen_tree::ScenarioTree,
                       node_dict::Dict{NodeID, Any}
                       )
    throw(UnimplementedError("ef_copy_model is unimplemented"))
end

"""
    ef_node_dict_constructor(::Type{S}) where S <: AbstractSubproblem

Construct dictionary that is used to carry information between subproblems for `ef_copy_model`.

**Arguments**

* `::Type{S}` : Subproblem type
"""
function ef_node_dict_constructor(::Type{S}) where S <: AbstractSubproblem
    throw(UnimplementedError("ef_node_dict_constructor is unimplemented"))
end


@testset "Renewable data misspecification" begin
    # See https://discourse.julialang.org/t/how-to-use-test-warn/15557/5 about testing for warning throwing
    warn_message = "The data doesn't include devices of type HydroDispatch, consider changing the device models"
    model = DeviceModel(PSY.HydroDispatch, PSI.HydroDispatchRunOfRiver)
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5)
    @test_logs (:warn, warn_message) construct_device!(op_problem, :Hydro, model)
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys14)
    @test_logs (:warn, warn_message) construct_device!(op_problem, :Hydro, model)
end

@testset "Hydro DCPLossLess FixedOutput" begin
    model = DeviceModel(PSY.HydroFix, PSI.HydroFixed)

    # Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hy; use_parameters = true)
    construct_device!(op_problem, :Hydro, model)
    moi_tests(op_problem, true, 0, 0, 0, 0, 0, false)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hy)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, false, 0, 0, 0, 0, 0, false)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Forecast Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hy; use_parameters = true, use_forecast_data = false)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, true, 0, 0, 0, 0, 0, false)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Forecast - No Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hy; use_forecast_data = false)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, false, 0, 0, 0, 0, 0, false)
    psi_checkobjfun_test(op_problem, GAEVF)

end

@testset "Hydro DCPLossLess HydroDispatch with HydroDispatchRunOfRiver formulations" begin
    model = DeviceModel(PSY.HydroDispatch, PSI.HydroDispatchRunOfRiver)

    # Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_parameters = true)
    construct_device!(op_problem, :Hydro, model)
    moi_tests(op_problem, true, 24, 0, 24, 0, 0, false)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, false, 24, 0, 24, 0, 0, false)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Forecast Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_parameters = true, use_forecast_data = false)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, true, 1, 0, 1, 0, 0, false)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Forecast - No Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_forecast_data = false)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, false, 1, 1, 0, 0, 0, false)
    psi_checkobjfun_test(op_problem, GAEVF)

end

# @testset "Hydro DCPLossLess HydroDispatch with HydroDispatchSeasonalFlow Formulations" begin
#     model = DeviceModel(PSY.HydroDispatch, PSI.HydroDispatchSeasonalFlow)

#     # Parameters Testing
#     op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_parameters = true)
#     construct_device!(op_problem, :Hydro, model)
#     moi_tests(op_problem, true, 24, 0, 24, 0, 0, false)
#     psi_checkobjfun_test(op_problem, GAEVF)

#     # No Parameters Testing
#     op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd)
#     construct_device!(op_problem, :Hydro, model);
#     moi_tests(op_problem, false, 24, 0, 24, 0, 0, false)
#     psi_checkobjfun_test(op_problem, GAEVF)

#     # No Forecast Testing
#     op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_parameters = true, use_forecast_data = false)
#     construct_device!(op_problem, :Hydro, model);
#     moi_tests(op_problem, true, 1, 0, 1, 0, 0, false)
#     psi_checkobjfun_test(op_problem, GAEVF)

#     # No Forecast - No Parameters Testing
#     op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_forecast_data = false)
#     construct_device!(op_problem, :Hydro, model);
#     moi_tests(op_problem, false, 1, 1, 0, 0, 0, false)
#     psi_checkobjfun_test(op_problem, GAEVF)

# end

@testset "Hydro DCPLossLess HydroDispatch with HydroCommitmentRunOfRiver Formulations" begin
    model = DeviceModel(PSY.HydroDispatch, PSI.HydroCommitmentRunOfRiver)

    # Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_parameters = true)
    construct_device!(op_problem, :Hydro, model)
    moi_tests(op_problem, true, 96, 0, 72, 0, 24, true)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, false, 96, 0, 48, 0, 24, true)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Forecast Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_parameters = true, use_forecast_data = false)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, true, 4, 0, 3, 0, 1, true)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Forecast - No Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_forecast_data = false)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, false, 4, 0, 2, 1, 1, true)
    psi_checkobjfun_test(op_problem, GAEVF)

end

@testset "Hydro DCPLossLess HydroDispatch with HydroCommitmentSeasonalFlow Formulations" begin
    model = DeviceModel(PSY.HydroDispatch, PSI.HydroCommitmentSeasonalFlow)

    # Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_parameters = true)
    construct_device!(op_problem, :Hydro, model)
    moi_tests(op_problem, true, 96, 0, 72, 0, 24, true)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, false, 96, 0, 48, 0, 24, true)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Forecast Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_parameters = true, use_forecast_data = false)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, true, 4, 0, 3, 0, 1, true)
    psi_checkobjfun_test(op_problem, GAEVF)

    # No Forecast - No Parameters Testing
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_hyd; use_forecast_data = false)
    construct_device!(op_problem, :Hydro, model);
    moi_tests(op_problem, false, 4, 0, 2, 1, 1, true)
    psi_checkobjfun_test(op_problem, GAEVF)

end
#=
@testset " Hydro Tests" begin
    PSI.activepower_variables(ps_model, generators_hg, 1:24)
    PSI.activepower_constraints(ps_model, generators_hg, PSI.HydroDispatchRunOfRiver, DCPPowerModel, 1:24)
    PSI.activepower_constraints(ps_model, generators_hg, PSI.HydroDispatchRunOfRiver, ACPPowerModel, 1:24)
    PSI.reactivepower_variables(ps_model, generators_hg, 1:24)
    PSI.reactivepower_constraints(ps_model, generators_hg, PSI.HydroDispatchRunOfRiver, ACPPowerModel, 1:24)
end

@testset " Hydro Tests" begin
    ps_model = PSI._canonical_init(length(sys5b.buses), nothing, PM.AbstractPowerModel, sys5b.time_periods)
    PSI.activepower_variables(ps_model, generators_hg, 1:24)
    PSI.commitment_variables(ps_model, generators_hg, 1:24);
    PSI.activepower_constraints(ps_model, generators_hg, PSI.HydroCommitmentRunOfRiver, DCPPowerModel, 1:24)
    PSI.activepower_constraints(ps_model, generators_hg, PSI.HydroCommitmentRunOfRiver, ACPPowerModel, 1:24)
    PSI.reactivepower_variables(ps_model, generators_hg, 1:24)
    PSI.reactivepower_constraints(ps_model, generators_hg, PSI.HydroCommitmentRunOfRiver, ACPPowerModel, 1:24)
end
=#


################################################################################
#
#	CONCRETE TYPE
#
#   Unitcell{D,N,L,S,B} <: AbstractUnitcell{S,B}
#   --> S is the site type (<: AbstractSite{LS,D})
#       --> D is the dimension of embedding space
#       --> LS is the label type of sites
#   --> B is the bond type (<: AbstractBond{LB,N})
#       --> N is the dimension of the Bravais lattice that the bond is located in
#       --> LB is the label type of bonds
#
#   FILE CONTAINS
#       - concrete struct definition
#       - interface implementation
#
################################################################################

################################################################################
#
#   CONCRETE STRUCT DEFINITION
#
################################################################################
mutable struct Unitcell{S,B} <: AbstractUnitcell{S,B}

    # basis vectors of the Bravais lattice
    lattice_vectors	:: Vector{Vector{Float64}}

    # basis sites within the unitcell
    sites			:: Vector{S}

    # list of bonds
    bonds			:: Vector{B}

end

# export the concrete type
export Unitcell

################################################################################
#
#	INTERFACING / ACCESSING UNITCELLS
#	(functions have to be overwritten by concrete types)
#
################################################################################

# default constructor interface
# used for creation of new unitcells
function newUnitcell(
            :: Type{Unitcell{S,B}},
            lattice_vectors :: Vector{<:Vector{<:Real}},
            sites           :: Vector{S},
            bonds           :: Vector{B}
        ) :: Unitcell{S,B} where {D,N,LS,LB,S<:AbstractSite{LS,D},B<:AbstractBond{LB,N}}

    # return a newly created object
    Unitcell{S,B}(lattice_vectors, sites, bonds)
end

# accessing a list of lattice vectors
function latticeVectors(
            unitcell :: Unitcell{S,B}
        ) :: Vector{Vector{Float64}} where {D,N,LS,LB,S<:AbstractSite{LS,D},B<:AbstractBond{LB,N}}

    # return the list of lattice vectors
    return unitcell.lattice_vectors
end
# setting a list of lattice vectors
function latticeVectors!(
            unitcell        :: Unitcell{S,B},
            lattice_vectors :: Vector{<:Vector{<:Real}}
        ) where {D,N,LS,LB,S<:AbstractSite{LS,D},B<:AbstractBond{LB,N}}

    # set the list of lattice vectors
    unitcell.lattice_vectors = lattice_vectors
end

# accessing a list of sites
function sites(
            unitcell :: Unitcell{S,B}
        ) :: Vector{S} where {D,N,LS,LB,S<:AbstractSite{LS,D},B<:AbstractBond{LB,N}}

    # return the list of sites
    return unitcell.sites
end
# setting a list of sites
function sites!(
            unitcell :: Unitcell{S,B},
            sites    :: Vector{S}
        ) where {D,N,LS,LB,S<:AbstractSite{LS,D},B<:AbstractBond{LB,N}}

    # set the list of sites
    unitcell.sites = sites
end

# accessing a list of bonds
function bonds(
            unitcell :: Unitcell{S,B}
        ) :: Vector{B} where {D,N,LS,LB,S<:AbstractSite{LS,D},B<:AbstractBond{LB,N}}

    # return the list of bonds
    return unitcell.bonds
end
# setting a list of bonds
function bonds!(
            unitcell :: Unitcell{S,B},
            bonds    :: Vector{B}
        ) where {D,N,LS,LB,S<:AbstractSite{LS,D},B<:AbstractBond{LB,N}}

    # set the list of bonds
    unitcell.bonds = bonds
end

function saveUnitcell(
        uc :: U,
        fn :: AbstractString,
        group :: AbstractString = "unitcell"
        ;
        append :: Bool = false
    ) where {LS,LB,D,N,S<:AbstractSite{LS,D},B<:AbstractBond{LB,N},U<:Unitcell{S,B}}

    # determine the mode based on if one wants to append stuff
    if append
        mode = "r+"
    else
        mode = "w"
    end

    # determine the site and bond group names
    group_sites = group*"/sites"
    group_bonds = group*"/bonds"

    # open the file in mode
    h5open(fn, mode) do ucfile
        # create the top level group
        group_uc = g_create(ucfile, group)
        # save into the attributes in which groups the sites and bonds are stored
        attrs(group_uc)["sites"] = group_sites
        attrs(group_uc)["bonds"] = group_bonds
        # save types into the attributes
        attrs(group_uc)["site_type"] = string(S)
        attrs(group_uc)["bond_type"] = string(B)
        # number of bravais lattice dimensions
        attrs(group_uc)["N"] = Int64(N)
        # save further attributes

        # write bravais lattice vectors
        for i in 1:N
            group_uc["a"*string(i)] = latticeVectors(uc)[i]
        end
    end

    # save sites and bonds into the file
    saveSites(sites(uc), fn, group_sites, append=true)
    saveBonds(bonds(uc), fn, group_bonds, append=true)

    # return nothing
    return nothing
end

function loadUnitcell(
        ::Type{UC},
        fn :: AbstractString,
        group :: AbstractString = "unitcell"
    ) where {SUC<:AbstractSite,BUC<:AbstractBond,UC<:Union{Unitcell{SUC,BUC}, Unitcell}}

    # read the attribute data
    attr_data = h5readattr(fn, group)

    # determine the bravais lattice dimension
    N = attr_data["N"]

    # determine the site and bond group names
    group_sites = attr_data["sites"]
    group_bonds = attr_data["bonds"]

    # load bravais lattice vectors
    lattice_vectors = Vector{Float64}[]
    for i in 1:N
        push!(lattice_vectors, h5read(fn, group*"/a"*string(i)))
    end

    # load the sites
    site_list = loadSites(fn, group_sites)
    # load the bonds
    bond_list = loadBonds(fn, group_bonds)

    # determine site and bond type
    S = typeof(site_list[1])
    B = typeof(bond_list[1])

    # return new unitcell
    return newUnitcell(
        Unitcell{S,B},
        lattice_vectors,
        site_list,
        bond_list
    )
end

# convinience function for standard type
function loadUnitcell(
        fn :: AbstractString,
        group :: AbstractString = "unitcell"
    )

    return loadUnitcell(Unitcell, fn, group)
end


# Packages required for testing
using Test
using Random
using LinearAlgebra
using Logging
using UUIDs
using SatelliteDynamics

# Package Under Test
using SatelliteTasking
using SatelliteTasking.SatellitePlanning
using SatelliteTasking.Analysis

# Set logging level
global_logger(SimpleLogger(stderr, Logging.Debug))

# Fix randomness during tests
Random.seed!(0)

# Check equality of two arrays
@inline function array_isapprox(x::AbstractArray{F},
                  y::AbstractArray{F};
                  rtol::F=sqrt(eps(F)),
                  atol::F=zero(F)) where {F<:AbstractFloat}

    # Easy check on matching size
    if length(x) != length(y)
        return false
    end

    for (a,b) in zip(x,y)
        @test isapprox(a,b, rtol=rtol, atol=atol)
    end
end

# Check if array equals a single value
@inline function array_isapprox(x::AbstractArray{F},
                  y::F;
                  rtol::F=sqrt(eps(F)),
                  atol::F=zero(F)) where {F<:AbstractFloat}

    for a in x
        @test isapprox(a, y, rtol=rtol, atol=atol)
    end
end

@inline function array_isapprox(x::AbstractArray{F}, y::AbstractArray{F}) where {F<:Integer}
    # Easy check on matching size
    if length(x) != length(y)
        return false
    end

    for (a,b) in zip(x,y)
        @test a == b
    end
end

# Check if array equals a single value
@inline function array_isapprox(x::AbstractArray{F}, y::F) where {F<:Integer}
    for a in x
        @test a == y
    end
end

@time @testset "SatelliteTasking Package Tests" begin
    testdir = joinpath(dirname(@__DIR__), "test")

end


using Pkg
Pkg.activate(@__DIR__)
Pkg.instantiate()
using CSV, DataFrames, Pipe

languagesF = "../data/languages.csv"
formsF = "../data/forms.csv"

!(isfile(languagesF) && isfile(formsF)) && begin
      asjpZip = download(
            "https://zenodo.org/record/3843469/files/lexibank/asjp-v19.1.zip",
            "tmp/asjp-19.1.zip",
      )
      run(`unzip -o $asjpZip -d tmp/`)
      cp("tmp/lexibank-asjp-0c18d44/cldf/forms.csv", formsF)
      cp("tmp/lexibank-asjp-0c18d44/cldf/languages.csv", languagesF)
      for f in readdir("tmp")
            rm("tmp/" * f, recursive = true)
      end
end

forms = CSV.read(formsF)
languages = CSV.read(languagesF)

function cleanASJP(word)
      @pipe word |>
            replace(_, r"[ \*~\"]" => "") |>
            replace(_, r"(.)(.)(.)\$" => s"\2")
end

languages[!, :longname] = @pipe languages |>
      zip(_.classification_wals, _.Name) |>
      join.(_, ".") |>
      replace.(_, "-" => "_")

forms[!, :simplified] = cleanASJP.(forms.Value)

languages = languages[.!occursin.("Oth.", languages.classification_wals), :]

conceptCoverage = @pipe forms |>
      unique(_, [:Language_ID, :gloss_in_source]) |>
      groupby(_, :gloss_in_source) |>
      combine(nrow, _) |>
      sort(_, :nrow, rev = true)

concepts = conceptCoverage.gloss_in_source[1:40]

forms40 = forms[map(x -> x ∈ concepts, forms.gloss_in_source), :]

languageCoverage = @pipe forms40 |>
      unique(_, [:Language_ID, :gloss_in_source]) |>
      groupby(_, :Language_ID) |>
      combine(nrow, _) |>
      sort(_, :nrow, rev = true)

doculects = languageCoverage.Language_ID[languageCoverage.nrow.>=30]

forms40 = forms40[map(x -> x ∈ doculects, forms40.Language_ID), :]

asjpLong = innerjoin(
      forms40,
      languages[:, [:Name, :longname]],
      on = :Language_ID => :Name,
)

asjpWide = @pipe asjpLong |>
      groupby(_, [:longname, :gloss_in_source]) |>
      combine(:simplified => x -> join(x, "-"), _) |>
      unstack(_, :longname, :gloss_in_source, :simplified_function)

CSV.write("../data/asjp19wide.csv", asjpWide)


# WAH Vector Cursors
# ==================
#
# A type that acts as a cursor through a WAHVector during some looping
# operations.
#
# This file is a part of BioJulia.
# License is MIT: https://github.com/BioJulia/WAHVectors.jl/blob/master/LICENSE.md

mutable struct WAHCursor
    wahvec::WAHVector
    len::Int
    word_i::Int
    fill::UInt32
    isfill::Bool
    fillbit::UInt32
    nwords::UInt32

    function WAHCursor(vec::WAHVector)
        x = new(vec, length(vec.data), 1, 0x00000000, false, 0x00000000, 0x00000000)
        decode!(x)
        return x
    end
end

@inline function decode!(x::WAHCursor)
    currentWord = x.wahvec.data[x.word_i]
    if isruns(currentWord)
        isones = is_ones_runs(currentWord)
        x.fill = ifelse(isones, 0x7FFFFFFF, 0x00000000)
        x.nwords = nwords(currentWord)
        x.isfill = true
        x.fillbit = ifelse(isones, 0x00000001, 0x00000000)
    else
        x.nwords = 0x00000001
        x.isfill = false
        x.fill = x.wahvec.data[x.word_i]
    end
end

@inline function check_to_move!(x::WAHCursor)
    if x.nwords == 0x00000000
        x.word_i += 1
        if x.word_i <= x.len
            decode!(x)
        end
    end
end

@inline function Base.:&(x::WAHCursor, y::WAHCursor)
    if x.isfill
        if y.isfill
            num_words = min(x.nwords, y.nwords)
            x.nwords -= num_words
            y.nwords -= num_words
            #answer = WAHElement(x.fillbit & y.fillbit, num_words)
            return WAHElement(x.fillbit & y.fillbit, num_words)
        else
            #answer = WAHElement(x.fill & y.wahvec.data[y.word_i])
            #answer = WAHElement(x.fill & y.fill)
            x.nwords -= 0x00000001
            y.nwords = 0x00000000
        end
    elseif y.isfill
        #answer = WAHElement(x.wahvec[x.word_i] & y.fill)
        #answer = WAHElement(x.fill & y.fill)
        y.nwords -= 0x00000001
        x.nwords = 0x00000000
    else
        #answer = WAHElement(x.wahvec[x.word_i] & y.wahvec[y.word_i])
        #answer = WAHElement(x.fill & y.fill)
        x.nwords = 0x00000000
        y.nwords = 0x00000000
    end
    #return answer
    return WAHElement(x.fill & y.fill)
end

@inline function _compute_and_vec(x::WAHCursor, y::WAHCursor)
    num_words = min(x.nwords, y.nwords)
    xyfill = x.isfill & y.isfill
    xfill = x.isfill & !y.isfill
    yfill = !x.isfill & x.isfill
    nofill = !x.isfill & y.isfill
    x.nwords -= ifelse(xyfill, num_words, 0)
    x.nwords -= ifelse(xfill, 1, 0)
    x.nwords = ifelse(yfill | nofill, 0, x.nwords)
    y.nwords -= ifelse(xyfill, num_words, 0)
    y.nwords -= ifelse(yfill, 1, 0)
    y.nwords = ifelse(xfill | nofill, 0, y.nwords)
    answer = ifelse(xyfill,
        WAHElement(x.fillbit & y.fillbit, num_words),
        WAHElement(x.fill & y.fill))
    return answer
end


using MPI
using MPIStragglers

MPI.Init()
const comm = MPI.COMM_WORLD
const root = 0
const rank = MPI.Comm_rank(comm)
# const nworkers = MPI.Comm_size(comm) - 1
isroot() = MPI.Comm_rank(comm) == root
const data_tag = 0
const control_tag = 1
const nelems = 100 # number of 64-bit floats sent in each direction per iteration and worker

function shutdown()
    for i in 1:(MPI.Comm_size(comm)-1)
        MPI.Isend(zeros(1), i, control_tag, comm)
    end
end

function root_main()

    # parse command-line-arguments
    nworkers = MPI.Comm_size(comm) - 1
    println((nworkers, rank, root))
    print("starting with arguments $ARGS\n")
    nsamples = parse(Int, ARGS[1])
    0 < nsamples || throw(ArgumentError("The number of samples must be positive"))
    filename = ARGS[2]

    # setup
    sendbuf = Vector{Float64}(undef, nelems)
    recvbuf = Vector{Float64}(undef, (nworkers+1)*nelems)

    # warmup
    print("warming up\n")
    MPI.Bcast!(sendbuf, root, comm)
    MPI.Gather!(sendbuf, recvbuf, root, comm)

    # output file
    f = open(filename, "a")
    write(f, "nworkers, nMB_m2w, nMB_w2m, rtt\n")
    nMB_m2w = nelems * 64/8 / 1e6 # MB sent from master to workers
    nMB_w2m = nelems * 64/8 / 1e6 # MB sent from workers to master

    # collect rtt samples
    print("starting test\n")
    for _ in 1:nsamples
        rtt = @elapsed begin
            MPI.Bcast!(sendbuf, root, comm)
            MPI.Gather!(sendbuf, recvbuf, root, comm)
        end
        write(f, "$nworkers, $nMB_m2w, $nMB_w2m, $rtt\n")
    end

    print("tests done\n")
    close(f)
    shutdown()
end

function worker_main()
    recvbuf = Vector{Float64}(undef, nelems)
    sendbuf = Vector{Float64}(undef, nelems)
    crreq = MPI.Irecv!(zeros(1), root, control_tag, comm)
    while true

        # check for exit message on control channel
        flag, _ = MPI.Test!(crreq)
        if flag
            break
        end

        # send/receive
        MPI.Bcast!(recvbuf, root, comm)
        sendbuf .= recvbuf
        MPI.Gather!(sendbuf, nothing, root, comm)
    end
end

if isroot()
    root_main()
else
    worker_main()
end
MPI.Barrier(comm)


module MetaGraphs
using LightGraphs
using JLD2

import Base:
    eltype, show, ==, Pair,
    Tuple, copy, length, size,
    issubset, zero, getindex
import Random:
    randstring, seed!

import LightGraphs:
    AbstractGraph, src, dst, edgetype, nv,
    ne, vertices, edges, is_directed,
    add_vertex!, add_edge!, rem_vertex!, rem_edge!,
    has_vertex, has_edge, inneighbors, outneighbors,
    weights, indegree, outdegree, degree,
    induced_subgraph,
    loadgraph, savegraph, AbstractGraphFormat,
    reverse

import LightGraphs.SimpleGraphs:
    AbstractSimpleGraph, SimpleGraph, SimpleDiGraph,
    SimpleEdge, fadj, badj

export
    AbstractMetaGraph,
    MetaGraph,
    MetaDiGraph,
    weighttype,
    props,
    get_prop,
    set_props!,
    set_prop!,
    rem_prop!,
    has_prop,
    clear_props!,
    weightfield!,
    defaultweight!,
    weightfield,
    defaultweight,
    filter_edges,
    filter_vertices,
    MGFormat,
    DOTFormat,
    set_indexing_prop!,
    reverse

const PropDict = Dict{Symbol,Any}
const MetaDict = Dict{Symbol,Dict{Any,Integer}}

abstract type AbstractMetaGraph{T} <: AbstractGraph{T} end

function show(io::IO, g::AbstractMetaGraph)
    dir = is_directed(g) ? "directed" : "undirected"
    print(io, "{$(nv(g)), $(ne(g))} $dir $(eltype(g)) metagraph with $(weighttype(g)) weights defined by :$(g.weightfield) (default weight $(g.defaultweight))")
end

@inline fadj(g::AbstractMetaGraph, x...) = fadj(g.graph, x...)
@inline badj(g::AbstractMetaGraph, x...) = badj(g.graph, x...)

eltype(g::AbstractMetaGraph) = eltype(g.graph)
edgetype(g::AbstractMetaGraph) = edgetype(g.graph)
nv(g::AbstractMetaGraph) = nv(g.graph)
vertices(g::AbstractMetaGraph) = vertices(g.graph)

ne(g::AbstractMetaGraph) = ne(g.graph)
edges(g::AbstractMetaGraph) = edges(g.graph)

has_vertex(g::AbstractMetaGraph, x...) = has_vertex(g.graph, x...)
@inline has_edge(g::AbstractMetaGraph, x...) = has_edge(g.graph, x...)

inneighbors(g::AbstractMetaGraph, v::Integer) = inneighbors(g.graph, v)
outneighbors(g::AbstractMetaGraph, v::Integer) = fadj(g.graph, v)

issubset(g::T, h::T) where T <: AbstractMetaGraph = issubset(g.graph, h.graph)

"""
    add_edge!(g, u, v, s, val)
    add_edge!(g, u, v, d)

    Add an edge `(u, v)` to MetaGraph `g` with optional property `s` having value `val`,
    or properties given by an optional dictionary `d` mapping symbols to values.

    return true if the edge has been added, false otherwise
"""
@inline add_edge!(g::AbstractMetaGraph, x...) = add_edge!(g.graph, x...)
function add_edge!(g::AbstractMetaGraph, u::Integer, v::Integer, s::Symbol, val)
    add_edge!(g, u, v) || return false
    set_prop!(g, u, v, s, val)
    return true
end

function add_edge!(g::AbstractMetaGraph, u::Integer, v::Integer, d::Dict)
    add_edge!(g, u, v) || return false
    set_props!(g, u, v, d)
    return true
end

@inline function rem_edge!(g::AbstractMetaGraph, x...)
    clear_props!(g, x...)
    rem_edge!(g.graph, x...)
end

"""
    add_vertex!(g)
    add_vertex!(g, s, v)
    add_vertex!(g, d)

    Add a vertex to MetaGraph `g` with optional property `s` having value `v`,
    or properties given by an optional Dicitionary `d` mapping symbols to values.

    return true if the vertex has been added, false otherwise.
"""
add_vertex!(g::AbstractMetaGraph) = add_vertex!(g.graph)
function add_vertex!(g::AbstractMetaGraph, d::Dict)
    add_vertex!(g) || return false
    set_props!(g, nv(g), d)
    return true
end

function add_vertex!(g::AbstractMetaGraph, s::Symbol, v)
    add_vertex!(g) || return false
    set_prop!(g, nv(g), s, v)
    return true
end

function rem_vertex!(g::AbstractMetaGraph, v::Integer)
    v in vertices(g) || return false
    lastv = nv(g)
    lastvprops = props(g, lastv)

    lasteoutprops = Dict(n => props(g, lastv, n) for n in outneighbors(g, lastv))
    lasteinprops = Dict(n => props(g, n, lastv) for n in inneighbors(g, lastv))
    for ind in g.indices
        if haskey(props(g,lastv),ind)
            pop!(g.metaindex[ind], get_prop(g, lastv, ind))
        end
        if haskey(props(g,v),ind)
            v != lastv && pop!(g.metaindex[ind], get_prop(g, v, ind))
        end
    end
    clear_props!(g, v)
    for n in outneighbors(g, lastv)
        clear_props!(g, lastv, n)
    end

    for n in inneighbors(g, lastv)
        clear_props!(g, n, lastv)
    end
    if v != lastv # ignore if we're removing the last vertex.
        for n in outneighbors(g, v)
            clear_props!(g, v, n)
        end
        for n in inneighbors(g, v)
            clear_props!(g, n, v)
        end
    end
    clear_props!(g, lastv)
    retval = rem_vertex!(g.graph, v)
    retval || return false
    if v != lastv # ignore if we're removing the last vertex.
        for (key, val) in lastvprops
            if key in g.indices
                set_indexing_prop!(g, v, key, val)
            else
                set_prop!(g, v, key, val)
            end
        end
        for n in outneighbors(g, v)
            set_props!(g, v, n, lasteoutprops[n])
        end

        for n in inneighbors(g, v)
            set_props!(g, n, v, lasteinprops[n])
        end
    end
    return true
end

struct MetaWeights{T <: Integer,U <: Real} <: AbstractMatrix{U}
    n::T
    weightfield::Symbol
    defaultweight::U
    eprops::Dict{SimpleEdge{T},PropDict}
    directed::Bool
end
show(io::IO, x::MetaWeights) = print(io, "metaweights")
show(io::IO, z::MIME"text/plain", x::MetaWeights) = show(io, x)

MetaWeights(g::AbstractMetaGraph) = MetaWeights{eltype(g),eltype(g.defaultweight)}(nv(g), g.weightfield, g.defaultweight, g.eprops, is_directed(g))

function getindex(w::MetaWeights{T,U}, u::Integer, v::Integer)::U where T <: Integer where U <: Real
    _e = Edge(u, v)
    e = !w.directed && !LightGraphs.is_ordered(_e) ? reverse(_e) : _e
    !haskey(w.eprops, e) && return w.defaultweight
    return U(get(w.eprops[e], w.weightfield, w.defaultweight))
end

function getindex(g::AbstractMetaGraph, prop::Symbol)
    !haskey(g.metaindex, prop) && error("':$prop' is not an index")
    return g.metaindex[prop]
end

function getindex(g::AbstractMetaGraph, indx::Any, prop::Symbol)
    !haskey(g, indx, prop) && error("No node with prop $prop and key $indx")
    return g.metaindex[prop][indx]
end

function Base.haskey(g::AbstractMetaGraph, indx::Any, prop::Symbol)
    haskey(g.metaindex, prop) || error("':$prop' is not an index")
    typeof(indx) <: eltype(keys(g.metaindex[prop])) || error("Index type does not match keys of metaindex '$prop'")
    return haskey(g.metaindex[prop], indx)
end

function getindex(g::AbstractMetaGraph, indx::Integer, prop::Symbol)
    haskey(g.metaindex, prop) || error("':$prop' is not an index")
    return props(g, indx)[prop]
end

size(d::MetaWeights) = (d.n, d.n)

weights(g::AbstractMetaGraph) = MetaWeights(g)

_hasdict(g::AbstractMetaGraph, v::Integer) = haskey(g.vprops, v)
_hasdict(g::AbstractMetaGraph, e::SimpleEdge) = haskey(g.eprops, e)

"""
    props(g)
    props(g, v)
    props(g, e)
    props(g, s, d)

Return a dictionary of all metadata from graph `g`, vertex `v`, or edge `e`
(optionally referenced by source vertex `s` and destination vertex `d`).
"""
props(g::AbstractMetaGraph) = g.gprops
props(g::AbstractMetaGraph, v::Integer) = get(PropDict, g.vprops, v)
# props for edges is dependent on directedness.
props(g::AbstractMetaGraph, u::Integer, v::Integer) = props(g, Edge(u, v))

"""
    get_prop(g, prop)
    get_prop(g, v, prop)
    get_prop(g, e, prop)
    get_prop(g, s, d, prop)

Return the property `prop` defined for graph `g`, vertex `v`, or edge `e`
(optionally referenced by source vertex `s` and destination vertex `d`).
If property is not defined, return an error.
"""
get_prop(g::AbstractMetaGraph, prop::Symbol) = props(g)[prop]
get_prop(g::AbstractMetaGraph, v::Integer, prop::Symbol) = props(g, v)[prop]
get_prop(g::AbstractMetaGraph, e::SimpleEdge, prop::Symbol) = props(g, e)[prop]

get_prop(g::AbstractMetaGraph, u::Integer, v::Integer, prop::Symbol) = get_prop(g, Edge(u, v), prop)

"""
    has_prop(g, prop)
    has_prop(g, v, prop)
    has_prop(g, e, prop)
    has_prop(g, s, d, prop)

Return true if the property `prop` is defined for graph `g`, vertex `v`, or
edge `e` (optionally referenced by source vertex `s` and destination vertex `d`).
"""
has_prop(g::AbstractMetaGraph, prop::Symbol) = haskey(g.gprops, prop)
has_prop(g::AbstractMetaGraph, v::Integer, prop::Symbol) = haskey(props(g, v), prop)
has_prop(g::AbstractMetaGraph, e::SimpleEdge, prop::Symbol) = haskey(props(g, e), prop)

has_prop(g::AbstractMetaGraph, u::Integer, v::Integer, prop::Symbol) = has_prop(g, Edge(u, v), prop)

"""
    set_props!(g, dict)
    set_props!(g, v, dict)
    set_props!(g, e, dict)
    set_props!(g, s, d, dict)

Bulk set (merge) properties contained in `dict` with graph `g`, vertex `v`, or
edge `e` (optionally referenced by source vertex `s` and destination vertex `d`).
Will return false if vertex or edge does not exist.
"""
function set_props!(g::AbstractMetaGraph, d::Dict)
    merge!(g.gprops, d)
    return true
end

function set_props!(g::AbstractMetaGraph, v::Integer, d::Dict)
    if has_vertex(g, v)
        if length(intersect(keys(d), g.indices)) != 0
            error("The following properties are indexing_props and cannot be updated: $(intersect(keys(d), g.indices))")
        elseif !_hasdict(g, v)
            g.vprops[v] = d
        else
            merge!(g.vprops[v], d)
        end
        return true
    end
    return false
end
# set_props!(g::AbstractMetaGraph, e::SimpleEdge, d::Dict) is dependent on directedness.

set_props!(g::AbstractMetaGraph{T}, u::Integer, v::Integer, d::Dict) where T = set_props!(g, Edge(T(u), T(v)), d)

"""
    set_prop!(g, prop, val)
    set_prop!(g, v, prop, val)
    set_prop!(g, e, prop, val)
    set_prop!(g, s, d, prop, val)

Set (replace) property `prop` with value `val` in graph `g`, vertex `v`, or
edge `e` (optionally referenced by source vertex `s` and destination vertex `d`).
Will return false if vertex or edge does not exist, true otherwise.
"""
set_prop!(g::AbstractMetaGraph, prop::Symbol, val) = set_props!(g, Dict(prop => val))
set_prop!(g::AbstractMetaGraph, v::Integer, prop::Symbol, val) = begin
    if in(prop, g.indices)
        set_indexing_prop!(g, v, prop, val)
    else
        set_props!(g, v, Dict(prop => val))
    end
end
set_prop!(g::AbstractMetaGraph, e::SimpleEdge, prop::Symbol, val) = set_props!(g, e, Dict(prop => val))

set_prop!(g::AbstractMetaGraph{T}, u::Integer, v::Integer, prop::Symbol, val) where T = set_prop!(g, Edge(T(u), T(v)), prop, val)

"""
    rem_prop!(g, prop)
    rem_prop!(g, v, prop)
    rem_prop!(g, e, prop)
    rem_prop!(g, s, d, prop)

Remove property `prop` from graph `g`, vertex `v`, or edge `e`
(optionally referenced by source vertex `s` and destination vertex `d`).
If property, vertex, or edge does not exist, will not do anything.
"""
rem_prop!(g::AbstractMetaGraph, prop::Symbol) = delete!(g.gprops, prop)
rem_prop!(g::AbstractMetaGraph, v::Integer, prop::Symbol) = delete!(g.vprops[v], prop)
rem_prop!(g::AbstractMetaGraph, e::SimpleEdge, prop::Symbol) = delete!(g.eprops[e], prop)

rem_prop!(g::AbstractMetaGraph{T}, u::Integer, v::Integer, prop::Symbol) where T = rem_prop!(g, Edge(T(u), T(v)), prop)

"""
    default_index_value(v, prop, index_values; exclude=nothing)

Provides a default index value for a vertex if no value currently exists. The default is a string: "\$prop\$i" where `prop` is the property name and `i` is the vertex number. If some other vertex already has this name, a randomized string is generated (though the way it is generated is deterministic).
"""
function default_index_value(v::Integer, prop::Symbol, index_values::Set{Any}; exclude=nothing)
    val = string(prop) * string(v)
    if in(val, index_values) || val == exclude
        seed!(v + hash(prop))
        val = randstring()
        @warn("'$(string(prop))$v' is already in index, setting ':$prop' for vertex $v to $val")
    end
    return val
end

"""
    set_indexing_prop!(g, prop)
    set_indexing_prop!(g, v, prop, val)

Make property `prop` into an indexing property. If any values for this property
are already set, each vertex must have unique values. Optionally, set the index
`val` for vertex `v`. Any vertices without values will be set to a default
("(prop)(v)").
"""
function set_indexing_prop!(g::AbstractMetaGraph, prop::Symbol; exclude=nothing)
    in(prop, g.indices) && return g.indices
    index_values = [g.vprops[v][prop] for v in keys(g.vprops) if haskey(g.vprops[v], prop)]
    length(index_values) != length(union(index_values)) && error("Cannot make $prop an index, duplicate values detected")
    index_values = Set(index_values)

    g.metaindex[prop] = Dict{Any,Integer}()
    for v in vertices(g)
        if !haskey(g.vprops, v) || !haskey(g.vprops[v], prop)
            val = default_index_value(v, prop, index_values, exclude=exclude)
            set_prop!(g, v, prop, val)
        end
        g.metaindex[prop][g.vprops[v][prop]] = v
    end
    push!(g.indices, prop)
    return g.indices
end

function set_indexing_prop!(g::AbstractMetaGraph, v::Integer, prop::Symbol, val::Any)
    !in(prop, g.indices) && set_indexing_prop!(g, prop, exclude=val)
    (haskey(g.metaindex[prop], val) && haskey(g.vprops, v) && haskey(g.vprops[v], prop) && g.vprops[v][prop] == val) && return g.indices
    haskey(g.metaindex[prop], val) && error("':$prop' index already contains $val")

    if !haskey(g.vprops, v)
        push!(g.vprops, v=>Dict{Symbol,Any}())
    end
    if haskey(g.vprops[v], prop)
        delete!(g.metaindex[prop], g.vprops[v][prop])
    end
    g.metaindex[prop][val] = v
    g.vprops[v][prop] = val
    return g.indices
end
"""
    clear_props!(g)
    clear_props!(g, v)
    clear_props!(g, e)
    clear_props!(g, s, d)

Remove all defined properties from graph `g`, vertex `v`, or edge `e`
(optionally referenced by source vertex `s` and destination vertex `d`).
"""
clear_props!(g::AbstractMetaGraph, v::Integer) = _hasdict(g, v) && delete!(g.vprops, v)
clear_props!(g::AbstractMetaGraph, e::SimpleEdge) = _hasdict(g, e) && delete!(g.eprops, e)
clear_props!(g::AbstractMetaGraph) = g.gprops = PropDict()

clear_props!(g::AbstractMetaGraph{T}, u::Integer, v::Integer) where T = clear_props!(g, Edge(T(u), T(v)))

"""
    weightfield!(g, prop)

Set the field that contains weight information to `prop`.
"""
weightfield!(g::AbstractMetaGraph, prop::Symbol) = (g.weightfield = prop)

"""
    weightfield(g)

Return the field that contains weight information for metagraph `g`.
"""
weightfield(g::AbstractMetaGraph) = g.weightfield

"""
    defaultweight!(g, weight)

Set the default weight for metagraph `g`
"""
defaultweight!(g::AbstractMetaGraph, weight::Real) =
    g.defaultweight = weight
"""
    defaultweight(g)

Return the default weight for metagraph `g`.
"""
defaultweight(g::AbstractMetaGraph) = g.defaultweight

"""
    filter_edges(g, prop[, val])
    filter_edges(g, fn)

Return an iterator to all edges that have property `prop` defined (optionally
as `val`), or where function `fn` returns `true` only for edges that should be
included in the iterator.

`fn` should be of the form
```
fn(g::AbstractMetaGraph{T}, e::SimpleEdge{T})::Boolean
```
where `e` is replaced with the edge being evaluated.
"""
filter_edges(g::AbstractMetaGraph, fn::Function) =
    Iterators.filter(e -> fn(g, e), edges(g))

filter_edges(g::AbstractMetaGraph, prop::Symbol) =
    filter_edges(g, (g, e) -> has_prop(g, e, prop))

filter_edges(g::AbstractMetaGraph, prop::Symbol, val) =
    filter_edges(g, (g, e) -> has_prop(g, e, prop) && get_prop(g, e, prop) == val)

"""
    filter_vertices(g, prop[, val])
    filter_vertices(g, fn)

Return an iterator to all vertices that have property `prop` defined (optionally
as `val`), or where function `fn` returns `true` only for vertices that should be
included in the iterator.

`fn` should be of the form
```
fn(g::AbstractMetaGraph, v::Integer)::Boolean
```
where `v` is replaced with the vertex being evaluated.
"""
filter_vertices(g::AbstractMetaGraph, fn::Function) =
    Iterators.filter(x -> fn(g, x), vertices(g))

filter_vertices(g::AbstractMetaGraph, prop::Symbol) =
    filter_vertices(g, (g, x) -> has_prop(g, x, prop))

filter_vertices(g::AbstractMetaGraph, prop::Symbol, val) =
    filter_vertices(g, (g, x) -> has_prop(g, x, prop) && get_prop(g, x, prop) == val)

function _copy_props!(oldg::T, newg::T, vmap) where T <: AbstractMetaGraph
    for (newv, oldv) in enumerate(vmap)
        p = props(oldg, oldv)
        if !isempty(p)
            set_props!(newg, newv, p)
        end
    end
    for newe in edges(newg)
        u, v = Tuple(newe)
        olde = Edge(vmap[u], vmap[v])
        if !is_directed(oldg) && !is_ordered(olde)
            olde = reverse(olde)
        end
        p = props(oldg, olde)
        if !isempty(p)
            set_props!(newg, newe, p)
        end
    end
    if !isempty(oldg.gprops)
        set_props!(newg, oldg.gprops)
    end
    defaultweight!(newg, defaultweight(oldg))
    weightfield!(newg, weightfield(oldg))
    return nothing
end

function induced_subgraph(g::T, v::AbstractVector{U}) where T <: AbstractMetaGraph where U <: Integer
    inducedgraph, vmap = induced_subgraph(g.graph, v)
    newg = T(inducedgraph)
    _copy_props!(g, newg, vmap)
    return newg, vmap
end

function induced_subgraph(g::T, v::AbstractVector{U}) where T <: AbstractMetaGraph where U <: SimpleEdge
    inducedgraph, vmap = induced_subgraph(g.graph, v)
    newg = T(inducedgraph)
    _copy_props!(g, newg, vmap)
    return newg, vmap
end

induced_subgraph(g::T, filt::Iterators.Filter) where T <: AbstractMetaGraph =
    induced_subgraph(g, collect(filt))

# TODO - would be nice to be able to apply a function to properties. Not sure
# how this might work, but if the property is a vector, a generic way to append to
# it would be a good thing.

==(x::AbstractMetaGraph, y::AbstractMetaGraph) = x.graph == y.graph

copy(g::T) where T <: AbstractMetaGraph = deepcopy(g)

include("metadigraph.jl")
include("metagraph.jl")
include("persistence.jl")
include("overrides.jl")
end # module


@testset "helper functions" begin
    @testset "_update! Array" begin
        # Hits fallback, since we can't update `Diagonal`s in place
        X = Diagonal([1, 1])
        Y = copy(X)
        @test ChainRules._update!(X, [1 2; 3 4]) == [2 2; 3 5]
        @test X == Y  # no change to X

        X = [1 2; 3 4]
        Y = copy(X)
        @test ChainRules._update!(X, Diagonal([1, 1])) == [2 2; 3 5]
        @test X != Y  # X has been updated
    end
    @testset "_update! Zero" begin
        X = [1 2; 3 4]
        @test ChainRules._update!(X, Zero()) === X
        @test ChainRules._update!(Zero(), X) === X
        @test ChainRules._update!(Zero(), Zero()) === Zero()
    end
    @testset "_update! NamedTuple" begin
        X = (A=[1 0; 0 1], B=[2 2; 2 2])
        old_X = deepcopy(X)
        Y = deepcopy(X)
        @test ChainRules._update!(X, Y, :A) == (A=[2 0; 0 2], B=[2 2; 2 2])
        @test X.A != old_X.A
        @test X.B == old_X.B
    end
    @testset "_checked_rrule" begin
        try
            @eval cool(x,y) = x + y
            # We defined a 2-arg method for `cool` but no `rrule`
            ChainRules._checked_rrule(cool, 1.0, 2.0)
        catch e
            @test e isa ArgumentError
            @test e.msg == "can't differentiate `cool(::Float64, ::Float64)`; no " *
                "matching `rrule` is defined"
        end
    end
end


"""
    setup_EM_jobs!(job_ids, obs_sets; delta_thresh, chains, init_function)

Given a vector of 'Chain_IDs', and the appropriate obs_sets dictionary, return the appropriate tuple of channels and chains for the 'execute_EM_jobs!' function. Given an existing chains dict, resumes any existing non-converged chains (as assessed by the provided 'delta_thresh' value), otherwise initialises hmms for new chains with optionally user-specified 'init_function' (default 'autotransition_init').

"""
function setup_EM_jobs!(job_ids::Vector{Chain_ID}, obs_sets::Dict{String,Vector{LongSequence{DNAAlphabet{2}}}}; delta_thresh::Float64=1e-3,  chains::Dict{Chain_ID,Vector{EM_step}}=Dict{Chain_ID,Vector{EM_step}}(), init_function::Function=autotransition_init)
    #argument checking
    length(job_ids) < 1 && throw(ArgumentError("Empty job id vector!"))
    length(obs_sets) < 1 && throw(ArgumentError("Empty observation sets!"))

    no_input_hmms = length(job_ids)
    input_channel= RemoteChannel(()->Channel{Tuple}(no_input_hmms*3)) #channel to hold BHMM learning jobs
    output_channel= RemoteChannel(()->Channel{Tuple}(Inf)) #channel to take EM iterates off of

    code_dict = code_job_obs(job_ids, obs_sets)

    @showprogress 1 "Setting up HMMs..." for id in job_ids #for each jobid, add an initial BHMM to input_channel for EM_workers
        if haskey(chains, id) && length(chains[id]) > 0 #true if resuming from incomplete chain
            chain_end=chains[id][end]
            if !chain_end.converged || (chain_end.converged && chain_end.delta > delta_thresh)#push the last hmm iterate for nonconverged chains to the input channel with coded observations and values for chain resumption
                put!(input_channel, (id, chain_end.iterate, chain_end.hmm, chain_end.log_p, code_dict[(id.obs_id, id.order)]))
            else #skip any jobs that have converged from previous runs
                no_input_hmms -= 1
            end

        else
            hmm = init_function(id.K, id.order, id.obs_id) #initialise first BHMM in chain
            chains[id] = Vector{EM_step}() #initialise the relevant chain
            obs=code_dict[(id.obs_id,id.order)]
            lh=obs_lh_given_hmm(obs,hmm,linear=false)
            put!(input_channel, (id, 1, hmm, lh, obs))
        end
    end
    return no_input_hmms, chains, input_channel, output_channel
end

"""
    execute_EM_jobs!(worker_pool, no_input_hmms, chains, input_channel, output_channel, chains_path; load_dict, EM_func, delta_thresh, max_iterates, verbose)

Given: a vector of worker IDs ([1] to use the master), the splatted output of 'setup_EM_jobs' (no_input_hmms, chains, input_channel, output_channel), and a valid path to save chains ('chains_path'); assign chains to workers using 'load_dict' to iteratively execute 'EM_func' (default linear_step) until 'delta_thresh' convergence criterion is obtained or 'max_iterates' have been calculated. Specifying 'verbose=true' gives some additional debug output from 'EM_converge!'.
"""
function execute_EM_jobs!(worker_pool::Vector{Int64}, no_input_hmms::Integer, chains::Dict{Chain_ID,Vector{EM_step}},  input_channel::RemoteChannel, output_channel::RemoteChannel, chains_path::String; load_dict=Dict{Int64,LoadConfig}(), EM_func::Function=linear_step, delta_thresh=1e-3, max_iterates=5000, verbose=false)
    #argument checking
    length(worker_pool) < 1 && throw(ArgumentError("Worker pool must contain one or more worker IDs!"))
    no_input_hmms < 1 && throw(ArgumentError("Zero input HMMs reported, likely continuing from chains already converged beyond default delta_thresh for setup_EM_jobs"))
    length(chains) < 1 && throw(ArgumentError("No chains supplied, likely job set from setup_EM_jobs passed incorrectly"))
    !isready(input_channel) && throw(ArgumentError("BHMM input channel has no contents, likely job set from setup_EM_jobs already executed"))

    #SEND BHMM FIT JOBS TO WORKERS
    if isready(input_channel) > 0
        @info "Fitting HMMs.."
        #WORKERS FIT HMMS
        for worker in worker_pool
            if worker in keys(load_dict)
                remote_do(EM_converge!, worker, input_channel, output_channel, no_input_hmms, load_config=load_dict[worker], EM_func=EM_func, delta_thresh=delta_thresh, max_iterates=max_iterates, verbose=verbose)
            else
                remote_do(EM_converge!, worker, input_channel, output_channel, no_input_hmms, EM_func=EM_func, delta_thresh=delta_thresh, max_iterates=max_iterates, verbose=verbose)
            end
        end
    else
        @warn "No input HMMs (all already converged?), skipping fitting.."
    end

    #GET LEARNT HMMS OFF REMOTECHANNEL, SERIALISE AT EVERY ITERATION, UPDATE PROGRESS METERS
    job_counter=no_input_hmms
    learning_meters=Dict{Chain_ID, ProgressHMM}()
    overall_meter=Progress(no_input_hmms,"Overall batch progress:")

    while job_counter > 0
        wait(output_channel)
        workerid, jobid, iterate, hmm, log_p, delta, converged, steptime = take!(output_channel)
        #either update an existing ProgressHMM meter or create a new one for the job
        if haskey(learning_meters, jobid) && iterate > 2
                update!(learning_meters[jobid], delta, steptime)
        else
            offset = workerid - 1
            if iterate <=2
                learning_meters[jobid] = ProgressHMM(delta_thresh, "$jobid on Wk $workerid:", offset, 2)
                update!(learning_meters[jobid], delta, steptime)
            else
                learning_meters[jobid] = ProgressHMM(delta_thresh, "$jobid on Wk $workerid:", offset, iterate)
                update!(learning_meters[jobid], delta, steptime)
            end
        end
        #push the hmm and related params to the results_dict
        push!(chains[jobid], EM_step(iterate, hmm, log_p, delta, converged))
        #try to serialize the results; catch interrupts and other errors to prevent corruption
        try
            serialize(chains_path, chains)
        catch e
            @warn "Serializing failed!"
            println(e)
        end

        #decrement the job counter, update overall progress meter, and save the current results dict on convergence or max iterate
        if converged || iterate == max_iterates
            job_counter -= 1
            ProgressMeter.update!(overall_meter, (no_input_hmms-job_counter))
            if !isready(input_channel) #if there are no more jobs to be learnt, retire the worker
                workerid!=1 && rmprocs(workerid)
            end
        end
    end

    #count converged & unconverged jobs, report results
    converged_counter = 0
    unconverged_counter = 0
    for (id, chain) in chains
        chain[end].converged == true ? (converged_counter += 1) : (unconverged_counter += 1)
    end

    @info "Background HMM batch learning task complete, $converged_counter converged jobs, $unconverged_counter jobs failed to converge in $max_iterates iterates since job start."
end


function ecRCCSDpT{T}() where T <: AbstractFloat
    @output "Calling CASCI module...\n"
    # Call CASCI
    cas = Fermi.ConfigurationInteraction.CASCI{T}()
    ecRCCSDpT{T}(cas)
end

function ecRCCSDpT{T}(cas::Fermi.ConfigurationInteraction.CASCI) where T <: AbstractFloat

    @output "Processing CAS data...\n"
    refdet, Casdata = process_cas(cas)
    refwfn = cas.ref

    ecccsd = ecRCCSD{T}(refdet, refwfn, Casdata, CTF())
    drop_occ = Fermi.CurrentOptions["drop_occ"]
    ecRCCSDpT{T}(ecccsd, refwfn.ints, refdet, Casdata["C3dets"], refwfn.ndocc, drop_occ)
end

function ecRCCSDpT{T}(ecccsd::RCCSD, ints::IntegralHelper, ref::Determinant, casT3::Array{Determinant,1}, ndocc::Int, fcn::Int) where T <: AbstractFloat

    @output "\n   • Perturbative Triples Started\n\n"
    @output "T3 within EC active space are going to be skipped\n\n"

    T1 = ecccsd.T1.data
    T2 = ecccsd.T2.data

    Vvvvo = permutedims(ints["OVVV"], (4,2,3,1))
    Vvooo = permutedims(ints["OOOV"], (4,2,1,3))
    Vvovo = permutedims(ints["OOVV"], (3,1,4,2))

    o,v = size(T1)
    Et::T = 0.0

    fo = diag(ints["FOO"])
    fv = diag(ints["FVV"])

    # Pre-allocate Intermediate arrays
    W  = Array{T}(undef, v,v,v)
    V  = Array{T}(undef, v,v,v)

    # Pre-allocate Views
    Vvvvo_4k =    Array{T}(undef,v,v,v)
    Vvooo_2k_3j = Array{T}(undef,v,o)
    Vvooo_2k_3i = Array{T}(undef,v,o)
    Vvooo_2i_3k = Array{T}(undef,v,o)
    Vvooo_2j_3k = Array{T}(undef,v,o)
    T2_1k_2j    = Array{T}(undef,v,v)
    T2_1k_2i    = Array{T}(undef,v,v)
    T2_1i_2k    = Array{T}(undef,v,v)
    T2_1j_2k    = Array{T}(undef,v,v)
    T2_1k       = Array{T}(undef,o,v,v)
    T1_1k       = Array{T}(undef,v)
    Vvovo_2j_4k = Array{T}(undef,v,v)
    Vvovo_2i_4k = Array{T}(undef,v,v)
    Vvvvo_4i    = Array{T}(undef,v,v,v)
    T2_1i       = Array{T}(undef,o,v,v)
    T1_1i       = Array{T}(undef,v)
    Vvvvo_4j    = Array{T}(undef,v,v,v)
    Vvooo_2i_3j = Array{T}(undef,v,o)
    Vvooo_2j_3i = Array{T}(undef,v,o)
    T2_1i_2j    = Array{T}(undef,v,v)
    T2_1j_2i    = Array{T}(undef,v,v)
    T2_1j       = Array{T}(undef,o,v,v)
    Vvovo_2i_4j = Array{T}(undef,v,v)
    T1_1j       = Array{T}(undef,v)
    @output "Computing energy contribution from occupied orbitals:\n"
    for i in 1:o
        iα = ref.α ⊻ 1 << (i+fcn-1)
        @output "→ Orbital {} of {}\n" i o
        Vvvvo_4i .= view(Vvvvo, :,:,:,i)
        T2_1i    .= view(T2, i,:,:,:)
        T1_1i    .= view(T1, i, :)
        for j in 1:i
            jβ = ref.β ⊻ 1 << (j+fcn-1)
            Vvvvo_4j    .= view(Vvvvo, :,:,:,j)
            Vvooo_2i_3j .= view(Vvooo,:,i,j,:)
            Vvooo_2j_3i .= view(Vvooo,:,j,i,:)
            T2_1i_2j    .= view(T2, i,j,:,:)
            T2_1j_2i    .= view(T2, j,i,:,:)
            T2_1j       .= view(T2, j,:,:,:)
            Vvovo_2i_4j .= view(Vvovo, :,i,:,j)
            T1_1j       .= view(T1, j, :)
            δij = Int(i == j)
            for k in 1:j
                kiα = iα ⊻ 1 << (k+fcn-1)
                Vvvvo_4k    .= view(Vvvvo, :,:,:,k)
                Vvooo_2k_3j .= view(Vvooo,:,k,j,:)
                Vvooo_2k_3i .= view(Vvooo,:,k,i,:)
                Vvooo_2i_3k .= view(Vvooo,:,i,k,:)
                Vvooo_2j_3k .= view(Vvooo,:,j,k,:)

                T2_1k_2j .= view(T2, k,j,:,:)
                T2_1k_2i .= view(T2, k,i,:,:)
                T2_1i_2k .= view(T2, i,k,:,:)
                T2_1j_2k .= view(T2, j,k,:,:)

                T2_1k .= view(T2, k,:,:,:)
                T1_1k .= view(T1, k, :)

                Vvovo_2j_4k .= view(Vvovo, :,j,:,k)
                Vvovo_2i_4k .= view(Vvovo, :,i,:,k)

                # Build intermediates W and V for the set i j k
                @tensoropt begin
                    W[a,b,c]  = (Vvvvo_4i[b,d,a]*T2_1k_2j[c,d] - Vvooo_2k_3j[c,l]*T2_1i[l,a,b]  # ijk abc
                              +  Vvvvo_4i[c,d,a]*T2_1j_2k[b,d] - Vvooo_2j_3k[b,l]*T2_1i[l,a,c]  # ikj acb
                              +  Vvvvo_4k[a,d,c]*T2_1j_2i[b,d] - Vvooo_2j_3i[b,l]*T2_1k[l,c,a]  # kij cab
                              +  Vvvvo_4k[b,d,c]*T2_1i_2j[a,d] - Vvooo_2i_3j[a,l]*T2_1k[l,c,b]  # kji cba
                              +  Vvvvo_4j[c,d,b]*T2_1i_2k[a,d] - Vvooo_2i_3k[a,l]*T2_1j[l,b,c]  # jki bca
                              +  Vvvvo_4j[a,d,b]*T2_1k_2i[c,d] - Vvooo_2k_3i[c,l]*T2_1j[l,b,a]) # jik bac

                    V[a,b,c]  = W[a,b,c] + Vvovo_2j_4k[b,c]*T1_1i[a] + Vvovo_2i_4k[a,c]*T1_1j[b] + Vvovo_2i_4j[a,b]*T1_1k[c]
                end

                # Compute Energy contribution
                δjk = Int(j == k)
                for a in 1:v
                    akiα = kiα | 1 << (a+ndocc-1)
                    for b in 1:a
                        bjβ = jβ | 1 << (b+ndocc-1)
                        δab = Int(a == b)
                        for c in 1:b
                            cakiα = akiα | 1 << (c+ndocc-1)
                            _det = Determinant(cakiα, bjβ)
                            if _det in casT3
                                continue
                            end
                            Dd = fo[i] + fo[j] + fo[k] - fv[a] - fv[b] - fv[c]
                            δbc = Int(b == c)
                            @inbounds X = (W[a,b,c]*V[a,b,c] + W[a,c,b]*V[a,c,b] + W[b,a,c]*V[b,a,c] + W[b,c,a]*V[b,c,a] + W[c,a,b]*V[c,a,b] + W[c,b,a]*V[c,b,a])
                            @inbounds Y = (V[a,b,c] + V[b,c,a] + V[c,a,b])
                            @inbounds Z = (V[a,c,b] + V[b,a,c] + V[c,b,a])
                            E = (Y - 2*Z)*(W[a,b,c] + W[b,c,a] + W[c,a,b]) + (Z - 2*Y)*(W[a,c,b]+W[b,a,c]+W[c,b,a]) + 3*X
                            Et += E*(2-δij-δjk)/(Dd*(1+δab+δbc))
                        end
                    end
                end
            end
        end
    end
    @output "Final (T) contribution:   {:15.10f}\n" Et
    @output "ecCCSD(T) energy:         {:15.10f}\n" Et+ecccsd.CorrelationEnergy
    return RCCSDpT{T}(ecccsd, Et)
end


"""
    PedestrianLatLonAccel
Pedestrian walking action. Acceleration in the Frenet frame, along with
desired lane after crossing the street.

# Fields
- `a_lat::Float64` lateral acceleration [m/s^2]
- `a_lon::Float64` longitudinal accelaration [m/s^2]
- `lane_des::Lane` desired lane to move to
"""
struct PedestrianLatLonAccel
    a_lat::Float64
    a_lon::Float64
    lane_des::Lane
end

function AutomotiveDrivingModels.propagate(ped::Entity{VehicleState, D, I},
                                           action::PedestrianLatLonAccel,
                                           roadway::Roadway,
                                           ΔT::Float64=0.1
                                           ) where {D,I}
    state = propagate(ped, LatLonAccel(action.a_lat, action.a_lon), roadway, ΔT)
    roadproj = proj(posg(state), roadway[action.lane_des.tag], roadway, move_along_curves=false)
    retval = VehicleState(Frenet(roadproj, roadway), roadway, vel(state))
    return retval
end


import OnlineStats: OnlineStat, Extrema, fit!, value, HistogramStat, Ash
import ...Cubes.Axes: CategoricalAxis, RangeAxis
import IterTools
using WeightedOnlineStats
import ProgressMeter: next!, Progress, ProgressUnknown

import WeightedOnlineStats: WeightedOnlineStat
abstract type TableAggregator end
struct OnlineAggregator{O,S} <: TableAggregator
    o::O
end
function OnlineAggregator(O::OnlineStat, s::Symbol)
    OnlineAggregator{typeof(O),s}(copy(O))
end
cubeeltype(t::OnlineAggregator) = Float64
function fitrow!(o::OnlineAggregator{T,S}, r) where {T<:OnlineStat,S}
    v = getproperty(r, S)
    !ismissing(v) && fit!(o.o, v)
end
value(o::OnlineAggregator) = value(o.o)
struct WeightOnlineAggregator{O,S,W} <: TableAggregator
    o::O
    w::W
end
function WeightOnlineAggregator(O::WeightedOnlineStat, s::Symbol, w)
    WeightOnlineAggregator{typeof(O),s,typeof(w)}(copy(O), w)
end
cubeeltype(t::WeightOnlineAggregator{T}) where {T} = cubeeltype(T)
value(o::WeightOnlineAggregator) = value(o.o)
function fitrow!(o::WeightOnlineAggregator{T,S}, r) where {T<:OnlineStat,S}
    v = getproperty(r, S)
    w = o.w(r)
    if !checkmiss(v) && !ismissing(w)
        fit!(o.o, v, w)
    end
end
checkmiss(v) = ismissing(v)
checkmiss(v::AbstractVector) = any(ismissing, v)
struct GroupedOnlineAggregator{O,S,BY,W,C} <: TableAggregator
    d::O
    w::W
    by::BY
    cloneobj::C
end
value(o::GroupedOnlineAggregator) = Dict(zip(keys(o.d), map(value, (values(o.d)))))
struct SymType{S} end
SymType(s::Symbol) = SymType{s}()
(f::SymType{S})(x) where {S} = getproperty(x, S)
getbytypes(et, by) =
    Tuple{map(i -> Base.nonmissingtype(Base.return_types(i, Tuple{et})[1]), by)...}
cubeeltype(t::GroupedOnlineAggregator{T}) where {T} = cubeeltype(T)
cubeeltype(t::Type{<:Dict{<:Any,T}}) where {T} = cubeeltype(T)
cubeeltype(t::Type{<:WeightedOnlineStat{T}}) where {T} = T
cubeeltype(t::Type{<:OnlineStat{<:Number}}) = Float64
cubeeltype(t::Type{<:WeightedCovMatrix{T}}) where {T} = T
cubeeltype(t::Type{<:Extrema{T}}) where {T} = T

function GroupedOnlineAggregator(O::OnlineStat, s::Symbol, by, w, iter)
    ost = typeof(O)
    et = eltype(iter)
    bytypes =
        Tuple{map(i -> Base.nonmissingtype(Base.return_types(i, Tuple{et})[1]), by)...}
    d = Dict{bytypes,ost}()
    GroupedOnlineAggregator{typeof(d),s,typeof(by),typeof(w),ost}(d, w, by, O)
end

dicteltype(::Type{<:Dict{K,V}}) where {K,V} = V
dictktype(::Type{<:Dict{K,V}}) where {K,V} = K
actval(v) = v
actval(v::SentinelMissings.SentinelMissing) = v[]
function fitrow!(o::GroupedOnlineAggregator{T,S,BY,W}, r) where {T,S,BY,W}
    v = getproperty(r, S)
    if !ismissing(v)
        w = o.w(r)
        if w===nothing
            bykey = map(i->actval(i(r)),o.by)
            if !any(ismissing,bykey)
                if haskey(o.d,bykey)
                    fit!(o.d[bykey],actval(v))
                else
                    o.d[bykey] = copy(o.cloneobj)
                    fit!(o.d[bykey],actval(v))
                end
            end
        else
            if !ismissing(w)
                bykey = map(i -> actval(i(r)), o.by)
                if !any(ismissing, bykey)
                    if haskey(o.d, bykey)
                        fit!(o.d[bykey], v, w)
                    else
                        o.d[bykey] = copy(o.cloneobj)
                        fit!(o.d[bykey], v, w)
                    end
                end
            end
        end
    end
end
export TableAggregator, fittable, cubefittable
function TableAggregator(iter, O, fitsym; by = (), weight = nothing)
    !isa(by, Tuple) && (by = (by,))
    if !isempty(by)
        weight === nothing && (weight = (i -> nothing))
        by = map(i -> isa(i, Symbol) ? (SymType(i)) : i, by)
        GroupedOnlineAggregator(O, fitsym, by, weight, iter)
    else
        if weight === nothing
            OnlineAggregator(O, fitsym)
        else
            WeightOnlineAggregator(O, fitsym, weight)
        end
    end
end

function tooutaxis(
    ::SymType{s},
    iter::CubeIterator{<:Any,<:Any,<:Any,<:Any,<:Any,S},
    k,
    ibc,
) where {s,S}
    ichosen = findfirst(i -> i === s, fieldnames(S))
    if ichosen <= length(iter.inars)
        bycube = iter.dc.incubes[ichosen].cube
        if haskey(bycube.properties, "labels")
            idict = bycube.properties["labels"]
            axname = get(bycube.properties, "name", "Label")
            outAxis = CategoricalAxis(axname, collect(String, values(idict)))
            convertdict = Dict(k => i for (i, k) in enumerate(keys(idict)))
        else
            sort!(k)
            outAxis = CategoricalAxis("Label$(ibc)", k)
            convertdict = Dict(k => i for (i, k) in enumerate(k))
        end
    else
        iax = findAxis(string(s), iter.dc.LoopAxes)
        outAxis = iter.dc.LoopAxes[iax]
        convertdict = Dict(k => i for (i, k) in enumerate(outAxis.values))
    end
    outAxis, convertdict
end
function tooutaxis(f, iter::CubeIterator{<:Any,<:Any,<:Any,<:Any,<:Any,S}, k, ibc) where {S}
    sort!(k)
    outAxis = CategoricalAxis("Category$(ibc)", k)
    convertdict = Dict(k => i for (i, k) in enumerate(k))
    outAxis, convertdict
end

varsym(::WeightOnlineAggregator{<:Any,S}) where {S} = S
varsym(::OnlineAggregator{<:Any,S}) where {S} = S
varsym(::GroupedOnlineAggregator{<:Any,S}) where {S} = S
axt(::CategoricalAxis) = CategoricalAxis
axt(::RangeAxis) = RangeAxis
getStatType(::WeightOnlineAggregator{T}) where {T} = T
getStatType(::OnlineAggregator{T}) where {T} = T
getStatType(t::GroupedOnlineAggregator{T}) where {T} = getStatType(T)
getStatType(t::Type{<:Dict{<:Any,T}}) where {T} = T

getStatOutAxes(tab, agg) = getStatOutAxes(tab, agg, getStatType(agg))
getStatOutAxes(tab, agg, ::Type{<:OnlineStat}) = ()
function getStatOutAxes(tab, agg, ::Type{<:Extrema})
    (CategoricalAxis(:Extrema, ["min", "max"]),)
end
function getStatOutAxes(tab, agg, ::Type{<:WeightedCovMatrix})
    varn = fieldnames(eltype(tab))
    s = varsym(agg)
    icube = findfirst(isequal(s), varn)
    ax = tab.dc.incubes[icube].axesSmall[1]
    oldname = YAXArrays.Cubes.Axes.axname(ax)
    coname = string("Co", oldname)
    v = ax.values
    axtype = axt(ax)
    a1 = axtype(oldname, copy(v))
    a2 = axtype(coname, copy(v))
    (a1, a2)
end
function getStatOutAxes(tab,agg,::Type{<:Union{Ash,HistogramStat, WeightedAdaptiveHist}})
    nbin = getnbins(agg)
    a1 = RangeAxis("Bin", 1:nbin)
    a2 = CategoricalAxis("Hist", ["MidPoints", "Frequency"])
    (a1, a2)
end
function getByAxes(iter, agg::GroupedOnlineAggregator)
    by = agg.by
    ntuple(length(by)) do ibc
        bc = agg.by[ibc]
        tooutaxis(bc, iter, unique(map(i -> i[ibc], collect(keys(agg.d)))), ibc)
    end
end
getByAxes(iter, agg) = ()

function tooutcube(agg, iter, post)
    outaxby = getByAxes(iter, agg)
    axby = map(i -> i[1], outaxby)
    convdictall = map(i -> i[2], outaxby)

    outaxstat = getStatOutAxes(iter, agg)
    outax = (outaxstat..., axby...)
    snew = map(length, outax)
    aout = fill!(zeros(Union{cubeeltype(agg),Missing}, snew), missing)
    filloutar(aout, convdictall, agg, map(i -> 1:length(i), outaxstat), post)

    YAXArray(collect(outax), aout)
end
function filloutar(aout, convdictall, agg::GroupedOnlineAggregator, s, post)
    for (k, v) in agg.d
        i = CartesianIndices((
            s...,
            map(
                (i, d) -> d[convert(keytype(d), i)]:d[convert(keytype(d), i)],
                k,
                convdictall,
            )...,
        ))
        aout[i.indices...] .= post(v)
    end
end
function filloutar(aout, convdictall, agg, g, post)
    copyto!(aout, post(agg.o))
end
"""
    fittable(tab,o,fitsym;by=(),weight=nothing)

Loops through an iterable table `tab` and thereby fitting an OnlineStat `o` with the values
specified through `fitsym`. Optionally one can specify a field (or tuple) to group by.
Any groupby specifier can either be a symbol denoting the entry to group by or an anynymous
function calculating the group from a table row.

For example the following would caluclate a weighted mean over a cube weighted by grid cell
area and grouped by country and month:

````julia
fittable(iter,WeightedMean,:tair,weight=(i->abs(cosd(i.lat))),by=(i->month(i.time),:country))
````
"""
function fittable(tab, o, fitsym; by = (), weight = nothing, showprog = false)
    agg = TableAggregator(tab, o, fitsym, by = by, weight = weight)
    if showprog
        runfitrows_progress(agg, tab)
    else
        foreach(i -> fitrow!(agg, i), tab)
    end
    agg
end
fittable(tab, o::Type{<:OnlineStat}, fitsym; kwargs...) =
    fittable(tab, o(), fitsym; kwargs...)

getmeter(tab) = getmeter(Base.IteratorSize(tab), tab)
getmeter(::Union{Base.HasLength,Base.HasShape}, tab) = Progress(length(tab))
getmeter(::Base.SizeUnknown, tab) = ProgressUnknown("Rows processed: ")

@noinline function runfitrows_progress(agg, tab)
    p = getmeter(tab)
    every = 0
    for row in tab
        fitrow!(agg, row)
        every += 1
        if every == 100
            next!(p, step = 100)
            every = 0
        end
    end
end

struct collectedValue{V,S,SY}
    value::V
    laststruct::S
end

function Base.getproperty(s::collectedValue{<:Any,<:Any,SY}, sy::Symbol) where {SY}
    if sy == SY
        getfield(s, :value)
    else
        getproperty(getfield(s, :laststruct), sy)
    end
end

function collectval(row::Union{Tuple,Vector}, ::Val{SY}) where {SY}
    nvars = length(row)
    v = ntuple(i -> getfield(row[i], SY), nvars) |> collect
    val = collectedValue{typeof(v),typeof(row[end]),SY}(v, row[end])
end

getpostfunction(s::OnlineStat) = getpostfunction(typeof(s))
getpostfunction(::Type{<:OnlineStat}) = value
function getpostfunction(hist::Union{Ash, HistogramStat, WeightedAdaptiveHist})
    nb = getnbins(hist)
    i->begin
        r = hcat(value(i)...)
        if size(r,1)<nb
            r = vcat(r,zeros(eltype(r),nb-size(r,1),2))
        end
        r
    end
end
getnbins(f::GroupedOnlineAggregator) = getnbins(f.cloneobj)
getnbins(f::TableAggregator) = getnbins(f.o)
getnbins(histogram::HistogramStat) = histogram.k
getnbins(whist::WeightedAdaptiveHist) = whist.alg.b
getnbins(a::Ash) = length(a.density)

fitfun(o) = fitfun(typeof(o))
fitfun(::Type{<:Any}) = fittable

"""
    cubefittable(tab,o,fitsym;post=getpostfunction(o),kwargs...)

Executes [`fittable`](@ref) on the [`@CubeTable`](@ref) `tab` with the
(Weighted-)OnlineStat `o`, looping through the values specified by `fitsym`.
Finally, writes the results from the `TableAggregator` to an output data cube.
"""
function cubefittable(tab, o, fitsym; post = getpostfunction(o), kwargs...)
    agg = fitfun(o)(tab, o, fitsym; showprog = true, kwargs...)
    tooutcube(agg, tab, post)
end

function tupleeltypebyname(::Type{NamedTuple{names,tt}}, s::Symbol) where {names,tt}
    i = findfirst(isequal(s), names)
    fieldtype(tt, i)
end


# This file includes the structure of Branch-and-Bound tree.

include_dependency("../dd/core.jl")
include_dependency("../dd/ordering.jl")
include_dependency("../dd/mathprog/dp_specification.jl")
include_dependency("../dd/mathprog/function_analysis.jl")

"""
Type to store node information in b&b
"""
struct NodeInfo
    parent_id::Int                  # id of the parent node in the tree (array)
    children_id::Tuple{Int, Int}      # id of the children nodes in the tree
    depth::Int                         # depth of the node in the tree
end

"""
Type to store model information in b&b
"""
struct ModelInfo
    m::JuMP.Model                       # the model after adding cuts in the node
    #    best_primal_sol::Array{Float64, 1}          # the best feasible solution at this node
    #    obj_primal::Float64                         # objective lower (primal) bound obtained at the node
    #    best_dual_sol::Array{Float64, 1}           # the optimal solution of the relaxation at this node
    #    obj_dual::Float64                         # objective upper (dual) bound obtained at the node
end

"""
Type to store DD information in b&b
"""
struct DDInfo
    dd::Array{DecisionDiagram,1}        # array of dds of all constraints in the model
    initial::Array{Float64, 1}              # array of intial states for constraints
    transition::Array{Array{UVFunc, 1}, 1}     # 1st dim: constraints, 2nd dim: variables, contenct: transtiion function
    con_function::Array{Function, 1}            # stores the function representing variable part of constraints
    domain::Array{Tuple{Float64, Float64, Symbol}, 1}   # array of variable domains
    order::Array{Ordering, 1}                   # ordering of variables in dd, for constraints
    lb_ub::Array{Array{Tuple{Float64, Float64}, 1},1}   # 1st dim: constraint, 2nd dim: variable, content: (lowerbound, upperbound) of the univariate function over the variable range defined in specs.
end

"""
Type to store Branching information in b&b
"""
struct BranchInfo
    #    branch_policy::?                   # which of the two policies is used in this branch form the parent node: variable range (arc label) branch, constraint value (node state) branch, and on which variables or constraints?
    #    refine_policy::Array{Bool, 1}        # do we reconstruct the dds in this branch (having more width) or we just leave it as it is after branching
    #    modified_node_layers::Array{Array{Array{Bool, 1}, 1}, 1}        # 1st dim: constraints, 2nd dim and 3rd dim: dd node layers, content: whether the node is removed (0) or not from the dd of the parent node
    #    modified_arc_layers::Array{Array{Array{Bool, 1}, 1}, 1}     # similar to the modified_node_layers, but for arcs
end

"""
Type to store branch-and-bound nodes
"""
struct BBNode
    node_info::NodeInfo
    model_info::ModelInfo
    dd_info::DDInfo
    branch_info::BranchInfo
end

"""
Type to store branch-and-bound tree
"""
struct BBTree
    original_model::JuMP.Model      # the main model of the problem
    LP_model::JuMP.Model            # the LP model used in the ECP
    tree::Array{BBNode, 1}          # an array that stores branch-and-bound nodes (works as a tree)
end

"""
Create a BB tree from a model

# Input
- `m1`: Original model
- `m2`: LP relaxation of the model used in the ECP
"""
function create_BB_tree(m1::JuMP.Model, m2::JuMP.Model)::BBTree
    bb_tree = BBTree(m1, m2, Array{BBNode, 1}())
    return bb_tree
end

"""
create root node in branch-and-bound

# Input
- `bb_tree`: the newly constructed object of type BBTree, which contains a model and its tree array field must be empty.
- `prop`: Keyword that contains an initial assessment for properties of the univariate functions (optional)
"""
function create_BB_root!(bb_tree::BBTree; prop::Array{Array{Array{UVFuncProp, 1}, 1}, 1} = [])
    @assert(length(bb_tree.tree) == 0)          # ensure that the tree array field must be empty

    n::Int = MathProgBase.numvar(bb_tree.original_model)      # number of variables in model
    c_n::Int = JuMP.numnlconstr(bb_tree.original_model) + MathProgBase.numquadconstr(bb_tree.original_model)      # number of nonlinear constraints in model

    node_info = NodeInfo(0, (0,0), 0)
    branch_info = BranchInfo()
    model_info = ModelInfo(copy(bb_tree.LP_model))
    dd_info = DDInfo(Array{DecisionDiagram,1}(  c_n), Array{Float64, 1}(  c_n), Array{Array{UVFunc, 1}, 1}(  c_n), Array{Function, 1}(  c_n), Array{Tuple{Float64, Float64, Symbol}, 1}(  n), Array{Ordering, 1}(  c_n), Array{Array{Tuple{Float64, Float64}, 1},1}(  c_n))

    # Determine constraint decomposition, ordering and variable domains
    eval = evaluate_separable_constraints(bb_tree.original_model)
    create_constraint_domain_function!(dd_info.domain, bb_tree.original_model)
    for i=1:c_n
        dd_info.order[i] = NoOrdering()
    end

    # Create specs for decision diagram
    for i::Int=1:c_n
        dd_info.initial[i] = create_constraint_initial_state(eval[i])
        dd_info.con_function[i] = create_constraint_single_function(eval[i])

        dd_info.transition[i] = Array{UVFunc, 1}(n)
        if isassigned(prop, i)
            create_constraint_transition_function!(dd_info.transition[i], eval[i], prp = prop[i])            # if functional property is given as input
        else
            create_constraint_transition_function!(dd_info.transition[i], eval[i])                 # no functional properties is given as input
        end

        dd_info.lb_ub[i] = Array{Tuple{Float64, Float64}, 1}(  n)
        for j::Int=1:n
            if isassigned(dd_info.transition[i], j)
                dd_info.lb_ub[i][j] = (uvf_lb(dd_info.transition[i][j].f, dd_info.transition[i][j].prop, dd_info.domain[j]), uvf_ub(dd_info.transition[i][j].f, dd_info.transition[i][j].prop, dd_info.domain[j]))
            end
        end

        println("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%")
        # Construct decision diagram
        dd_info.dd[i] = construct_DD(dd_info.transition[i], dd_info.initial[i], dd_info.domain, dd_info.lb_ub[i], ordering=dd_info.order[i], reduced_arc=true)
        println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&")
    end

    push!(bb_tree.tree, BBNode(node_info, model_info, dd_info, branch_info))

end


"""
`module  JAC.MultipolePolarizibility`
    ... a submodel of JAC that contains all methods for computing dynamic multipole polarizibilities, including
        (dynanmic) electric- & magnetic-dipole polarizibilities and others.
"""
module MultipolePolarizibility

    using Printf, ..Basics, ..Defaults, ..ManyElectron, ..Nuclear, ..Radial, ..TableStrings

    """
    `struct  MultipolePolarizibility.Amplitude`
        ... defines a type for an individual multipole term as defined by the multipole and frequency.

        + multipole  ::EmMultipole         ... Multipole of the dynamic polarizibility contribution.
        + omega      ::Float64             ... (dynamic) frequency of the polarizibility.
        + value      ::Complex{Float64}    ... Value of this multipole amplitude.

    """
    struct  Amplitude
        multipole    ::EmMultipole
        omega        ::Float64
        value        ::Complex{Float64}
    end

    """
    `MultipolePolarizibility.Amplitude()`  ... constructor for an `empty` instance of MultipolePolarizibility.Amplitude.
    """
    function Amplitude()
        Amplitude(E1, 0., 0.)
    end

    # `Base.show(io::IO, amplitude::MultipolePolarizibility.Amplitude)`  ... prepares a proper printout of the variable amplitude.
    function Base.show(io::IO, amplitude::MultipolePolarizibility.Amplitude)
        println(io, "multipole :       $(amplitude.multipole)  ")
        println(io, "omega:            $(amplitude.omega)  ")
        println(io, "value:            $(amplitude.value)  ")
    end

    """
    `struct  MultipolePolarizibility.Outcome`  ... defines a type to keep the outcome of a dynamic-polarizibility computation.

        + level        ::Level                                       ... Atomic level to which the outcome refers to.
        + amplitudes   ::Array{MultipolePolarizibility.Amplitude,1}  ... multipole (and frequency-dependent) contribution to the polarizibility.
    """
    struct Outcome
        level          ::Level
        amplitudes     ::Array{MultipolePolarizibility.Amplitude,1}
    end

    """
    `MultipolePolarizibility.Outcome()`
        ... constructor for an `empty` instance of MultipolePolarizibility.Outcome for the computation of dynamic
            polarizibilities.
    """
    function Outcome()
        Outcome(Level(), MultipolePolarizibility.Amplitude[])
    end

    # `Base.show(io::IO, outcome::MultipolePolarizibility.Outcome)`
    #		 ... prepares a proper printout of the variable outcome::MultipolePolarizibility.Outcome.
    function Base.show(io::IO, outcome::MultipolePolarizibility.Outcome)
        println(io, "level:                     $(outcome.level)  ")
        println(io, "amplitudes:                $(outcome.amplitudes)  ")
    end

    """
    `struct  MultipolePolarizibility.Settings`
        ... defines a type for the details and parameters of computing multipolar polarizibilities.

        + multipoles                ::Array{EmMultipole,1}  ... Multipoles to be considered for the polarity.
        + nLower                    ::Int64                 ... Lower and upper indices in the configurations for the summation
        + nUpper                    ::Int64                     over the intermediate levels.
        + omegas                    ::Array{Float64,1}      ... List of omegas (energies) of the dynamic polarizibility.
        + printBefore               ::Bool                  ... True if a list of selected levels is printed before the
                                                                actual computations start.
        + levelSelection            ::LevelSelection        ... Specifies the selected levels, if any.
    """
    struct Settings
        multipoles                  ::Array{EmMultipole,1}
        nLower                      ::Int64
        nUpper                      ::Int64
        omegas                      ::Array{Float64,1}
        printBefore                 ::Bool
        levelSelection              ::LevelSelection
    end

    """
    `MultipolePolarizibility.Settings()`
        ... constructor for an `empty` instance of MultipolePolarizibility.Settings for the computation dynamic polarizibilities.
    """
    function Settings()
        Settings(EmMultipole[], 0, 0, Float64[], false, LevelSelection() )
    end

    # `Base.show(io::IO, settings:MultipolePolarizibility.Settings)`
    #		... prepares a proper printout of the variable settings::MultipolePolarizibility.Settings.
    function Base.show(io::IO, settings::MultipolePolarizibility.Settings)
        println(io, "multipoles:               $(settings.multipoles)  ")
        println(io, "nLower:                   $(settings.nLower)  ")
        println(io, "nUpper:                   $(settings.nUpper)  ")
        println(io, "omegas:                   $(settings.omegas)  ")
        println(io, "printBefore:              $(settings.printBefore)  ")
        println(io, "levelSelection:           $(settings.levelSelection)  ")
    end

    """
    `MultipolePolarizibility.computeAmplitudesProperties(outcome::MultipolePolarizibility.Outcome, grid::Radial.Grid,
                                                         settings::MultipolePolarizibility.Settings)
        ... to compute all amplitudes and properties for a given level; an outcome::MultipolePolarizibility.Outcome is
            returned for which the amplitudes and properties are now evaluated explicitly.
    """
    function  computeAmplitudesProperties(outcome::MultipolePolarizibility.Outcome, grid::Radial.Grid, settings::MultipolePolarizibility.Settings)
        ## global JAC_counter
        ## return( newOutcome )
    end

    """
    `MultipolePolarizibility.computeOutcomes(multiplet::Multiplet, nm::Nuclear.Model, grid::Radial.Grid,
                                                 settings::MultipolePolarizibility.Settings; output=true)`
        ... to compute (as selected) the alpha-variation parameters for the levels of the given multiplet and as specified by
            the given settings. The results are printed in neat tables to screen but nothing is returned otherwise.
    """
    function computeOutcomes(multiplet::Multiplet, nm::Nuclear.Model, grid::Radial.Grid,
             settings::MultipolePolarizibility.Settings; output=true)
        println("")
        printstyled("MultipolePolarizibility.computeOutcomes(): The computation of multipole polarizibilities starts now ... \n", color=:light_green)
        printstyled("------------------------------------------------------------------------------------------------------- \n", color=:light_green)
        #
        outcomes = MultipolePolarizibility.determineOutcomes(multiplet, settings)
        # Display all selected levels before the computations start
        if  settings.printBefore    MultipolePolarizibility.displayOutcomes(outcomes)    end
        # Calculate all amplitudes and requested properties
        newOutcomes = MultipolePolarizibility.Outcome[]
        for  outcome in outcomes
            ## newOutcome = MultipolePolarizibility.computeAmplitudesProperties(outcome, nm, grid, settings)
            newOutcome = MultipolePolarizibility.Outcome(outcome.level, [ MultipolePolarizibility.Amplitude(E1, 1., 2.)] )
            push!( newOutcomes, newOutcome)
        end
        # Print all results to screen
        MultipolePolarizibility.displayResults(stdout, newOutcomes)
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        if  printSummary    MultipolePolarizibility.displayResults(iostream, newOutcomes)   end
        #
        if    output    return( newOutcomes )
        else            return( nothing )
        end
    end

    """
    `MultipolePolarizibility.determineOutcomes(multiplet::Multiplet, settings::MultipolePolarizibility.Settings)`
        ... to determine a list of Outcomes's for the computation of the multipole polarizibilities for the given multiplet. It takes
            into account the particular selections and settings. An Array{MultipolePolarizibility.Outcome,1} is returned. Apart from the
            level specification, all physical properties are set to zero during the initialization process.
    """
    function  determineOutcomes(multiplet::Multiplet, settings::MultipolePolarizibility.Settings)
        outcomes = MultipolePolarizibility.Outcome[]
        for  level  in  multiplet.levels
            if  Basics.selectLevel(level, settings.levelSelection)
                push!( outcomes, MultipolePolarizibility.Outcome(level, MultipolePolarizibility.Amplitude[]) )
            end
        end
        return( outcomes )
    end

    """
    `MultipolePolarizibility.displayOutcomes(outcomes::Array{MultipolePolarizibility.Outcome,1})`
        ... to display a list of levels that have been selected for the computations. A small neat table of all selected
            levels and their energies is printed but nothing is returned otherwise.
    """
    function  displayOutcomes(outcomes::Array{MultipolePolarizibility.Outcome,1})
        nx = 43
        println(" ")
        println("  Selected MultipolePolarizibility levels:")
        println(" ")
        println("  ", TableStrings.hLine(nx))
        sa = "  ";   sb = "  "
        sa = sa * TableStrings.center(10, "Level"; na=2);                             sb = sb * TableStrings.hBlank(12)
        sa = sa * TableStrings.center(10, "J^P";   na=4);                             sb = sb * TableStrings.hBlank(14)
        sa = sa * TableStrings.center(14, "Energy"; na=4);
        sb = sb * TableStrings.center(14, TableStrings.inUnits("energy"); na=4)
        println(sa);    println(sb);    println("  ", TableStrings.hLine(nx))
        #
        for  outcome in outcomes
            sa  = "  ";    sym = LevelSymmetry( outcome.level.J, outcome.level.parity)
            sa = sa * TableStrings.center(10, TableStrings.level(outcome.level.index); na=2)
            sa = sa * TableStrings.center(10, string(sym); na=4)
            sa = sa * @sprintf("%.8e", Defaults.convertUnits("energy: from atomic", outcome.level.energy)) * "    "
            println( sa )
        end
        println("  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

    """
    `MultipolePolarizibility.displayResults(stream::IO, outcomes::Array{MultipolePolarizibility.Outcome,1})`
        ... to display the energies, M_ms and F-parameters, etc. for the selected levels. A neat table is printed but nothing is
            returned otherwise.
    """
    function  displayResults(stream::IO, outcomes::Array{MultipolePolarizibility.Outcome,1})
        nx = 64
        println(stream, " ")
        println(stream, "  Multipole polarizibilities and amplitudes:")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "  ";   sb = "  "
        sa = sa * TableStrings.center(10, "Level"; na=2);                             sb = sb * TableStrings.hBlank(12)
        sa = sa * TableStrings.center(10, "J^P";   na=4);                             sb = sb * TableStrings.hBlank(14)
        sa = sa * TableStrings.center(14, "Energy"; na=4)
        sb = sb * TableStrings.center(14, TableStrings.inUnits("energy"); na=4)
        sa = sa * TableStrings.center(14, "xxx";     na=4)
        sb = sb * TableStrings.center(14, "    " ; na=4)
        println(stream, sa);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  outcome in outcomes
            sa  = "  ";    sym = LevelSymmetry( outcome.level.J, outcome.level.parity)
            sa = sa * TableStrings.center(10, TableStrings.level(outcome.level.index); na=2)
            sa = sa * TableStrings.center(10, string(sym); na=4)
            energy = 1.0
            sa = sa * @sprintf("%.8e", Defaults.convertUnits("energy: from atomic", energy))              * "    "
            println(stream, sa )
        end
        println(stream, "  ", TableStrings.hLine(nx), "\n\n")
        #
        return( nothing )
    end

end # module