# infdecomp.jl
module InfDecomp

using InfDecomp_Base
using ExpCone
using GradDesc

using MathProgBase

features_list  = [:Grad,:Jac,:JacVec,:Hess]::Vector{Symbol} # not doing :HessVec right now

# ---------------------------------------------
# M a t h P r o g B a s e    I n t e r f a c e
# ---------------------------------------------

import MathProgBase.initialize
import MathProgBase.features_available
import MathProgBase.eval_f
import MathProgBase.eval_g
import MathProgBase.eval_grad_f
import MathProgBase.jac_structure
import MathProgBase.hesslag_structure
import MathProgBase.eval_jac_g
import MathProgBase.eval_jac_prod
import MathProgBase.eval_jac_prod_t
import MathProgBase.eval_hesslag_prod
import MathProgBase.eval_hesslag
import MathProgBase.isobjlinear
import MathProgBase.isobjquadratic
import MathProgBase.isconstrlinear
# import MathProgBase.obj_expr
# import MathProgBase.constr_expr
import MathProgBase.getreducedcosts
import MathProgBase.getconstrduals
import MathProgBase.getsolution
import MathProgBase.status
import MathProgBase.getsolvetime

# ------------
# B a s i c s
# ------------

# initialize()
function initialize(e::My_Eval, requested_features::Vector{Symbol})
    println("here!")
    for feat in requested_features
        if feat ∉ features_list
            error("infdecomp.jl: initialize():\n-   JuliaOpt:MathProgBase is asking for a feature ($feat) that I don't have.-   Maybe use another solver?")
        end
    end
end

# features_available()
features_available(::My_Eval) = features_list

# Properties:
isobjlinear(::My_Eval)           = false
isobjquadratic(::My_Eval)        = false
isconstrlinear(::My_Eval, ::Integer) = true

#global count_hesseval = 0
# ------------------------------------------
# E v a l u a t i o n :   0 t h   o r d e r
# ------------------------------------------
function eval_f{TFloat_2,TFloat}(e::My_Eval, x::Vector{TFloat_2},dummy::TFloat=Float64(0)) :: TFloat
    local condent::Float64
    if e.TmpFloat==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
        condent = condEntropy(e,x,BigFloat(0.))
        setprecision(BigFloat,prc)
    else
        condent = condEntropy(e,x,Float64(0))
    end
    e.count_eval_f += 1
    return -condent
    ;
end #^ eval_f()

# eval_g --- eval of constraint into g
function eval_g(e::My_Eval, g::Vector{Float64}, x::Vector{Float64})  :: Void
    g .= reshape( reshape(x,1,e.n)*e.Gt , e.m, ) .- e.rhs
    e.count_eval_g += 1
    return nothing
    ;
end # eval_g()

# ------------------------------------------
# E v a l u a t i o n :   1 s t   o r d e r
# ------------------------------------------

####################################################################################################
# G L O B A L   V A R I A B L E
global__ill_sol   = "What?!???"
ill_sol__copy_sol = "Oh, no!!!"
function ill_sol__do_copy_sol(x) :: Void
    if global__ill_sol == nothing
        global global__ill_sol = x[:]
    elseif typeof(global__ill_sol)==typeof(x)
        global global__ill_sol .= x
    else
        @show "FUCK!!!!!"
    end
    return nothing
end
function  ill_sol__dont_copy_sol(x) :: Void
    return nothing
end

function set_copy_sol_behaviour(doit::Bool)
    if doit
        global global__ill_sol   = nothing
        global ill_sol__copy_sol = ill_sol__do_copy_sol
    else
        global global__ill_sol   = nothing
        global ill_sol__copy_sol = ill_sol__dont_copy_sol
    end
    ;
end
####################################################################################################

# eval_grad_f --- eval gradient of objective function
function eval_grad_f(e::My_Eval, g::Vector{Float64}, x::Vector{Float64}) :: Void
    if e.TmpFloat==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
        ∇f(e,g,x,BigFloat(0.))
        setprecision(BigFloat,prc)
    else
        ∇f(e,g,x,Float64(0))
    end
    e.count_eval_grad_f += 1

    # Crap:
    ill_sol__copy_sol(x)

    return nothing
    ;
end # eval_grad_f()

# Constraint Jacobian
# jac_structure() --- zero-nonzero pattern of constraint Jacobian
jac_structure(e::My_Eval) :: Tuple{Vector{Int64},Vector{Int64}}   =  ( e.Gt_L , e.Gt_K )
# Note: Gt is transposed, so K and L are swapped [K: rows of G^T; L: columns of G^T]

# eval_jac_g() --- constraint Jacobian   -> J
function eval_jac_g(e::My_Eval, J::Vector{Float64}, x::Vector{Float64}) :: Void
    J .= e.Gt.nzval
    e.count_eval_jac_g += 1
    return nothing
    ;
end # eval_jac_g()

# eval_jac_prod() --- constraint_Jacobian * w   -> y
function eval_jac_prod(e::My_Eval, y::Vector{Float64}, x::Vector{Float64}, w::Vector{Float64}) :: Void
    y .= reshape( reshape(w,1,e.n)*e.Gt , e.m, )
    return nothing
    ;
end # eval_jac_prod()

# eval_jac_prod_t() --- constraint_Jacobian^T * w  -> y
function eval_jac_prod_t{T<:AbstractFloat}(e::My_Eval, y::Vector{T}, x::Vector{T}, w::Vector{T}) :: Void
    y .= e.Gt*w
    return nothing
end

# ------------------------------------------
# E v a l u a t i o n :   2 n d   o r d e r
# ------------------------------------------

# Lagrangian:
# L(x, (σ,μ) ) = σ f(x) + μ' G(x)
# Since G(.) is linear, it's Hessian is 0 anyway, so it won't bother us.

# hesslag_structure() --- zero-nonzero pattern of Hessian [wrt x] of the Lagrangian
function hesslag_structure(e::My_Eval)  :: Tuple{Vector{Int64},Vector{Int64}}
    K = Vector{Int64}()
    L = Vector{Int64}()
    counter = 0
    for y = 1:e.n_y
        for z = 1:e.n_z
            # Start with the diagonal
            for x = 1:e.n_x
                i = e.varidx[x,y,z]
                if i>0
                    counter += 1
                    push!(K,i)
                    push!(L,i)
                end
            end
            # Now off-diagonal.
            # H is  treated as a symmetric matrix, but:
            # if both (k,l) & (l,k) are present, their values will be added!
            for x = 1:e.n_x
                for u = 1:(x-1)
                    i_x = e.varidx[x,y,z]
                    i_u = e.varidx[u,y,z]
                    if i_x>0 && i_u>0
                        counter += 1
                        push!(K,i_x)
                        push!(L,i_u)
                    end
                end
            end
        end# for z
    end #^ for y
    return (K,L)
    ;
end # hesslag_structure()

function Hess{TFloat}(e::My_Eval, H::Vector{Float64}, p::Vector{Float64}, σ::Float64, dummy::TFloat) :: Void
    counter = 0
    for y = 1:e.n_y
        for z = 1:e.n_z
            # make marginal P(*yz)
            P_yz  ::TFloat = TFloat(0.)
            for x = 1:e.n_x
                i = e.varidx[x,y,z]
                if i>0
                    P_yz += p[i]
                end
            end

            # now: for all pairs x,u we have:
            # if x ≠  u:   -1/P_yz;
            # if x == u:   ( P_yz - P(xyz) )/(  P_yz * P(xyz) )

            # Start with the diagonal
            for x = 1:e.n_x
                i = e.varidx[x,y,z]
                if i>0
                    counter += 1
                    P_xyz = p[i]
                    H[counter] = Float64(   (P_xyz == 0 ) ?  1.e50  : TFloat(σ)*( P_yz - P_xyz )/(  P_yz * P_xyz )  )
                end
            end
            # Now off-diagonal.
            # H is  treated as a symmetric matrix, but:
            # if both (k,l) & (l,k) are present, their values will be added!
            for x = 1:e.n_x
                for u = 1:(x-1)
                    i_x = e.varidx[x,y,z]
                    i_u = e.varidx[u,y,z]
                    if i_x>0 && i_u>0
                        counter += 1
                        H[counter] = -TFloat(σ)/P_yz
                    end
                end
            end
        end#for z
    end# for y
    return nothing
    ;
end # eval_hesslag()

# eval_hesslag() --- Hessian [wrt x] of the Lagrangian
function eval_hesslag(e::My_Eval, H::Vector{Float64}, x::Vector{Float64}, σ::Float64, μ::Vector{Float64}) :: Void
    if e.TmpFloat==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
        Hess(e,H,x,σ,BigFloat(0.))
        setprecision(BigFloat,prc)
        e.count_hesseval += 1
    else
        Hess(e,H,x,σ,Float64(0))
        e.count_hesseval += 1
    end
    return nothing
    ;
end # eval_hesslag()

# eval_hesslag() --- ( Hessian [wrt x] of the Lagrangian ) * v
# function eval_hesslag_prod{T<:AbstractFloat}(e::My_Eval, h::Vector{T}, x::Vector{T}, v::Vector{T}, σ::T, μ::Vector{T}) :: Void
#     h .= σ .* Hf(x)*v
#     return nothing
# end

#------------------
# M A R G I N A L S
#------------------

function marginal_X{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: Array{TFloat_2,1}
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
    end
    marg_x::Array{TFloat_2,1}  = zeros(TFloat_2,e.n_x)
    if TFloat_2==BigFloat
        setprecision(BigFloat,prc)
    end
    for x in 1:e.n_x
        for y in 1:e.n_y
            for z in 1:e.n_z
                i = e.varidx[x,y,z]
                if i>0
                    marg_x[x] += p[i]
                end
            end
        end
    end
    return marg_x
    ;
end#^ marginal_X

function marginal_Y{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: Array{TFloat_2,1}
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
    end
    marg_y::Array{TFloat_2,1}  = zeros(TFloat_2,e.n_y)
    if TFloat_2==BigFloat
        setprecision(BigFloat,prc)
    end
    for y in 1:e.n_y
        for x in 1:e.n_x
            for z in 1:e.n_z
                i = e.varidx[x,y,z]
                if i>0
                    marg_y[y] += p[i]
                end
            end
        end
    end
    return marg_y
    ;
end#^ marginal_Y

function marginal_Z{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: Array{TFloat_2,1}
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
    end
    marg_z::Array{TFloat_2,1}  = zeros(TFloat_2,e.n_z)
    if TFloat_2==BigFloat
        setprecision(BigFloat,prc)
    end
    for z in 1:e.n_z
        for x in 1:e.n_x
            for y in 1:e.n_y
                i = e.varidx[x,y,z]
                if i>0
                    marg_z[z] += p[i]
                end
            end
        end
    end
    return marg_z
    ;
end#^ marginal_Z

function marginal_XY{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: Array{TFloat_2,2}
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
        setprecision(BigFloat,prc)
    end
    marg_xy::Array{TFloat_2,2}  = zeros(TFloat_2,e.n_x,e.n_y)
    if TFloat_2==BigFloat
        setprecision(BigFloat,prc)
    end
    for x in 1:e.n_x
        for y in 1:e.n_y
            for z in 1:e.n_z
                i = e.varidx[x,y,z]
                if i>0
                    marg_xy[x,y] += p[i]
                end
            end
        end
    end
    return marg_xy
    ;
end#^ marginals_XY

function marginal_XZ{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: Array{TFloat_2,2}
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
    end
    marg_xz::Array{TFloat_2,2}  = zeros(TFloat_2,e.n_x,e.n_z)
    if TFloat_2==BigFloat
        setprecision(BigFloat,prc)
    end
    for x in 1:e.n_x
        for z in 1:e.n_z
            for y in 1:e.n_y
                i = e.varidx[x,y,z]
                if i>0
                    marg_xz[x,z] += p[i]
                end
            end
        end
    end
    return marg_xz
    ;
end#^ marginal_XZ

function marginal_YZ{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: Array{TFloat_2,2}
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
    end
    marg_yz::Array{TFloat_2,2}  = zeros(TFloat_2,e.n_y,e.n_z)
    if TFloat_2==BigFloat
        setprecision(BigFloat,prc)
    end
    for y in 1:e.n_y
        for z in 1:e.n_z
            for x in 1:e.n_x
                i = e.varidx[x,y,z]
                if i>0
                    marg_yz[y,z] += p[i]
                end
            end
        end
    end
    return marg_yz
    ;
end#^ marginal_YZ

#------------------------------------------
# I N F O R M A T I O N - F U N C T I O N S
#------------------------------------------

# entropy_X --- Shannon entropy of X --- H(X)

function entropy_X{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: TFloat_2
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
        s::BigFloat = BigFloat(0.)
        setprecision(BigFloat,prc)
    else
        s = Float64(0)
    end
    marg_x = marginal_X(e, p, TFloat)
    for x in 1:e.n_x
        s  +=  (  ( marg_[x] ≤ 0 )  ?   TFloat(0.)   :   marg_x[x]*log(marg_x[x])  )
    end
    return s
    ;
end#^ entropy_X

# I_X_Y --- Mutual information of  X & Y --- I(X;Y)

function I_X_Y{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: TFloat_2
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
        s::BigFloat = BigFloat(0.)
        setprecision(BigFloat,prc)
    else
        s = Float64(0)
    end
    marg_x  = marginal_X(e, p, TFloat)
    marg_y  = marginal_Y(e, p, TFloat)
    marg_xy = marginal_XY(e, p, TFloat)
    for x in 1:e.n_x
        for y in 1:e.n_y
            s  +=  (  (marg_xy[x,y]  ≤ 0 || marg_x[x] ≤ 0 || marg_y[y] ≤ 0)  ?   TFloat_2(0.)   :   marg_xy[x,y]*log( marg_xy[x,y] / (marg_y[y] * marg_x[x]) )  )
        end
    end# ^y
    return s/log(2)
    ;
end#^ I_X_Y

# I_X_Y__Z --- Mutual information of X & Y|Z --- I(X;Y|Z)

function I_X_Y__Z{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: TFloat_2
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
        s::BigFloat = BigFloat(0.)
        setprecision(BigFloat,prc)
    else
        s = Float64(0)
    end
    marg_z  = marginal_Z(e, p, TFloat)
    marg_xz = marginal_XZ(e, p, TFloat)
    marg_yz = marginal_YZ(e, p, TFloat)
    for x in 1:e.n_x
        for y in 1:e.n_y
            for z in 1:e.n_z
                i = e.varidx[x,y,z]
                if i>0
                    s  +=  (  (marg_yz[y,z]  ≤ 0 || marg_z[z] ≤ 0 || marg_xz[x,z] ≤ 0 || p[i] ≤ 0 )  ?   TFloat_2(0.)   :   p[i]*log( (p[i] * marg_z[z]) / (marg_xz[x,z] * marg_yz[y,z]) )  )
                end
            end
        end
    end
    return s/log(2)
    ;
end#^ I_X_Y__Z

# I_X_Z__Y --- Mutual information of X & Z|Y --- I(X;Z|Y)

function I_X_Z__Y{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: TFloat_2
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
        s::BigFloat = BigFloat(0.)
        setprecision(BigFloat,prc)
    else
        s = Float64(0)
    end
    marg_y  = marginal_Y(e, p, TFloat)
    marg_xy = marginal_XY(e, p, TFloat)
    marg_yz = marginal_YZ(e, p, TFloat)
    for x in 1:e.n_x
        for y in 1:e.n_y
            for z in 1:e.n_z
                i = e.varidx[x,y,z]
                if i>0
                    s  +=  (  (marg_yz[y,z]  ≤ 0 || marg_y[y] ≤ 0 || marg_xy[x,y] ≤ 0 || p[i] ≤ 0 )  ?   TFloat_2(0.)   :   p[i]*log( (p[i] * marg_y[y]) / (marg_xy[x,y] * marg_yz[y,z]) )  )
                end
            end
        end
    end
    return s/log(2)
end#^ I_X_Y__Z

# SI --- Shared Information of Y & Z --- SI(Y;Z)

function SI{TFloat_2,TFloat}(e::My_Eval, p::Vector{TFloat_2}, dummy::TFloat)   :: TFloat_2
    if TFloat_2==BigFloat
        prc = precision(BigFloat)
        setprecision(BigFloat,e.bigfloat_nbits)
        s::BigFloat = BigFloat(0.)
        setprecision(BigFloat,prc)
    else
        s = Float64(0)
    end
    return I_X_Y(e, p, TFloat) - I_X_Y__Z(e, p, TFloat)
    ;
end#^ SI

# ----------------
# U S I N G   I T
# ----------------

numo_vars(e::My_Eval)                   :: Int64            = e.n
numo_constraints(e::My_Eval)            :: Int64            = e.m
constraints_lowerbounds_vec(e::My_Eval) :: Vector{Float64}  = zeros(Float64,e.m)
constraints_upperbounds_vec(e::My_Eval) :: Vector{Float64}  = zeros(Float64,e.m)
vars_lowerbounds_vec(e::My_Eval)        :: Vector{Float64}  = zeros(Float64,e.n)
vars_upperbounds_vec(e::My_Eval)        :: Vector{Float64}  = [Inf  for j=1:e.n]

using Base.Test

function do_it{T1,T2,T3}(pdf::Dict{Tuple{T1,T2,T3},Float64}, solver, tmpFloat::DataType=BigFloat ;  warmstart::Bool=false, model_type=:IPM)
    print("Starting optimization now (",now(),")!\n")

    # global count_hesseval = 0
    const sd,myeval = create_stuff(pdf,tmpFloat)

    stats = nothing

    if model_type ∈ [:ECOS_L,:SCS_L]
        # Conic Programming, large model
        stats = ExpCone.model_L(myeval,solver)
        return sd,myeval,stats

    elseif model_type ∈ [:ECOS_S,:SCS_S]
        # Conic Programming, small model
        stats = ExpCone.model_S(myeval,solver)
        return sd,myeval,stats
    elseif model_type == :SCS_D
        # Conic Programming, Dual problem
        stats = ExpCone.model_D(myeval,solver)
        return sd,myeval,stats

    elseif model_type == :My_GradDesc
        stats = my_gradient_descent(myeval;
                                    max_iter       = 1000,
                                    eps_grad       = 1.e-20,
                                    eps_steplength = 1.e-20,
                                    stepfactor     = .1)
        return sd,myeval,stats

    else # not Conic Program
        const lb = constraints_lowerbounds_vec(myeval)
        const ub = constraints_upperbounds_vec(myeval)
        const l = vars_lowerbounds_vec(myeval)
        const u = vars_upperbounds_vec(myeval)
        const model = MathProgBase.NonlinearModel(solver)
        MathProgBase.loadproblem!(model, myeval.n, myeval.m, l, u, lb, ub, :Min, myeval)

        if warmstart
            MathProgBase.setwarmstart!(model,ones(Float64,myeval.n))
        end

        MathProgBase.optimize!(model)
        # stat = MathProgBase.status(model)

        return sd,myeval,model;
    end

end #^ do_it()

#-----------------------------------------------
# Solution & Statistics
#-----------------------------------------------
type Solution_and_Stats
    instance_name       :: String
    solver              :: Symbol
    var_num             :: Int64
    x_sz                :: Int64
    y_sz                :: Int64
    z_sz                :: Int64
    status              :: Symbol
    obj_val             :: BigFloat
    q_nonneg_viol       :: BigFloat
    q_min_entry         :: BigFloat
    marginals_1         :: BigFloat
    marginals_2         :: BigFloat
    marginals_Inf       :: BigFloat
    mu_nonneg_viol      :: BigFloat
    complementarity_max :: BigFloat
    complementarity_sum :: BigFloat
    # MI_X_YZ             :: BigFloat
    CI                  :: BigFloat
    SI                  :: BigFloat
    UI_Y                :: BigFloat
    UI_Z                :: BigFloat
    num_eval_f          :: Int64
    num_eval_grad_f     :: Int64
    num_eval_g          :: Int64
    num_eval_jac_g      :: Int64
    num_hessevals       :: Int64
    opt_time            :: BigFloat
    # entropy_X           :: BigFloat
end #^ type Solution_and_Stats

function check_feasibility(name, model, myeval, solver) :: Solution_and_Stats
    if solver ∈ [:ECOS_L,:SCS_L,:ECOS_S,:SCS_S,:SCS_D]
        # Conic Program
        stats = model
        model = stats.model

        fstat = Solution_and_Stats(name,solver, 0,0,0,0, status(model) ,  big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),0,0,0,0,0,big(0.))
        fstat.x_sz = myeval.n_x
        fstat.y_sz = myeval.n_y
        fstat.z_sz = myeval.n_z

        fstat.var_num = MathProgBase.numvar(model)
        if status(model) ∈ [:Solve_Succeeded,:Optimal,:NearOptimal,:Suboptimal,:UserLimit,:FeasibleApproximate,:Error]
            # fstat.obj_val = stats.optimum
            fourtuple = information_quantities(myeval,fstat.obj_val,stats.q)
            (fstat.CI, fstat.SI, fstat.UI_Y, fstat.UI_Z) = fourtuple

            fstat.obj_val = eval_f(myeval,stats.q,Float64(0))

            fstat.q_nonneg_viol = max(-minimum(stats.q),0)
            fstat.q_min_entry   = max(minimum(stats.q),0)

            equation = (stats.q'*myeval.Gt)' - myeval.rhs
            fstat.marginals_1   = norm(equation,1)
            fstat.marginals_2   = norm(equation,2)
            fstat.marginals_Inf = norm(equation,Inf)
        end
        fstat.opt_time = stats.time

        return fstat

    elseif solver == :My_GradDesc
        # First order method: Projected Gradient Descent
        stats = model

        fstat = Solution_and_Stats(name,solver, 0,0,0,0, stats.status ,  big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),0,0,0,0,0,big(0.))
        fstat.x_sz = myeval.n_x
        fstat.y_sz = myeval.n_y
        fstat.z_sz = myeval.n_z

        fstat.var_num = myeval.n
        if stats.status ∈ [:grad0 :step0 :iter]
            # fstat.obj_val = stats.optimum
            fourtuple = information_quantities(myeval,fstat.obj_val,stats.q)
            (fstat.CI, fstat.SI, fstat.UI_Y, fstat.UI_Z) = fourtuple

            fstat.obj_val = eval_f(myeval,stats.q,Float64(0))

            fstat.q_nonneg_viol = max(-minimum(stats.q),0)
            fstat.q_min_entry   = max(minimum(stats.q),0)

            equation = (stats.q'*myeval.Gt)' - myeval.rhs
            fstat.marginals_1   = norm(equation,1)
            fstat.marginals_2   = norm(equation,2)
            fstat.marginals_Inf = norm(equation,Inf)
        end
        fstat.opt_time = stats.time

        return fstat

    else
        # Other solvers mainly Interior Point solvers
        fstat = Solution_and_Stats(name,solver, 0,0,0,0, status(model) ,  big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),big(0.),0,0,0,0,0,big(0.))
        fstat.x_sz = myeval.n_x
        fstat.y_sz = myeval.n_y
        fstat.z_sz = myeval.n_z

        fstat.var_num = myeval.n
        if status(model)==:Solve_Succeeded || status(model)==:Optimal || status(model)==:NearOptimal || status(model)==:Suboptimal || status(model)==:KnitroError || ( status(model)==:UserLimit && solver !=:Mosek ) || status(model)==:FeasibleApproximate
            q = Vector{Float64}( getsolution(model) )
            grad = zeros(Float64, myeval.n)
            InfDecomp.∇f(myeval, grad, q, Float64(.0))

            lambda = Vector{Float64}( getconstrduals(model) )

            mu = grad - myeval.Gt*lambda

            fstat.mu_nonneg_viol = max(-minimum(mu),0)

            fstat.complementarity_max = maximum( abs.(mu) .* abs.(q) )
            fstat.complementarity_sum = sum( abs.(mu) .* abs.(q) )
        else
            if global__ill_sol != nothing
                q = Vector{Float64}( global__ill_sol )
            end
            fstat.mu_nonneg_viol = -10
            fstat.complementarity_max = -10
            fstat.complementarity_sum = -10
        end# ^if status

        fstat.obj_val = eval_f(myeval,q,Float64(0))

        fstat.q_nonneg_viol = max(-minimum(q),0)
        fstat.q_min_entry   = max(minimum(q),0)

        equation = (q'*myeval.Gt)' - myeval.rhs
        fstat.marginals_1   = norm(equation,1)
        fstat.marginals_2   = norm(equation,2)
        fstat.marginals_Inf = norm(equation,Inf)
        # fstat.entropy_X   = entropy_X(myeval,q,Float64(0))
        # fstat.MI_X_YZ     = fstat.entropy_X + fstat.obj_val

        fstat.CI, fstat.SI, fstat.UI_Y, fstat.UI_Z =
            information_quantities(myeval,fstat.obj_val,q)

        fstat.num_eval_f        = myeval.count_eval_f
        fstat.num_eval_grad_f   = myeval.count_eval_grad_f
        fstat.num_eval_g        = myeval.count_eval_g
        fstat.num_eval_jac_g    = myeval.count_eval_jac_g
        fstat.num_hessevals = myeval.count_hesseval
        if solver != :Ipopt
            fstat.opt_time = getsolvetime(model)
        end
        #  end #^if status
        return fstat
    end
    ;
end #^ check_feasibility()

function information_quantities(myeval,optimum,q)
    p = Float64[]
    for x = 1:myeval.n_x
        for y = 1:myeval.n_y
            for z = 1:myeval.n_z
                if myeval.marg_xy[x,y] > 0  &&  myeval.marg_xz[x,z] > 0
                    push!(p,myeval.prb_xyz[x,y,z])
                end
            end# z
        end# y
    end# x

    ci = -(eval_f(myeval,p,Float64(0)) + optimum)/log(2)
    si = SI(myeval,q,Float64(0))
    ui_Y = I_X_Y__Z(myeval,q,Float64(0))
    ui_Z = I_X_Z__Y(myeval,q,Float64(0))

    return ci,si, ui_Y, ui_Z
end

end #^ module

; # EOF


# Define functions to find ∆ω, ∆a, ∆ψ by the implicit Newton step calculation algorithm.
# Note that ω, a, ψ are fixed in this calculation.

# We can save computations by implementing the assumption ∆D = 0, and also by updating ψ
# directly without calculating ∆ψ.

export LasingSol, LasingVar

# Below (and also in nonlasing.jl), many constructors take some arguments as templates, and
# those template arguments are not directly set as fields: separate copies are created
# (usually by `similar`) and set as fields.
#
# - An alternative would be to take parameters for creating those fields and create the
# fields.  E.g., we could take the length and the element type of a vector, create a vector,
# and set it as a field.  However, we are aiming to support many different types of vectors
# (e.g., PETSc vector).  We cannot predict all the parameters needed for creating these
# vectors.  By taking an actual instance of a vector in this case, we can support any types
# of vectors.
#
# - Then, the next possibility would be to let the users to provide a copy, instead of
# automatically generating a copy inside code.  I avoid taking this route for consistency in
# regard to automatic type conversion inside constructors.  Suppose I create a custom type
# named `MyType` containing `v::Vector{Float64}` as a field.  If I instantiate `MyType` by
# giving a `vi::Vector{Int64}` as an argument, `vi` is automatically type-converted into a
# Vector{Float64} and set to `v`.  Here, the resulting `v::Vector{Float64}` is a separate
# copy from `vi::Vector{Int64}`.  On the other hand, if I instantiate `MyType` by giving a
# `vf::Vector{Float64}` as an argument, a copy is not created and the `vf` itself is set to
# `v`.  This inconsistency is confusing, because in one case changing the contents of the
# outside vector `vi` does not affect the `MyType` instance, whereas in the other case it
# does.  In order to avoid this inconsistency, I decide to always create a copy of a
# template argument.  This way, no matter whether automatic type conversion occurs or not,
# the stored field is a separate copy from the argument given to a constructor.

# Solutions to the lasing equation.
mutable struct LasingSol{VF<:AbsVecFloat,VC<:AbsVecComplexF}  # VF and VC can be PETSc vectors with N entries
    ω::VecFloat  # M real numbers: frequencies of modes (M = # of total modes, including both lasing and nonlasing)
    a²::VecFloat  # M real numbers: squared "amplitudes" of modes inside hole-burning term; this includes γ(ω)² factor
    ψ::Vector{VC}  # M complex vectors: normalized modes
    abs2ψ::Vector{VF}  # M complex vectors: absolute squares of normalized modes; this is not abs2.(ψ) because of interpolation
    iₐ::VecInt  # M integers: row indices where amplitudes are measured
    vtemp::VC  # temporary storage for N complex numbers
    wtemp::VC  # temporary storage for N complex numbers
    activated::VecBool  # activated[m] == true if mode m is just activated; used in simulate!
    active::VecBool  # active[m] == true if mode m is active (i.e., lasing)
    m_active::VecInt  # vector of active (i.e., lasing) mode indices; collection of m such that active[m] == true
    function LasingSol{VF,VC}(ω::AbsVecReal,
                              a²::AbsVecReal,
                              ψ::AbsVec{<:AbsVecNumber},
                              abs2ψ::AbsVec{<:AbsVecReal},
                              iₐ::AbsVecInteger,
                              vtemp::AbsVecComplexF) where {VF<:AbsVecFloat,VC<:AbsVecComplexF}
        M = length(ω)
        length(a²)==length(ψ)==length(abs2ψ)==length(iₐ)==M ||
            @error "length.((ω,a²,ψ,abs2ψ)) = $(length.((ω,a²,ψ,abs2ψ))) should be all same."

        N = length(vtemp)
        for m = 1:M
            length(ψ[m])==length(abs2ψ[m])==N ||
                @error "length.((ψ[$m],abs2ψ[$m],vtemp)) = $(length.((ψ[m],abs2ψ[m],vtemp))) should be all same."
        end

        return new(ω, a², ψ, abs2ψ, iₐ, vtemp, copy(vtemp), fill(false,M), fill(false,M), VecInt(undef,0))  # active[m]=false for all m: no mode is lasing
    end
end
LasingSol(ω::AbsVecReal, a²::AbsVecReal, ψ::AbsVec{VC}, abs2ψ::AbsVec{VF}, iₐ::AbsVecInteger, vtemp::VC) where {VF<:AbsVecFloat,VC<:AbsVecComplexF} =
    LasingSol{VF,VC}(ω, a², ψ, abs2ψ, iₐ, vtemp)

# To do: check if the following works for vtemp of PETSc vector type.
LasingSol(vtemp::AbsVec, M::Integer) =  # vtemp has N entries
    LasingSol(zeros(M), zeros(M), [similar(vtemp,ComplexF).=0 for m = 1:M], [similar(vtemp,Float).=0 for m = 1:M], zeros(Int,M), similar(vtemp,ComplexF))
LasingSol(N::Integer, M::Integer) = LasingSol(VecFloat(undef,N), M)

Base.length(lsol::LasingSol) = length(lsol.ω)  # number of total modes, including both lasing and nonlasing
num_active(lsol::LasingSol) = sum(lsol.active)  # number of modes currently lasing

# Note that this function changes iₐ.  Therefore, this should not be called inside the
# iteration for finding the solution for a given pump strength, because iₐ should be kept the
# same for a given pump strength in order to solve the equations with the same normalization
# conditions.
function LinearAlgebra.normalize!(lsol::LasingSol)
    for m = lsol.m_active
        ψ = lsol.ψ[m]
        iₐ = argmax(abs2, ψ)
        lsol.iₐ[m] = iₐ

        lsol.a²[m] *= abs2(ψ[iₐ])
        ψ ./= ψ[iₐ]  # make ψ[iₐ] = 1
    end
end

# Lasing equation variables that are reduced from all lasing modes
mutable struct LasingReducedVar{VF<:AbsVecFloat}  # VF can be PETSc vector with N entries
    D::Vector{VF}  # length-K vector: each entry is N-vector storing population inversion, where K is number of atomic classes
    D′::Vector{VF}  # length-K vector: each entry is N-vector storing derivative of population inversion with respect to hole-burning term
    function LasingReducedVar{VF}(D::AbsVec{<:AbsVecReal},
                                  D′::AbsVec{<:AbsVecReal}) where {VF<:AbsVecFloat}
        K = length(D)  # number of atomic classes
        length(D′)==K || @error "length(D) = $K and length(D′) = $(length(D′)) should be same"

        if K ≥ 1
            N = length(D[1])
            for k = 1:K
                length(D[k])==length(D′[k])==N ||
                    @error "length(D[$k]) = $(length(D[k])) and length(D′[$k]) = $(length(D′[k])) should be $N."
            end
        end

        return new(D, D′)
    end
end
LasingReducedVar(D::AbsVec{<:AbsVecReal}, D′::AbsVec{<:AbsVecReal}) = LasingReducedVar{eltype(D)}(D, D′)

# To do: check if the following works for vtemp of PETSc vector type.
LasingReducedVar(vtemp::AbsVec, K::Integer) = LasingReducedVar([similar(vtemp,Float).=0 for k=1:K], [similar(vtemp,Float).=0 for k=1:K])  # vtemp has N entries
LasingReducedVar(N::Integer, K::Integer) =  LasingReducedVar(VecFloat(undef,N), K)

# Initialize the variables that are NOT specific to a specific lasing mode.  These variables
# are constructed by summing up the contributions from all lasing modes.
function init_reduced_var!(rvar::LasingReducedVar, lsol::LasingSol, gp::GainProfile)
    # abs2ψ should be updated before calling this function; see init_lvar!.

    hb = lsol.vtemp  # temporary storage for hole-burning term
    for k = 1:length(gp)  # atomic classes
        # Calculate the (1 + hole-burning term) for atomic class k.
        abs2γ = gp.abs2gain[k]  # |γ²| function for atomic class k
        hole_burning!(hb, abs2γ, lsol.ω, lsol.a², lsol.abs2ψ)

        # Calculate the population inversion and the derivative of the population inversion
        # for the class-k atoms.
        #
        # hb is a complex vector storing real, and it is faster to take its real part first
        # and perform the further calculation on it.
        wt = gp.wt[k]  # weight function for atomic class k
        rvar.D[k] .= wt.(gp.D₀) ./ real.(hb)
        rvar.D′[k] .= (-1.0) .* wt.(gp.D₀) ./ abs2.(real.(hb))
    end

    return nothing
end

# Lasing equation variables that are unique to each lasing mode
mutable struct LasingModalVar{MXS<:AbstractMaxwellSolver,VC<:AbsVecComplexF}  # VC can be PETSc vector
    # Consider adding εeff as a field, in case the user wants to solve the "linear" eigenvalue
    # equation for some reason.
    mxsolver::MXS
    ∂f∂ω::VC
end

# To do: check if the following works for vtemp of PETSc vector type.
LasingModalVar(mxsolver_temp::AbstractMaxwellSolver, vtemp::AbsVec) =  # for, e.g., PETSc vector vtemp; vtemp has N entries
    LasingModalVar(similar(mxsolver_temp), similar(vtemp,ComplexF))

LasingModalVar(mxsolver_temp::AbstractMaxwellSolver, N::Integer) =
    LasingModalVar(similar(mxsolver_temp), VecComplexF(undef,N))

# Initialize the variables that are specific to a specific lasing mode.
function init_modal_var!(mvar::LasingModalVar,
                         m::Integer,  # index of lasing mode
                         lsol::LasingSol,
                         D::AbsVec{<:AbsVecFloat},
                         gp::GainProfile,
                         εc::AbsVecComplexF)
    init_modal_var_impl!(mvar.mxsolver, mvar.∂f∂ω, lsol.vtemp, lsol.ω[m], lsol.ψ[m], D, gp, εc)
end

# This is used in nonlasing.jl as well.
function init_modal_var_impl!(mxsolver::AbstractMaxwellSolver,
                              ∂f∂ω::AbsVecComplexF,
                              vtemp::AbsVecComplexF,
                              ω::Number,
                              ψ::AbsVecComplexF,
                              D::AbsVec{<:AbsVecFloat},
                              gp::GainProfile,
                              εc::AbsVecComplexF)
    K = length(gp)  # number of atomic classes
    γ = gp.gain  # K-vector whose kth entry is γ function of atomic class k
    γ′ = gp.gain′  # K-vector whose kth entry is γ′ function of atomic class k

    # Calculate the effective permittivity.
    εeff = vtemp  # temporary storage for effective permitivity: εc + ∑ₖ(γₖ Dₖ)
    εeff .= εc
    for k = 1:K
        εeff .+= γ[k](ω) .* D[k]
    end

    init_mxsolver!(mxsolver, ω, εeff)

    # Calculate ∂f∂ω: derivative of SALT function w.r.t. ω, ignoring ω-dependence of D.
    ∂f∂ω .= 2ω .* εeff
    for k = 1:K
        σ = ω^2 * γ′[k](ω)  # scalar
        ∂f∂ω .+= σ .* D[k]
    end
    ∂f∂ω .*= ψ

    return nothing
end

mutable struct LasingConstraint
    A::MatFloat  # constraint matrix
    b::VecFloat  # constraint vector
    ∆ωa²::VecFloat  # vector of calculated ∆ω and ∆a²: ∆ω[m] = ∆ωa²[2m-1] and ∆a²[m] = ∆ωa²[2m]
    LasingConstraint(M::Integer) = new(MatFloat(undef,2M,2M), VecFloat(undef,2M), VecFloat(undef,2M))
end

# Components necessary for fixed-point iteration
mutable struct LasingVar{MXS<:AbstractMaxwellSolver,VF<:AbsVecFloat,VC<:AbsVecComplexF}
    rvar::LasingReducedVar{VF}
    mvar_vec::Vector{LasingModalVar{MXS,VC}}
    cst::LasingConstraint
    inited::Bool
end
LasingVar(mxsolver_temp::AbstractMaxwellSolver, vtemp::AbsVec, K::Integer, M::Integer) =  # K: number of atomic classes, M: number of modes
    LasingVar(LasingReducedVar(vtemp, K), [LasingModalVar(mxsolver_temp, vtemp) for m = 1:M], LasingConstraint(M), false)
LasingVar(mxsolver_temp::AbstractMaxwellSolver, N::Integer, K::Integer, M::Integer) = LasingVar(mxsolver_temp, VecFloat(undef,N), K, M)

function init_lvar!(lvar::LasingVar, lsol::LasingSol, gp::GainProfile, εc::AbsVecComplexF)
    rvar = lvar.rvar
    mvar_vec = lvar.mvar_vec

    # Update abs2ψ.
    for m = lsol.m_active
        abs2_interp!(lsol.abs2ψ[m], mvar_vec[m].mxsolver, lsol.ψ[m])
        # lsol.abs2ψ[m] .= abs2.(lsol.ψ[m])
    end

    init_reduced_var!(rvar, lsol, gp)

    for m = lsol.m_active
        init_modal_var!(mvar_vec[m], m, lsol, rvar.D, gp, εc)
    end

    lvar.inited = true

    return nothing
end

function norm_leq_impl(lsol::LasingSol, mvar_vec::AbsVec{<:LasingModalVar})
    leq = 0.0
    b = lsol.vtemp
    for m = lsol.m_active
        mxsolver = mvar_vec[m].mxsolver
        ψ = lsol.ψ[m]
        mxapply!(b, mxsolver, ψ)  # b = A * ψ
        leq = max(leq, norm(b))  # 2-norm for each mode, ∞-norm between modes
    end

    return leq  # return 0.0 if lsol.m_active is empty
end

function norm_leq(lsol::LasingSol, lvar::LasingVar, gp::GainProfile)
    lvar.inited || @error "lvar is uninitialized: call init_lvar!(...) first."

    return norm_leq_impl(lsol, lvar.mvar_vec)
end

# Create the mth constraint equation for ∆ω and ∆a.
function set_constraint!(cst::LasingConstraint,
                         lsol::LasingSol,
                         indₘ::Integer,  # index of m in m_active
                         mvar::LasingModalVar,  # modal variables for mth lasing mode
                         rvar::LasingReducedVar,
                         gp::GainProfile)
    # Retrieve necessary variables for constructing the constraint.
    m = lsol.m_active[indₘ]  # modal index of equation of interest
    L = num_active(lsol)  # number of currently lasing modes
    K = length(gp)  # number of atomic classes

    A = cst.A  # left-hand-side matrix of constraint equation
    b = cst.b  # right-hand-side vector of constraint equation

    ω = lsol.ω  # M-vector whose mth entry is current approximate mth eigenfrequency
    a² = lsol.a²  # M-vector whose mth entry is squared amplitude of current approximate mth eigenmode
    ψ = lsol.ψ  # M-vector whose mth entry is current approximate mth eigenmode
    abs2ψ = lsol.abs2ψ  # M-vector whose mth entry is absolute square of current approximate mth eigenmode

    ωₘ = ω[m]
    ωₘ² = ωₘ^2
    ψₘ = ψ[m]
    iₐₘ = lsol.iₐ[m]  # where mth eigenmode is normalized

    vtemp = lsol.vtemp  # scratch space 1
    wtemp = lsol.wtemp  # scratch space 2

    γ = gp.gain  # K-vector whose kth entry is γ function of atomic class k
    abs2γ = gp.abs2gain  # K-vector whose kth entry is |γ|² function of atomic class k
    abs2γ′ = gp.abs2gain′  # K-vector whose kth entry is ∂|γ|²/∂ω function of atomic class k

    D′ = rvar.D′  # K-vector whose kth entry is derivative of population inversion of class-k atoms w.r.t. hole-burning term

    # Calculate the iₐth row of A⁻¹ and keep it as a column vector
    eᵢₐ = vtemp
    eᵢₐ .= 0
    eᵢₐ[iₐₘ] = 1
    r = wtemp  # storage for row vector
    mxsolve_transpose!(r, mvar.mxsolver, eᵢₐ)

    # Construct A and b.  Note that they are initialized to zero outside the present
    # function, by update_lsol_impl!.

    # Set the mth complex row of right-hand-side vector b of the constraint.
    @assert ψₘ[iₐₘ] ≈ 1
    b[2indₘ-1] = 1.0  # real(ψₘ[iₐₘ])
    b[2indₘ] = 0.0  # imag(ψₘ[iₐₘ])

    # Set the mth complex row of the left-hand-side matrix A of the constraint.
    for indⱼ = 1:L
        j = lsol.m_active[indⱼ]  # jth complex column

        ωⱼ = ω[j]
        a²ⱼ = a²[j]
        abs2ψⱼ = abs2ψ[j]

        # Set the coefficient of ∆ωⱼ.
        vtemp .= 0
        ωₘ²a²ⱼ = ωₘ² * a²ⱼ  # scalar
        for k = 1:K
            σ = (ωₘ²a²ⱼ * abs2γ′[k](ωⱼ)) * γ[k](ωₘ)  # scalar: (real * real) * complex
            vtemp .+= σ .* D′[k]
        end
        vtemp .*= ψₘ .* abs2ψⱼ

        # The mth complex row has an additional contribution in the mth complex column from
        # the ωₘ-dependence outside the ω-dependent D.
        j==m && (vtemp .+= mvar.∂f∂ω)

        mω = BLAS.dotu(r, vtemp)
        A[2indₘ-1, 2indⱼ-1] = real(mω)
        A[2indₘ, 2indⱼ-1] = imag(mω)

        # Set the coefficient of ∆a²ⱼ.
        vtemp .= 0
        for k = 1:K
            σ = (ωₘ² * abs2γ[k](ωⱼ)) * γ[k](ωₘ)  # scalar: (real * real) * complex
            vtemp .+= σ .* D′[k]
        end
        vtemp .*= ψₘ .* abs2ψⱼ

        ma² = BLAS.dotu(r, vtemp)
        A[2indₘ-1, 2indⱼ] = real(ma²)
        A[2indₘ, 2indⱼ] = imag(ma²)
    end

    return nothing
end

# Update ψ for the mth mode.  This function does not update ω and a².
function update_ψₘ!(lsol::LasingSol,
                    ∆ωa²::VecFloat,
                    indₘ::Integer,  # index of m in m_active
                    mvar::LasingModalVar,
                    rvar::LasingReducedVar,
                    gp::GainProfile)
    # Retrieve necessary variables for constructing the constraint.
    m = lsol.m_active[indₘ]  # modal index of equation of interest
    L = num_active(lsol)  # number of currently lasing modes
    K = length(gp)  # number of atomic classes

    ω = lsol.ω  # M-vector whose mth entry is current approximate mth eigenfrequency
    a² = lsol.a²  # M-vector whose mth entry is squared amplitude of current approximate mth eigenmode
    ψ = lsol.ψ  # M-vector whose mth entry is current approximate mth eigenmode
    abs2ψ = lsol.abs2ψ  # M-vector whose mth entry is absolute square of current approximate mth eigenmode

    ωₘ = ω[m]
    ωₘ² = ωₘ^2
    ψₘ = ψ[m]
    iₐₘ = lsol.iₐ[m]  # where mth eigenmode is normalized

    vtemp = lsol.vtemp  # scratch space 1
    wtemp = lsol.wtemp  # scratch space 1

    γ = gp.gain  # K-vector whose kth entry is γ function of atomic class k
    abs2γ = gp.abs2gain  # K-vector whose kth entry is |γ|² function of atomic class k
    abs2γ′ = gp.abs2gain′  # K-vector whose kth entry is ∂|γ|²/∂ω function of atomic class k

    D′ = rvar.D′  # K-vector whose kth entry is derivative of population inversion of class-k atoms w.r.t. hole-burning term

    wtemp .= 0  # initialize for mth row
    for indⱼ = 1:L
        j = lsol.m_active[indⱼ]  # jth complex column

        ωⱼ = ω[j]
        a²ⱼ = a²[j]
        abs2ψⱼ = abs2ψ[j]

        ∆ωⱼ = ∆ωa²[2indⱼ-1]
        ∆a²ⱼ = ∆ωa²[2indⱼ]

        # Add the contribution of ∆ωⱼ.
        vtemp .= 0  # initialize for ∆ωⱼ
        ωₘ²a²ⱼ = ωₘ² * a²ⱼ  # scalar
        for k = 1:K
            σ = (∆ωⱼ * ωₘ²a²ⱼ * abs2γ′[k](ωⱼ)) * γ[k](ωₘ)  # scalar: (real * real * real) * complex
            vtemp .+= σ .* D′[k]
        end
        vtemp .*= abs2ψⱼ  # ψₘ will be multiplied outside for loop, because it is common for both ∆ω and ∆a²
        wtemp .+= vtemp

        # Add the contribution of ∆a²ⱼ.
        vtemp .= 0  # initialize for ∆a²ⱼ
        for k = 1:K
            σ = (∆a²ⱼ * ωₘ² * abs2γ[k](ωⱼ)) * γ[k](ωₘ)  # scalar: (real * real * real) * complex
            vtemp .+= σ .* D′[k]
        end
        vtemp .*= abs2ψⱼ  # ψₘ will be multiplied outside for loop, because it is common for both ∆ω and ∆a²
        wtemp .+= vtemp
    end
    wtemp .*= ψₘ

    # The mth complex row has an additional contribution from the ωₘ-dependence outside the
    # ω-dependent D.
    ∆ωₘ = ∆ωa²[2indₘ-1]
    wtemp .+= ∆ωₘ .* mvar.∂f∂ω

    # @info "‖A‖₁ = $(opnorm(mvar.A,1)), ‖vtemp‖ = $(norm(vtemp))"
    mxsolve!(ψₘ, mvar.mxsolver, wtemp)  # ψₘ .= mvar.mxsolver \ wtemp

    # Normalize ψₘ just in case ψₘ[iₐₘ] ≠ 1.
    # ψₘ[iₐₘ]≈1 || @warn "lasing mode m = $m is slightly nonnormal: |ψₘ[iₐₘ]-1| = $(abs(ψₘ[iₐₘ]-1)).  The mode will be renormalized."
    # @info "ψₘ[iₐₘ] = $(ψₘ[iₐₘ])"
    ψₘ ./= ψₘ[iₐₘ]

    return nothing
end

# lvar should be already initialized by init_lvar! before starting the fixed-point iteration.
function update_lsol!(lsol::LasingSol, lvar::LasingVar, gp::GainProfile)
    lvar.inited || @error "lvar is uninitialized: call init_lvar!(...) first."
    update_lsol_impl!(lsol, lvar.mvar_vec, lvar.rvar, lvar.cst, gp)
    lvar.inited = false

    return nothing
end

# Fixed-point equation for lsol.  Calculate ∆ω and ∆a, and then ∆ψ, and move lsol by them.
#
# This function is expensive to call, because it involves 2Mₗ linear solves, where Mₗ is the
# number of lasing modes.  Therefore, try not to call this function unnecessarily.
#
# init_lvar! should be called before using this function to make lvar prepared.  However,
# init_lvar! is not exported in order to force checking the norm by norm_leq to avoid
# calling this function when the norm is small enough.
function update_lsol_impl!(lsol::LasingSol,
                           mvar_vec::AbsVec{<:LasingModalVar},  # should be already initialized
                           rvar::LasingReducedVar,  # should be already initialized
                           cst::LasingConstraint,
                           gp::GainProfile)
    # Construct the constraint equation on ∆ω and ∆a.
    L = num_active(lsol)

    A = cst.A
    b = cst.b
    ∆ωa² = cst.∆ωa²

    A .= 0
    b .= 0
    ∆ωa² .= 0

    # Below, we iterate over the contiguous indices indₘ to retrieve the currently lasing
    # mode index m from m_active, instead of directly iterating over m_active.  This is
    # because we cannot use a submatrix @view(A[ind,ind]) in ldiv! when ind is not a
    # contiguous range of indices.  See https://github.com/JuliaLang/julia/issues/30097.
    for indₘ = 1:L
        m = lsol.m_active[indₘ]
        set_constraint!(cst, lsol, indₘ, mvar_vec[m], rvar, gp)
    end

    # Calculate ∆ω and ∆a.
    # @info "A = $(A[1:2L,1:2L]), b = $(b[1:2L]), ∆ωa² = $(∆ωa²[1:2L])"
    LU = lu!(@view(A[1:2L,1:2L]))
    ldiv!(@view(∆ωa²[1:2L]), LU, @view(b[1:2L]))

    # Update ψ.
    # @info "lsol.ω = $(lsol.ω), lsol.a² = $(lsol.a²), lsol.m_active = $(lsol.m_active)"
    for indₘ = 1:L
        m = lsol.m_active[indₘ]
        # @info "before apply: ‖lsol.ψ[$m]‖ = $(norm(lsol.ψ[m]))"
        update_ψₘ!(lsol, ∆ωa², indₘ, mvar_vec[m], rvar, gp)
        # @info "after apply: ‖lsol.ψ[$m]‖ = $(norm(lsol.ψ[m]))"
    end

    # Update ω and a².
    #
    # abs2ψ is not updated here.  In each iteration, the Anderson acceleration evaluates the
    # fixed-point function g once to get g(xₖ), but this g(xₖ) is not the new solution xₖ₊₁.
    # xₖ₊₁ is formed as an affine combinination of g(xₖ), ..., g(xₖ₋ₘ).  Therefore,
    # abs2ψ evaluated at the end of the evaluation of the fixed point function g is abs2ψ of
    # g(xₖ), not the abs2ψ of xₖ₊₁.  Therefore, in order to calculate abs2ψ for xₖ₊₁, which
    # is used inside g for evaluating g(xₖ₊₁), abs2ψ should be updated at the beginning of the
    # fixed-point function (where abs2ψ is used for the first time in that iteration step)
    # rather than at the end.  This is inside init_lvar!, whose evaluation in checked at the
    # beginning of update_lsol!.
    #
    # The following loop should not be merged with the loop above, because update_ψₘ! uses
    # un-updated ω and a².
    for indₘ = 1:L
        m = lsol.m_active[indₘ]

        ∆ωₘ = ∆ωa²[2indₘ-1]
        ∆a²ₘ = ∆ωa²[2indₘ]

        lsol.ω[m] += ∆ωₘ
        lsol.a²[m] += ∆a²ₘ
    end

    return nothing
end


using MathOptInterface
const MOI = MathOptInterface
const MOIU = MOI.Utilities
const AFFEQ = MOI.ConstraintIndex{MOI.ScalarAffineFunction{Cdouble}, MOI.EqualTo{Cdouble}}

mutable struct Optimizer <: MOI.AbstractOptimizer
    objconstant::Cdouble
    objsign::Int
    blockdims::Vector{CSDP_INT}
    varmap::Vector{Tuple{Int, Int, Int}} # Variable Index vi -> blk, i, j
    num_entries::Dict{Tuple{Int, Int}, Int}
    b::Vector{Cdouble}
    C::blockmatrix
    problem::Union{Nothing, LoadingProblem}
    X::blockmatrix
    y::Union{Nothing, Vector{Cdouble}}
    Z::blockmatrix
    status::CSDP_INT
    pobj::Cdouble
    dobj::Cdouble
    solve_time::Float64
    silent::Bool
    options::Dict{Symbol, Any}
    function Optimizer(; kwargs...)
        optimizer = new(
            zero(Cdouble), 1, CSDP_INT[], Tuple{Int, Int, Int}[],
            Dict{Tuple{Int, Int}, Int}(), Cdouble[],
            blockmatrix(), nothing, blockmatrix(), nothing, blockmatrix(),
            -1, NaN, NaN, NaN, false, Dict{Symbol, Any}())
        for (key, value) in kwargs
            MOI.set(optimizer, MOI.RawParameter(String(key)), value)
        end
        # May need to call `free_loaded_prob` and `free_loading_prob`.
        finalizer(MOI.empty!, optimizer)
        return optimizer
    end
end

varmap(optimizer::Optimizer, vi::MOI.VariableIndex) = optimizer.varmap[vi.value]

function MOI.supports(optimizer::Optimizer, param::MOI.RawParameter)
    return Symbol(param.name) in ALLOWED_OPTIONS
end
function MOI.set(optimizer::Optimizer, param::MOI.RawParameter, value)
    if !(param.name isa String)
        Base.depwarn(
            "passing `$(param.name)` to `MOI.RawParameter` as type " *
            "`$(typeof(param.name))` is deprecated. Use a string instead.",
            Symbol("MOI.set")
        )
    end
    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.
    if !(param.name isa String)
        Base.depwarn(
            "passing `$(param.name)` to `MOI.RawParameter` as type " *
            "`$(typeof(param.name))` is deprecated. Use a string instead.",
            Symbol("MOI.set")
        )
    end
    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) = "CSDP"
# See table "Return codes for easy_sdp() and CSDP" in `doc/csdpuser.pdf`.
const RAW_STATUS = [
    "Problem solved to optimality.",
    "Problem is primal infeasible.",
    "Problem is dual infeasible.",
    "Problem solved to near optimality.",
    "Maximum iterations reached.",
    "Stuck at edge of primal feasibility.",
    "Stuck at edge of dual feasibility.",
    "Lack of progress.",
    "X, Z, or O is singular.",
    "NaN or Inf values encountered.",
    "Program stopped by signal (SIXCPU, SIGTERM, etc.)"]

function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString)
    return RAW_STATUS[optimizer.status + 1]
end
function MOI.get(optimizer::Optimizer, ::MOI.SolveTime)
    return optimizer.solve_time
end

function MOI.is_empty(optimizer::Optimizer)
    return iszero(optimizer.objconstant) &&
        isone(optimizer.objsign) &&
        isempty(optimizer.blockdims) &&
        isempty(optimizer.varmap) &&
        isempty(optimizer.num_entries) &&
        isempty(optimizer.b) &&
        iszero(optimizer.C.nblocks) &&
        optimizer.C.blocks == C_NULL &&
        optimizer.problem === nothing
end

function MOI.empty!(optimizer::Optimizer)
    optimizer.objconstant = zero(Cdouble)
    optimizer.objsign = 1
    empty!(optimizer.blockdims)
    empty!(optimizer.varmap)
    empty!(optimizer.num_entries)
    empty!(optimizer.b)
    if optimizer.problem !== nothing
        if optimizer.y !== nothing
            free_loaded_prob(optimizer.problem, optimizer.X, optimizer.y, optimizer.Z)
        end
        free_loading_prob(optimizer.problem)
    end
    optimizer.problem = nothing
    optimizer.C.nblocks = 0
    optimizer.C.blocks = C_NULL
    optimizer.X.nblocks = 0
    optimizer.X.blocks = C_NULL
    optimizer.y = nothing
    optimizer.Z.nblocks = 0
    optimizer.Z.blocks = C_NULL
    optimizer.status = -1
    optimizer.pobj = 0.0
    optimizer.dobj = 0.0
end

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

function MOI.supports_constraint(
    ::Optimizer, ::Type{MOI.VectorOfVariables}, ::Type{MOI.Reals})
    return false
end
const SupportedSets = Union{MOI.Nonnegatives, MOI.PositiveSemidefiniteConeTriangle}
function MOI.supports_constraint(
    ::Optimizer, ::Type{MOI.VectorOfVariables},
    ::Type{<:SupportedSets})
    return true
end
function MOI.supports_constraint(
    ::Optimizer, ::Type{MOI.ScalarAffineFunction{Cdouble}},
    ::Type{MOI.EqualTo{Cdouble}})
    return true
end

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

function MOIU.allocate(optimizer::Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense)
    # To be sure that it is done before load(optimizer, ::ObjectiveFunction, ...), we do it in allocate
    optimizer.objsign = sense == MOI.MIN_SENSE ? -1 : 1
end
function MOIU.allocate(::Optimizer, ::MOI.ObjectiveFunction, ::MOI.ScalarAffineFunction) end

function MOIU.load(::Optimizer, ::MOI.ObjectiveSense, ::MOI.OptimizationSense) end
# Loads objective coefficient α * vi
function load_objective_term!(optimizer::Optimizer, α, vi::MOI.VariableIndex)
    blk, i, j = varmap(optimizer, vi)
    # in SDP format, it is max and in MPB Conic format it is min
    coef = optimizer.objsign * α
    if i != j
        coef /= 2
    end
    addentry(optimizer.problem, 0, blk, i, j, coef, true)
end
function MOIU.load(optimizer::Optimizer, ::MOI.ObjectiveFunction, f::MOI.ScalarAffineFunction)
    obj = MOIU.canonical(f)
    optimizer.objconstant = f.constant
    for t in obj.terms
        if !iszero(t.coefficient)
            load_objective_term!(optimizer, t.coefficient, t.variable_index)
        end
    end
end

function new_block(optimizer::Optimizer, set::MOI.Nonnegatives)
    push!(optimizer.blockdims, -MOI.dimension(set))
    blk = length(optimizer.blockdims)
    for i in 1:MOI.dimension(set)
        push!(optimizer.varmap, (blk, i, i))
    end
end

function new_block(optimizer::Optimizer, set::MOI.PositiveSemidefiniteConeTriangle)
    push!(optimizer.blockdims, set.side_dimension)
    blk = length(optimizer.blockdims)
    for i in 1:set.side_dimension
        for j in 1:i
            push!(optimizer.varmap, (blk, i, j))
        end
    end
end

function MOIU.allocate_constrained_variables(optimizer::Optimizer,
                                             set::SupportedSets)
    offset = length(optimizer.varmap)
    new_block(optimizer, set)
    ci = MOI.ConstraintIndex{MOI.VectorOfVariables, typeof(set)}(offset + 1)
    return [MOI.VariableIndex(i) for i in offset .+ (1:MOI.dimension(set))], ci
end

function MOIU.load_constrained_variables(
    optimizer::Optimizer, vis::Vector{MOI.VariableIndex},
    ci::MOI.ConstraintIndex{MOI.VectorOfVariables},
    set::SupportedSets)
end

function MOIU.load_variables(optimizer::Optimizer, nvars)
    @assert nvars == length(optimizer.varmap)
    dummy = isempty(optimizer.b)
    if dummy
        # See https://github.com/coin-or/Csdp/issues/2
        optimizer.b = [one(Cdouble)]
        optimizer.blockdims = [optimizer.blockdims; CSDP_INT(-1)]
        count_entry(optimizer, 1, length(optimizer.blockdims))
    end
    optimizer.C.nblocks = length(optimizer.blockdims)
    num_entries = zeros(CSDP_INT, length(optimizer.b), length(optimizer.blockdims))
    for (key, value) in optimizer.num_entries
        num_entries[key...] = value
    end
    optimizer.problem = allocate_loading_prob(Ref(optimizer.C), optimizer.blockdims, length(optimizer.b), num_entries, 3)
    if dummy
        # See https://github.com/coin-or/Csdp/issues/2
        duplicate = addentry(optimizer.problem, 1, length(optimizer.blockdims), 1, 1, 1.0, true)
        @assert !duplicate
    end

end

function count_entry(optimizer::Optimizer, con_idx::Integer, blk::Integer)
    key = (con_idx, blk)
    optimizer.num_entries[key] = get(optimizer.num_entries, key, 0) + 1
end

function MOIU.allocate_constraint(optimizer::Optimizer,
                                  func::MOI.ScalarAffineFunction{Cdouble},
                                  set::MOI.EqualTo{Cdouble})
    if !iszero(MOI.constant(func))
        throw(MOI.ScalarFunctionConstantNotZero{
            Cdouble, MOI.ScalarAffineFunction{Cdouble}, MOI.EqualTo{Cdouble}}(
                MOI.constant(func)))
    end
    push!(optimizer.b, MOI.constant(set))
    func = MOIU.canonical(func) # sum terms with same variables and same output_index
    for t in func.terms
        if !iszero(t.coefficient)
            blk, i, j = varmap(optimizer, t.variable_index)
            count_entry(optimizer, length(optimizer.b), blk)
        end
    end
    return AFFEQ(length(optimizer.b))
end

function MOIU.load_constraint(optimizer::Optimizer, ci::AFFEQ,
                              f::MOI.ScalarAffineFunction, s::MOI.EqualTo)
    if !iszero(MOI.constant(f))
        throw(MOI.ScalarFunctionConstantNotZero{
            Cdouble, MOI.ScalarAffineFunction{Cdouble}, MOI.EqualTo{Cdouble}}(
                MOI.constant(f)))
    end
    setconstant(optimizer.problem, ci.value, MOI.constant(s))
    f = MOIU.canonical(f) # sum terms with same variables and same output_index
    if isempty(f.terms)
        throw(ArgumentError("Empty constraint $ci: $f-in-$s. Not supported by CSDP."))
    end
    for t in f.terms
        if !iszero(t.coefficient)
            blk, i, j = varmap(optimizer, t.variable_index)
            coef = t.coefficient
            if i != j
                coef /= 2
            end
            duplicate = addentry(optimizer.problem, ci.value, blk, i, j, coef, true)
            @assert !duplicate
        end
    end
end

function MOI.optimize!(optimizer::Optimizer)
    write_prob(optimizer)

    start_time = time()
    optimizer.y = loaded_initsoln(optimizer.problem, length(optimizer.b), Ref(optimizer.X), Ref(optimizer.Z))

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

    optimizer.status, optimizer.pobj, optimizer.dobj = loaded_sdp(
        optimizer.problem, Ref(optimizer.X), optimizer.y,
        Ref(optimizer.Z), options)
    optimizer.solve_time = time() - start_time
end

function MOI.get(m::Optimizer, ::MOI.TerminationStatus)
    status = m.status
    if status == -1
        return MOI.OPTIMIZE_NOT_CALLED
    elseif status == 0
        return MOI.OPTIMAL
    elseif status == 1
        return MOI.INFEASIBLE
    elseif status == 2
        return MOI.DUAL_INFEASIBLE
    elseif status == 3
        return MOI.ALMOST_OPTIMAL
    elseif status == 4
        return MOI.ITERATION_LIMIT
    elseif 5 <= status <= 7
        return MOI.SLOW_PROGRESS
    elseif 8 <= status <= 9
        return MOI.NUMERICAL_ERROR
    else
        error("Internal library error: status=$status")
    end
end

function MOI.get(m::Optimizer, attr::MOI.PrimalStatus)
    if attr.N > MOI.get(m, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    status = m.status
    if status == 0
        return MOI.FEASIBLE_POINT
    elseif status == 1
        return MOI.INFEASIBLE_POINT
    elseif status == 2
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == 3
        return MOI.NEARLY_FEASIBLE_POINT
    elseif 4 <= status <= 9
        return MOI.UNKNOWN_RESULT_STATUS
    else
        error("Internal library error: status=$status")
    end
end

function MOI.get(m::Optimizer, attr::MOI.DualStatus)
    if attr.N > MOI.get(m, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    status = m.status
    if status == 0
        return MOI.FEASIBLE_POINT
    elseif status == 1
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == 2
        return MOI.INFEASIBLE_POINT
    elseif status == 3
        return MOI.NEARLY_FEASIBLE_POINT
    elseif 4 <= status <= 9
        return MOI.UNKNOWN_RESULT_STATUS
    else
        error("Internal library error: status=$status")
    end
end

MOI.get(m::Optimizer, ::MOI.ResultCount) = 1
function MOI.get(m::Optimizer, attr::MOI.ObjectiveValue)
    MOI.check_result_index_bounds(m, attr)
    return m.objsign * m.pobj + m.objconstant
end
function MOI.get(m::Optimizer, attr::MOI.DualObjectiveValue)
    MOI.check_result_index_bounds(m, attr)
    return m.objsign * m.dobj + m.objconstant
end
struct PrimalSolutionMatrix <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::PrimalSolutionMatrix) = true
MOI.get(optimizer::Optimizer, ::PrimalSolutionMatrix) = optimizer.X

struct DualSolutionVector <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::DualSolutionVector) = true
MOI.get(optimizer::Optimizer, ::DualSolutionVector) = optimizer.y

struct DualSlackMatrix <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::DualSlackMatrix) = true
MOI.get(optimizer::Optimizer, ::DualSlackMatrix) = optimizer.Z

function block(optimizer::Optimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables})
    return optimizer.varmap[ci.value][1]
end
function dimension(optimizer::Optimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables})
    blockdim = optimizer.blockdims[block(optimizer, ci)]
    if blockdim < 0
        return -blockdim
    else
        return MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(blockdim))
    end
end
function vectorize_block(M, blk::Integer, s::Type{MOI.Nonnegatives})
    return diag(block(M, blk))
end
function vectorize_block(M::AbstractMatrix{Cdouble}, blk::Integer, s::Type{MOI.PositiveSemidefiniteConeTriangle}) where T
    B = block(M, blk)
    d = LinearAlgebra.checksquare(B)
    n = MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(d))
    v = Vector{Cdouble}(undef, n)
    k = 0
    for j in 1:d
        for i in 1:j
            k += 1
            v[k] = B[i, j]
        end
    end
    @assert k == n
    return v
end

function MOI.get(optimizer::Optimizer, attr::MOI.VariablePrimal, vi::MOI.VariableIndex)
    MOI.check_result_index_bounds(optimizer, attr)
    blk, i, j = varmap(optimizer, vi)
    return block(MOI.get(optimizer, PrimalSolutionMatrix()), blk)[i, j]
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)
    return vectorize_block(MOI.get(optimizer, PrimalSolutionMatrix()), block(optimizer, ci), S)
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.ConstraintDual,
                 ci::MOI.ConstraintIndex{MOI.VectorOfVariables, S}) where S<:SupportedSets
    MOI.check_result_index_bounds(optimizer, attr)
    return vectorize_block(MOI.get(optimizer, DualSlackMatrix()), block(optimizer, ci), S)
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintDual, ci::AFFEQ)
    MOI.check_result_index_bounds(optimizer, attr)
    return -MOI.get(optimizer, DualSolutionVector())[ci.value]
end


#predevelopment and consenting

function OMCost!(options::zefiroOptions,sizes::zefiroSizes,data::zefiroData,output::zefiroOutput)
    key = (options.postprocess ? "postprocessing" : "evaluating")
    println(key*" operation cost")
    operation!(options,sizes,data,output)
    println(key*" maintenance cost")
    maintenance!(options,sizes,data,output)
    nothing

end

function operation!(options::zefiroOptions,sizes::zefiroSizes,data::zefiroData,output::zefiroOutput)

    if options.operation == 0
        if options.postprocess
            nothing
        else
            Crent = (data.l .* data.Pe) .* output.AEP * data.duration .* transpose(data.turbineQuantity)
            CinsO = zeros(sizes.winds,sizes.turbines)
            CtransO = zeros(sizes.winds,sizes.turbines)
            for i in 1:sizes.winds,j in 1:sizes.turbines
                CinsO[i,j] += data.ComIns[i] * data.IC[j]
                CtransO[i,j] += data.Ctransunit[i] * data.IC[j]
            end
            output.OPEX += Crent + CinsO + CtransO
            output.operation += Crent + CinsO + CtransO
        end
    else
        error("operation mode not found.")
    end
    nothing

end

function maintenance!(options::zefiroOptions,sizes::zefiroSizes,data::zefiroData,output::zefiroOutput)

    if options.maintenance == 0
        if options.postprocess
            nothing
        else
            Ccm = zeros(sizes.winds,sizes.turbines)
            Cmdir = zeros(sizes.winds,sizes.turbines)
            Cmind = zeros(sizes.winds,sizes.turbines)

            Ctrans = 2*(data.d .* data.tc)
            Clab = data.N1dOM .* data.LrOM
            Ccm .+= Ctrans .+ transpose(Clab) .+ data.consumCost

            for i in 1:sizes.winds,j in 1:sizes.turbines
                Cmdir[i,j] = (1-data.Pd[j])*data.lambda[j]*Ccm[i,j] + data.Pd[j]*data.Csm[j]
            end

            Cmind .+= (data.Cindport + data.Cindves + data.Cindlab).*transpose(data.turbineQuantity)

            output.OPEX += Cmdir + Cmind
            output.maintenance += Cmdir + Cmind
        end
    else
        error("maintenance mode not found.")
    end
    nothing

end


function remove_tasks(gamma::Float64, LS::LoadingSequence, CTS::Constants)
    new_LS = deepcopy(LS)
    mod_gamma = Int(floor(CTS.P*gamma))
    if new_LS.order[mod_gamma+1].task.p == 0 && new_LS.order[mod_gamma+1].task.c == 0
        mod_gamma -= 1
    end
    for t = 1:mod_gamma
        if new_LS.order[end].task.p != 0 && new_LS.order[end].task.c != 0
            new_LS.len -= 1
            new_LS.tasks_left += 1
        end
        deleteat!(new_LS.filled_pos, findall(x->x==new_LS.order[length(new_LS.order)].task.p, new_LS.filled_pos))
        deleteat!(new_LS.loaded_cont, findall(x->x==new_LS.order[length(new_LS.order)].task.c, new_LS.loaded_cont))
        deleteat!(new_LS.order, length(new_LS.order))
    end
    return(new_LS)
end

function last_qc_moves(LS::LoadingSequence, CTS::Constants)
    QC_MOVES=Dict{Int, Array{NamedTuple{(:bay, :end_time, :start_time, :time),Tuple{Int64,Int64,Int64,Int64}}, 1}}()
    min_q = (q=0, end_time=CTS.H, start_time=CTS.H, time=CTS.H)

    flag = Array{Int,1}()
    while Set(collect(1:CTS.Q)) != Set(flag)
        for t in reverse(LS.order)
            for q = 1:CTS.Q
                if t.qc == q && !(q in flag)
                    push!(flag, q)
                    if t.start_time + 2*t.task.t < min_q.end_time
                        min_q = (q=q, end_time=t.start_time + 2*t.task.t, start_time=t.start_time, time = t.task.t)
                        break
                    end
                end
            end
        end
    end

    flag = Array{Int,1}()
    while Set(collect(1:CTS.Q)) != Set(flag)
        for q = 1:CTS.Q
            l=Array{NamedTuple{(:bay, :end_time, :start_time, :time),Tuple{Int64,Int64,Int64,Int64}}, 1}()
            for t in reverse(LS.order)
                if t.qc == q
                    if t.start_time > min_q.end_time
                        push!(l, (bay=t.task.b, end_time=t.start_time + 2*t.task.t, start_time=t.start_time, time=t.task.t))
                    else
                        push!(l, (bay=t.task.b, end_time=t.start_time + 2*t.task.t, start_time=t.start_time, time=t.task.t))
                        push!(flag, q)
                        break
                    end
                end
            end
            QC_MOVES[q]=reverse(l)
        end
    end
    return(min_q, QC_MOVES)
end

function get_current_state(LS::LoadingSequence, CTS::Constants)
    TIME = init_timer(CTS)
    TIME.available_cranes = Array{Int, 1}()
    QC = init_qc(CTS)
    min_q, QC_MOVES = last_qc_moves(LS, CTS)
    QC_TRAVEL = get_qc_travel(QC_MOVES, CTS)

    TIME.period = min_q.end_time
    TIME.available_cranes = Array{Int, 1}()

    for q = 1:CTS.Q
        QC[q].task_buffer = Array{LTask, 1}()
        #idle crane
        if QC_MOVES[q][end].end_time == TIME.period && length(QC_MOVES[q]) == 1
            QC[q].status = "idle"
            QC[q].time_left = 0
            QC[q].current_bay = QC_MOVES[q][end].bay
            QC[q].next_bay = QC_MOVES[q][end].bay
            push!(TIME.available_cranes, q)
        #loading crane
        elseif TIME.period >= QC_MOVES[q][end].start_time
            QC[q].status = "loading"
            QC[q].time_left = QC_MOVES[q][end].end_time - TIME.period
            QC[q].current_bay = QC_MOVES[q][end].bay
            QC[q].next_bay = QC_MOVES[q][end].bay
        #moving or waiting to move/load crane
        elseif QC_MOVES[q][end-1].end_time <= TIME.period && TIME.period < QC_MOVES[q][end].start_time
            QC[q].next_bay = QC_MOVES[q][end].bay
            push!(QC[q].task_buffer, LTask(0, QC_MOVES[q][end].bay, 0, QC_MOVES[q][end].time))
            if QC_MOVES[q][end-1].bay == QC_MOVES[q][end].bay
                QC[q].status = "waiting to load"
                QC[q].time_left = QC_MOVES[q][end].start_time - TIME.period
                QC[q].current_bay = QC_MOVES[q][end].bay
            elseif QC_TRAVEL[q][1].start_time <= TIME.period
                QC[q].status = "moving"
                QC[q].time_left = QC_TRAVEL[q][1].end_time - TIME.period
                QC[q].current_bay = QC_MOVES[q][end].bay - QC[q].time_left/CTS.tt
            else
                QC[q].status = "waiting to move"
                QC[q].time_left = QC_TRAVEL[q][1].start_time - TIME.period
                QC[q].current_bay = QC_MOVES[q][end-1].bay
            end
        #still loading the previous task
        elseif  QC_MOVES[q][end-1].start_time <= TIME.period
            if length(QC_MOVES[q]) == 2
                QC[q].status = "loading"
                QC[q].time_left = QC_MOVES[q][end-1].end_time - TIME.period
                QC[q].current_bay = QC_MOVES[q][end-1].bay
                QC[q].next_bay = QC_MOVES[q][end].bay
                push!(QC[q].task_buffer, LTask(0, QC_MOVES[q][end].bay, 0, QC_MOVES[q][end].time))
            end
        else
            for i = 1:length(QC_MOVES[q])-1
                #moving or waiting to move/load crane
                if QC_MOVES[q][i].end_time <= TIME.period && TIME.period < QC_MOVES[q][i+1].start_time
                    QC[q].next_bay = QC_MOVES[q][i+1].bay
                    for i = 1:length(QC_MOVES[q])-1
                        push!(QC[q].task_buffer, LTask(0, QC_MOVES[q][i+1].bay, 0, QC_MOVES[q][i+1].time))
                    end
                    if QC_MOVES[q][i].bay == QC_MOVES[q][i+1].bay
                        QC[q].status = "waiting to load"
                        QC[q].time_left = QC_MOVES[q][i+1].start_time - TIME.period
                        QC[q].current_bay = QC_MOVES[q][i+1].bay
                    elseif QC_TRAVEL[q][1].start_time <= TIME.period
                        QC[q].status = "moving"
                        QC[q].time_left = QC_TRAVEL[q][1].end_time - TIME.period
                        QC[q].current_bay = QC_MOVES[q][i+1].bay - QC[q].time_left/CTS.tt
                    else
                        QC[q].status = "waiting to move"
                        QC[q].time_left = QC_TRAVEL[q][1].start_time - TIME.period
                        QC[q].current_bay = QC_MOVES[q][i].bay
                    end
                #still loading the previous task
                elseif  QC_MOVES[q][i].start_time <= TIME.period
                    QC[q].status = "loading"
                    QC[q].time_left = QC_MOVES[q][i].end_time - TIME.period
                    QC[q].current_bay = QC_MOVES[q][i].bay
                    QC[q].next_bay = QC_MOVES[q][i].bay
                    for i = 1:length(QC_MOVES[q])-1
                        push!(QC[q].task_buffer, LTask(0, QC_MOVES[q][i+1].bay, 0, QC_MOVES[q][i+1].time))
                    end
                end
            end
        end
    end
    return(true, TIME, QC)
end

function get_single_moves(QC_MOVES::Dict{Int, Array{NamedTuple{(:bay, :end_time, :start_time, :time),Tuple{Int64,Int64,Int64,Int64}}, 1}}, CTS::Constants)
    single_moves = Array{NamedTuple{(:qc, :bay, :start_time, :time),Tuple{Int64, Int64, Int64, Int64}},1}()
    check = false
    for q = 1:CTS.Q
        for t = 2:length(QC_MOVES[q])-1
            if QC_MOVES[q][t-1].bay < QC_MOVES[q][t].bay && QC_MOVES[q][t].bay > QC_MOVES[q][t+1].bay
                if QC_MOVES[q][t].time != 0
                    push!(single_moves, (qc=q, bay=QC_MOVES[q][t].bay, start_time = QC_MOVES[q][t].start_time, time = QC_MOVES[q][t].time))
                    check = true
                end
            elseif QC_MOVES[q][t-1].bay > QC_MOVES[q][t].bay && QC_MOVES[q][t].bay < QC_MOVES[q][t+1].bay
                if QC_MOVES[q][t].time != 0
                    push!(single_moves, (qc=q, bay=QC_MOVES[q][t].bay, start_time = QC_MOVES[q][t].start_time, time = QC_MOVES[q][t].time))
                    check = true
                end
            end
        end

        if QC_MOVES[q][end-1].bay != QC_MOVES[q][end-2].bay && QC_MOVES[q][end-1].time != 0
            push!(single_moves, (qc=q, bay=QC_MOVES[q][end-1].bay, start_time = QC_MOVES[q][end-1].start_time, time = QC_MOVES[q][end-1].time))
            check = true
        end
        if QC_MOVES[q][1].bay != init_bay(q,CTS) && QC_MOVES[q][1].bay != QC_MOVES[q][2].bay && QC_MOVES[q][2].time != 0
            push!(single_moves, (qc=q, bay=QC_MOVES[q][1].bay, start_time = QC_MOVES[q][1].start_time, time = QC_MOVES[q][1].time))
            check = true
        end
    end

    if check == false
        return(false)
    else
        return(single_moves)
    end
end

function get_single_task(single::NamedTuple{(:qc, :bay, :start_time, :time),Tuple{Int64,Int64,Int64,Int64}}, LS::LoadingSequence, CTS::Constants)
    single_task = (task=LTask(0,0,0,0), start_time=0, qc=0)
    left_flag = false
    right_flag = false
    l = Array{NamedTuple{(:task, :start_time, :qc),Tuple{LTask,Int64,Int64}},1}()
    r = Array{NamedTuple{(:task, :start_time, :qc),Tuple{LTask,Int64,Int64}},1}()
    for t in LS.order
        #single taks
        if t.start_time == single.start_time && t.qc == single.qc && t.task.b == single.bay && t.task.t == single.time
                single_task = t
        end
        if t.start_time < single.start_time && t.task.b == single.bay && t.qc == single.qc
            if left_flag == false
                left_flag = true
            elseif l[end].start_time + 2*l[end].task.t == t.start_time
                deleteat!(l, length(l))
            end
            push!(l, t)
        elseif single.start_time < t.start_time && t.task.b == single.bay && t.qc == single.qc
            if right_flag == false
                right_flag = true
            elseif r[end].start_time + 2*r[end].task.t == t.start_time
                deleteat!(r, length(r))
            end
            push!(r, t)
        end
    end

    return(single_task, left_flag, right_flag, l, r)
end

function trim_left(single::NamedTuple{(:task, :start_time, :qc),Tuple{LTask,Int64,Int64}}, left::NamedTuple{(:task, :start_time, :qc),Tuple{LTask,Int64,Int64}}, LS::LoadingSequence, CTS::Constants)
    LS1 = deepcopy(LS)
    LS2 = deepcopy(LS)
    LS3 = deepcopy(LS)

    #delete single_task
    deleteat!(LS1.filled_pos, findall(x->x==single.task.p, LS1.filled_pos))
    deleteat!(LS1.loaded_cont, findall(x->x==single.task.c, LS1.loaded_cont))
    deleteat!(LS1.order, findall(x->x==single, LS1.order))
    deleteat!(LS2.filled_pos, findall(x->x==single.task.p, LS2.filled_pos))
    deleteat!(LS2.loaded_cont, findall(x->x==single.task.c, LS2.loaded_cont))
    deleteat!(LS2.order, findall(x->x==single, LS2.order))
    deleteat!(LS3.filled_pos, findall(x->x==single.task.p, LS3.filled_pos))
    deleteat!(LS3.loaded_cont, findall(x->x==single.task.c, LS3.loaded_cont))
    deleteat!(LS3.order, findall(x->x==single, LS3.order))

    #trim LS
    for t in LS.order
        if  left.start_time < t.start_time
            deleteat!(LS1.filled_pos, findall(x->x==t.task.p, LS1.filled_pos))
            deleteat!(LS1.loaded_cont, findall(x->x==t.task.c, LS1.loaded_cont))
            deleteat!(LS1.order, findall(x->x==t, LS1.order))
            if single.start_time <= t.start_time
                deleteat!(LS2.filled_pos, findall(x->x==t.task.p, LS2.filled_pos))
                deleteat!(LS2.loaded_cont, findall(x->x==t.task.c, LS2.loaded_cont))
                deleteat!(LS2.order, findall(x->x==t, LS2.order))
            elseif t.start_time <= single.start_time + 2*single.task.t
                deleteat!(LS3.filled_pos, findall(x->x==t.task.p, LS3.filled_pos))
                deleteat!(LS3.loaded_cont, findall(x->x==t.task.c, LS3.loaded_cont))
                deleteat!(LS3.order, findall(x->x==t, LS3.order))
            end
        elseif t.start_time <= left.start_time
            deleteat!(LS2.filled_pos, findall(x->x==t.task.p, LS2.filled_pos))
            deleteat!(LS2.loaded_cont, findall(x->x==t.task.c, LS2.loaded_cont))
            deleteat!(LS2.order, findall(x->x==t, LS2.order))
            deleteat!(LS3.filled_pos, findall(x->x==t.task.p, LS3.filled_pos))
            deleteat!(LS3.loaded_cont, findall(x->x==t.task.c, LS3.loaded_cont))
            deleteat!(LS3.order, findall(x->x==t, LS3.order))
        end
    end

    return(LS1, LS2, LS3)
end

function trim_right(single::NamedTuple{(:task, :start_time, :qc),Tuple{LTask,Int64,Int64}}, right::NamedTuple{(:task, :start_time, :qc),Tuple{LTask,Int64,Int64}}, LS::LoadingSequence, CTS::Constants)
    LS1 = deepcopy(LS)
    LS2 = deepcopy(LS)
    LS3 = deepcopy(LS)

    #delete single_task
    deleteat!(LS1.filled_pos, findall(x->x==single.task.p, LS1.filled_pos))
    deleteat!(LS1.loaded_cont, findall(x->x==single.task.c, LS1.loaded_cont))
    deleteat!(LS1.order, findall(x->x==single, LS1.order))
    deleteat!(LS2.filled_pos, findall(x->x==single.task.p, LS2.filled_pos))
    deleteat!(LS2.loaded_cont, findall(x->x==single.task.c, LS2.loaded_cont))
    deleteat!(LS2.order, findall(x->x==single, LS2.order))
    deleteat!(LS3.filled_pos, findall(x->x==single.task.p, LS3.filled_pos))
    deleteat!(LS3.loaded_cont, findall(x->x==single.task.c, LS3.loaded_cont))
    deleteat!(LS3.order, findall(x->x==single, LS3.order))

    #trim LS
    for t in LS.order
        if  t.start_time >= single.start_time
            deleteat!(LS1.filled_pos, findall(x->x==t.task.p, LS1.filled_pos))
            deleteat!(LS1.loaded_cont, findall(x->x==t.task.c, LS1.loaded_cont))
            deleteat!(LS1.order, findall(x->x==t, LS1.order))
            if t.start_time >= right.start_time + 2*right.task.t
                deleteat!(LS2.filled_pos, findall(x->x==t.task.p, LS2.filled_pos))
                deleteat!(LS2.loaded_cont, findall(x->x==t.task.c, LS2.loaded_cont))
                deleteat!(LS2.order, findall(x->x==t, LS2.order))
            elseif t.start_time <= right.start_time
                deleteat!(LS3.filled_pos, findall(x->x==t.task.p, LS3.filled_pos))
                deleteat!(LS3.loaded_cont, findall(x->x==t.task.c, LS3.loaded_cont))
                deleteat!(LS3.order, findall(x->x==t, LS3.order))
            end
        elseif t.start_time < single.start_time
            deleteat!(LS2.filled_pos, findall(x->x==t.task.p, LS2.filled_pos))
            deleteat!(LS2.loaded_cont, findall(x->x==t.task.c, LS2.loaded_cont))
            deleteat!(LS2.order, findall(x->x==t, LS2.order))
            deleteat!(LS3.filled_pos, findall(x->x==t.task.p, LS3.filled_pos))
            deleteat!(LS3.loaded_cont, findall(x->x==t.task.c, LS3.loaded_cont))
            deleteat!(LS3.order, findall(x->x==t, LS3.order))
        end
    end
    return(LS1, LS2, LS3)
end

function merge_left(single::NamedTuple{(:task, :start_time, :qc),Tuple{LTask,Int64,Int64}}, LS1::LoadingSequence, LS2::LoadingSequence, LS3::LoadingSequence, CTS::Constants)
    #merge single task
    push!(LS1.order, (task=single.task, start_time = get_qc_last_time(single.qc, LS1) + travel_time(get_qc_last_bay(single.qc, LS1), single.task.b, CTS), qc=single.qc))
    push!(LS1.filled_pos, single.task.p)
    push!(LS1.loaded_cont, single.task.c)
    #update and merge LS2
    for t in LS2.order
        if t.qc == single.qc
            nt = (task=t.task, start_time = t.start_time + 2*single.task.t, qc=single.qc)
            push!(LS1.order, nt)
        else
            push!(LS1.order, t)
        end
        push!(LS1.filled_pos, t.task.p)
        push!(LS1.loaded_cont, t.task.c)
    end
    #update and merge LS3
    if length(LS3.order) > 0
    dif = 2*travel_time(get_qc_last_bay(single.qc, LS2), single.task.b, CTS)
        for t in LS3.order
            if t.qc == single.qc
                nt = (task=t.task, start_time = t.start_time - dif, qc=single.qc)
                push!(LS1.order, nt)
            else
                push!(LS1.order, t)
            end
            push!(LS1.filled_pos, t.task.p)
            push!(LS1.loaded_cont, t.task.c)
        end
    end
    return(LS1)
end

function merge_right(single::NamedTuple{(:task, :start_time, :qc),Tuple{LTask,Int64,Int64}}, LS1::LoadingSequence, LS2::LoadingSequence, LS3::LoadingSequence, CTS::Constants)
    #update and merge LS2
    if length(LS1.order) > 0
        dif = 2*travel_time(get_qc_last_bay(single.qc, LS1), single.task.b, CTS) + 2*single.task.t
    else
        dif = 2*travel_time(init_bay(single.qc, CTS), single.task.b, CTS) + 2*single.task.t
    end
    for t in LS2.order
        if t.qc == single.qc
            nt = (task=t.task, start_time = t.start_time - dif, qc=single.qc)
            push!(LS1.order, nt)
        else
            push!(LS1.order, t)
        end
        push!(LS1.filled_pos, t.task.p)
        push!(LS1.loaded_cont, t.task.c)
    end

    #merge single task
    push!(LS1.order, (task=single.task, start_time = get_qc_last_time(single.qc, LS1) + travel_time(get_qc_last_bay(single.qc, LS1), single.task.b, CTS), qc=single.qc))
    push!(LS1.filled_pos, single.task.p)
    push!(LS1.loaded_cont, single.task.c)

    #update and merge LS3
    dif = dif - 2*single.task.t
    for t in LS3.order
        if t.qc == single.qc
            nt = (task=t.task, start_time = t.start_time - dif, qc=single.qc)
            push!(LS1.order, nt)
        else
            push!(LS1.order, t)
        end
        push!(LS1.filled_pos, t.task.p)
        push!(LS1.loaded_cont, t.task.c)
    end
    return(LS1)
end

function remove_single_moves(prec::Dict{Int, Array}, makespan::Int, LS::LoadingSequence, CTS::Constants)
    QC_MOVES = get_qc_moves(LS, CTS)
    single_moves = get_single_moves(QC_MOVES, CTS)
    if single_moves == false
        return(LS)
    else
        for single in single_moves
            if single.qc == LS.order[end].qc
                single_task, left_flag, right_flag, l, r = get_single_task(single, LS, CTS)
                if left_flag == true
                    for l_task in l
                        LS1, LS2, LS3 = trim_left(single_task, l_task, LS, CTS)
                        new_LS = merge_left(single_task, LS1, LS2, LS3, CTS)
                        if check_solution(prec, new_LS, CTS) == true
                            if total_makespan(new_LS, CTS) < makespan
                                return(new_LS)
                            end
                        end
                    end
                end
                if right_flag == true
                    for r_task in r
                        LS1, LS2, LS3 = trim_right(single_task, r_task, LS, CTS)
                        new_LS = merge_right(single_task, LS1, LS2, LS3, CTS)
                        if check_solution(prec, new_LS, CTS) == true
                            if total_makespan(new_LS, CTS) < makespan
                                return(new_LS)
                            end
                        end
                    end
                end
            end
        end
        return(LS)
    end
end

function remove_useless_travel(LS::LoadingSequence)
    if LS.order[end].task.t == 0
        deleteat!(LS.order, length(LS.order))
    end
end

function get_final_state(LS::LoadingSequence, CTS::Constants)
    TIME = init_timer(CTS)
    TIME.available_cranes = Array{Int, 1}()
    QC = init_qc(CTS)
    min_q, moves, QC_MOVES = ls_qc_moves(best_LS, CTS)
    QC_TRAVEL = get_qc_travel(QC_MOVES, CTS)

    TIME.period = min_q.end_time
    TIME.available_cranes = Array{Int, 1}()

    for q = 1:CTS.Q
        QC[q].task_buffer = Array{LTask, 1}()
        #idle crane
        if QC_MOVES[q][1].end_time == TIME.period
            QC[q].status = "idle"
            QC[q].time_left = 0
            QC[q].current_bay = QC_MOVES[q][1].bay
            QC[q].next_bay = QC_MOVES[q][1].bay
            push!(TIME.available_cranes, q)
        #loading crane
        elseif QC_MOVES[q][1].start_time <= TIME.period && TIME.period < QC_MOVES[q][1].end_time
            QC[q].status = "loading"
            QC[q].time_left = QC_MOVES[q][1].end_time - TIME.period
            QC[q].current_bay = QC_MOVES[q][1].bay
            QC[q].next_bay = QC_MOVES[q][1].bay
        else
            QC[q].status = "idle"
            QC[q].time_left = 0
            QC[q].current_bay = QC_MOVES[q][1].bay
            QC[q].next_bay = QC_MOVES[q][1].bay
            push!(TIME.available_cranes, q)
        end
    end
    return(min_q.q, moves, TIME, QC)
end

function remove_final_tasks(moves::Int, LS::LoadingSequence, CTS::Constants)
    new_LS = deepcopy(LS)

    if moves > 2
        while new_LS.len >= LS.len - (moves - 2)
            if new_LS.order[end].task.p != 0 && new_LS.order[end].task.c != 0
                new_LS.len -= 1
                new_LS.tasks_left += 1
            end
            deleteat!(new_LS.filled_pos, findall(x->x==new_LS.order[length(new_LS.order)].task.p, new_LS.filled_pos))
            deleteat!(new_LS.loaded_cont, findall(x->x==new_LS.order[length(new_LS.order)].task.c, new_LS.loaded_cont))
            deleteat!(new_LS.order, length(new_LS.order))
        end
        if new_LS.order[end].task.p == 0 && new_LS.order[end].task.c == 0
            deleteat!(new_LS.order, length(new_LS.order))
        end
    end
    return(new_LS)
end

function ls_qc_moves(LS::LoadingSequence, CTS::Constants)
    QC_MOVES=Dict{Int, Array{NamedTuple{(:bay, :end_time, :start_time, :time),Tuple{Int64,Int64,Int64,Int64}}, 1}}()
    min_q = (q=0, end_time=CTS.H, start_time=CTS.H, time=CTS.H)
    moves = 0
    flag = Array{Int,1}()
    while Set(collect(1:CTS.Q)) != Set(flag)
        for t in reverse(LS.order)
            for q = 1:CTS.Q
                if t.qc == q && !(q in flag) && t.task.t != 0
                    push!(flag, q)
                    if t.start_time + 2*t.task.t < min_q.end_time
                        min_q = (q=q, end_time=t.start_time + 2*t.task.t, start_time=t.start_time, time = t.task.t)
                        break
                    end
                end
            end
        end
    end

    flag = Array{Int,1}()
    while Set(collect(1:CTS.Q)) != Set(flag)
        for q = 1:CTS.Q
            l=Array{NamedTuple{(:bay, :end_time, :start_time, :time),Tuple{Int64,Int64,Int64,Int64}}, 1}()
            for t in reverse(LS.order)
                if t.qc == q
                    if t.start_time > min_q.end_time
                        push!(l, (bay=t.task.b, end_time=t.start_time + 2*t.task.t, start_time=t.start_time, time=t.task.t))
                        if t.task.t != 0
                            moves += 1
                        end
                    elseif t.start_time < min_q.start_time && t.start_time + 2*t.task.t < min_q.start_time
                        push!(l, (bay=t.task.b, end_time=t.start_time, start_time=t.start_time, time=0))
                        push!(flag, q)
                        break
                    else
                        push!(l, (bay=t.task.b, end_time=t.start_time + 2*t.task.t, start_time=t.start_time, time=t.task.t))
                        push!(flag, q)
                        if t.task.t != 0
                            moves += 1
                        end
                        break
                    end
                end
            end
            QC_MOVES[q]=reverse(l)
        end
    end
    return(min_q, moves, QC_MOVES)
end


using Oceananigans
using Oceananigans.Architectures: arch_array
using Oceananigans: AbstractModel
using Oceananigans.Models.HydrostaticFreeSurfaceModels: HydrostaticFreeSurfaceModel

function get_parameter(filename, group, parameter_name, default=nothing)
    parameter = default

    jldopen(filename) do file
        if parameter_name ∈ keys(file["$group"])
            parameter = file["$group/$parameter_name"]
        end
    end

    return parameter
end

"""
    OneDimensionalEnsembleModel(observations::OneDimensionalTimeSeriesBatch; architecture = CPU(), ensemble_size = 50, kwargs...)

Build an Oceananigans `HydrostaticFreeSurfaceModel` with many independent
columns. The model grid is given by the data in `observations`, and the
dynamics in each column is attached to its own `CATKEVerticalDiffusivity` closure stored in
an `(Nx, Ny)` Matrix of closures. `ensemble_size = 50` is the
desired count of ensemble members for calibration with Ensemble Kalman Inversion (EKI), and the
remaining keyword arguments `kwargs` define the default closure across all columns.

In the "many columns" configuration, we run the model on a 3D grid with `(Flat, Flat, Bounded)` boundary
conditions so that many independent columns can be evolved at once with much of the computational overhead
split among the columns. The `Nx` rows of vertical columns are each reserved for an "ensemble" member
whose attached parameter value (updated at each iteration of EKI) sets the diffusivity closure
used to predict the model solution for the `Ny` physical scenarios described by the simulation-specific
`OneDimensionalTimeSeries` objects in `observations`.
"""
function OneDimensionalEnsembleModel(observations::OneDimensionalTimeSeriesBatch;
                       architecture = CPU(),
                       ensemble_size = 50,
                       closure = CATKEVerticalDiffusivity(Float64; warning=false, kwargs...)
                      )

    #
    # Harvest observation file metadata
    #

    datapath = observations.file_path

    α = get_parameter(datapath, "buoyancy", "equation_of_state/α", 2e-4)
    g = get_parameter(datapath, "buoyancy", "gravitational_acceleration", 9.81)
    αg = α * g

    f = get_parameter(datapath, "coriolis", "f", 0.0)

    # Surface fluxes
    Qᵘ = get_parameter(datapath, "parameters", "boundary_condition_u_top", 0.0)
    Qᵛ = get_parameter(datapath, "parameters", "boundary_conditions_v_top", 0.0)
    Qᶿ = get_parameter(datapath, "parameters", "boundary_condition_θ_top", 0.0)
    Qᵇ = Qᶿ * αg

    # Bottom gradients
    dudz_bottom = get_parameter(datapath, "parameters", "boundary_condition_u_bottom", 0.0)
    dθdz_bottom = get_parameter(datapath, "parameters", "boundary_condition_θ_bottom", 0.0)
    dbdz_bottom = dθdz_bottom * αg

    #
    # Build model using metadata
    #

    grid = OneDimensionalEnsembleGrid(observations.grid; size=(ensemble_size, length(observations), data_grid.Nz))

    closure = [closure for i=1:ensemble_size, j=1:length(observations)]

    coriolis = [FPlane(f=td.constants[:f]) for i=1:ensemble_size, td in observations]
    coriolis = arch_array(architecture, coriolis)

    bc_matrix(x) = [x for i = 1:ensemble_size, d in observations]
    Qᵇ = bc_matrix(Qᵇ)
    Qᵘ = bc_matrix(Qᵘ)
    Qᵛ = bc_matrix(Qᵛ)
    dbdz_bottom = bc_matrix(dbdz_bottom)
    dudz_bottom = bc_matrix(dudz_bottom)

    # Convert to CuArray if necessary
    closure = arch_array(architecture, closure)
    Qᵇ = arch_array(architecture, Qᵇ)
    Qᵘ = arch_array(architecture, Qᵘ)
    Qᵛ = arch_array(architecture, Qᵛ)
    dbdz_bottom = arch_array(architecture, dbdz_bottom)
    dudz_bottom = arch_array(architecture, dudz_bottom)

    u_bcs = FieldBoundaryConditions(top = FluxBoundaryCondition(Qᵘ),
                                    bottom = GradientBoundaryCondition(dudz_bottom))
    v_bcs = FieldBoundaryConditions(top = FluxBoundaryCondition(Qᵛ))
    b_bcs = FieldBoundaryConditions(top = FluxBoundaryCondition(Qᵇ),
                                    bottom = GradientBoundaryCondition(dbdz_bottom))

    model = HydrostaticFreeSurfaceModel(architecture = architecture,
                                         grid = grid,
                                         tracers = (:b, :e),
                                         buoyancy = BuoyancyTracer(),
                                         coriolis = coriolis,
                                         boundary_conditions = (b=b_bcs, u=u_bcs, v=v_bcs),
                                         closure = closure)

    return model
end