using LinearAlgebra # For rmul!

using MathOptInterface
const MOI = MathOptInterface
const MOIU = MOI.Utilities
const AFFEQ = MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}}
const BoundsSet = Union{MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, MOI.Interval{Float64}}

@enum VariableType NNEG PSD

struct VariableInfo
    variable_type::VariableType
    cone_index::Int
    index_in_cone::Int
end

mutable struct Optimizer <: MOI.AbstractOptimizer
    b::Vector{Float64}
    l::Vector{Float64}
    u::Vector{Float64}

    variable_info::Vector{VariableInfo}
    single_variable_mask::Vector{UInt8}

    # PSDCone variables
    psdc_dims::Vector{Int}
    psdc_Cvar::Vector{Vector{Int}}
    psdc_Cval::Vector{Vector{Float64}}
    psdc_Avar::Vector{Vector{Int}}
    psdc_Acon::Vector{Vector{Int}}
    psdc_Aval::Vector{Vector{Float64}}
    psdc_Bvar::Vector{Vector{Int}}
    psdc_Bcon::Vector{Vector{Int}}
    psdc_Bval::Vector{Vector{Float64}}
    psdc_L::Vector{Matrix{Float64}}
    psdc_U::Vector{Matrix{Float64}}

    # NonNEGatives variables
    num_nneg::Int
    nneg_info::Vector{Int} # Similar to `info` field of `MOI.Bridges.Variable.Map`.
    nneg_Cvar::Vector{Int}
    nneg_Cval::Vector{Float64}
    nneg_Avar::Vector{Int}
    nneg_Acon::Vector{Int}
    nneg_Aval::Vector{Float64}
    nneg_Bvar::Vector{Int}
    nneg_Bcon::Vector{Int}
    nneg_Bval::Vector{Float64}
    nneg_L::Vector{Float64}
    nneg_U::Vector{Float64}

    objective_sense::MOI.OptimizationSense
    objective_constant::Float64

    primal_objective_value::Float64
    dual_objective_value::Float64
    nneg_X::Vector{Float64}
    psdc_X::Vector{Vector{Float64}}
    s::Vector{Float64}
    y::Vector{Float64}
    y2::Vector{Float64}
    nneg_Z1::Vector{Float64}
    nneg_Z2::Vector{Float64}
    psdc_Z1::Vector{Vector{Float64}}
    psdc_Z2::Vector{Vector{Float64}}
    info::Dict{String, Any}
    runhist::Dict{String, Any}
    status::Union{Nothing, Int}
    solve_time::Float64

    silent::Bool
    options::Dict{Symbol, Any}
    function Optimizer(; kwargs...)
        optimizer = new(
            Float64[], Float64[], Float64[], VariableInfo[], UInt8[],
            Int[], Vector{Int}[], Vector{Float64}[], Vector{Int}[], Vector{Int}[], Vector{Float64}[], Vector{Int}[], Vector{Int}[], Vector{Float64}[], Matrix{Float64}[], Matrix{Float64}[],
            0, Int[], Int[], Float64[], Int[], Int[], Float64[], Int[], Int[], Float64[], Float64[], Float64[],
            MOI.FEASIBILITY_SENSE, 0.0,
            NaN, NaN,
            Float64[], Vector{Float64}[], # X
            Float64[], # s
            Float64[], Float64[], # y, y2 ≈ v
            Float64[], Float64[], Vector{Float64}[], Vector{Float64}[], # Z
            Dict{String, Any}(), Dict{String, Any}(),
            nothing, NaN,
            false, Dict{Symbol, Any}())
        for (key, value) in kwargs
            MOI.set(optimizer, MOI.RawParameter(string(key)), value)
        end
        return optimizer
    end
end

function MOI.supports(optimizer::Optimizer, param::MOI.RawParameter)
    return param.name in ALLOWED_OPTIONS
end
function MOI.set(optimizer::Optimizer, param::MOI.RawParameter, value)
    if !MOI.supports(optimizer, param)
        throw(MOI.UnsupportedAttribute(param))
    end
    optimizer.options[Symbol(param.name)] = value
end
function MOI.get(optimizer::Optimizer, param::MOI.RawParameter)
    # TODO: This gives a poor error message if the name of the parameter is invalid.
    return optimizer.options[Symbol(param.name)]
end

MOI.supports(::Optimizer, ::MOI.Silent) = true
function MOI.set(optimizer::Optimizer, ::MOI.Silent, value::Bool)
    optimizer.silent = value
end
MOI.get(optimizer::Optimizer, ::MOI.Silent) = optimizer.silent

MOI.get(::Optimizer, ::MOI.SolverName) = "SDPNAL"
function MOI.get(optimizer::Optimizer, ::MOI.SolveTime)
    return optimizer.solve_time
end

function MOI.is_empty(optimizer::Optimizer)
    return isempty(optimizer.b) && isempty(optimizer.l) && isempty(optimizer.u) &&
        isempty(optimizer.variable_info) &&
        iszero(optimizer.psdc_dims) &&
        iszero(optimizer.num_nneg) &&
        optimizer.objective_sense == MOI.FEASIBILITY_SENSE &&
        iszero(optimizer.objective_constant)
end

function MOI.empty!(optimizer::Optimizer)
    empty!(optimizer.b)
    empty!(optimizer.l)
    empty!(optimizer.u)

    empty!(optimizer.variable_info)
    empty!(optimizer.single_variable_mask)

    empty!(optimizer.psdc_dims)
    empty!(optimizer.psdc_Cvar)
    empty!(optimizer.psdc_Cval)
    empty!(optimizer.psdc_Avar)
    empty!(optimizer.psdc_Acon)
    empty!(optimizer.psdc_Aval)
    empty!(optimizer.psdc_Bvar)
    empty!(optimizer.psdc_Bcon)
    empty!(optimizer.psdc_Bval)
    empty!(optimizer.psdc_L)
    empty!(optimizer.psdc_U)

    optimizer.num_nneg = 0
    empty!(optimizer.nneg_info)
    empty!(optimizer.nneg_Cvar)
    empty!(optimizer.nneg_Cval)
    empty!(optimizer.nneg_Avar)
    empty!(optimizer.nneg_Acon)
    empty!(optimizer.nneg_Aval)
    empty!(optimizer.nneg_Bvar)
    empty!(optimizer.nneg_Bcon)
    empty!(optimizer.nneg_Bval)
    empty!(optimizer.nneg_L)
    empty!(optimizer.nneg_U)

    optimizer.objective_sense = MOI.FEASIBILITY_SENSE
    optimizer.objective_constant = 0.0

    optimizer.primal_objective_value = NaN
    optimizer.dual_objective_value = NaN
    empty!(optimizer.nneg_X)
    empty!(optimizer.psdc_X)
    empty!(optimizer.s)
    empty!(optimizer.y)
    empty!(optimizer.y2)
    empty!(optimizer.nneg_Z1)
    empty!(optimizer.nneg_Z2)
    empty!(optimizer.psdc_Z1)
    empty!(optimizer.psdc_Z2)
    empty!(optimizer.info)
    empty!(optimizer.runhist)
    optimizer.status = nothing
    optimizer.solve_time = NaN
end

function MOI.supports(
    optimizer::Optimizer,
    ::Union{MOI.ObjectiveSense,
            MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}})
    return true
end

MOI.supports_add_constrained_variables(::Optimizer, ::Type{MOI.Reals}) = false

const SupportedSets = Union{MOI.Nonnegatives, MOI.PositiveSemidefiniteConeTriangle}
MOI.supports_add_constrained_variables(::Optimizer, ::Type{<:SupportedSets}) = true
function MOI.supports_constraint(
    ::Optimizer, ::Type{MOI.ScalarAffineFunction{Float64}},
    ::Type{<:Union{MOI.EqualTo{Float64}, BoundsSet}})
    return true
end

function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kws...)
    return MOIU.automatic_copy_to(dest, src; kws...)
end
MOIU.supports_default_copy_to(::Optimizer, copy_names::Bool) = !copy_names

# Variables
function _add_nonneg_variable(optimizer::Optimizer)
    optimizer.num_nneg += 1
    push!(optimizer.variable_info, VariableInfo(NNEG, 1, optimizer.num_nneg))
    push!(optimizer.single_variable_mask, 0x0)
    return MOI.VariableIndex(length(optimizer.variable_info))
end
function _add_psdc_variable(optimizer::Optimizer, index_in_cone)
    push!(optimizer.variable_info, VariableInfo(PSD, length(optimizer.psdc_dims), index_in_cone))
    push!(optimizer.single_variable_mask, 0x0)
    return MOI.VariableIndex(length(optimizer.variable_info))
end
function _add_constrained_variables(optimizer::Optimizer, set::MOI.Nonnegatives)
    push!(optimizer.nneg_info, MOI.dimension(set))
    for i in 2:MOI.dimension(set)
        push!(optimizer.nneg_info, i)
    end
    for i in 1:MOI.dimension(set)
        push!(optimizer.nneg_L, -Inf)
        push!(optimizer.nneg_U, Inf)
    end
    return [_add_nonneg_variable(optimizer) for i in 1:MOI.dimension(set)]
end
function _add_constrained_variables(optimizer::Optimizer, set::MOI.PositiveSemidefiniteConeTriangle)
    d = MOI.side_dimension(set)
    push!(optimizer.psdc_dims, d)
    push!(optimizer.psdc_Cvar, Int[])
    push!(optimizer.psdc_Cval, Float64[])
    push!(optimizer.psdc_Avar, Int[])
    push!(optimizer.psdc_Acon, Int[])
    push!(optimizer.psdc_Aval, Float64[])
    push!(optimizer.psdc_Bvar, Int[])
    push!(optimizer.psdc_Bcon, Int[])
    push!(optimizer.psdc_Bval, Float64[])
    push!(optimizer.psdc_L, fill(-Inf,  d, d))
    push!(optimizer.psdc_U, fill(Inf,  d, d))
    return [_add_psdc_variable(optimizer, i) for i in 1:MOI.dimension(set)]
end
function MOI.add_constrained_variables(optimizer::Optimizer, set::SupportedSets)
    vis = _add_constrained_variables(optimizer, set)
    ci = MOI.ConstraintIndex{MOI.VectorOfVariables, typeof(set)}(first(vis).value)
    return vis, ci
end

# Variable bounds
function MOI.supports_constraint(
    optimizer::Optimizer, ::Type{MOI.SingleVariable}, ::Type{<:BoundsSet})
    return true
end
function MOI.add_constraint(
    optimizer::Optimizer, f::MOI.SingleVariable, set::BoundsSet)
    vi = f.variable
    flag = MOIU.single_variable_flag(typeof(set))
    mask = optimizer.single_variable_mask[vi.value]
    MOIU.throw_if_lower_bound_set(vi, typeof(set), mask, Float64)
    MOIU.throw_if_upper_bound_set(vi, typeof(set), mask, Float64)
    info = optimizer.variable_info[vi.value]
    if !iszero(flag & MOIU.LOWER_BOUND_MASK)
        if info.variable_type == NNEG
            L = optimizer.nneg_L
        else
            L = optimizer.psdc_L[info.cone_index]
        end
        L[info.index_in_cone] = MOIU.extract_lower_bound(set)
    end
    if !iszero(flag & MOIU.UPPER_BOUND_MASK)
        if info.variable_type == NNEG
            U = optimizer.nneg_U
        else
            U = optimizer.psdc_U[info.cone_index]
        end
        U[info.index_in_cone] = MOIU.extract_upper_bound(set)
    end
    optimizer.single_variable_mask[vi.value] = mask | flag
    return MOI.ConstraintIndex{MOI.SingleVariable, typeof(set)}(vi.value)
end

# Objective
function MOI.get(optimizer::Optimizer, ::MOI.ObjectiveSense)
    return optimizer.objective_sense
end
sense_to_sign(sense::MOI.OptimizationSense) = sense == MOI.MAX_SENSE ? -1 : 1
function MOI.set(optimizer::Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense)
    if sense != optimizer.objective_sense
        sign = sense_to_sign(sense)
        rmul!(optimizer.nneg_Cval, -1)
        for i in eachindex(optimizer.psdc_dims)
            rmul!(optimizer.psdc_Cval[i], -1)
        end
    end
    optimizer.objective_sense = sense
end
function MOI.set(optimizer::Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}},
                 func::MOI.ScalarAffineFunction{Float64})
    optimizer.objective_constant = MOI.constant(func)
    empty!(optimizer.nneg_Cvar)
    empty!(optimizer.nneg_Cval)
    for i in eachindex(optimizer.psdc_dims)
        empty!(optimizer.psdc_Cvar[i])
        empty!(optimizer.psdc_Cval[i])
    end
    sign = sense_to_sign(optimizer.objective_sense)
    for term in func.terms
        info = optimizer.variable_info[term.variable_index.value]
        if info.variable_type == NNEG
            push!(optimizer.nneg_Cvar, info.index_in_cone)
            push!(optimizer.nneg_Cval, sign * term.coefficient)
        else
            @assert info.variable_type == PSD
            push!(optimizer.psdc_Cvar[info.cone_index], info.index_in_cone)
            push!(optimizer.psdc_Cval[info.cone_index], sign * term.coefficient)
        end
    end
end

# Constraints
function is_diagonal_index(k)
    # See https://www.juliaopt.org/MathOptInterface.jl/v0.9.3/apireference/#MathOptInterface.AbstractSymmetricMatrixSetTriangle
    i = div(1 + isqrt(8k - 7), 2)
    j = k - div((i - 1) * i, 2)
    return i == j
end
function MOI.add_constraint(optimizer::Optimizer, func::MOI.ScalarAffineFunction{Float64}, set::MOI.EqualTo{Float64})
    if !iszero(MOI.constant(func))
        # We use the fact that the initial function constant was zero to
        # implement getters for `MOI.ConstraintPrimal`.
        throw(MOI.ScalarFunctionConstantNotZero{
             Float64, typeof(func), typeof(set)}(MOI.constant(func)))
    end
    push!(optimizer.b, MOI.constant(set))
    con = length(optimizer.b)
    for term in func.terms
        info = optimizer.variable_info[term.variable_index.value]
        if info.variable_type == NNEG
            push!(optimizer.nneg_Avar, info.index_in_cone)
            push!(optimizer.nneg_Acon, con)
            push!(optimizer.nneg_Aval, term.coefficient)
        else
            @assert info.variable_type == PSD
            push!(optimizer.psdc_Avar[info.cone_index], info.index_in_cone)
            push!(optimizer.psdc_Acon[info.cone_index], con)
            coef = is_diagonal_index(info.index_in_cone) ? term.coefficient : term.coefficient / √2
            push!(optimizer.psdc_Aval[info.cone_index], coef)
        end
    end
    return AFFEQ(con)
end
function MOI.add_constraint(optimizer::Optimizer, func::MOI.ScalarAffineFunction{Float64}, set::BoundsSet)
    if !iszero(MOI.constant(func))
        # We use the fact that the initial function constant was zero to
        # implement getters for `MOI.ConstraintPrimal`.
        throw(MOI.ScalarFunctionConstantNotZero{
             Float64, typeof(func), typeof(set)}(MOI.constant(func)))
    end
    flag = MOIU.single_variable_flag(typeof(set))
    push!(optimizer.l, iszero(flag & MOIU.LOWER_BOUND_MASK) ? -Inf : MOIU.extract_lower_bound(set))
    push!(optimizer.u, iszero(flag & MOIU.UPPER_BOUND_MASK) ? Inf : MOIU.extract_upper_bound(set))
    con = length(optimizer.l)
    for term in func.terms
        info = optimizer.variable_info[term.variable_index.value]
        if info.variable_type == NNEG
            push!(optimizer.nneg_Bvar, info.index_in_cone)
            push!(optimizer.nneg_Bcon, con)
            push!(optimizer.nneg_Bval, term.coefficient)
        else
            @assert info.variable_type == PSD
            push!(optimizer.psdc_Bvar[info.cone_index], info.index_in_cone)
            push!(optimizer.psdc_Bcon[info.cone_index], con)
            coef = is_diagonal_index(info.index_in_cone) ? term.coefficient : term.coefficient / √2
            push!(optimizer.psdc_Bval[info.cone_index], coef)
        end
    end
    return MOI.ConstraintIndex{typeof(func), typeof(set)}(con)
end

# TODO could do something more efficient here
#      `SparseMatrixCSC` is returned in SumOfSquares.jl test `sos_horn`
symvec(Q::SparseMatrixCSC) = symvec(Matrix(Q))
function symvec(Q::Matrix)
    n = LinearAlgebra.checksquare(Q)
    vec_dim = MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(n))
    q = Vector{eltype(Q)}(undef, vec_dim)
    k = 0
    for j in 1:n
        for i in 1:j
            k += 1
            q[k] = Q[i, j]
        end
    end
    @assert k == length(q)
    return q
end
function symvec(Q::Vector)
    if length(Q) > 1
        error("Expected square matrix from SDPNAL but got a vector of length $(length(Q))")
    end
    return Q
end
function _vec(x::SparseMatrixCSC)
    if size(x, 2) > 1
        error("Expected a vector from SDPNAL but got a sparse matrix of size $(size(x))")
    end
    return Vector(x[:, 1])
end
_vec(x::Float64) = [x]
_vec(x::Vector) = x

function MOI.optimize!(optimizer::Optimizer)
    options = optimizer.options
    if optimizer.silent
        options = copy(options)
        options[:printlevel] = 0
    end

    blk1 = Any[]
    blk2 = Any[]
    m = length(optimizer.b)
    p = length(optimizer.l)
    At = SparseArrays.SparseMatrixCSC{Float64,Int}[]
    Bt = SparseArrays.SparseMatrixCSC{Float64,Int}[]
    # FIXME I get a strange failure with sparse vectors, need to investigate
    C = Union{Matrix{Float64}, Vector{Float64}}[]
    L = Vector{Union{Matrix{Float64}, Vector{Float64}}}(undef, 0)
    U = Vector{Union{Matrix{Float64}, Vector{Float64}}}(undef, 0)
    if !isempty(optimizer.psdc_dims)
        for (i, dim) in enumerate(optimizer.psdc_dims)
            vec_dim = MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(dim))
            push!(At, sparse(optimizer.psdc_Avar[i], optimizer.psdc_Acon[i], optimizer.psdc_Aval[i], vec_dim, m))
            push!(Bt, sparse(optimizer.psdc_Bvar[i], optimizer.psdc_Bcon[i], optimizer.psdc_Bval[i], vec_dim, p))
            c = Vector(sparsevec(optimizer.psdc_Cvar[i], optimizer.psdc_Cval[i], vec_dim))
            Ci = zeros(dim, dim)
            Li_vec = optimizer.psdc_L[i]
            Li = zeros(dim, dim)
            Ui_vec = optimizer.psdc_U[i]
            Ui = zeros(dim, dim)
            k = 0
            for col in 1:dim
                for row in 1:(col - 1)
                    k += 1
                    Ci[row, col] = c[k] / 2
                    Ci[col, row] = c[k] / 2
                    Li[row, col] = Li_vec[k]
                    Li[col, row] = Li_vec[k]
                    Ui[row, col] = Ui_vec[k]
                    Ui[col, row] = Ui_vec[k]
                end
                k += 1
                Ci[col, col] = c[k]
                Li[col, col] = Li_vec[k]
                Ui[col, col] = Ui_vec[k]
            end
            push!(C, Ci)
            push!(L, Li)
            push!(U, Ui)
            push!(blk1, "s")
            push!(blk2, Float64(dim))
        end
    end
    if !iszero(optimizer.num_nneg)
        push!(At, sparse(optimizer.nneg_Avar, optimizer.nneg_Acon, optimizer.nneg_Aval, optimizer.num_nneg, m))
        push!(Bt, sparse(optimizer.nneg_Bvar, optimizer.nneg_Bcon, optimizer.nneg_Bval, optimizer.num_nneg, p))
        push!(C, Vector(sparsevec(optimizer.nneg_Cvar, optimizer.nneg_Cval, optimizer.num_nneg)))
        push!(blk1, "l")
        push!(blk2, Float64(optimizer.num_nneg))
        push!(L, optimizer.nneg_L)
        push!(U, optimizer.nneg_U)
    end
    blk = [blk1 blk2]

    options = optimizer.options
    if optimizer.silent
        options = copy(options)
        options[:printlevel] = 0
    end

    obj, X, s, y, Z1, Z2, y2, v, optimizer.info, optimizer.runhist = sdpnalplus(
        blk, At, C, optimizer.b, L, U, Bt, optimizer.l, optimizer.u; options...)

    optimizer.primal_objective_value, optimizer.dual_objective_value = obj
    k = 0
    optimizer.psdc_X = symvec.(X[k .+ eachindex(optimizer.psdc_dims)])
    optimizer.psdc_Z1 = symvec.(Z1[k .+ eachindex(optimizer.psdc_dims)])
    optimizer.psdc_Z2 = symvec.(Z2[k .+ eachindex(optimizer.psdc_dims)])
    k += length(optimizer.psdc_dims)
    if iszero(optimizer.num_nneg)
        empty!(optimizer.nneg_X)
        empty!(optimizer.nneg_Z1)
        empty!(optimizer.nneg_Z2)
    else
        k += 1
        optimizer.nneg_X = X[k]
        optimizer.nneg_Z1 = Z1[k]
        optimizer.nneg_Z2 = _vec(Z2[k])
    end
    optimizer.s = s
    optimizer.y = y
    optimizer.y2 = _vec(y2) # Should we used `v` instead ? They should be approximately equal
    optimizer.status = optimizer.info["termcode"]
    optimizer.solve_time = optimizer.info["totaltime"]
end

function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString)
    if optimizer.status === nothing
        throw(MOI.OptimizeNotCalled())
    else
        # See `solver_main_default/sdpnalplus.m`
        if optimizer.status == 0
            return "SDP is solved to the required accuracy"
        elseif optimizer.status == 1
            return "SDP is not solved successfully"
        else
            if !(-1 <= optimizer.status <= -3)
                error("Unrecognized SDPNAL termcode `$(optimizer.status)`.")
            end
            stoptol = get(optimizer.options, "stoptol", 1e-6)
            prim = optimizer.status == -2 ? 1.05stoptol : 10stoptol
            dual = optimizer.status == -1 ? 1.05stoptol : (optimizer.status == -3 ? 10stoptol : stoptol)
            return "SDP is partially solved successfully: primfeasorg < $prim, dualfeasorg < $dual"
        end
    end
end

const TERMINATION_STATUS = [
    MOI.NUMERICAL_ERROR,
    MOI.NUMERICAL_ERROR,
    MOI.NUMERICAL_ERROR,
    MOI.OPTIMAL,
    MOI.NUMERICAL_ERROR
]
function MOI.get(optimizer::Optimizer, ::MOI.TerminationStatus)
    if optimizer.status === nothing
        return MOI.OPTIMIZE_NOT_CALLED
    else
        return TERMINATION_STATUS[4 + optimizer.status]
    end
end

const PRIMAL_STATUS = [
    MOI.NEARLY_FEASIBLE_POINT,
    MOI.NEARLY_FEASIBLE_POINT,
    MOI.NEARLY_FEASIBLE_POINT,
    MOI.FEASIBLE_POINT,
    MOI.NO_SOLUTION
]
function MOI.get(optimizer::Optimizer, attr::MOI.PrimalStatus)
    if attr.N > MOI.get(optimizer, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    return PRIMAL_STATUS[4 + optimizer.status]
end

function MOI.get(optimizer::Optimizer, attr::MOI.DualStatus)
    if attr.N > MOI.get(optimizer, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    return PRIMAL_STATUS[4 + optimizer.status]
end

MOI.get(::Optimizer, ::MOI.ResultCount) = 1
function MOI.get(optimizer::Optimizer, attr::MOI.ObjectiveValue)
    MOI.check_result_index_bounds(optimizer, attr)
    sign = sense_to_sign(optimizer.objective_sense)
    return sign * optimizer.primal_objective_value + optimizer.objective_constant
end
function MOI.get(optimizer::Optimizer, attr::MOI.DualObjectiveValue)
    MOI.check_result_index_bounds(optimizer, attr)
    sign = sense_to_sign(optimizer.objective_sense)
    return sign * optimizer.dual_objective_value + optimizer.objective_constant
end

function MOI.get(optimizer::Optimizer, attr::MOI.VariablePrimal, vi::MOI.VariableIndex)
    MOI.check_result_index_bounds(optimizer, attr)
    info = optimizer.variable_info[vi.value]
    if info.variable_type == NNEG
        return optimizer.nneg_X[info.index_in_cone]
    else
        @assert info.variable_type == PSD
        return optimizer.psdc_X[info.cone_index][info.index_in_cone]
    end
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintPrimal,
                 ci::MOI.ConstraintIndex{MOI.SingleVariable, <:BoundsSet})
    MOI.check_result_index_bounds(optimizer, attr)
    return MOI.get(optimizer, MOI.VariablePrimal(attr.N),
                   MOI.VariableIndex(ci.value))
end

function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintPrimal,
                 ci::MOI.ConstraintIndex{MOI.VectorOfVariables, S}) where S<:SupportedSets
    MOI.check_result_index_bounds(optimizer, attr)
    info = optimizer.variable_info[ci.value]
    if info.variable_type == NNEG
        dim = optimizer.nneg_info[info.index_in_cone]
        return optimizer.nneg_X[(info.index_in_cone - 1) .+ (1:dim)]
    else
        @assert info.variable_type == PSD
        return optimizer.psdc_X[info.cone_index]
    end
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintPrimal, ci::AFFEQ)
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.b[ci.value]
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintPrimal,
                 ci::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},
                                         <:BoundsSet})
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.s[ci.value]
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintDual,
                 ci::MOI.ConstraintIndex{MOI.VectorOfVariables, <:SupportedSets})
    MOI.check_result_index_bounds(optimizer, attr)
    info = optimizer.variable_info[ci.value]
    if info.variable_type == NNEG
        dim = optimizer.nneg_info[info.index_in_cone]
        return optimizer.nneg_Z1[(info.index_in_cone - 1) .+ (1:dim)]
    else
        @assert info.variable_type == PSD
        return optimizer.psdc_Z1[info.cone_index]
    end
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintDual,
                 ci::MOI.ConstraintIndex{MOI.SingleVariable, <:BoundsSet})
    MOI.check_result_index_bounds(optimizer, attr)
    info = optimizer.variable_info[ci.value]
    if info.variable_type == NNEG
        Z2 = optimizer.nneg_Z2
    else
        @assert info.variable_type == PSD
        Z2 = optimizer.psdc_Z2[info.cone_index]
    end
    return Z2[info.index_in_cone]
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintDual, ci::AFFEQ)
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.y[ci.value]
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintDual,
                 ci::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},
                                         <:BoundsSet})
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.y2[ci.value]
end


    export perform

    """
    `Basics.perform(computation::Atomic.Computation)`
        ... to perform the computation as prescribed by comp. All relevant intermediate and final results are printed to screen (stdout).
            Nothing is returned.

    `Basics.perform(computation::Atomic.Computation; output=true)`
        ... to perform the same but to return the complete output in a dictionary; the particular output depends on the type and
            specifications of the computations but can easily accessed by the keys of this dictionary.
    """
    function Basics.perform(computation::Atomic.Computation; output::Bool=false)
        if  output    results = Dict{String, Any}()    else    results = nothing    end
        nModel = computation.nuclearModel

        # Distinguish between the computation of level energies and properties and the simulation of atomic processes
        if   length(computation.configs) != 0
            basis     = Basics.performSCF(computation.configs, nModel, computation.grid, computation.asfSettings)
            multiplet = Basics.performCI( basis, nModel, computation.grid, computation.asfSettings)
            LSjj.expandLevelsIntoLS(multiplet, computation.asfSettings.jjLS)
            #
            if output    results = Base.merge( results, Dict("multiplet:" => multiplet) )
                         results = Base.merge( results, Dict("grid:"      => computation.grid) )  end

            # Now compute all requested properties
            for settings  in computation.propertySettings
                if      typeof(settings) == Einstein.Settings
                    outcome = Einstein.computeLines(multiplet,        computation.grid, settings)
                    if output    results = Base.merge( results, Dict("Einstein lines:" => outcome) )                  end
                    #
                elseif  typeof(settings) == Hfs.Settings    && settings.calcIJFexpansion
                    outcome = Hfs.computeHyperfineMultiplet(multiplet, nModel, computation.grid, settings)
                    if output    results = Base.merge( results, Dict("IJF multiplet:" => outcome) )                   end
                    #
                elseif  typeof(settings) == Hfs.Settings
                    outcome = Hfs.computeOutcomes(multiplet, nModel,  computation.grid, settings)
                    if output    results = Base.merge( results, Dict("HFS outcomes:" => outcome) )                    end
                    #
                elseif  typeof(settings) == LandeZeeman.Settings
                    outcome = LandeZeeman.computeOutcomes(multiplet, nModel,  computation.grid, settings)
                    if output    results = Base.merge( results, Dict("Zeeman parameter outcomes:" => outcome) )       end
                    #
                elseif  typeof(settings) == IsotopeShift.Settings
                    outcome = IsotopeShift.computeOutcomes(multiplet, nModel, computation.grid, settings)
                    if output    results = Base.merge( results, Dict("Isotope parameter outcomes:" => outcome) )      end
                    #
                elseif  typeof(settings) == AlphaVariation.Settings
                    outcome = AlphaVariation.computeOutcomes(multiplet, nModel, computation.grid, settings)
                    if output    results = Base.merge( results, Dict("alpha variation parameter outcomes:" => outcome) )      end
                    #
                elseif  typeof(settings) == FormFactor.Settings
                    outcome = FormFactor.computeOutcomes(multiplet, nModel, computation.grid, settings)
                    if output    results = Base.merge( results, Dict("Form factor outcomes:" => outcome) )            end
                    #
                elseif  typeof(settings) == DecayYield.Settings
                    outcome = DecayYield.computeOutcomes(computation.configs, computation.asfSettings,
                                                            multiplet, nModel, computation.grid, settings)
                    if output    results = Base.merge( results, Dict("Fluorescence and AutoIonization yield outcomes:" => outcome) )   end
                    #
                elseif  typeof(settings) == MultipolePolarizibility.Settings
                    outcome = MultipolePolarizibility.computeOutcomes(multiplet, nModel, computation.grid, settings)
                    if output    results = Base.merge( results, Dict("Polarizibility outcomes:" => outcome) )         end
                    #
                elseif  typeof(settings) == ReducedDensityMatrix.Settings
                    outcome = ReducedDensityMatrix.computeOutcomes(multiplet, nModel, computation.grid, settings)
                    if output    results = Base.merge( results, Dict("RDM outcomes:" => outcome) )         end
                    #
                elseif  typeof(settings) == PlasmaShift.Settings
                    outcome = PlasmaShift.computeOutcomes(multiplet, nModel, computation.grid, computation.asfSettings, settings)
                    if output    results = Base.merge( results, Dict("Plasma shift outcomes:" => outcome) )            end
                end
            end

        # Evaluate processes that need special SCF and CI procedures
        elseif  typeof(computation.processSettings)  in [PlasmaShift.AugerSettings, PlasmaShift.PhotoSettings]
            pSettings        = computation.processSettings
            plasmaSettings   = PlasmaShift.Settings(pSettings.plasmaModel, pSettings.lambdaDebye, pSettings.ionSphereR0, pSettings.NoBoundElectrons)
            initialBasis     = Basics.performSCF(computation.initialConfigs, nModel, computation.grid, computation.initialAsfSettings)
            initialMultiplet = perform("computation: CI for plasma",  initialBasis, nModel, computation.grid, computation.initialAsfSettings, plasmaSettings)
            finalBasis       = Basics.performSCF(computation.finalConfigs, nModel, computation.grid, computation.finalAsfSettings)
            finalMultiplet   = perform("computation: CI for plasma",  finalBasis, nModel, computation.grid, computation.finalAsfSettings, plasmaSettings)
            #
            if      typeof(computation.processSettings)  == PlasmaShift.AugerSettings
                outcome = AutoIonization.computeLinesPlasma(finalMultiplet, initialMultiplet, nModel, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("AutoIonization lines in plasma:" => outcome) )         end
            elseif  typeof(computation.processSettings)  == PlasmaShift.PhotoSettings
                outcome = PhotoIonization.computeLinesPlasma(finalMultiplet, initialMultiplet, nModel, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("Photo lines in plasma:" => outcome) )                  end
            else
                error("stop a")
            end

        else
            initialBasis     = Basics.performSCF(computation.initialConfigs, nModel, computation.grid, computation.initialAsfSettings)
            initialMultiplet = Basics.performCI( initialBasis, nModel, computation.grid, computation.initialAsfSettings)
            LSjj.expandLevelsIntoLS(initialMultiplet, computation.initialAsfSettings.jjLS)
            finalBasis       = Basics.performSCF(computation.finalConfigs, nModel, computation.grid, computation.finalAsfSettings)
            finalMultiplet   = Basics.performCI(   finalBasis, nModel, computation.grid, computation.finalAsfSettings)
            LSjj.expandLevelsIntoLS(finalMultiplet, computation.finalAsfSettings.jjLS)
            #
            if typeof(computation.processSettings) in [PhotoExcitationFluores.Settings, PhotoExcitationAutoion.Settings, PhotoIonizationFluores.Settings,
                                                       PhotoIonizationAutoion.Settings, ImpactExcitationAutoion.Settings, Dielectronic.Settings]
                intermediateBasis     = Basics.performSCF(computation.intermediateConfigs, nModel, computation.grid, computation.intermediateAsfSettings)
                intermediateMultiplet = Basics.performCI( intermediateBasis, nModel, computation.grid, computation.intermediateAsfSettings)
            end
            #
            if      typeof(computation.processSettings) == AutoIonization.Settings
                outcome = AutoIonization.computeLines(finalMultiplet, initialMultiplet, nModel, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("AutoIonization lines:" => outcome) )                  end
            elseif  typeof(computation.processSettings) == RayleighCompton.Settings
                outcome = RayleighCompton.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("Rayleigh-Compton lines:" => outcome) )                end
            elseif  typeof(computation.processSettings) == DoubleAutoIonization.Settings
                outcome = DoubleAutoIonization.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("Double-Auger lines:" => outcome) )                    end
            elseif  typeof(computation.processSettings) == Dielectronic.Settings
                outcome = Dielectronic.computePathways(finalMultiplet, intermediateMultiplet, initialMultiplet, nModel,
                                                                    computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("dielectronic recombination pathways:" => outcome) )   end
            elseif  typeof(computation.processSettings) == MultiPhotonIonization.Settings
                outcome = MultiPhotonDeExcitation.computeLines(computation.processSettings.process,
                                                               finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("multi-photon-de-excitation lines:" => outcome) )      end
            elseif  typeof(computation.processSettings) == PhotoIonization.Settings
                outcome = PhotoIonization.computeLines(finalMultiplet, initialMultiplet, nModel, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("photoionization lines:" => outcome) )                 end
            elseif  typeof(computation.processSettings) == PhotoDoubleIonization.Settings
                outcome = PhotoDoubleIonization.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("Single-photon double-ionization lines:" => outcome) ) end
            elseif  typeof(computation.processSettings) == PhotoExcitation.Settings
                outcome = PhotoExcitation.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("photo-excitation lines:" => outcome) )                end
            elseif  typeof(computation.processSettings) == PhotoExcitationAutoion.Settings
                outcome = PhotoExcitationAutoion.computePathways(finalMultiplet, intermediateMultiplet, initialMultiplet, nModel,
                                                                 computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("photo-excitation-autoionization pathways:" => outcome) )      end
            elseif  typeof(computation.processSettings) == PhotoExcitationFluores.Settings
                outcome = PhotoExcitationFluores.computePathways(finalMultiplet, intermediateMultiplet, initialMultiplet,
                                                                 computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("photo-excitation-fluorescence pathways:" => outcome) )        end
            elseif  typeof(computation.processSettings) == PhotoEmission.Settings
                outcome = PhotoEmission.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("radiative lines:" => outcome) )                       end
            elseif  typeof(computation.processSettings) == RadiativeAuger.Settings
                outcome = RadiativeAuger.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("radiative Auger sharings:" => outcome) )              end
            elseif  typeof(computation.processSettings) == PhotoRecombination.Settings
                outcome = PhotoRecombination.computeLines(finalMultiplet, initialMultiplet, nModel, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("photo recombination lines:" => outcome) )             end
            elseif  typeof(computation.processSettings) == ImpactExcitation.Settings
                outcome = ImpactExcitation.computeLines(finalMultiplet, initialMultiplet, nModel, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("impact-excitation lines:" => outcome) )               end
                #
                #
            elseif  typeof(computation.processSettings) == PairA1P()
                outcome = PairAnnihilation1Photon.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("pair-annihilation-1-photon lines:" => outcome) )       end
            elseif  typeof(computation.processSettings) == Coulex()
                outcome = CoulombExcitation.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("Coulomb excitation lines:" => outcome) )               end
            elseif  typeof(computation.processSettings) == Coulion()
                outcome = CoulombIonization.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("Coulomb ionization lines:" => outcome) )               end
            elseif  typeof(computation.processSettings) == PhotoIonAuto()
                outcome = PhotoIonizationAutoion.computePathways(finalMultiplet, intermediateMultiplet, initialMultiplet,
                                                                    computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("photo-ionization-autoionization pathways:" => outcome) )      end
            elseif  typeof(computation.processSettings) == PhotoIonFluor()
                outcome = PhotoIonizationFluores.computePathways(finalMultiplet, intermediateMultiplet, initialMultiplet,
                                                                    computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("photo-ionizatiton-fluorescence pathways:" => outcome) )        end
            elseif  typeof(computation.processSettings) == ImpactExcAuto()
                outcome = ImpactExcitationAutoion.computePathways(finalMultiplet, intermediateMultiplet, initialMultiplet,
                                                                    computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("impact-excitation-autoionization pathways:" => outcome) )     end
            elseif  typeof(computation.processSettings) == MultiPI()
                outcome = MultiPhotonIonization.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("multi-photon single ionization:" => outcome) )        end
            elseif  typeof(computation.processSettings) == MultiPDI()
                outcome = MultiPhotonDoubleIon.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("multi-photon double ionization:" => outcome) )        end
            elseif  typeof(computation.processSettings) == InternalConv()
                outcome = InternalConversion.computeLines(finalMultiplet, initialMultiplet, computation.grid, computation.processSettings)
                if output    results = Base.merge( results, Dict("internal conversion lines:" => outcome) )        end
            else
                error("stop b")
            end
        end

        Defaults.warn(PrintWarnings())
        Defaults.warn(ResetWarnings())
        return( results )
    end

    """
    `Basics.perform(comp::Cascade.Computation)`
        ... to set-up and perform a cascade computation that starts from a given set of initial configurations and proceeds via
            various steps until a given number of electrons has been removed or the decay stops at some stable levels with regard
            to the given atomic processes. The results of all individual steps are printed to screen but nothing is returned
            otherwise.

    `Basics.perform(comp::Cascade.Computation; output::Bool=true, outputToFile::Bool=true)`
        ... to perform the same but to return the complete output in a dictionary;  the particular output depends on the type
            and specifications of the cascade but can easily accessed by the keys of this dictionary.
    """
    function Basics.perform(comp::Cascade.Computation; output::Bool=false, outputToFile::Bool=true)
        Cascade.perform(comp.scheme, comp::Cascade.Computation, output=output, outputToFile=outputToFile)
    end

    """
    `Basics.perform(comp::Cascade.Simulation)`
        ... to set-up and perform a cascade computation that starts from a given set of initial configurations and proceeds via
            various steps until a given number of electrons has been removed or the decay stops at some stable levels with regard
            to the given atomic processes. The results of all individual steps are printed to screen but nothing is returned
            otherwise.

    `Basics.perform(comp::Cascade.Simulation; output=true)`
        ... to perform the same but to return the complete output in a dictionary;  the particular output depends on the type
            and specifications of the cascade but can easily accessed by the keys of this dictionary.
    """
    function Basics.perform(comp::Cascade.Simulation; output::Bool=false)
        Cascade.perform(comp::Cascade.Simulation, output=output)
    end

    """
    `Basics.perform("computation: mutiplet from orbitals, no CI, CSF diagonal", configs::Array{Configuration,1},
                    initalOrbitals::Dict{Subshell, Orbital}, nuclearModel::Nuclear.Model, grid::Radial.Grid, settings::AsfSettings; printout::Bool=true)`
        ... to generate from the given initial orbitals a multiplet of single-CSF levels by just using the diagonal
            part of the Hamiltonian matrix; a multiplet::Multiplet is returned.
    """
    function Basics.perform(sa::String, configs::Array{Configuration,1}, initalOrbitals::Dict{Subshell, Orbital}, nuclearModel::Nuclear.Model,
                    grid::Radial.Grid, settings::AsfSettings; printout::Bool=true)
        !(sa == "computation: mutiplet from orbitals, no CI, CSF diagonal")   &&   error("Unsupported keystring = $sa")
        return( Basics.performCI(configs::Array{Configuration,1}, initalOrbitals::Dict{Subshell, Orbital}, nuclearModel::Nuclear.Model,
                                 grid::Radial.Grid, settings::AsfSettings; printout=printout) )
    end

    """
    `Basics.perform("computation: CI for plasma", basis::Basis, nuclearModel::Nuclear.Model, grid::Radial.Grid,
        settings::AsfSettings, plasmaSettings::PlasmaShift.Settings; printout::Bool=true)`
        ... to  set-up and diagonalize from the given (SCF) basis the configuration-interaction matrix and to derive and
            display the level structure of the corresponding multiplet due to the given settings. Here, the CI matrix
            includes the modifications of the Hamiltonian due to the given plasmaSettings; a multiplet::Multiplet is returned.
    """
    function Basics.perform(sa::String, basis::Basis, nuclearModel::Nuclear.Model, grid::Radial.Grid,
                    settings::AsfSettings, plasmaSettings::PlasmaShift.Settings; printout::Bool=true)
        !(sa == "computation: CI for plasma")   &&   error("Unsupported keystring = $sa")
        return( Basics.performCI(basis::Basis, nuclearModel::Nuclear.Model, grid::Radial.Grid,
                                 settings::AsfSettings, plasmaSettings::PlasmaShift.Settings; printout=printout) )
    end

    """
    `Basics.performSCF(configs::Array{Configuration,1}, nuclearModel::Nuclear.Model, grid::Radial.Grid, settings::AsfSettings;
                       printout::Bool=true)`
        ... to generate an atomic basis and to compute the self-consistent field (SCF) for this basis due to the given settings;
            a basis::Basis is returned.
    """
    function Basics.performSCF(configs::Array{Configuration,1}, nuclearModel::Nuclear.Model, grid::Radial.Grid, settings::AsfSettings;
                               printout::Bool=true)
        if  printout    println("\n... in performSCF ...")    end

        # Generate a list of relativistic configurations and determine an ordered list of subshells for these configurations
        relconfList = ConfigurationR[]
        for  conf in configs
            wa = Basics.generate("configuration list: relativistic", conf)
            append!( relconfList, wa)
        end
        if  printout    for  i = 1:length(relconfList)    println(">> include ", relconfList[i])    end   end
        subshellList = Basics.generate("subshells: ordered list for relativistic configurations", relconfList)
        Defaults.setDefaults("relativistic subshell list", subshellList; printout=printout)

        # Generate the relativistic CSF's for the given subshell list
        csfList = CsfR[]
        for  relconf in relconfList
            newCsfs = Basics.generate("CSF list: from single ConfigurationR", relconf, subshellList)
            append!( csfList, newCsfs)
        end

        # Determine the number of electrons and the list of coreSubshells
        NoElectrons      = sum( csfList[1].occupation )
        coreSubshellList = Subshell[]
        for  k in 1:length(subshellList)
            mocc = Basics.subshell_2j(subshellList[k]) + 1;    is_filled = true
            for  csf in csfList
                if  csf.occupation[k] != mocc    is_filled = false;    break   end
            end
            if   is_filled    push!( coreSubshellList, subshellList[k])    end
        end

        wa = Bsplines.generatePrimitives(grid)

        # Generate start orbitals
        if  typeof(settings.startScfFrom) == StartFromHydrogenic
            if  printout   println("Start SCF process with hydrogenic orbitals.")   end
            # Generate start orbitals for the SCF field by using B-splines
            ##x orbitals  = Bsplines.generateOrbitalsHydrogenic(waL, nsL, waS, nsS, nuclearModel, subshellList)
            orbitals  = Bsplines.generateOrbitalsHydrogenic(wa, nuclearModel, subshellList; printout=printout)
        elseif  typeof(settings.startScfFrom) == StartFromPrevious
            if  printout   println("Start SCF process from given list of orbitals.energy")    end
            # Taking starting orbitals for the given dictionary; non-relativistic orbitals with a proper nuclear charge
            # are adapted if no orbital is found
            orbitals = Dict{Subshell, Orbital}()
            for  subsh in subshellList
                if  haskey(settings.startScfFrom.orbitals, subsh)
                    orb = settings.startScfFrom.orbitals[subsh]
                    orbitals = Base.merge( orbitals, Dict( subsh => orb) )
                else
                    println("Start orbitals do not contain an Orbital for subshell $subsh ")
                    orb      = HydrogenicIon.radialOrbital(subsh, nuclearModel.Z, grid)
                    orb      = Orbital(orb.subshell, orb.isBound, true, orb.energy, orb.P, orb.Q, orb.Pprime, orb.Qprime, Radial.Grid())
                    orbitals = Base.merge( orbitals, Dict( subsh => orb) )
                end
            end
        else  error("stop a")
        end

        # Perform the SCF computations for the orbitals due to the given settings
        basis    = Basis(true, NoElectrons, subshellList, csfList, coreSubshellList, orbitals)
        if   settings.scField in [Basics.DFSField(), Basics.HSField()]
            basis = Bsplines.solveSelfConsistentMeanField(wa, nuclearModel, basis, settings; printout=printout)
        elseif   settings.scField in [Basics.NuclearField()]  && settings.startScfFrom == StartFromHydrogenic()
            # Return the basis as already generated.
        elseif   settings.scField in [Basics.ALField()]
            basis = Bsplines.solveSelfConsistentALField(wa, nuclearModel, basis, settings; printout=printout)
        elseif   typeof(settings.scField) == Basics.EOLField
            basis = Bsplines.solveSelfConsistentEOLField(wa, nuclearModel, basis, settings; printout=printout)
        else  error("stop b")
        end

        return( basis )
    end

    """
    `Basics.performSCF(basis::Basis, nuclearModel::Nuclear.Model, grid::Radial.Grid, frozenShells::Array{Shell,1},
                       settings::AtomicState.RasSettings; printout::Bool=true)`
        ... to generate an atomic basis and to compute the self-consistent field (SCF) for this basis due to the given settings;
            all subshells due to the list of frozenShells are kept fixed. A newBasis::Basis is returned.
    """
    function Basics.performSCF(basis::Basis, nuclearModel::Nuclear.Model, grid::Radial.Grid, frozenShells::Array{Shell,1},
                               settings::AtomicState.RasSettings; printout::Bool=true)
        if  printout    println("\n... in performSCF['for RAS step'] ...")    end

        # Determine the list of frozen subshells
        frozenSubshells = Subshell[]
        for  sh in frozenShells
            if  sh.l == 0    push!( frozenSubshells, Subshell(sh.n, -1))
            else             push!( frozenSubshells, Subshell(sh.n, sh.l));    push!( frozenSubshells, Subshell(sh.n, -sh.l -1))
            end
        end

        wa = Bsplines.generatePrimitives(grid)
        # Specify the AsfSettings for the given RAS step
        asfSettings = AsfSettings(true, CoulombInteraction(), Basics.DFSField(), StartFromHydrogenic(),
                                  settings.maxIterationsScf, settings.accuracyScf, Subshell[], frozenSubshells,
                                  CoulombInteraction(), NoneQed(), LSjjSettings(false), settings.levelSelectionCI )

        basis = Bsplines.solveSelfConsistentMeanField(wa, nuclearModel, basis, asfSettings; printout=printout)

        return( basis )
    end

    """
    `Basics.performCI(configs::Array{Configuration,1}, initalOrbitals::Dict{Subshell, Orbital}, nuclearModel::Nuclear.Model,
                      grid::Radial.Grid, settings::AsfSettings; printout::Bool=true)`
        ... to generate from the given initial orbitals a multiplet of single-CSF levels by just using the diagonal
            part of the Hamiltonian matrix; a multiplet::Multiplet is returned.
    """
    function Basics.performCI(configs::Array{Configuration,1}, initalOrbitals::Dict{Subshell, Orbital}, nuclearModel::Nuclear.Model,
                              grid::Radial.Grid, settings::AsfSettings; printout::Bool=true)
        if  printout    println("\n... in perform['computation: mutiplet from orbitals, no CI, CSF diagonal'] ...")    end

        # Generate a list of relativistic configurations and determine an ordered list of subshells for these configurations
        relconfList = ConfigurationR[]
        for  conf in configs
            wa = Basics.generate("configuration list: relativistic", conf)
            append!( relconfList, wa)
        end
        if  printout    for  i = 1:length(relconfList)    println(">> include ", relconfList[i])    end   end
        subshellList = Basics.generate("subshells: ordered list for relativistic configurations", relconfList)
        Defaults.setDefaults("relativistic subshell list", subshellList; printout=printout)

        # Generate the relativistic CSF's for the given subshell list
        csfList = CsfR[]
        for  relconf in relconfList
            newCsfs = Basics.generate("CSF list: from single ConfigurationR", relconf, subshellList)
            append!( csfList, newCsfs)
        end

        # Determine the number of electrons and the list of coreSubshells
        NoElectrons      = sum( csfList[1].occupation )
        coreSubshellList = Subshell[]
        for  k in 1:length(subshellList)
            mocc = Basics.subshell_2j(subshellList[k]) + 1;    is_filled = true
            for  csf in csfList
                if  csf.occupation[k] != mocc    is_filled = false;    break   end
            end
            if   is_filled    push!( coreSubshellList, subshellList[k])    end
        end

        # Set-up a basis for calculating the Hamiltonian matrix
        basis = Basis(true, NoElectrons, subshellList, csfList, coreSubshellList, initalOrbitals)

        # Calculate the diagonal part of the Hamiltonian matrix and define a multiplet for these approximate ASf;
        # Generate first an effective nuclear charge Z(r) on the given grid
        potential = Nuclear.nuclearPotential(nuclearModel, grid)

        keep = true
        InteractionStrength.XL_Coulomb_reset_storage(keep)
        #
        n = length(csfList);    matrix = zeros(Float64, n, n)
        for  r = 1:n
            ##x wa = compute("angular coefficients: e-e, Ratip2013", basis.csfs[r], basis.csfs[r])
            # Calculate the spin-angular coefficients
            if  Defaults.saRatip()
                waR = compute("angular coefficients: e-e, Ratip2013", basis.csfs[r], basis.csfs[r])
                wa  = waR
            end
            if  Defaults.saGG()
                subshellList = basis.subshells
                opa  = SpinAngular.OneParticleOperator(0, plus, true)
                waG1 = SpinAngular.computeCoefficients(opa, basis.csfs[r], basis.csfs[r], subshellList)
                opa  = SpinAngular.TwoParticleOperator(0, plus, true)
                waG2 = SpinAngular.computeCoefficients(opa, basis.csfs[r], basis.csfs[r], subshellList)
                wa   = [waG1, waG2]
            end
            if  Defaults.saRatip() && Defaults.saGG() && true
                if  length(waR[1]) != 0     println(  ">> Angular coeffients from Ratip2013   = $(waR[1]) ")    end
                if  length(waG1)   != 0     println("\n>> Angular coeffients from SpinAngular = $waG1 ")        end
                if  length(waR[2]) != 0     println(  ">> Angular coeffients from Ratip2013   = $(waR[2]) ")    end
                if  length(waG2)   != 0     println("\n>> Angular coeffients from SpinAngular = $waG2 ")        end
            end
            #
            me = 0.
            for  coeff in wa[1]
                jj = Basics.subshell_2j(basis.orbitals[coeff.a].subshell)
                me = me + coeff.T * sqrt( jj + 1) * RadialIntegrals.GrantIab(basis.orbitals[coeff.a], basis.orbitals[coeff.b], grid, potential)
            end

            for  coeff in wa[2]
                if  settings.eeInteractionCI in [CoulombInteraction(), CoulombBreit()]
                    me = me + coeff.V * InteractionStrength.XL_Coulomb(coeff.nu, basis.orbitals[coeff.a], basis.orbitals[coeff.b],
                                                                                basis.orbitals[coeff.c], basis.orbitals[coeff.d], grid, keep=keep)   end
                if  settings.eeInteractionCI in [BreitInteraction(), CoulombBreit()]
                    me = me + coeff.V * InteractionStrength.XL_Breit(coeff.nu, basis.orbitals[coeff.a], basis.orbitals[coeff.b],
                                                                                basis.orbitals[coeff.c], basis.orbitals[coeff.d], grid)     end
            end
            matrix[r,r] = me
        end
        #
        eigen  = Basics.diagonalize("matrix: LinearAlgebra", matrix)
        levels = Level[]
        for  ev = 1:length(eigen.values)
            vector = eigen.vectors[ev];   ns = 0
            for  r = 1:length(vector)
                if      (vector[r])^2 > 0.99    ns = r;   break
                elseif  (vector[r])^2 > 0.01    error("stop a; unexpected mixing coefficieint; vector = $vector ")
                end
            end
            J = csfList[ns].J
            newlevel = Level( J, AngularM64(J.num//J.den), csfList[ns].parity, 0, eigen.values[ev], 0., true, basis, vector )
            push!( levels, newlevel)
        end
        mp = Multiplet("noName", levels)
        mp = Basics.sortByEnergy(mp)

        levelNos = Int64[]
        for (ilev, level) in  enumerate(mp.levels)       push!(levelNos, ilev)   end

        # Display all level energies and energy splittings
        if  printout
            Basics.tabulate(stdout, "multiplet: energies", mp, levelNos)
            Basics.tabulate(stdout, "multiplet: energy relative to immediately lower level",    mp, levelNos)
            Basics.tabulate(stdout, "multiplet: energy of each level relative to lowest level", mp, levelNos)
        end
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        if  printSummary
            Basics.tabulate(iostream, "multiplet: energies", mp, levelNos)
            Basics.tabulate(iostream, "multiplet: energy relative to immediately lower level",    mp, levelNos)
            Basics.tabulate(iostream, "multiplet: energy of each level relative to lowest level", mp, levelNos)
        end

        return( mp )
    end

    """
    `Basics.performCI(basis::Basis, nuclearModel::Nuclear.Model, grid::Radial.Grid, settings::AsfSettings; printout::Bool=true)`
        ... to  set-up and diagonalize from the (SCF) basis the configuration-interaction matrix and to derive and display the
            level structure of the corresponding multiplet due to the given settings; a multiplet::Multiplet is returned.
    """
    function Basics.performCI(basis::Basis, nuclearModel::Nuclear.Model, grid::Radial.Grid, settings::AsfSettings; printout::Bool=true)

        # Determine the J^P symmetry blocks
        symmetries = Dict{LevelSymmetry,Int64}()
        for  csf in basis.csfs
            sym = LevelSymmetry(csf.J, csf.parity)
            if     haskey(symmetries, sym)    symmetries[sym] = symmetries[sym] + 1
            else   symmetries = Base.merge( symmetries, Dict( sym => 1 ) )
            end
        end

        # Test the total number of CSF
        NoCsf = 0
        for (sym,v) in symmetries   NoCsf = NoCsf + v   end

        # Calculate for each symmetry block the corresponding CI matrix, diagonalize it and append a Multiplet for this block
        multiplets = Multiplet[]
        for  (sym,v) in  symmetries
            # Skip the symmetry block if it not selected
            ##x @show  sym, settings.levelSelectionCI,  Basics.selectSymmetry(sym, settings.levelSelectionCI)
            if  !Basics.selectSymmetry(sym, settings.levelSelectionCI)     continue    end
            matrix = compute("matrix: CI, J^P symmetry", sym, basis, nuclearModel, grid, settings; printout=printout)
            #
            ##x if  size(matrix,1) == 2   matrix[1,2] = matrix[2,1] = -1.0 * matrix[2,1]     end
            eigen  = Basics.diagonalize("matrix: LinearAlgebra", matrix)
            ##x eigen  = Basics.diagonalize("matrix: Julia, eigfact", matrix)
            ##x @show matrix, eigen
            levels = Level[]
            for  ev = 1:length(eigen.values)
                # Construct the eigenvector with regard to the given basis (not w.r.t the symmetry block)
                evSym = eigen.vectors[ev];    vector = zeros( length(basis.csfs) );   ns = 0
                for  r = 1:length(basis.csfs)
                    if LevelSymmetry(basis.csfs[r].J, basis.csfs[r].parity) == sym    ns = ns + 1;   vector[r] = evSym[ns]   end
                end
                newlevel = Level( sym.J, AngularM64(sym.J.num//sym.J.den), sym.parity, 0, eigen.values[ev], 0., true, basis, vector )
                push!( levels, newlevel)
            end
            wa = Multiplet("noName", levels)
            push!( multiplets, wa)
        end

        # Merge all multiplets into a single one
        mp = Basics.merge(multiplets)
        mp = Basics.sortByEnergy(mp)

        # Determine the level list to be printed out
        levelNos = Int64[]
        for (ilev, level) in  enumerate(mp.levels)
            if  Basics.selectLevel(level, settings.levelSelectionCI)    push!(levelNos, ilev)    end
        end

        # Display all level energies and energy splittings
        if  printout
            Basics.tabulate(stdout, "multiplet: energies", mp, levelNos)
            Basics.tabulate(stdout, "multiplet: energy relative to immediately lower level",    mp, levelNos)
            Basics.tabulate(stdout, "multiplet: energy of each level relative to lowest level", mp, levelNos)
        end
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        if  printSummary
            Basics.tabulate(iostream, "multiplet: energies", mp, levelNos)
            Basics.tabulate(iostream, "multiplet: energy relative to immediately lower level",    mp, levelNos)
            Basics.tabulate(iostream, "multiplet: energy of each level relative to lowest level", mp, levelNos)
        end

        return( mp )
    end

    """
    `Basics.performCI(basis::Basis, nuclearModel::Nuclear.Model, grid::Radial.Grid, settings::AsfSettings,
                      plasmaSettings::PlasmaShift.Settings; printout::Bool=true)`
        ... to  set-up and diagonalize from the given (SCF) basis the configuration-interaction matrix and to derive and
            display the level structure of the corresponding multiplet due to the given settings. Here, the CI matrix
            includes the modifications of the Hamiltonian due to the given plasmaSettings; a multiplet::Multiplet is returned.
    """
    function Basics.performCI(basis::Basis, nuclearModel::Nuclear.Model, grid::Radial.Grid,
                              settings::AsfSettings, plasmaSettings::PlasmaShift.Settings; printout::Bool=true)

        # Determine the J^P symmetry blocks
        symmetries = Dict{LevelSymmetry,Int64}()
        for  csf in basis.csfs
            sym = LevelSymmetry(csf.J, csf.parity)
            if     haskey(symmetries, sym)    symmetries[sym] = symmetries[sym] + 1
            else   symmetries = Base.merge( symmetries, Dict( sym => 1 ) )
            end
        end

        # Test the total number of CSF
        NoCsf = 0
        for (sym,v) in symmetries   NoCsf = NoCsf + v   end

        # Calculate for each symmetry block the corresponding CI matrix for the given plasma model and parameters as specified by
        # plasmaSettings, diagonalize it and append a Multiplet for this block
        multiplets = Multiplet[]
        for  (sym,v) in  symmetries
            matrix = compute("matrix: CI for plasma, J^P symmetry", sym, basis, nuclearModel, grid, settings, plasmaSettings; printout=printout)
            eigen  = Basics.diagonalize("matrix: LinearAlgebra", matrix)
            levels = Level[]
            for  ev = 1:length(eigen.values)
                # Construct the eigenvector with regard to the given basis (not w.r.t the symmetry block)
                evSym = eigen.vectors[ev];    vector = zeros( length(basis.csfs) );   ns = 0
                for  r = 1:length(basis.csfs)
                    if LevelSymmetry(basis.csfs[r].J, basis.csfs[r].parity) == sym    ns = ns + 1;   vector[r] = evSym[ns]   end
                end
                newlevel = Level( sym.J, AngularM64(sym.J.num//sym.J.den), sym.parity, 0, eigen.values[ev], 0., true, basis, vector )
                push!( levels, newlevel)
            end
            wa = Multiplet("noName", levels)
            push!( multiplets, wa)
        end

        # Merge all multiplets into a single one
        mp = Basics.merge(multiplets)
        mp = Basics.sortByEnergy(mp)

        levelNos = Int64[]
        for (ilev, level) in  enumerate(mp.levels)       push!(levelNos, ilev)   end

        # Display all level energies and energy splittings
        if  printout
            Basics.tabulate(stdout, "multiplet: energies", mp, levelNos)
            Basics.tabulate(stdout, "multiplet: energy relative to immediately lower level",    mp, levelNos)
            Basics.tabulate(stdout, "multiplet: energy of each level relative to lowest level", mp, levelNos)
        end
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        if  printSummary
            Basics.tabulate(iostream, "multiplet: energies", mp, levelNos)
            Basics.tabulate(iostream, "multiplet: energy relative to immediately lower level",    mp, levelNos)
            Basics.tabulate(iostream, "multiplet: energy of each level relative to lowest level", mp, levelNos)
        end

        return( mp )
    end


# Functions used to initiate ycreator type, for adjusting phenotypes with gensel outputs
function build_ycreator()
    return YCreator(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
end
#
function add_markers_gensel(ycreator, markersfileprefix)
    info = read_bim_file(markersfileprefix .* ".bim")
    iid = read_fam_file(markersfileprefix .* ".fam")
    bed = read_bed_file(markersfileprefix .* ".bed")
    genselvalues = (bedvals[bed] .- 1).*-1  # convert to -1, 0, 1 # *-1 as plinks ref/alt coding is differnt for bed files vs raw files. gensel alphas are calculated wrt to raw files.
    ycreator.genselmarkers = GenselMarkers(iid, info, genselvalues)
end
#
function add_alphas(ycreator, alphasfilename)
    read_filtered_alphas(ycreator, alphasfilename)
end
# rsplit acts in reverse, and limit means it just splits on the 1st occurence. (This prevents removal of missingIDs (CHRN_POS) format)
function read_filtered_alphas(ycreator, alphasfilename)
    alphas = readdlm(alphasfilename, '\t', Any, '\n'; header = false)
    ycreator.alphas = Alphas(alphas[:,1], alphas[:,2:end], 0 ,0)
    ycreator.alphas.m = size(ycreator.alphas.markerNames, 1)
    ycreator.alphas.k = size(ycreator.alphas.alphas, 2)
    ycreator.alphas.markerNames = map(x -> String(x[1]), rsplit.(ycreator.alphas.markerNames, '_'; limit = 2))
end

function add_phenotypes(ycreator, phenotypesfilename)
    ycreator.phenotypes = read_y(phenotypesfilename)

end

function read_y(phenotypesfilename)
    y = readdlm(phenotypesfilename, ' ', Float64, '\n'; header = true)
    return Phenotypes(map(x->string(Int64(x)), y[1][:,1]), y[1][:,2:2])
end
# marker and alpha orders should be the same
function sort_ycreator(ycreator)
    markerOrderM = collect(1:ycreator.alphas.m)
    markerOrderN, phenoOrderN = get_orders(ycreator.genselmarkers.IID, ycreator.phenotypes.IID)
    get_genselmarker_intersect!(ycreator.genselmarkers, markerOrderN, markerOrderM)
    get_phenotype_intersect!(ycreator.phenotypes, phenoOrderN)
end

function get_genselmarker_intersect!(genselmarkers, new_orderN, new_orderM)
    genselmarkers.IID = genselmarkers.IID[new_orderN]
    genselmarkers.SNPID = genselmarkers.SNPID[new_orderM, :]
    genselmarkers.markers = genselmarkers.markers[new_orderN, new_orderM]
end

function get_alpha_intersect!(alphas, new_order)
    alphas.markerNames = alphas.markerNames[new_order]
    alphas.alphas = alphas.alphas[new_order, :]
    alphas.m = size(alphas.markerNames, 1)
end

function initiate_ycreator(ycreator, interval, chr, outputprefix, region, code)
    ycreator.n = size(ycreator.phenotypes.IID, 1)
    ycreator.m = size(ycreator.genselmarkers.SNPID, 1)
    ycreator.k = size(ycreator.alphas.k)
    ycreator.outputprefix = outputprefix
    ycreator.interval = interval
    ycreator.region = region
    ycreator.chr = chr
    ycreator.code = code
    if ycreator.code == "LOCO"
        ycreator.chrlength = 0
    else
        ycreator.chrlength = get_chr_length(ycreator.genselmarkers.SNPID, ycreator.chr)
    end
    ycreator.nlayer1 = ceil(ycreator.chrlength / ycreator.interval)
    ycreator.nlayer2 = ycreator.nlayer1 - 1
end

function get_chr_length(markerinfo, chr)
    chrindex = findall(x -> x == chr, markerinfo[:,1])
    chrview = view(markerinfo, chrindex, 2)
    chrviewfloat = map(x -> parse(Float64, x), chrview)
    chrlength = maximum(chrviewfloat)
    return chrlength
end

function fill_missing_genotypes!(ycreator)
    if any(ismissing.(ycreator.genselmarkers.markers))
        ycreator.genselmarkers.markers = Array{Union{Missing, Float64},2}(ycreator.genselmarkers.markers)
        for i in 1:ycreator.m
            l = length(ycreator.genselmarkers.markers[ismissing.(ycreator.genselmarkers.markers[:,i]),i])
            if l != 0
                ycreator.genselmarkers.markers[ismissing.(ycreator.genselmarkers.markers[:,i]),i] = fill(mean(skipmissing(ycreator.genselmarkers.markers[:,i])),l)
            end
        end
    end
end


function read_navheader(f::IOStream,
            nav_hstrings_set::Set{String}=nav_hstrings_set,
        )::Dict{String,String}
    header_hs = Dict{String,String}()
    if get_header!(header_hs, f, nav_hstrings_set)
        haskey(header_hs, ohsVersion) ||
        throw(error("header error, not $ohsVersion"))

        return header_hs
    end
    throw(error("read header error!"))
end

function read_navheader(filename::String)
    open(filename, "r") do f
        return read_navheader(f)
    end
end

function parse_navheader(header_hs::Dict{String,String})

    version     = parse_nhsVersion(get(header_hs, nhsVersion, ""))
    runBy       = parse_nhsRunBy(get(header_hs, nhsRunBy, ""))
    comment     = parse_nhsComment(get(header_hs, nhsComment, ""))
    ionoCorr    = parse_nhsIonoCorr(get(header_hs, nhsIonoCorr, ""))
    corrSysTime = get(header_hs, nhsCorrSysTime, "")
    deltaUTC    = get(header_hs, nhsDeltaUTC, "")
    dUTC        = get(header_hs, nhsDUTC, "")
    if version[1] < 3
        if version[4] == sstGlonass
            timeSysCorr = parse_nhsCorrSysTime(corrSysTime) # R2.11GPS
        elseif version[4] == sstGPS
            timeSysCorr = parse_nhsDeltaUTC(deltaUTC)
        elseif version[4] == sstGeosync
            timeSysCorr = parse_nhsDUTC(dUTC) # R2.11GEO

        end
    else
        timeSysCorr = parse_nhsTimeSysCorr(get(header_hs, nhsTimeSysCorr, ""))
    end
    leapSeconds = parse_nhsLeapSeconds(get(header_hs, nhsLeapSeconds, "")) # R2.10GLO
    ionAlpha    = parse_nhsIonAlpha(get(header_hs, nhsIonAlpha, "")) # R2.11
    ionBeta     = parse_nhsIonBeta(get(header_hs, nhsIonBeta, ""))
    eoH         = parse_nhsEoH(get(header_hs, nhsEoH, ""))

    RinexNavHeader(version,runBy,comment,
                ionoCorr,timeSysCorr,
                leapSeconds,corrSysTime,deltaUTC,dUTC,
                ionAlpha,ionBeta,eoH )
end
function parse_navd(str::SubString)
    if str[16] == ' ' return 0.0 end

    # b = parse(Float64, str[1:15])
    # e = parse(Float64, str[17:19])
    # b * 10^e
    parse(Float64, replace(str, 'D' => 'e'))
end

function read_navline(line::SubString, d::Array{Float64,1})
    len = length(line)
    start = 1
    # @show line
    while start < len
        push!(d, parse_navd(line[start:start + 18]))
        start += 19
    end
    return
end

const pvaSSTs = Set([sstGlonass,sstGeosync])

function parse_2navdt(sbdt::SubString)
    # @show sbdt
    y  = parse(Int64, sbdt[1:2])
    if y < 70
        y += 2000
    else
        y += 1900
    end
    m  = parse(Int64, sbdt[4:5])
    d  = parse(Int64, sbdt[7:8])
    H  = parse(Int64, sbdt[10:11])
    M  = parse(Int64, sbdt[13:14])
    S  = parse(Float64, sbdt[15:19])
    ds = Millisecond(0)
    if S >= 60.0
        ds = Millisecond(S * 1000)
        S = 0.0
    end
    DateTime(y, m, d, H, M, S) + ds
end

function parse_3navdt(sbdt::SubString)
    # @show sbdt
    y  = parse(Int64, sbdt[1:4])

    m  = parse(Int64, sbdt[6:7])
    d  = parse(Int64, sbdt[9:10])
    H  = parse(Int64, sbdt[12:13])
    M  = parse(Int64, sbdt[15:16])
    S  = parse(Int64, sbdt[18:19])
    ds = Second(0)
    if S >= 60
        ds = Second(S)
        S = 0
    end
    DateTime(y, m, d, H, M, S) + ds
end

function read_navdata(nh::RinexNavHeader, f::IOStream, fc::IOStream)

    if nh.version[1] > 2.99
        s = sstUnknown
        index_plus = 1
        check_index = [4,9,12,15,18,21]
    else
        s = nh.version[4]
        index_plus = 0
        check_index = [3, 6, 9, 12, 15, 18]
    end
    index_health = 25
    rnd = Dict{SatID,EphEpochStore}()

    while !eof(f)
        time_existed = false
        line = readline(f)
        for i in check_index
            line[i] == ' ' ||
                throw(error("no valid epoch line:\n|$line|"))
        end
        if index_plus == 0
            id = parse(Int64, line[1:2])
            si = SatID(s, id)
            dto = parse_2navdt(SubString(line, 4, 22), )
        else
            si = SatID(line[1:3])
            s = si.ss
            dto = parse_3navdt(SubString(line, 5, 23), )
        end

        # @show stt2tst[s]
        # @show diff2UTC(dto, stt2tst[s])
        dt = dto + diff2UTC(dto, stt2tst[s])
        if in(s, pvaSSTs)
            n = 3
            index_health = 7
        else
            n = 7
            index_health = 25
        end
        if haskey(rnd, si)
            # @show si "haskey rnd si"
            ees = rnd[si]
            if haskey(ees.eph, dt)
                # @show ees.keys, dt, line
                # @show dt "haskey ees.eph dt"
                # @show line
                existed_arr = ees.eph[dt].d
                time_existed = true
                # throw(error("duplicate epoch? $si-$dt"))
            end
            # @show dt "this dt"
        else
            ees = EphEpochStore(si, Array{DateTime,1}(),
                            Dict{DateTime,EphEpoch}(),)
            rnd[si] = ees
        end
        arr = Array{Float64,1}()
        sizehint!(arr, 3 + n * 4)
        read_navline(SubString(line, 23 + index_plus,
                                        79 + index_plus),
                    arr, )
        for i in 1:n
            line = readline(f)
            len = length(line)
            read_navline(SubString(line, 4 + index_plus, len),
                            arr, )
        end

        arr[index_health] == 0 || continue
        if second(dto) != 0 continue end

        if s == sstGlonass
            if minute(dto) != 15 && minute(dto) != 45
                continue
            end
        elseif s == sstGalileo
            if minute(dto) % 10 != 0 continue end
        end

        if time_existed
            # @show si "time_existed"
            # @show dt "time_existed"
            # @show ees.eph[dt].d
            time_existed = false
            for (iien, (iex, inow)) in enumerate(zip(existed_arr, arr, ))
                if iex == inow continue end
                iienstr = @sprintf("%02d",iien)
                warnline = "$si $dto NO.$iienstr data different: $iex, $inow"
                write(fc, warnline, "\n")
                if s == sstGlonass
                    # @warn warnline
                    continue
                end
                if s == sstGalileo
                    # @warn warnline
                    continue
                #  Test this by comparing Toe values. OrbitEphStore.cpp Line 276
                end
                if s == sstBeiDou
                    # @warn warnline
                    continue
                end
                # throw(error(warnline))
                @warn warnline
            end
            continue
        end
        push!(ees.keys, dt)
        sort!(ees.keys)
        ees.eph[dt] = EphEpoch(si, dt, arr)
    end
    # @show rnd
    RinexNavData(rnd)
end

function load_nav(filename::String)
    open(filename, "r") do f
        check_file = string(filename, ".check")
        open(check_file, "w") do fc
            nhd = read_navheader(f)
            nh  = parse_navheader(nhd)
            nd  = read_navdata(nh, f, fc)
            return RinexNav(basename(filename), nh, nd)
        end
    end
end

function merge_navs(navs::Array{RinexNav,1})

    data = Dict{SatID,EphEpochStore}()
    mheaders = Array{RinexNavHeader,1}()

    for inav in navs
        push!(mheaders, inav.header)
        for (isat, ees) in inav.data.data
            if haskey(data, isat)
                this_data = data[isat]
                this_keys = this_data.keys
                this_eph = this_data.eph
            else
                data[isat] = EphEpochStore(ees.si, ees.keys, ees.eph)
                continue
            end
            for (idt, iee) in ees.eph
                if haskey(this_eph, idt)
                    continue
                end
                this_eph[idt] = iee
                push!(this_keys, idt)
            end
            sort!(this_keys)
        end
    end
    mheaders, data
end

function load_navs(nav_files::Array{String,1})
    navs = Array{RinexNav,1}(undef, length(nav_files))
    for (i, this_nav_file) in enumerate(nav_files)
        navs[i] = load_nav(this_nav_file)
    end
    merge_navs(navs)
end

function get_freq(navdata::Dict{SatID,EphEpochStore},
    basefrequencies::Dict{SatelliteSystemType,Dict{CarrierBand,Tuple{Float64,Float64}}}=basefrequencies)
    freq = Dict{SatID,Dict{CarrierBand,Float64}}()
    for (isat, ees) in navdata

        freqn = 0.0
        if isat.ss == sstGlonass
            freqng = NaN
            for (idt, iee) in ees.eph
                if isnan(freqng) freqng = iee.d[11]
                elseif freqng == iee.d[11] continue
                else @warn string("$isat frequency number is different? $freqng ", iee[11])
                end
            end
            if !isnan(freqng) freqn = freqng end
        end
        this_freq = Dict{CarrierBand,Float64}()
        for (icb, ibf) in basefrequencies[isat.ss]
            this_freq[icb] = ibf[1]  + ibf[2] * freqn
        end
        freq[isat]  = this_freq
    end
    freq
end


#
# FourierOptics.jl -
#
# Tools for Fourier optics computations in Julia.
#

__precompile__(true)

module FourierOptics

export
    CoordinateTransform,
    Region,
    center,
    circularmask!,
    circularmask,
    fftshiftphasor!,
    fftshiftphasor,
    recenter

using LazyAlgebra
import LazyAlgebra: Complexes, Reals

using AbstractFFTs
import FFTW

# Declaration of functions to be extended by sub-modules.
function center end
function recenter end

include("units.jl")
using .Units

include("coords.jl")
import .CoordinateTransforms:
    CoordinateTransform,
    compose,
    jacobian,
    origin

include("regions.jl")
using .Regions

include("types.jl")
include("utils.jl")
include("lenses.jl")

end # module


abstract type AbstractLiklihood end

struct Gaussian <:AbstractLiklihood end


mutable struct Cart
    x::Int64
    y::Int64
    direction::Char
    next_turn::Char
end

import Base.isless
function isless(cart1::Cart, cart2::Cart)::Bool
    if cart1.y < cart2.y
        return true
    elseif cart1.y == cart2.y
        if cart1.x < cart2.x
            return true
        end
    end
    return false
end

function parse_map()::Tuple{Array{Char,2},Array{Cart,1}}
    lines = []
    for row in readlines(open("inputs/13.txt"))
        push!(lines, row)
    end
    carts = []
    longest_line = maximum(map(x->length(x), lines))
    grid = map(x->' ', zeros(length(lines),longest_line))
    for y in 1:length(lines)
        for x in 1:length(lines[y])
            c = lines[y][x]
            if lines[y][x] == '>'
                cart = Cart(x,y,'E','L')
                c = '-'
                push!(carts, cart)
            elseif lines[y][x] == '<'
                cart = Cart(x,y,'W','L')
                c = '-'
                push!(carts, cart)
            elseif lines[y][x] == 'v'
                cart = Cart(x,y,'S','L')
                c = '|'
                push!(carts, cart)
            elseif lines[y][x] == '^'
                cart = Cart(x,y,'N','L')
                c = '|'
                push!(carts, cart)
            end
            grid[y,x] = c
        end
    end
    return (grid, carts)
end

function check_for_crashes(grid, carts)
    occupied_coords = Set()
    for cart in carts
        if in((cart.x, cart.y), occupied_coords)
            return (true, cart.x-1, cart.y-1)
        end
        push!(occupied_coords, (cart.x, cart.y))
    end
    return (false, 0, 0)
end

function remove_crashed_carts(grid, carts)
    coords = [(cart.x, cart.y) for cart in carts]
    u = unique(coords)
    d=Dict([(i,count(x->x==i,coords)) for i in u])
    crashpoints = filter(x->x[2] > 1, collect(d))
    for point in crashpoints
        deleted = 0
        for cart in length(carts):-1:1
            if (carts[cart].x, carts[cart].y) == point[1]
                println(carts[cart])
                deleteat!(carts,cart)
                deleted += 1
            end
        end
        @assert deleted == 2
    end
end

function move_cart(grid::Array{Char,2}, cart::Cart)
    straight_track = '|'
    curve_right = '/'
    right_turn_new_dir = 'E'
    curve_left = '\\'
    left_turn_new_dir = 'W'
    next_x = cart.x
    next_y = cart.y-1

    if cart.direction == 'S'
        straight_track = '|'
        curve_right = '/'
        right_turn_new_dir = 'W'
        curve_left = '\\'
        left_turn_new_dir = 'E'
        next_x = cart.x
        next_y = cart.y+1
    elseif cart.direction == 'W'
        straight_track = '-'
        curve_right = '\\'
        right_turn_new_dir = 'N'
        curve_left = '/'
        left_turn_new_dir = 'S'
        next_x = cart.x-1
        next_y = cart.y
    elseif cart.direction == 'E'
        straight_track = '-'
        curve_right = '\\'
        right_turn_new_dir = 'S'
        curve_left = '/'
        left_turn_new_dir = 'N'
        next_x = cart.x+1
        next_y = cart.y
    end

    if grid[next_y,next_x] == straight_track
        cart.x = next_x
        cart.y = next_y
    elseif grid[next_y,next_x] == curve_right
        cart.x = next_x
        cart.y = next_y
        cart.direction = right_turn_new_dir
    elseif grid[next_y,next_x] == curve_left
        cart.x = next_x
        cart.y = next_y
        cart.direction = left_turn_new_dir
    elseif grid[next_y,next_x] == '+'
        cart.x = next_x
        cart.y = next_y
        if cart.next_turn == 'L'
            cart.direction = left_turn_new_dir
            cart.next_turn = 'S'
        elseif cart.next_turn == 'R'
            cart.direction = right_turn_new_dir
            cart.next_turn = 'L'
        elseif cart.next_turn == 'S'
            cart.direction = cart.direction
            cart.next_turn = 'R'
        end
    end
end

function run_tick(grid, carts)
    sorted_carts = sort(carts)
    for cart in sorted_carts
        result = move_cart(grid, cart)
        sorted_carts = sort(carts)
        remove_crashed_carts(aocmap...)
    end
    return check_for_crashes(grid,carts)
end

aocmap = parse_map()

while true
    crashes = run_tick(aocmap...)
    if crashes[1]
        println(crashes)
        break
    end
end

remove_crashed_carts(aocmap...)

while length(aocmap[2]) > 1
    run_tick(aocmap...)
end


import Base.position

export AbstractSystem
export BoundaryCondition, DirichletZero, Periodic, infinite_box
export bounding_box, boundary_conditions, periodicity, n_dimensions, species_type
export position, velocity, element, atomic_mass, atomic_number, atomic_symbol

#
# Identifier for boundary conditions per dimension
#
abstract type BoundaryCondition end
struct DirichletZero <: BoundaryCondition end  # Dirichlet zero boundary (i.e. molecular context)
struct Periodic <: BoundaryCondition end  # Periodic BCs

infinite_box(::Val{1}) = [[Inf]]u"bohr"
infinite_box(::Val{2}) = [[Inf, 0], [0, Inf]]u"bohr"
infinite_box(::Val{3}) = [[Inf, 0, 0], [0, Inf, 0], [0, 0, Inf]]u"bohr"
infinite_box(dim::Int) = infinite_box(Val(dim))

#
# Abstract system
#
"""
    AbstractSystem{D}

A `D`-dimensional system.
"""
abstract type AbstractSystem{D} end

"""
    bounding_box(sys::AbstractSystem{D})

Return a vector of length `D` of vectors of length `D` that describe the "box" in which the system `sys` is defined.
"""
function bounding_box end

"""
    boundary_conditions(sys::AbstractSystem{D})

Return a vector of length `D` of `BoundaryCondition` objects, one for each direction described by `bounding_box(sys)`.
"""
function boundary_conditions end

"""
    species_type(::AbstractSystem)

Return the type used to represent a species or atom.
"""
function species_type end

"""Return vector indicating whether the system is periodic along a dimension."""
periodicity(sys::AbstractSystem) = [isa(bc, Periodic) for bc in boundary_conditions(sys)]

"""
    n_dimensions(::AbstractSystem)
    n_dimensions(atom)

Return number of dimensions.
"""
n_dimensions(::AbstractSystem{D}) where {D} = D
# Note: Can't use ndims, because that is ndims(sys) == 1 (because of indexing interface)

# indexing and iteration interface...need to implement getindex and length, here are default dispatches for others
Base.size(s::AbstractSystem) = (length(s),)
Base.firstindex(::AbstractSystem) = 1
Base.lastindex(s::AbstractSystem) = length(s)
# default to 1D indexing
Base.iterate(sys::AbstractSystem, state = firstindex(sys)) =
    (firstindex(sys) <= state <= length(sys)) ? (@inbounds sys[state], state + 1) : nothing

# TODO Support similar, push, ...

#
# Species property accessors from systems and species
#
"""The element corresponding to a species/atom (or missing)."""
function element end

"""
    position(sys::AbstractSystem{D})
    position(sys::AbstractSystem, index)
    position(species)

Return a vector of positions of every particle in the system `sys`. Return type
should be a vector of vectors each containing `D` elements that are
`<:Unitful.Length`. If an index is passed or the action is on a `species`,
return only the position of the referenced `species` / species on that index.
"""
position(sys::AbstractSystem)        = position.(sys)    # in Cartesian coordinates!
position(sys::AbstractSystem, index) = position(sys[index])

"""
    velocity(sys::AbstractSystem{D})
    velocity(sys::AbstractSystem, index)
    velocity(species)

Return a vector of velocities of every particle in the system `sys`. Return
type should be a vector of vectors each containing `D` elements that are
`<:Unitful.Velocity`. If an index is passed or the action is on a `species`,
return only the velocity of the referenced `species`. Returned value of the function
may be `missing`.
"""
velocity(sys::AbstractSystem)        = velocity.(sys)    # in Cartesian coordinates!
velocity(sys::AbstractSystem, index) = velocity(sys[index])

"""
    atomic_mass(sys::AbstractSystem)
    atomic_mass(sys::AbstractSystem, i)
    atomic_mass(species)

Vector of atomic masses in the system `sys` or the atomic mass of a particular `species` /
the `i`th species in `sys`. The elements are `<: Unitful.Mass`.
"""
atomic_mass(sys::AbstractSystem)        = atomic_mass.(sys)
atomic_mass(sys::AbstractSystem, index) = atomic_mass(sys[index])

"""
    atomic_symbol(sys::AbstractSystem)
    atomic_symbol(sys::AbstractSystem, i)
    atomic_symbol(species)

Vector of atomic symbols in the system `sys` or the atomic symbol of a particular `species` /
the `i`th species in `sys`.
"""
atomic_symbol(sys::AbstractSystem)        = atomic_symbol.(sys)
atomic_symbol(sys::AbstractSystem, index) = atomic_symbol(sys[index])

"""
    atomic_number(sys::AbstractSystem)
    atomic_number(sys::AbstractSystem, i)
    atomic_number(species)

Vector of atomic numbers in the system `sys` or the atomic number of a particular `species` /
the `i`th species in `sys`.
"""
atomic_number(sys::AbstractSystem)        = atomic_number.(sys)
atomic_number(sys::AbstractSystem, index) = atomic_number(sys[index])


using AtomicLevels

function nonh()
    info("Executing nonh")
    # Angular integrals
    clean_mpi_tmp(r"fort.[0-9]+",
                  r"yint.lst.[0-9]+",
                  r"c.lst.[0-9]+")
    mpi_run(mpi_cmd("$atsp/nonh_mpi"))
    info("Finished executing nonh")
end

function hf(name, term::Term, Z,
            closed::Config, ref_set::Config,
            vary = "all",
            default_electron_parameters = true,
            rem_parameters = ["y","n","T,1.D-8","y","y"],
            additional_parameters = [])
    closed = join([@sprintf("%2d%s", c[1], ells[c[2]+1]) for c in closed], " ")

    pipe_file_run("$atsp/hf",
                  """$name,$(string(term,false)),$(float(Z)) ! Name, final term, Z (next row: closed orbitals)
 $closed
$(orbital_string(ref_set, true, false; escape_principal_qn = true)) ! Electrons outside closed orbitals
$vary ! Which orbitals to vary
$(y_or_n(default_electron_parameters)) ! Default electron parameters
$(y_or_n(length(rem_parameters)==0)) ! Remaining parameters
$(join(rem_parameters, "\n"))
$(y_or_n(length(additional_parameters)>0)) ! Additional parameters
$(join(additional_parameters, "\n"))
n ! Don't continue along the sequence
""")
end

function mchf(name,Z,
              term_weights,
              active::Config,
              spectroscopic::Config = [];
              iter = 200,
              cfg_tol = 1e-8,
              scf_tol = 1e-8)
    pipe_file_run("$atsp/mchf_mpi",
                  """$name,$(float(Z))
$(join(term_weights, '\n'))
$(orbital_string(active,false,false,","; escape_principal_qn = true))
$(orbital_string(spectroscopic,false,false,"," ; escape_principal_qn = true))
y ! Default electron parameters
n ! Custom other parameters
y ! Default values for NO,REL,STRONG
n ! Custom values for PRINT,CFGTOL,SCFTOL
F,$cfg_tol,$scf_tol
n ! Custom values for NSCF,IC
$iter,0
y ! Default values for ACFG,LD,TRACE
""")
end

function atsp_save_run(config, term)
    cpf("wfn.out", "wfn.inp")
    name = active_file(config, term)
    cpf("wfn.out", "$name.w")
    cpf("summry", "$name.s")
    cpf("clist.out", "$name.c")
    cpf(filter(f -> ismatch(Regex("^$(string(term)).*\\.l\$"), f), readdir())[1], "$name.l")
end

function breit_pauli(config, term;
                     restarting = false,
                     guesses = false,
                     which_op = 2,
                     all_rel = true,
                     all_inter = true,
                     atom_mass = Inf)
    name = active_file(config, term)

    (isfile("$name.l") && isfile("$name.c")) || error("Could not find $name.l/c")
    cpf("$name.l", "$name.l.bak")
    Js = J_range(term)
    j2 = map(Js) do JJ
        round(Int, 2JJ)
    end
    eiv = join([1 for JJ in reverse(Js)], '\n')
    println("$term ⟹ J ∈ $Js")

    atom_mass_s = (atom_mass == Inf) ? "" : "\n$atom_mass"

    pipe_file_run("$atsp/bpci",
                  """$name, y, n ! Atom, relativistic, mass correction
$(y_or_n(restarting)) ! Restarting
$(y_or_n(guesses)) ! Existing $name.l/.j as guesses
$(maximum(j2)),$(minimum(j2)) ! max(2J),min(2J)
$eiv ! Eigenvalues
$which_op ! Non-relativistic operators and selected relativistic
$(y_or_n(all_rel)) ! All relativistic operators
$(y_or_n(all_inter)) ! All interactions
$(y_or_n(atom_mass == Inf)) ! Default Rydberg constant$(atom_mass_s)
""")
    cpf("$name.l.bak", "$name.l")
end

function atsp_cp_wfn(a,b)
    mkpath("$b")
    cpf("$a/wfn.out", "$b/wfn.inp")
end

function read_hf_eng()
    # Read the total energy from hf.log
    open("hf.log") do file
        float(split(readall(file))[end-11])
    end
end

function read_eng_blocks(f::Function, config, term, ending)
    open("$(active_file(config, term)).$ending") do file
        f() do
            while !ismatch(r"^[ ]*2\*J =", readline(file)); end
            readline(file)
            float(split(readline(file))[2])
        end
    end
end

function read_mchf_eng(config, term)
    # Read the total (non-relativistic) energy from $name.l
    read_eng_blocks(config, term, "l") do read_block
        read_block()
    end
end

function read_breit_pauli_eng(config, term)
    Js = J_range(term)
    # Read the total (Breit–Pauli corrected) energies from $name.j
    read_eng_blocks(config, term, "j") do read_block
        [read_block()
         for JJ in reverse(Js)]
    end
end

function atsp_clean()
    # Clean up temporary files that take up a lot of space
    for f in filter(f -> contains(f, ".lst"), readdir())
        rm(f)
    end
end

function hf_mchf_bp(config::Config,
                    term::Term,
                    Z,
                    ncorr,
                    wfn0 = nothing;
                    active::Function = active_set,
                    overwrite = true,
                    atom_mass = Inf,
                    csf_filter = (a...) -> true,
                    hf_core = true,
                    bp = true)
    println(repeat("=", 80))
    conf = active_file(config, term)
    println(config, " ", term)

    overwrite_i = i -> overwrite && isfile("$i/wfn.out") || !isfile("$i/wfn.out")
    dir_run(conf) do
        energies = zeros(ncorr+1)

        Js = J_range(term)
        bp_energies = zeros(ncorr+1, length(Js))

        # Hartree--Fock run
        overwrite_i(0) && dir_run("0") do
            if wfn0 != nothing
                println("Initial guess: $wfn0")
                cpf(wfn0, "wfn.inp")
            end
            csfgenerate(active(config, 0),
                        csf_filter,
                        (term, 0, config))
            nonh()
            hf(conf, term, Z,
               closed(config),
               open(config),
               hf_core ? "all" : "=$(length(open(config)))")
            atsp_clean()
        end

        dir_run("0") do
            energies[1] = read_hf_eng()
            bp_energies[1,:] = energies[1]
        end

        # Multiconfigurational Hartree--Fock, layer by layer, with
        # additional Breit--Pauli calculation at the end of each run
        for i = 1:ncorr
            atsp_cp_wfn(i-1,i)
            overwrite_i(i) && dir_run("$i") do
                act = csfgenerate(active(config, i),
                                  csf_filter,
                                  (term, 2, config))
                nonh()
                mchf(conf, Z, 1,
                     act, config)
                atsp_save_run(config, term)
                bp && breit_pauli(config, term; atom_mass = atom_mass)
                atsp_clean()
            end

            dir_run("$i") do
                energies[i+1] = read_mchf_eng(config, term)
                bp && (bp_energies[i+1,:] = read_breit_pauli_eng(config, term))
            end
        end

        save_eng([energies bp_energies]')
    end
    conf, abspath("$conf/$ncorr/wfn.out")
end

export nonh, hf, mchf,
atsp_save_run, breit_pauli,
atsp_cp_wfn, read_hf_eng, read_mchf_eng, read_breit_pauli_eng,
atsp_clean,
hf_mchf_bp


module VDPTag2Experiments

import ARDESPOT

using POMDPs
using MCTS
using ParticleFilters
using StaticArrays
using VDPTag2

using POMDPModelTools: GenerativeBeliefMDP
using Random: Random, MersenneTwister, AbstractRNG, randperm

export
    RootToNextMLFirst,
    ObsAdaptiveParticleFilter,
    VDPUpper

struct RootToNextMLFirst
    rng::MersenneTwister
end

function MCTS.next_action(gen::RootToNextMLFirst, p::VDPTagPOMDP, b, node)
    if isroot(node) && n_children(node) < 1
        target_sum=MVector(0.0, 0.0)
        agent_sum=MVector(0.0, 0.0)
        for s in particles(b::ParticleCollection)
            target_sum += s.target
            agent_sum += s.agent
        end
        next = VDPTag2.next_ml_target(mdp(p), target_sum/n_particles(b))
        diff = next-agent_sum/n_particles(b)
        return TagAction(false, atan(diff[2], diff[1]))
    else
        return rand(gen.rng, actions(p))
    end
end

MCTS.next_action(gen::RootToNextMLFirst, p::GenerativeBeliefMDP, b, node) = next_action(gen, p.pomdp, b, node)

struct ObsAdaptiveParticleFilter{P<:POMDP,S,R,RNG<:AbstractRNG} <: Updater
    pomdp::P
    resample::R
    max_frac_replaced::Float64
    n_init::Int
    rng::RNG
    _pm::Vector{S}
    _wm::Vector{Float64}
end

function ObsAdaptiveParticleFilter(p::POMDP, resample, max_frac_replaced, n_init, rng::AbstractRNG)
    S = statetype(p)
    return ObsAdaptiveParticleFilter(p, resample, max_frac_replaced, n_init, rng, S[], Float64[])
end

POMDPs.initialize_belief(up::ObsAdaptiveParticleFilter, d::Any) = ParticleCollection([rand(up.rng, d) for i in 1:up.n_init])
POMDPs.update(up::ObsAdaptiveParticleFilter, b, a, o) = update(up, resample(up.resample, b, up.rng), a, o)

function POMDPs.update(up::ObsAdaptiveParticleFilter, b::ParticleCollection, a, o)
    if n_particles(b) > 2*up.resample.n
        b = resample(up.resample, b, up.rng)
    end

    ps = particles(b)
    pm = up._pm
    wm = up._wm
    resize!(pm, 0)
    resize!(wm, 0)

    all_terminal = true
    for i in 1:n_particles(b)
        s = ps[i]
        if !isterminal(up.pomdp, s)
            all_terminal = false
            sp = @gen(:sp)(up.pomdp, s, a, up.rng)
            push!(pm, sp)
            od = observation(up.pomdp, s, a, sp)
            push!(wm, pdf(od, o))
        end
    end
    ws = sum(wm)
    if all_terminal || ws < eps(1.0/length(wm))
        # warn("All states in particle collection were terminal.")
        return initialize_belief(up, reset_distribution(up.pomdp, b, a, o))
    end

    pc = resample(up.resample, WeightedParticleBelief{statetype(up.pomdp)}(pm, wm, ws, nothing), up.rng)
    ps = particles(pc)

    mpw = max_possible_weight(up.pomdp, a, o)
    frac_replaced = up.max_frac_replaced*max(0.0, 1.0 - maximum(wm)/mpw)
    n_replaced = floor(Int, frac_replaced*length(ps))
    is = randperm(up.rng, length(ps))[1:n_replaced]
    for i in is
        ps[i] = new_particle(up.pomdp, b, a, o, up.rng)
    end
    return pc
end

max_possible_weight(pomdp::VDPTagPOMDP, a::TagAction, o) = 0.0
new_particle(pomdp::VDPTagPOMDP, a::TagAction, o) = error("shouldn't get here")
reset_distribution(p::POMDP, b, a, o) = initialstate(p)

struct VDPUpper end

function ARDESPOT.ubound(ub::VDPUpper, pomdp::POMDP, b::ARDESPOT.ScenarioBelief)
    if all(isterminal(pomdp, s) for s in particles(b))
        return 0.0
    else
        return mdp(cproblem(pomdp)).tag_reward
    end
end

Random.seed!(p::Policy, ::Any) = nothing

end