export PARAMETER_DEFAULT, PARAMETER_UNSTABLE_BUT_FAST, PARAMETER_STABLE_BUT_SLOW

using MathOptInterface
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{Int}
    varmap::Vector{Tuple{Int, Int, Int}} # Variable Index vi -> blk, i, j
    b::Vector{Cdouble}
    problem::Union{Nothing, SDPAProblem}
    solve_time::Float64
    silent::Bool
    options::Dict{Symbol, Any}
    function Optimizer(; kwargs...)
		optimizer = new(
            zero(Cdouble), 1, Int[], Tuple{Int, Int, Int}[], Cdouble[],
            nothing, NaN, false, Dict{Symbol, Any}())
		for (key, value) in kwargs
			MOI.set(optimizer, MOI.RawParameter(key), value)
		end
		return optimizer
    end
end

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

function MOI.supports(optimizer::Optimizer, param::MOI.RawParameter)
	return param.name in keys(SET_PARAM)
end
function MOI.set(optimizer::Optimizer, param::MOI.RawParameter, value)
	if !MOI.supports(optimizer, param)
		throw(MOI.UnsupportedAttribute(param))
	end
	optimizer.options[param.name] = value
end
function MOI.get(optimizer::Optimizer, param::MOI.RawParameter)
	# TODO: This gives a poor error message if the name of the parameter is invalid.
	return optimizer.options[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) = "SDPA"

# See https://www.researchgate.net/publication/247456489_SDPA_SemiDefinite_Programming_Algorithm_User's_Manual_-_Version_600
# "SDPA (SemiDefinite Programming Algorithm) User's Manual — Version 6.00" Section 6.2
const RAW_STATUS = Dict(
    noINFO     => "The iteration has exceeded the maxIteration and stopped with no informationon the primal feasibility and the dual feasibility.",
    pdOPT      => "The normal termination yielding both primal and dual approximate optimal solutions.",
    pFEAS      => "The primal problem got feasible but the iteration has exceeded the maxIteration and stopped.",
    dFEAS      => "The dual problem got feasible but the iteration has exceeded the maxIteration and stopped.",
    pdFEAS     => "Both primal problem and the dual problem got feasible, but the iterationhas exceeded the maxIteration and stopped.",
    pdINF      => "At least one of the primal problem and the dual problem is expected to be infeasible.",
    pFEAS_dINF => "The primal problem has become feasible but the dual problem is expected to be infeasible.",
    pINF_dFEAS => "The dual problem has become feasible but the primal problem is expected to be infeasible.",
    pUNBD      => "The primal problem is expected to be unbounded.",
    dUNBD      => "The dual problem is expected to be unbounded.")

function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString)
    if optimizer.problem === nothing
        return "`MOI.optimize!` not called."
    end
	return RAW_STATUS[getPhaseValue(optimizer.problem)]
end
function MOI.get(optimizer::Optimizer, ::MOI.SolveTime)
	return optimizer.solve_time
end

function MOI.is_empty(optimizer::Optimizer)
    return iszero(optimizer.objconstant) &&
        optimizer.objsign == 1 &&
        isempty(optimizer.blockdims) &&
        isempty(optimizer.varmap) &&
        isempty(optimizer.b)
end
function MOI.empty!(optimizer::Optimizer)
    optimizer.objconstant = zero(Cdouble)
    optimizer.objsign = 1
    empty!(optimizer.blockdims)
    empty!(optimizer.varmap)
    empty!(optimizer.b)
    optimizer.problem = nothing
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)
    coef = optimizer.objsign * α
    if i != j
        coef /= 2
    end
    # in SDP format, it is max and in MPB Conic format it is min
    inputElement(optimizer.problem, 0, blk, i, j, float(coef), false)
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
        optimizer.b = [one(Cdouble)]
        optimizer.blockdims = [optimizer.blockdims; -1]
    end
    optimizer.problem = SDPAProblem()
    setParameterType(optimizer.problem, PARAMETER_DEFAULT)
	# TODO Take `silent` into account here
    setparameters!(optimizer.problem, optimizer.options)
    inputConstraintNumber(optimizer.problem, length(optimizer.b))
    inputBlockNumber(optimizer.problem, length(optimizer.blockdims))
    for (i, blkdim) in enumerate(optimizer.blockdims)
        inputBlockSize(optimizer.problem, i, blkdim)
        inputBlockType(optimizer.problem, i, blkdim < 0 ? LP : SDP)
    end
    initializeUpperTriangleSpace(optimizer.problem)
    for i in eachindex(optimizer.b)
        inputCVec(optimizer.problem, i, optimizer.b[i])
    end
    if dummy
        inputElement(optimizer.problem, 1, length(optimizer.blockdims), 1, 1, one(Cdouble), false)
    end
end

function MOIU.allocate_constraint(optimizer::Optimizer,
                                  func::MOI.ScalarAffineFunction{Cdouble},
                                  set::MOI.EqualTo{Cdouble})
    push!(optimizer.b, MOI.constant(set))
    return AFFEQ(length(optimizer.b))
end

function MOIU.load_constraint(m::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
    f = MOIU.canonical(f) # sum terms with same variables and same outputindex
    for t in f.terms
        if !iszero(t.coefficient)
            blk, i, j = varmap(m, t.variable_index)
            coef = t.coefficient
            if i != j
                coef /= 2
            end
            inputElement(m.problem, ci.value, blk, i, j, float(coef), false)
        end
    end
end

function MOI.optimize!(m::Optimizer)
	start_time = time()
    SDPA.initializeUpperTriangle(m.problem, false)
    SDPA.initializeSolve(m.problem)
    SDPA.solve(m.problem)
    m.solve_time = time() - start_time
end

function MOI.get(m::Optimizer, ::MOI.TerminationStatus)
    if m.problem === nothing
        return MOI.OPTIMIZE_NOT_CALLED
    end
    status = getPhaseValue(m.problem)
    if status == noINFO
        return MOI.ITERATION_LIMIT
    elseif status == pFEAS
        return MOI.SLOW_PROGRESS
    elseif status == dFEAS
        return MOI.SLOW_PROGRESS
    elseif status == pdFEAS
        return MOI.OPTIMAL
    elseif status == pdINF
        return MOI.INFEASIBLE_OR_UNBOUNDED
    elseif status == pFEAS_dINF
        return MOI.DUAL_INFEASIBLE
    elseif status == pINF_dFEAS
        return MOI.INFEASIBLE
    elseif status == pdOPT
        return MOI.OPTIMAL
    elseif status == pUNBD
        return MOI.DUAL_INFEASIBLE
    elseif status == dUNBD
        return MOI.INFEASIBLE
    end
end

function MOI.get(m::Optimizer, attr::MOI.PrimalStatus)
    if attr.N > MOI.get(m, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    status = getPhaseValue(m.problem)
    if status == noINFO
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == pFEAS
        return MOI.FEASIBLE_POINT
    elseif status == dFEAS
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == pdFEAS
        return MOI.FEASIBLE_POINT
    elseif status == pdINF
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == pFEAS_dINF
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == pINF_dFEAS
        return MOI.INFEASIBLE_POINT
    elseif status == pdOPT
        return MOI.FEASIBLE_POINT
    elseif status == pUNBD
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == dUNBD
        return MOI.INFEASIBLE_POINT
    end
end

function MOI.get(m::Optimizer, attr::MOI.DualStatus)
    if attr.N > MOI.get(m, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    status = getPhaseValue(m.problem)
    if status == noINFO
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == pFEAS
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == dFEAS
        return MOI.FEASIBLE_POINT
    elseif status == pdFEAS
        return MOI.FEASIBLE_POINT
    elseif status == pdINF
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == pFEAS_dINF
        return MOI.INFEASIBLE_POINT
    elseif status == pINF_dFEAS
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == pdOPT
        return MOI.FEASIBLE_POINT
    elseif status == pUNBD
        return MOI.INFEASIBLE_POINT
    elseif status == dUNBD
        return MOI.INFEASIBILITY_CERTIFICATE
    end
end

MOI.get(m::Optimizer, ::MOI.ResultCount) = m.problem === nothing ? 0 : 1
function MOI.get(m::Optimizer, attr::MOI.ObjectiveValue)
    MOI.check_result_index_bounds(m, attr)
    return m.objsign * getPrimalObj(m.problem) + m.objconstant
end
function MOI.get(m::Optimizer, attr::MOI.DualObjectiveValue)
    MOI.check_result_index_bounds(m, attr)
    return m.objsign * getDualObj(m.problem) + m.objconstant
end
struct PrimalSolutionMatrix <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::PrimalSolutionMatrix) = true
MOI.get(optimizer::Optimizer, ::PrimalSolutionMatrix) = PrimalSolution(optimizer.problem)

struct DualSolutionVector <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::DualSolutionVector) = true
function MOI.get(optimizer::Optimizer, ::DualSolutionVector)
    return unsafe_wrap(Array, getResultXVec(optimizer.problem), getConstraintNumber(optimizer.problem))
end

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

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(m::Optimizer, attr::MOI.ConstraintPrimal, ci::AFFEQ)
    MOI.check_result_index_bounds(m, attr)
    return m.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


"""
        BoundaryFunction{B, X1, X2}(func, parameters=nothing)

A wrapper for the user-defined boundary condition function `func`, on the
boundary specified by symbol `B` and at location `(X1, X2)`, and with `parameters`.

Example
=======

julia> using Oceananigans, Oceananigans.BoundaryConditions, Oceananigans.Fields

julia> top_tracer_flux = BoundaryFunction{:z, Cell, Cell}((x, y, t) -> cos(2π*x) * cos(t))
(::BoundaryFunction{:z,Cell,Cell,var"#7#8",Nothing}) (generic function with 1 method)

julia> top_tracer_bc = BoundaryCondition(Flux, top_tracer_flux);

julia> flux_func(x, y, t, p) = cos(p.k * x) * cos(p.ω * t); # function with parameters

julia> parameterized_u_velocity_flux = BoundaryFunction{:z, Face, Cell}(flux_func, (k=4π, ω=3.0))
(::BoundaryFunction{:z,Face,Cell,typeof(flux_func),NamedTuple{(:k, :ω),Tuple{Float64,Float64}}}) (generic function with 1 method)

julia> top_u_bc = BoundaryCondition(Flux, parameterized_u_velocity_flux);
"""
struct BoundaryFunction{B, X1, X2, F, P} <: Function
    func :: F
    parameters :: P

    function BoundaryFunction{B, X1, X2}(func, parameters=nothing) where {B, X1, X2}
        B ∈ (:x, :y, :z) || throw(ArgumentError("The boundary B at which the BoundaryFunction is
                                                to be applied must be either :x, :y, or :z."))
        return new{B, X1, X2, typeof(func), typeof(parameters)}(func, parameters)
    end
end

@inline call_boundary_function(func, ξ, η, t, ::Nothing) = func(ξ, η, t)
@inline call_boundary_function(func, ξ, η, t, parameters) = func(ξ, η, t, parameters)

@inline (bc::BoundaryFunction{:x, Y, Z})(j, k, grid, clock, state) where {Y, Z} =
    call_boundary_function(bc.func, ynode(Y, j, grid), znode(Z, k, grid), clock.time, bc.parameters)

@inline (bc::BoundaryFunction{:y, X, Z})(i, k, grid, clock, state) where {X, Z} =
    call_boundary_function(bc.func, xnode(X, i, grid), znode(Z, k, grid), clock.time, bc.parameters)

@inline (bc::BoundaryFunction{:z, X, Y})(i, j, grid, clock, state) where {X, Y} =
    call_boundary_function(bc.func, xnode(X, i, grid), ynode(Y, j, grid), clock.time, bc.parameters)

#####
##### Convenience constructors
#####

""" Returns the location of `loc` on the boundary `:x`, `:y`, or `:z`. """
boundary_location(::Val{:x}, loc) = (loc[2], loc[3])
boundary_location(::Val{:y}, loc) = (loc[1], loc[3])
boundary_location(::Val{:z}, loc) = (loc[1], loc[2])

""" Returns a boundary function at on the boundary `B` at the appropriate
    location for a tracer field. """
TracerBoundaryFunction(B, args...) = BoundaryFunction{B, Cell, Cell}(args...)

""" Returns a boundary function at on the boundary `B` at the appropriate
    location for u, the x-velocity field. """
function UVelocityBoundaryFunction(B, args...)
    loc = boundary_location(Val(B), (Face, Cell, Cell))
    return BoundaryFunction{B, loc[1], loc[2]}(args...)
end

""" Returns a boundary function at on the boundary `B` at the appropriate
    location for v, the y-velocity field. """
function VVelocityBoundaryFunction(B, args...)
    loc = boundary_location(Val(B), (Cell, Face, Cell))
    return BoundaryFunction{B, loc[1], loc[2]}(args...)
end

""" Returns a boundary function at on the boundary `B` at the appropriate
    location for w, the z-velocity field. """
function WVelocityBoundaryFunction(B, args...)
    loc = boundary_location(Val(B), (Cell, Cell, Face))
    return BoundaryFunction{B, loc[1], loc[2]}(args...)
end

"""
    TracerBoundaryCondition(bctype, B, args...)

Returns a `BoundaryCondition` of type `bctype`, that applies the function
`func` to a tracer on the boundary `B`, which is one of `:x, :y, :z`.
The boundary function has the signature

    `func(ξ, η, t)`

where `t` is time, and `ξ` and `η` are coordinates along the
boundary, eg: `(y, z)` for `B = :x`, `(x, z)` for `B = :y`, or
`(x, y)` for `B = :z`.
"""
TracerBoundaryCondition(bctype, B, args...) =
    BoundaryCondition(bctype, TracerBoundaryFunction(B, args...))

"""
    UVelocityBoundaryCondition(bctype, B, args...)

Returns a `BoundaryCondition` of type `bctype`, that applies the function
`func` to `u`, the `x`-velocity field, on the boundary `B`, which is one
of `:x, :y, :z`. The boundary function has the signature

    `func(ξ, η, t)`

where `t` is time, and `ξ` and `η` are coordinates along the
boundary, eg: `(y, z)` for `B = :x`, `(x, z)` for `B = :y`, or
`(x, y)` for `B = :z`.
"""
UVelocityBoundaryCondition(bctype, B, args...) =
    BoundaryCondition(bctype, UVelocityBoundaryFunction(B, args...))

"""
    VVelocityBoundaryCondition(bctype, B, args...)

Returns a `BoundaryCondition` of type `bctype`, that applies the function
`func` to `v`, the `y`-velocity field, on the boundary `B`, which is one
of `:x, :y, :z`. The boundary function has the signature

    `func(ξ, η, t)`

where `t` is time, and `ξ` and `η` are coordinates along the
boundary, eg: `(y, z)` for `B = :x`, `(x, z)` for `B = :y`, or
`(x, y)` for `B = :z`.
"""
VVelocityBoundaryCondition(bctype, B, args...) =
    BoundaryCondition(bctype, VVelocityBoundaryFunction(B, args...))

"""
    VVelocityBoundaryCondition(bctype, B, args...)

Returns a `BoundaryCondition` of type `bctype`, that applies the function
`func` to `w`, the `z`-velocity field, on the boundary `B`, which is one
of `:x, :y, :z`. The boundary function has the signature

    `func(ξ, η, t)`

where `t` is time, and `ξ` and `η` are coordinates along the
boundary, eg: `(y, z)` for `B = :x`, `(x, z)` for `B = :y`, or
`(x, y)` for `B = :z`.
"""
WVelocityBoundaryCondition(bctype, B, args...) =
    BoundaryCondition(bctype, WVelocityBoundaryFunction(B, args...))


function opf_model_add_variables(opfmodel::JuMP.Model, opfdata::OPFData,
                                 modelinfo::ModelInfo,
                                 algparams::AlgParams,
                                 kIdx::Int = 0, tIdx::Int = 0)
    # shortcuts for compactness
    buses = opfdata.buses
    gens  = opfdata.generators
    ngen  = length(gens)
    nbus  = length(buses)
    nline = length(opfdata.lines)
    T = modelinfo.num_time_periods
    K = modelinfo.num_ctgs + 1 # base case counted separately
    @assert T >= 1
    @assert K >= 1
    @assert algparams.mode ∈ [:nondecomposed, :lyapunov_bound] || (tIdx >= 1 && kIdx >= 1)
    @assert algparams.mode ∉ [:nondecomposed, :lyapunov_bound] || (tIdx == 0 && kIdx == 0)
    @assert algparams.mode ∈ [:nondecomposed, :lyapunov_bound] || algparams.decompCtgs || kIdx == 1

    # Variables
    @variable(opfmodel, Pg[1:ngen,1:K,1:T])
    @variable(opfmodel, Qg[1:ngen,1:K,1:T])
    @variable(opfmodel, Vm[1:nbus,1:K,1:T])
    @variable(opfmodel, Va[1:nbus,1:K,1:T])
    @variable(opfmodel, ωt[1:K,1:T])        # frequency
    @variable(opfmodel, St[1:ngen,1:T])     # slack for ramp constraints
    @variable(opfmodel, Zt[1:ngen,1:T])     # slack for ramp constraints to guarantee ADMM converges
    @variable(opfmodel, Sk[1:ngen,1:K,1:T]) # slack for ctgs_link_constr
    @variable(opfmodel, Zk[1:ngen,1:K,1:T]) # slack for ctgs_link_constr to guarantee ADMM converges
    if modelinfo.allow_constr_infeas
        @variable(opfmodel, sigma_real[1:nbus,1:K,1:T], start = 0)
        @variable(opfmodel, sigma_imag[1:nbus,1:K,1:T], start = 0)
        @variable(opfmodel, sigma_lineFr[1:nline,1:K,1:T] >= 0, start = 0)
        @variable(opfmodel, sigma_lineTo[1:nline,1:K,1:T] >= 0, start = 0)
    end

    # Variable bounds and start values
    for i=1:ngen
        set_lower_bound.(Pg[i,:,:], gens[i].Pmin)
        set_upper_bound.(Pg[i,:,:], gens[i].Pmax)
        set_start_value.(Pg[i,:,:], 0.5*(gens[i].Pmax + gens[i].Pmin))
        set_lower_bound.(Qg[i,:,:], gens[i].Qmin)
        set_upper_bound.(Qg[i,:,:], gens[i].Qmax)
        set_start_value.(Qg[i,:,:], 0.5*(gens[i].Qmax + gens[i].Qmin))
    end

    fix.(Va[opfdata.bus_ref,:,:], buses[opfdata.bus_ref].Va)
    for i=1:nbus
        set_lower_bound.(Vm[i,:,:], buses[i].Vmin)
        set_upper_bound.(Vm[i,:,:], buses[i].Vmax)
        set_start_value.(Vm[i,:,:], 0.5*(buses[i].Vmax + buses[i].Vmin))
        set_start_value.(Va[i,:,:], buses[opfdata.bus_ref].Va)
    end
    fix.(Zt[:,1], 0)
    fix.(Zk[:,1,:], 0)
    set_start_value.(Zt, 0)
    set_start_value.(Zk, 0)
    for k=2:K
        set_lower_bound.(ωt[k,:], -1)
        set_upper_bound.(ωt[k,:], +1)
        set_start_value.(ωt[k,:], 0)
    end
    for i=1:ngen
        set_lower_bound.(St[i,:], 0)
        set_upper_bound.(St[i,:], 2gens[i].ramp_agc)
        set_start_value.(St[i,:],  gens[i].ramp_agc)
        set_lower_bound.(Sk[i,:,:], 0)
        set_upper_bound.(Sk[i,:,:], 2gens[i].scen_agc)
        set_start_value.(Sk[i,:,:],  gens[i].scen_agc)
    end
    if algparams.mode ∈ [:nondecomposed, :lyapunov_bound]
        fix.(ωt[1,:], 0)
        fix.(St[:,1], 0; force = true)
        fix.(Sk[:,1,:], 0; force = true)
        if modelinfo.time_link_constr_type == :inequality && algparams.mode == :nondecomposed
            fix.(St, 0; force = true)
        end
    else
        if kIdx > 1 || tIdx == 1
            fix.(St[:,1], 0; force = true)
        end
        if !algparams.decompCtgs || kIdx == 1
            fix.(ωt[1,:], 0)
            fix.(Sk[:,1,:], 0; force = true)
        end
    end
    if modelinfo.ctgs_link_constr_type ∉ [:frequency_equality, :frequency_penalty]
        fix.(ωt, 0; force = true)
    end
    if modelinfo.time_link_constr_type != :penalty
        fix.(Zt, 0; force = true)
    end
    if modelinfo.ctgs_link_constr_type ∈ [:frequency_equality, :frequency_penalty, :preventive_equality, :preventive_penalty] ||
        (modelinfo.ctgs_link_constr_type == :corrective_inequality && (algparams.mode == :nondecomposed || !algparams.decompCtgs))
        fix.(Sk, 0; force = true)
    end
    if modelinfo.ctgs_link_constr_type ∉ [:frequency_penalty, :preventive_penalty, :corrective_penalty]
        fix.(Zk, 0; force = true)
    end
end

function opf_model_add_block_constraints(opfmodel::JuMP.Model, opfdata::OPFData, rawdata::RawData, modelinfo::ModelInfo)
    T = modelinfo.num_time_periods
    K = (modelinfo.num_ctgs + 1)
    if modelinfo.allow_constr_infeas
        @views for t=1:T, k=1:K
            opfdata_c = (k == 1) ? opfdata :
                opf_loaddata(rawdata;
                    lineOff = opfdata.lines[rawdata.ctgs_arr[k - 1]],
                    time_horizon_start = t,
                    time_horizon_end = t,
                    load_scale = modelinfo.load_scale,
                    ramp_scale = modelinfo.ramp_scale,
                    corr_scale = modelinfo.corr_scale)
            opf_model_add_real_power_balance_constraints(opfmodel, opfdata_c, opfmodel[:Pg][:,k,t], opfdata.Pd[:,t], opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], opfmodel[:sigma_real][:,k,t])
            opf_model_add_imag_power_balance_constraints(opfmodel, opfdata_c, opfmodel[:Qg][:,k,t], opfdata.Qd[:,t], opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], opfmodel[:sigma_imag][:,k,t])
            if modelinfo.allow_line_limits
                opf_model_add_line_power_constraints(opfmodel, opfdata_c, opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], opfmodel[:sigma_lineFr][:,k,t], opfmodel[:sigma_lineTo][:,k,t])
            end
        end
    else
        zb = zeros(length(opfdata.buses))
        zl = zeros(length(opfdata.lines))
        @views for t=1:T, k=1:K
            opfdata_c = (k == 1) ? opfdata :
                opf_loaddata(rawdata;
                    lineOff = opfdata.lines[rawdata.ctgs_arr[k - 1]],
                    time_horizon_start = t,
                    time_horizon_end = t,
                    load_scale = modelinfo.load_scale,
                    ramp_scale = modelinfo.ramp_scale,
                    corr_scale = modelinfo.corr_scale)
            opf_model_add_real_power_balance_constraints(opfmodel, opfdata_c, opfmodel[:Pg][:,k,t], opfdata.Pd[:,t], opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], zb)
            opf_model_add_imag_power_balance_constraints(opfmodel, opfdata_c, opfmodel[:Qg][:,k,t], opfdata.Qd[:,t], opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], zb)
            if modelinfo.allow_line_limits
                opf_model_add_line_power_constraints(opfmodel, opfdata_c, opfmodel[:Vm][:,k,t], opfmodel[:Va][:,k,t], zl, zl)
            end
        end
    end
    return nothing
end

function opf_model_add_line_power_constraints(opfmodel::JuMP.Model, opfdata::OPFData, Vm, Va, sigma_lineFr, sigma_lineTo)
    # Network data short-hands
    baseMVA = opfdata.baseMVA
    buses = opfdata.buses
    lines = opfdata.lines
    busIdx = opfdata.BusIdx
    YffR,YffI,YttR,YttI,YftR,YftI,YtfR,YtfI,YshR,YshI = computeAdmitances(lines, buses, baseMVA)

    for l in 1:length(lines)
        line = lines[l]
        from = busIdx[line.from]
        to = busIdx[line.to]
        if line.rateA ≈ 0 || line.rateA >= 1.0e10
            continue
        end
        flowmax=(line.rateA/baseMVA)^2

        #branch apparent power limits (from bus)
        Yff_abs2=YffR[l]^2+YffI[l]^2; Yft_abs2=YftR[l]^2+YftI[l]^2
        Yre=YffR[l]*YftR[l]+YffI[l]*YftI[l]; Yim=-YffR[l]*YftI[l]+YffI[l]*YftR[l]
        @NLconstraint(opfmodel,
            Vm[from]^2 *
            ( Yff_abs2*Vm[from]^2 + Yft_abs2*Vm[to]^2
            + 2*Vm[from]*Vm[to]*(Yre*cos(Va[from]-Va[to])-Yim*sin(Va[from]-Va[to]))
            )
            - flowmax
            - (sigma_lineFr[l]/baseMVA)
            <=0
        )

        #branch apparent power limits (to bus)
        Ytf_abs2=YtfR[l]^2+YtfI[l]^2; Ytt_abs2=YttR[l]^2+YttI[l]^2
        Yre=YtfR[l]*YttR[l]+YtfI[l]*YttI[l]; Yim=-YtfR[l]*YttI[l]+YtfI[l]*YttR[l]
        @NLconstraint(opfmodel,
            Vm[to]^2 *
            ( Ytf_abs2*Vm[from]^2 + Ytt_abs2*Vm[to]^2
            + 2*Vm[from]*Vm[to]*(Yre*cos(Va[from]-Va[to])-Yim*sin(Va[from]-Va[to]))
            )
            - flowmax
            - (sigma_lineTo[l]/baseMVA)
            <=0
        )
    end
end

function opf_model_add_real_power_balance_constraints(opfmodel::JuMP.Model, opfdata::OPFData, Pg, Pd, Vm, Va, sigma_real)
    # Network data short-hands
    baseMVA = opfdata.baseMVA
    buses = opfdata.buses
    busIdx = opfdata.BusIdx
    BusGeners = opfdata.BusGenerators
    Ybus = opfdata.Ybus

    rows = Ybus.rowval
    yvals = Ybus.nzval
    g_ij = real.(yvals)
    b_ij = imag.(yvals)

    # Power Balance Equations
    for b in 1:length(buses)
        #real part
        b_start, b_end = Ybus.colptr[b], Ybus.colptr[b+1]-1
        @NLconstraint(opfmodel,
            Vm[b] * sum(Vm[rows[c]] * (g_ij[c] * cos(Va[b] - Va[rows[c]]) + b_ij[c] * sin(Va[b] - Va[rows[c]])) for c in b_start:b_end)
            - ( sum(baseMVA*Pg[g] for g in get(BusGeners, b, Int[])) - Pd[b] + sigma_real[b]) / baseMVA      # Sbus part
            ==0
        )
    end
end

function opf_model_add_imag_power_balance_constraints(opfmodel::JuMP.Model, opfdata::OPFData, Qg, Qd, Vm, Va, sigma_imag)
    # Network data short-hands
    baseMVA = opfdata.baseMVA
    buses = opfdata.buses
    busIdx = opfdata.BusIdx
    BusGeners = opfdata.BusGenerators
    Ybus = opfdata.Ybus

    rows = Ybus.rowval
    yvals = Ybus.nzval
    g_ij = real.(yvals)
    b_ij = imag.(yvals)

    # Power Balance Equations
    for b in 1:length(buses)
        #imaginary part
        b_start, b_end = Ybus.colptr[b], Ybus.colptr[b+1]-1
        @NLconstraint(opfmodel,
            Vm[b] * sum(Vm[rows[c]] * (g_ij[c] * sin(Va[b] - Va[rows[c]]) - b_ij[c] * cos(Va[b] - Va[rows[c]])) for c in b_start:b_end)
            - ( sum(baseMVA*Qg[g] for g in get(BusGeners, b, Int[])) - Qd[b] + sigma_imag[b]) / baseMVA      #Sbus part
            ==0
        )
    end
end

function opf_model_add_time_linking_constraints(opfmodel::JuMP.Model, opfdata::OPFData, modelinfo::ModelInfo)
    (ngen, K, T) = size(opfmodel[:Pg])

    if T > 1
        link = compute_time_linking_constraints(opfmodel, opfdata, modelinfo)

        if modelinfo.time_link_constr_type == :inequality
            @constraint(opfmodel, ramping_p[g=1:ngen,t=2:T], link[:ramping_p][g,t] <= 0)
            @constraint(opfmodel, ramping_n[g=1:ngen,t=2:T], link[:ramping_n][g,t] <= 0)
        else
            @constraint(opfmodel, ramping[g=1:ngen,t=2:T], link[:ramping][g,t] == 0)
        end
    end

    return nothing
end

function opf_model_add_ctgs_linking_constraints(opfmodel::JuMP.Model, opfdata::OPFData, modelinfo::ModelInfo)
    (ngen, K, T) = size(opfmodel[:Pg])

    if K > 1
        link = compute_ctgs_linking_constraints(opfmodel, opfdata, modelinfo)

        if modelinfo.ctgs_link_constr_type == :corrective_inequality
            @constraint(opfmodel, ctgs_p[g=1:ngen,k=2:K,t=1:T], link[:ctgs_p][g,k,t] <= 0)
            @constraint(opfmodel, ctgs_n[g=1:ngen,k=2:K,t=1:T], link[:ctgs_p][g,k,t] <= 0)
        else
            @constraint(opfmodel, ctgs[g=1:ngen,k=2:K,t=1:T], link[:ctgs][g,k,t] == 0)
        end
    end

    return nothing
end

function compute_objective_function(
    opfdict,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    algparams::AlgParams,
    kIdx::Int, tIdx::Int
)
    Pg = opfdict[:Pg]
    ωt = opfdict[:ωt]
    Zt = opfdict[:Zt]
    Zk = opfdict[:Zk]

    (ngen, K, T) = size(Pg)
    k = algparams.decompCtgs ? min(kIdx, K) : kIdx
    t = min(tIdx, T)
    @assert 1 <= t <= T
    @assert 1 <= k <= K

    if kIdx == 1 && modelinfo.allow_obj_gencost
        baseMVA = opfdata.baseMVA
        gens = opfdata.generators
        obj_gencost = sum(gens[g].coeff[gens[g].n-2]*(baseMVA*Pg[g,k,t])^2
                        + gens[g].coeff[gens[g].n-1]*(baseMVA*Pg[g,k,t])
                        + gens[g].coeff[gens[g].n  ]
                        for g=1:ngen)
    else
        obj_gencost = 0
    end
    if modelinfo.allow_constr_infeas
        sigma_real = opfdict[:sigma_real]
        sigma_imag = opfdict[:sigma_imag]
        sigma_lineFr = opfdict[:sigma_lineFr]
        sigma_lineTo = opfdict[:sigma_lineTo]
        nbus = size(sigma_real, 1)
        nline = size(sigma_lineFr, 1)
        obj_constr_infeas = 0.5*sum(sigma_real[b,k,t]^2 + sigma_imag[b,k,t]^2 for b=1:nbus) +
                            0.5*sum(sigma_lineFr[l,k,t]^2 + sigma_lineTo[l,k,t]^2 for l=1:nline)
        obj_constr_infeas = (kIdx > 1 ? modelinfo.weight_ctgs : 1.0)*obj_constr_infeas
    else
        obj_constr_infeas = 0
    end
    if modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty, :frequency_equality]
        obj_freq_ctrl = 0.5*ωt[k,t]^2
    else
        obj_freq_ctrl = 0
    end
    if kIdx == 1 && modelinfo.time_link_constr_type == :penalty
        obj_bigM_penalty_time = 0.5*sum(Zt[:,t].^2)
    else
        obj_bigM_penalty_time = 0
    end
    if modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty, :preventive_penalty, :corrective_penalty]
        obj_bigM_penalty_ctgs = 0.5*sum(Zk[:,k,t].^2)
    else
        obj_bigM_penalty_ctgs = 0
    end

    return modelinfo.obj_scale*(
            obj_gencost +
            (modelinfo.weight_constr_infeas*obj_constr_infeas) +
            (modelinfo.weight_freq_ctrl*obj_freq_ctrl) +
            (algparams.θ_t*obj_bigM_penalty_time) +
            (algparams.θ_c*obj_bigM_penalty_ctgs)
        )
end

function compute_time_linking_constraints(
    opfdict,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    tIdx::Int = 0
)
    Pg = opfdict[:Pg]
    St = opfdict[:St]
    Zt = opfdict[:Zt]

    gens = opfdata.generators
    (ngen, K, T) = size(Pg)
    Trange = (tIdx == 0) ? (1:T) : (tIdx:tIdx)

    link = Dict{Symbol, Array}()
    if modelinfo.time_link_constr_type == :inequality
        link[:ramping_p] = [(t > 1) ? (+Pg[g,1,t-1] - Pg[g,1,t] - gens[g].ramp_agc) : 0.0 for g=1:ngen,t=Trange]
        link[:ramping_n] = [(t > 1) ? (-Pg[g,1,t-1] + Pg[g,1,t] - gens[g].ramp_agc) : 0.0 for g=1:ngen,t=Trange]
    elseif modelinfo.time_link_constr_type == :equality
        link[:ramping] = [(t > 1) ? (Pg[g,1,t-1] - Pg[g,1,t] + St[g,t] - gens[g].ramp_agc) : 0.0 for g=1:ngen,t=Trange]
    else
        link[:ramping] = [(t > 1) ? (Pg[g,1,t-1] - Pg[g,1,t] + St[g,t] + Zt[g,t] - gens[g].ramp_agc) : 0.0 for g=1:ngen,t=Trange]
    end

    return link
end

function compute_ctgs_linking_constraints(
    opfdict,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    kIdx::Int = 0, tIdx::Int = 0
)
    Pg = opfdict[:Pg]
    ωt = opfdict[:ωt]
    Sk = opfdict[:Sk]
    Zk = opfdict[:Zk]

    gens = opfdata.generators
    (ngen, K, T) = size(Pg)
    Trange = (tIdx == 0) ? (1:T) : (tIdx:tIdx)
    Krange = (kIdx == 0) ? (1:K) : (kIdx:kIdx)

    link = Dict{Symbol, Array}()
    if modelinfo.ctgs_link_constr_type == :corrective_inequality
        link[:ctgs_p] = [(k > 1) ? (+Pg[g,1,t] - Pg[g,k,t] - gens[g].scen_agc) : 0.0 for g=1:ngen,k=Krange,t=Trange]
        link[:ctgs_n] = [(k > 1) ? (-Pg[g,1,t] + Pg[g,k,t] - gens[g].scen_agc) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    elseif modelinfo.ctgs_link_constr_type == :corrective_equality
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t] + Sk[g,k,t] - gens[g].scen_agc) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    elseif modelinfo.ctgs_link_constr_type == :corrective_penalty
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t] + Sk[g,k,t] + Zk[g,k,t] - gens[g].scen_agc) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    elseif modelinfo.ctgs_link_constr_type == :preventive_equality
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t]) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    elseif modelinfo.ctgs_link_constr_type == :preventive_penalty
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t] + Zk[g,k,t]) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    elseif modelinfo.ctgs_link_constr_type == :frequency_equality
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t] + (gens[g].alpha*ωt[k,t])) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    else
        link[:ctgs] = [(k > 1) ? (Pg[g,1,t] - Pg[g,k,t] + (gens[g].alpha*ωt[k,t]) + Zk[g,k,t]) : 0.0 for g=1:ngen,k=Krange,t=Trange]
    end

    return link
end

function compute_quadratic_penalty(
    opfdict,
    opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo,
    algparams::AlgParams
)
    (ngen, K, T) = size(opfdict[:Pg])
    block = opfBlockData.blkIndex[blk]
    k = block[1]
    t = block[2]

    if t > 1 && k == 1
        inequality = (modelinfo.time_link_constr_type == :inequality)
        inequality && (modelinfo.time_link_constr_type = :equality)
        link = compute_time_linking_constraints(opfdict, opfdata, modelinfo, t)
        inequality && (modelinfo.time_link_constr_type = :inequality)
        lyapunov_quadratic_penalty_time = sum(link[:ramping][:].^2)
    else
        lyapunov_quadratic_penalty_time = 0
    end

    if k > 1 && algparams.decompCtgs
        inequality = (modelinfo.ctgs_link_constr_type == :corrective_inequality)
        inequality && (modelinfo.ctgs_link_constr_type = :corrective_equality)
        link = compute_ctgs_linking_constraints(opfdict, opfdata, modelinfo, k, t)
        inequality && (modelinfo.ctgs_link_constr_type = :corrective_inequality)

        lyapunov_quadratic_penalty_ctgs = sum(link[:ctgs][:].^2)
    else
        lyapunov_quadratic_penalty_ctgs = 0
    end

    return ((0.5algparams.ρ_t*lyapunov_quadratic_penalty_time) +
            (0.5algparams.ρ_c*lyapunov_quadratic_penalty_ctgs))
end

function compute_quadratic_penalty(
    opfdict,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    algparams::AlgParams
)
    (ngen, K, T) = size(opfdict[:Pg])

    if T > 1
        inequality = (modelinfo.time_link_constr_type == :inequality)
        inequality && (modelinfo.time_link_constr_type = :equality)
        link = compute_time_linking_constraints(opfdict, opfdata, modelinfo)
        inequality && (modelinfo.time_link_constr_type = :inequality)

        lyapunov_quadratic_penalty_time = sum(link[:ramping][:,2:T].^2)
    else
        lyapunov_quadratic_penalty_time = 0
    end

    if K > 1 && algparams.decompCtgs
        inequality = (modelinfo.ctgs_link_constr_type == :corrective_inequality)
        inequality && (modelinfo.ctgs_link_constr_type = :corrective_equality)
        link = compute_ctgs_linking_constraints(opfdict, opfdata, modelinfo)
        inequality && (modelinfo.ctgs_link_constr_type = :corrective_inequality)

        lyapunov_quadratic_penalty_ctgs = sum(link[:ctgs][:,2:K,:].^2)
    else
        lyapunov_quadratic_penalty_ctgs = 0
    end

    return ((0.5algparams.ρ_t*lyapunov_quadratic_penalty_time) +
            (0.5algparams.ρ_c*lyapunov_quadratic_penalty_ctgs))
end

function compute_lagrangian_function(
    opfdict, λ::OPFDualSolution,
    opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo,
    algparams::AlgParams
)

    (ngen, K, T) = size(opfdict[:Pg])
    block = opfBlockData.blkIndex[blk]
    k = block[1]
    t = block[2]

    obj = compute_objective_function(opfdict, opfdata, opfBlockData, blk, modelinfo, algparams)

    if t > 1 && k == 1
        link = compute_time_linking_constraints(opfdict, opfdata, modelinfo, t)
        lagrangian_t = sum(λ.ramping[:,t].*link[:ramping][:])
    else
        lagrangian_t = 0
    end

    if k > 1 && algparams.decompCtgs
        link = compute_ctgs_linking_constraints(opfdict, opfdata, modelinfo, k, t)
        lagrangian_c = sum(λ.ctgs[:,k,t].*link[:ctgs][:])
    else
        lagrangian_c = 0
    end

    return obj + lagrangian_t + lagrangian_c
end

function compute_proximal_function(
    x1::OPFPrimalSolution, x2::OPFPrimalSolution,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo,
    algparams::AlgParams
)
    block = opfBlockData.blkIndex[blk]
    k = block[1]
    t = block[2]

    prox_pg = algparams.τ*sum((x1.Pg[:,k,t] .- x2.Pg[:,k,t]).^2)
    prox_penalty = 0

    if modelinfo.time_link_constr_type == :penalty
        if t > 1 && k == 1
            # Note: algparams.ρ_t/32.0 is the prox weight for the zt variables
            prox_penalty += (algparams.ρ_t/32.0)*sum((x1.Zt[:,t] .- x2.Zt[:,t]).^2)
        end
    end

    if k > 1 && algparams.decompCtgs
        # Note: algparams.ρ_c/32.0 is the prox weight for the zk variables
        if modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty, :preventive_penalty, :corrective_penalty]
            prox_penalty += (algparams.ρ_c/32.0)*sum((x1.Zk[:,k,t] .- x2.Zk[:,k,t]).^2)
        end
    end

    return 0.5*(prox_pg + prox_penalty)
end

function compute_objective_function(
    opfdict, opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo, algparams::AlgParams
)
    K = size(opfdict[:Pg],2)
    block = opfBlockData.blkIndex[blk]
    t = block[2]
    if K > 1 && algparams.decompCtgs
        k = block[1]
        return compute_objective_function(opfdict, opfdata, modelinfo, algparams, k, t)
    else
        return sum(compute_objective_function(opfdict, opfdata, modelinfo, algparams, k, t) for k=1:K)
    end
end

function compute_objective_function(
    opfdict, opfdata::OPFData,
    modelinfo::ModelInfo, algparams::AlgParams
)
    (_, K, T) = size(opfdict[:Pg])
    return sum(compute_objective_function(opfdict, opfdata, modelinfo, algparams, k, t) for k=1:K, t=1:T)
end

function compute_objective_function(
    x::OPFPrimalSolution, opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo, algparams::AlgParams
)
    d = Dict(:Pg => x.Pg,
             :ωt => x.ωt,
             :Zt => x.Zt,
             :Zk => x.Zk,
             :sigma_real => x.sigma_real,
             :sigma_imag => x.sigma_imag,
             :sigma_lineFr => x.sigma_lineFr,
             :sigma_lineTo => x.sigma_lineTo)
    return compute_objective_function(d, opfdata, opfBlockData, blk, modelinfo, algparams)
end

function compute_lyapunov_function(
    x::OPFPrimalSolution, λ::OPFDualSolution, opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    xref::OPFPrimalSolution,
    modelinfo::ModelInfo,
    algparams::AlgParams
)
    d = Dict(:Pg => x.Pg,
             :ωt => x.ωt,
             :St => x.St,
             :Zt => x.Zt,
             :Sk => x.Sk,
             :Zk => x.Zk,
             :sigma_real => x.sigma_real,
             :sigma_imag => x.sigma_imag,
             :sigma_lineFr => x.sigma_lineFr,
             :sigma_lineTo => x.sigma_lineTo)
    lagrangian = compute_lagrangian_function(d, λ, opfdata, opfBlockData, blk, modelinfo, algparams)
    quadratic_penalty = compute_quadratic_penalty(d, opfdata, opfBlockData, blk, modelinfo, algparams)
    proximal = compute_proximal_function(x, xref, opfBlockData, blk, modelinfo, algparams)

    return lagrangian + quadratic_penalty + (0.5*proximal)
end

function compute_dual_error(
    x::OPFPrimalSolution, xprev::OPFPrimalSolution,
    λ::OPFDualSolution, λprev::OPFDualSolution,
    opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo, algparams::AlgParams;
    lnorm = Inf
)
    (ngen, K, T) = size(x.Pg)
    k = opfBlockData.blkIndex[blk][1]
    t = opfBlockData.blkIndex[blk][2]

    err_pg = zeros(ngen)
    err_ωt = 0.0
    err_st = zeros(ngen)
    err_sk = zeros(ngen)

    # prox errors
    err_pg .-= algparams.τ*(x.Pg[:,k,t] .- xprev.Pg[:,k,t])

    # time linking errors
    if k == 1
        # for convenience
        β = [g.ramp_agc for g in opfdata.generators]
        if t > 1
            temp_arr = -λ.ramping[:,t] .+ λprev.ramping[:,t] .+
                        algparams.ρ_t*(
                            (algparams.jacobi ? xprev.Pg[:,1,t-1] : x.Pg[:,1,t-1]) .-
                            x.Pg[:,1,t] .+ x.St[:,t] .+ xprev.Zt[:,t] .- β
                        )
            err_pg .+= temp_arr
            err_st .+= -temp_arr
        end
        if t < T
            err_pg .+= λ.ramping[:,t+1] .- λprev.ramping[:,t+1] .-
                        algparams.ρ_t*(
                            x.Pg[:,1,t] .- xprev.Pg[:,1,t+1] .+ xprev.St[:,t+1] .+ xprev.Zt[:,t+1] .- β
                        )
        end
    end

    # ctgs linking errors
    if K > 1 && algparams.decompCtgs
        # for convenience
        β = zeros(ngen)
        if modelinfo.ctgs_link_constr_type ∈ [:corrective_inequality, :corrective_equality, :corrective_penalty]
            for g in 1:ngen
                β[g] = opfdata.generators[g].scen_agc
            end
        end

        if k == 1
            err_pg .+= dropdims(sum(
                            λ.ctgs[:,2:K,t] - λprev.ctgs[:,2:K,t] .-
                            algparams.ρ_c*(
                                -xprev.Pg[:,2:K,t] .+ xprev.Sk[:,2:K,t] .+ xprev.Zk[:,2:K,t]
                            ); dims = 2); dims = 2) .+
                        (algparams.ρ_c*(K-1)*(-x.Pg[:,1,t] .+ β)) .-
                        [algparams.ρ_c*g.alpha*sum(xprev.ωt[2:K,t]) for g in opfdata.generators]
        end

        if k > 1
            temp_arr = -λ.ctgs[:,k,t] .+ λprev.ctgs[:,k,t] .+
                        algparams.ρ_c*(
                            (algparams.jacobi ? xprev.Pg[:,1,t] : x.Pg[:,1,t]) .-
                            x.Pg[:,k,t] .+ x.Sk[:,k,t] .+ xprev.Zk[:,k,t] .- β .+
                            [g.alpha*x.ωt[k,t] for g in opfdata.generators]
                        )
            err_pg .+= temp_arr
            if modelinfo.ctgs_link_constr_type ∈ [:corrective_penalty]
                err_sk .+= -temp_arr
            end
            if modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty]
                err_ωt += -sum(opfdata.generators[g].alpha * temp_arr[g] for g in 1:ngen)
            end
        end
    end

    return norm([err_pg; err_ωt; err_st; err_sk], lnorm)
end

function compute_dual_error(
    x::OPFPrimalSolution, xprev::OPFPrimalSolution,
    λ::OPFDualSolution, λprev::OPFDualSolution,
    opfdata::OPFData,
    modelinfo::ModelInfo, algparams::AlgParams;
    lnorm = Inf
)
    (ngen, K, T) = size(x.Pg)

    err_pg = zeros(ngen, K, T)
    err_ωt = zeros(K, T)
    err_st = zeros(ngen, T)
    err_sk = zeros(ngen, K, T)

    if T > 1
        @assert modelinfo.time_link_constr_type == :penalty
        @views begin
            # for convenience
            β = zeros(ngen, T)
            for g=1:ngen
                β[g,:] .= opfdata.generators[g].ramp_agc
            end

            #----------------------------------------------------------------------------
            true_pg_dual = -λ.ramping[:,2:T]
            lagrangian_pg_err = -λprev.ramping[:,2:T]
            penalty_pg_forward_err = -algparams.ρ_t*(
                                        (algparams.jacobi ? xprev.Pg[:,1,1:(T-1)] : x.Pg[:,1,1:(T-1)]) .-
                                        x.Pg[:,1,2:T] .+ x.St[:,2:T] .+ xprev.Zt[:,2:T] .- β[:,2:T]
                                    )
            penalty_pg_reverse_err = +algparams.ρ_t*(
                                        x.Pg[:,1,1:(T-1)] .- xprev.Pg[:,1,2:T] .+ xprev.St[:,2:T] .+ xprev.Zt[:,2:T] .- β[:,2:T]
                                    )
            prox_pg_err = algparams.τ*(x.Pg[:,1,:] .- xprev.Pg[:,1,:])
            #----------------------------------------------------------------------------

            err_pg[:,1,1:(T-1)] += -true_pg_dual .+ lagrangian_pg_err .- penalty_pg_reverse_err
            err_pg[:,1,2:T] += true_pg_dual .- lagrangian_pg_err .- penalty_pg_forward_err
            err_pg[:,1,1:T] -= prox_pg_err[:,1:T]
            err_st[:,2:T] += penalty_pg_forward_err
            err_st[:,2:T] += -true_pg_dual .+ lagrangian_pg_err
        end
    end

    if K > 1 && algparams.decompCtgs
        @assert modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty, :preventive_penalty, :corrective_penalty]
        @views begin
            # for convenience
            β = zeros(ngen, K, T)
            pg_base = zeros(ngen, K, T)
            pg_base_prev = zeros(ngen, K, T)
            if modelinfo.ctgs_link_constr_type ∈ [:corrective_inequality, :corrective_equality, :corrective_penalty]
                for g=1:ngen
                    β[g,:,:] .= opfdata.generators[g].scen_agc
                end
            end
            for k=2:K, g=1:ngen
                pg_base[g,k,:] .= x.Pg[g,1,:]
                pg_base_prev[g,k,:] .= xprev.Pg[g,1,:]
            end

            #----------------------------------------------------------------------------
            true_pg_ctgs_dual = -λ.ctgs[:,2:K,:]
            lagrangian_pg_ctgs_err = -λprev.ctgs[:,2:K,:]
            penalty_pg_base_err = pg_base[:,2:K,:] .-
                                        xprev.Pg[:,2:K,:] .+ xprev.Sk[:,2:K,:] .+ xprev.Zk[:,2:K,:] .- β[:,2:K,:]
            penalty_pg_ctgs_err = (algparams.jacobi ? pg_base_prev[:,2:K,:] : pg_base[:,2:K,:]) -
                                            x.Pg[:,2:K,:] .+     x.Sk[:,2:K,:] .+ xprev.Zk[:,2:K,:] .- β[:,2:K,:]
            prox_pg_err = algparams.τ*(x.Pg[:,2:K,:] .- xprev.Pg[:,2:K,:])
            for g=1:ngen
                penalty_pg_ctgs_err[g,:,:] += opfdata.generators[g].alpha*x.ωt[2:K,:]
                penalty_pg_base_err[g,:,:] += opfdata.generators[g].alpha*xprev.ωt[2:K,:]
            end
            penalty_pg_base_err .= algparams.ρ_c*penalty_pg_base_err
            penalty_pg_ctgs_err .= -algparams.ρ_c*penalty_pg_ctgs_err
            #----------------------------------------------------------------------------

            err_pg[:,1,:] += dropdims(sum(-true_pg_ctgs_dual .+ lagrangian_pg_ctgs_err .- penalty_pg_base_err; dims = 2); dims = 2)
            err_pg[:,2:K,:] += true_pg_ctgs_dual .- lagrangian_pg_ctgs_err .- penalty_pg_ctgs_err .- prox_pg_err
            if modelinfo.ctgs_link_constr_type ∈ [:corrective_penalty]
                err_sk[:,2:K,:] += penalty_pg_ctgs_err - true_pg_ctgs_dual .+ lagrangian_pg_ctgs_err
            end
            if modelinfo.ctgs_link_constr_type ∈ [:frequency_penalty]
                err_ωt[2:K,:] += sum(opfdata.generators[g].alpha * (-true_pg_ctgs_dual[g,:,:] .+ lagrangian_pg_ctgs_err[g,:,:] .+ penalty_pg_ctgs_err[g,:,:]) for g=1:ngen)
            end
        end
    end

    err_pg_view = view(err_pg, :, :, :)
    err_ωt_view = view(err_ωt, :, :)
    err_st_view = view(err_st, :, :)
    err_sk_view = view(err_sk, :, :, :)

    dual_error = CatView(err_pg_view, err_ωt_view, err_st_view, err_sk_view)

    return norm(dual_error, lnorm)
end

function compute_true_ramp_error(
    x::OPFPrimalSolution,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    tIdx::Int = 0
)
    d = Dict(:Pg => x.Pg,
             :St => x.St,
             :Zt => x.Zt)
    if modelinfo.time_link_constr_type == :inequality
        link = compute_time_linking_constraints(d, opfdata, modelinfo, tIdx)
        return maximum(max.(link[:ramping_p], link[:ramping_n], 0.0))
    end

    true_time_link_constr_type = modelinfo.time_link_constr_type
    modelinfo.time_link_constr_type = :equality
    link = compute_time_linking_constraints(d, opfdata, modelinfo, tIdx)
    modelinfo.time_link_constr_type = true_time_link_constr_type

    return maximum(abs.(link[:ramping]))
end

function compute_true_ctgs_error(
    x::OPFPrimalSolution,
    opfdata::OPFData,
    modelinfo::ModelInfo,
    kIdx::Int = 0, tIdx::Int = 0
)
    d = Dict(:Pg => x.Pg,
             :ωt => x.ωt,
             :Sk => x.Sk,
             :Zk => x.Zk)
    if modelinfo.ctgs_link_constr_type == :corrective_inequality
        link = compute_ctgs_linking_constraints(d, opfdata, modelinfo, kIdx, tIdx)
        return maximum(max.(abs.(link[:ctgs_p][:]), abs.(link[:ctgs_n][:]), 0.0))
    end

    true_ctgs_link_constr_type = modelinfo.ctgs_link_constr_type
    if true_ctgs_link_constr_type ∈ [:corrective_penalty]
        modelinfo.ctgs_link_constr_type = :corrective_equality
    end
    if true_ctgs_link_constr_type ∈ [:preventive_penalty]
        modelinfo.ctgs_link_constr_type = :preventive_equality
    end
    if true_ctgs_link_constr_type ∈ [:frequency_penalty]
        modelinfo.ctgs_link_constr_type = :frequency_equality
    end
    link = compute_ctgs_linking_constraints(d, opfdata, modelinfo, kIdx, tIdx)
    modelinfo.ctgs_link_constr_type = true_ctgs_link_constr_type

    return maximum(abs.(link[:ctgs][:]))
end

function compute_true_ramp_error(
    x::OPFPrimalSolution, opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo,
)
    block = opfBlockData.blkIndex[blk]
    k = block[1]
    t = block[2]
    if k > 1
        return 0.0
    end
    return compute_true_ramp_error(x, opfdata, modelinfo, t)
end

function compute_true_ctgs_error(
    x::OPFPrimalSolution, opfdata::OPFData,
    opfBlockData::OPFBlocks, blk::Int,
    modelinfo::ModelInfo,
)
    block = opfBlockData.blkIndex[blk]
    k = block[1]
    t = block[2]
    return compute_true_ctgs_error(x, opfdata, modelinfo, k, t)
end

"""
    opf_block_get_auglag_penalty_expr(
        opfmodel::JuMP.Model,
        modelinfo::ModelInfo,
        opfdata::OPFData,
        k::Int, t::Int,
        algparams::AlgParams,
        primal::OPFPrimalSolution,
        dual::OPFDualSolution
    )

Let `k` and `t` denote the contingency number and time period of
the NLP `block`. Then, depending on `algparams.decompCtgs`,
this function must return an appropriate expression.

We use ``\\mathbb{I}[...]`` to denote the indicator function.
Also, unless otherwise indicated,
- ``z_g`` are parameters and must be taken from `primal.Zt`
- ``p_g`` and ``s_g`` variables that are not indexed with `t`
    are parameters and must be taken from `primal.Pg` and `primal.St`
- ``\\lambda`` (without `k` subscript) must be taken from `dual.ramping`
- ``\\rho_t`` must be taken from `algparams.ρ_t`
- ``\\tau`` must be taken from `algparams.τ`

* If `algparams.decompCtgs == false`, then this function must return:
```math
\\begin{aligned}
\\sum_{g \\in G} \\Bigg\\{
& 0.5\\tau [p^0_{g,t} - \\mathrm{primal}.p^0_{g,t}]^2 \\\\
&+\\mathbb{I}[t > 1]\\Big(
\\lambda_{gt}[p^0_{g,t-1} - p^0_{g,t} + s_{g,t} + z_{g,t} - r_g] +
0.5\\rho_{t}[p^0_{g,t-1} - p^0_{g,t} + s_{g,t} + z_{g,t} - r_g]^2
\\Big) \\\\
&+\\mathbb{I}[t < T]\\Big(
\\lambda_{g,t+1}[p^0_{g,t} - p^0_{g,t+1} + s_{g,t+1} + z_{g,t+1} - r_g] +
0.5\\rho_{t}[p^0_{g,t} - p^0_{g,t+1} + s_{g,t+1} + z_{g,t+1} - r_g]^2
\\Big) \\Bigg\\}
\\end{aligned}
```

* If `algparams.decompCtgs == true`, then this function must return:
(to do)

"""
function opf_block_get_auglag_penalty_expr(
    opfmodel::JuMP.Model,
    modelinfo::ModelInfo,
    opfdata::OPFData,
    k::Int, t::Int,
    algparams::AlgParams,
    primal::OPFPrimalSolution,
    dual::OPFDualSolution
)

    (ngen, K, T) = size(primal.Pg)
    gens = opfdata.generators
    @assert t >= 1 && t <= T
    @assert k >= 1 && k <= K
    @assert modelinfo.num_time_periods == 1
    @assert modelinfo.num_ctgs >= 0
    @assert k == 1 || algparams.decompCtgs

    # get variables
    Pg = opfmodel[:Pg]
    ωt = opfmodel[:ωt]
    St = opfmodel[:St]
    Sk = opfmodel[:Sk]

    auglag_penalty = @expression(opfmodel, 0.5*algparams.τ*sum((Pg[g,1,1] - primal.Pg[g,k,t])^2 for g=1:ngen))

    if !algparams.decompCtgs || k == 1
        if t > 1
            ramp_link_expr_prev =
                @expression(opfmodel,
                    [g=1:ngen],
                        primal.Pg[g,1,t-1] - Pg[g,1,1] + St[g,1] + primal.Zt[g,t] - gens[g].ramp_agc
                )
            auglag_penalty += sum(  dual.ramping[g,t]*(+ramp_link_expr_prev[g])   +
                                    0.5*algparams.ρ_t*(+ramp_link_expr_prev[g])^2
                                for g=1:ngen)
        end
        if t < T
            ramp_link_expr_next =
                @expression(opfmodel,
                    [g=1:ngen],
                        Pg[g,1,1] - primal.Pg[g,1,t+1] + primal.St[g,t+1] + primal.Zt[g,t+1] - gens[g].ramp_agc
                )
            auglag_penalty += sum(  dual.ramping[g,t+1]*(+ramp_link_expr_next[g]) +
                                    0.5*algparams.ρ_t*(+ramp_link_expr_next[g])^2
                                for g=1:ngen)
        end
    end

    if algparams.decompCtgs && K > 1
        @assert modelinfo.num_ctgs == 0
        β = [gens[g].scen_agc for g=1:ngen]
        (modelinfo.ctgs_link_constr_type ∉ [:corrective_inequality, :corrective_equality, :corrective_penalty]) &&
            (β .= 0)

        if k > 1
            ctgs_link_expr_prev =
                @expression(opfmodel,
                    [g=1:ngen],
                        primal.Pg[g,1,t] - Pg[g,1,1] + (gens[g].alpha*ωt[1,1]) + Sk[g,1,1] + primal.Zk[g,k,t] - β[g]
                )
            auglag_penalty += sum(    dual.ctgs[g,k,t]*(+ctgs_link_expr_prev[g]) +
                                    0.5*algparams.ρ_c*(+ctgs_link_expr_prev[g])^2
                                for g=1:ngen)
        else
            ctgs_link_expr_next =
                @expression(opfmodel,
                    [g=1:ngen,j=2:K],
                        Pg[g,1,1] - primal.Pg[g,j,t] + (gens[g].alpha*primal.ωt[j,t]) + primal.Sk[g,j,t] + primal.Zk[g,j,t] - β[g]
                )
            auglag_penalty += sum(     dual.ctgs[g,j,t]*(+ctgs_link_expr_next[g,j]) +
                                    0.5*algparams.ρ_c*(+ctgs_link_expr_next[g,j])^2
                                for j=2:K, g=1:ngen)
        end
    end

    drop_zeros!(auglag_penalty)

    return auglag_penalty
end


module ARCHER
using ..AGFFileReader: Glass, GlassID, AGF
export S_LAL12M, S_BSL7M, S_LAL13M, S_NSL3M, S_BAL41M, S_LAM60M, S_BAL3M, L_BAL35M, S_LAH60M, S_TIM2M, S_FSL5M, L_LAM69M, K_VC79M, S_BAL42M, S_LAH53M

const S_LAL12M = Glass(GlassID(AGF, 914), 1, 2.753166, -0.01207074, 0.01625163, 0.001041721, -0.0001156154, 6.773623e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, -7.683e-7, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0047, 4.2, -1.0, 7.2, 2.0, 0, 53.0, [(0.27, 0.0, 10.0), (0.28, 0.06, 10.0), (0.29, 0.15, 10.0), (0.3, 0.29, 10.0), (0.31, 0.45, 10.0), (0.32, 0.61, 10.0), (0.334, 0.774, 10.0), (0.35, 0.89, 10.0), (0.365, 0.952, 10.0), (0.37, 0.962, 10.0), (0.38, 0.976, 10.0), (0.39, 0.984, 10.0), (0.4, 0.988, 10.0), (0.404, 0.989, 10.0), (0.42, 0.992, 10.0), (0.436, 0.994, 10.0), (0.46, 0.995, 10.0), (0.5, 0.998, 10.0), (0.546, 0.999, 10.0), (0.58, 0.998, 10.0), (0.62, 0.998, 10.0), (0.66, 0.998, 10.0), (0.7, 0.998, 10.0), (1.06, 0.996, 10.0), (1.529, 0.991, 10.0), (1.97, 0.966, 10.0), (2.325, 0.809, 10.0)], 1.674061, -1.0, -1.0, 0, 55.017718, 0, 4.01, 0)
const S_BSL7M = Glass(GlassID(AGF, 915), 1, 2.262505, -0.01037921, 0.009341724, 0.0006453071, -7.463858e-5, 4.096215e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, 3.095e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0024, 2.0, -1.0, 7.2, 1.5, 0, 1.0, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.08, 10.0), (0.3, 0.31, 10.0), (0.31, 0.58, 10.0), (0.32, 0.77, 10.0), (0.334, 0.909, 10.0), (0.35, 0.968, 10.0), (0.365, 0.988, 10.0), (0.37, 0.991, 10.0), (0.38, 0.991, 10.0), (0.39, 0.996, 10.0), (0.4, 0.997, 10.0), (0.404, 0.997, 10.0), (0.42, 0.996, 10.0), (0.436, 0.995, 10.0), (0.46, 0.995, 10.0), (0.5, 0.996, 10.0), (0.546, 0.998, 10.0), (0.58, 0.997, 10.0), (0.62, 0.997, 10.0), (0.66, 0.997, 10.0), (0.7, 0.998, 10.0), (1.06, 0.996, 10.0), (1.529, 0.989, 10.0), (1.97, 0.965, 10.0), (2.325, 0.862, 10.0)], 1.513232, -1.0, -1.0, 0, 63.709676, 0, 2.52, 0)
const S_LAL13M = Glass(GlassID(AGF, 916), 1, 2.800915, -0.0140473, 0.01703856, 0.001170426, -0.0001225957, 6.868454e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, 7.171e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0081, 3.0, -1.0, 5.7, 1.5, 0, 52.0, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.01, 10.0), (0.3, 0.03, 10.0), (0.31, 0.08, 10.0), (0.32, 0.19, 10.0), (0.334, 0.411, 10.0), (0.35, 0.68, 10.0), (0.365, 0.844, 10.0), (0.37, 0.88, 10.0), (0.38, 0.932, 10.0), (0.39, 0.958, 10.0), (0.4, 0.972, 10.0), (0.404, 0.977, 10.0), (0.42, 0.986, 10.0), (0.436, 0.989, 10.0), (0.46, 0.993, 10.0), (0.5, 0.996, 10.0), (0.546, 0.997, 10.0), (0.58, 0.996, 10.0), (0.62, 0.995, 10.0), (0.66, 0.995, 10.0), (0.7, 0.996, 10.0), (1.06, 0.995, 10.0), (1.529, 0.99, 10.0), (1.97, 0.962, 10.0), (2.325, 0.762, 10.0)], 1.689007, -1.0, -1.0, 0, 52.782035, 0, 3.6, 0)
const S_NSL3M = Glass(GlassID(AGF, 917), 1, 2.267846, -0.008623029, 0.01087772, 0.0005394286, -5.529385e-5, 3.416585e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, -1.095e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0005, 1.0, -1.0, 9.0, 1.0, 0, 1.0, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.0, 10.0), (0.3, 0.0, 10.0), (0.31, 0.0, 10.0), (0.32, 0.15, 10.0), (0.334, 0.655, 10.0), (0.35, 0.924, 10.0), (0.365, 0.978, 10.0), (0.37, 0.984, 10.0), (0.38, 0.99, 10.0), (0.39, 0.995, 10.0), (0.4, 0.997, 10.0), (0.404, 0.997, 10.0), (0.42, 0.997, 10.0), (0.436, 0.997, 10.0), (0.46, 0.997, 10.0), (0.5, 0.998, 10.0), (0.546, 0.999, 10.0), (0.58, 0.999, 10.0), (0.62, 0.999, 10.0), (0.66, 0.998, 10.0), (0.7, 0.998, 10.0), (1.06, 0.997, 10.0), (1.529, 0.992, 10.0), (1.97, 0.937, 10.0), (2.325, 0.827, 10.0)], 1.516509, -1.0, -1.0, 0, 58.745047, 0, 2.48, 0)
const S_BAL41M = Glass(GlassID(AGF, 918), 1, 2.402809, -0.01041283, 0.01136728, 0.0007464327, -8.517315e-5, 4.764139e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, 2.775e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0031, 3.0, -1.0, 7.5, 2.5, 0, 51.2, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.0, 10.0), (0.3, 0.05, 10.0), (0.31, 0.25, 10.0), (0.32, 0.51, 10.0), (0.334, 0.782, 10.0), (0.35, 0.925, 10.0), (0.365, 0.971, 10.0), (0.37, 0.978, 10.0), (0.38, 0.985, 10.0), (0.39, 0.99, 10.0), (0.4, 0.993, 10.0), (0.404, 0.993, 10.0), (0.42, 0.994, 10.0), (0.436, 0.994, 10.0), (0.46, 0.995, 10.0), (0.5, 0.998, 10.0), (0.546, 0.999, 10.0), (0.58, 0.998, 10.0), (0.62, 0.998, 10.0), (0.66, 0.997, 10.0), (0.7, 0.998, 10.0), (1.06, 0.997, 10.0), (1.529, 0.992, 10.0), (1.97, 0.974, 10.0), (2.325, 0.861, 10.0)], 1.560983, -1.0, -1.0, 0, 60.308093, 0, 2.78, 0)
const S_LAM60M = Glass(GlassID(AGF, 919), 1, 2.958448, -0.01542036, 0.02030973, 0.001302646, -0.0001238708, 7.401598e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, 8.217e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0085, 2.0, -1.0, 5.4, 2.0, 0, 52.0, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.0, 10.0), (0.3, 0.06, 10.0), (0.31, 0.27, 10.0), (0.32, 0.53, 10.0), (0.334, 0.786, 10.0), (0.35, 0.922, 10.0), (0.365, 0.967, 10.0), (0.37, 0.975, 10.0), (0.38, 0.984, 10.0), (0.39, 0.989, 10.0), (0.4, 0.992, 10.0), (0.404, 0.992, 10.0), (0.42, 0.993, 10.0), (0.436, 0.993, 10.0), (0.46, 0.995, 10.0), (0.5, 0.998, 10.0), (0.546, 0.999, 10.0), (0.58, 0.998, 10.0), (0.62, 0.998, 10.0), (0.66, 0.998, 10.0), (0.7, 0.998, 10.0), (1.06, 0.997, 10.0), (1.529, 0.993, 10.0), (1.97, 0.98, 10.0), (2.325, 0.867, 10.0)], 1.737928, -1.0, -1.0, 0, 49.027571, 0, 4.06, 0)
const S_BAL3M = Glass(GlassID(AGF, 920), 1, 2.417473, -0.008685888, 0.01396835, 0.0006180845, -5.274288e-5, 3.679696e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, -3.881e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0005, 1.0, -1.0, 9.5, 2.0, 0, 1.2, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.0, 10.0), (0.3, 0.0, 10.0), (0.31, 0.0, 10.0), (0.32, 0.0, 10.0), (0.334, 0.096, 10.0), (0.35, 0.63, 10.0), (0.365, 0.894, 10.0), (0.37, 0.928, 10.0), (0.38, 0.963, 10.0), (0.39, 0.979, 10.0), (0.4, 0.988, 10.0), (0.404, 0.99, 10.0), (0.42, 0.994, 10.0), (0.436, 0.994, 10.0), (0.46, 0.995, 10.0), (0.5, 0.997, 10.0), (0.546, 0.998, 10.0), (0.58, 0.998, 10.0), (0.62, 0.998, 10.0), (0.66, 0.998, 10.0), (0.7, 0.998, 10.0), (1.06, 0.997, 10.0), (1.529, 0.991, 10.0), (1.97, 0.951, 10.0), (2.325, 0.847, 10.0)], 1.568151, -1.0, -1.0, 0, 52.737315, 0, 2.98, 0)
const L_BAL35M = Glass(GlassID(AGF, 921), 1, 2.481845, -0.01213254, 0.01179439, 0.000804573, -9.005814e-5, 4.898641e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, 6.78e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0043, 1.0, -1.0, 6.6, 2.5, 0, 4.2, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.0, 10.0), (0.3, 0.06, 10.0), (0.31, 0.27, 10.0), (0.32, 0.53, 10.0), (0.334, 0.786, 10.0), (0.35, 0.922, 10.0), (0.365, 0.967, 10.0), (0.37, 0.975, 10.0), (0.38, 0.984, 10.0), (0.39, 0.989, 10.0), (0.4, 0.992, 10.0), (0.404, 0.992, 10.0), (0.42, 0.993, 10.0), (0.436, 0.993, 10.0), (0.46, 0.995, 10.0), (0.5, 0.998, 10.0), (0.546, 0.999, 10.0), (0.58, 0.998, 10.0), (0.62, 0.998, 10.0), (0.66, 0.998, 10.0), (0.7, 0.998, 10.0), (1.06, 0.997, 10.0), (1.529, 0.993, 10.0), (1.97, 0.98, 10.0), (2.325, 0.867, 10.0)], 1.58642, -1.0, -1.0, 0, 60.713872, 0, 2.82, 0)
const S_LAH60M = Glass(GlassID(AGF, 922), 1, 3.244481, -0.01528454, 0.03201574, 0.001865793, -0.000141547, 1.246734e-5, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, 9.635e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0037, 1.0, -1.0, 5.6, 1.0, 0, 4.2, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.0, 10.0), (0.3, 0.0, 10.0), (0.31, 0.0, 10.0), (0.32, 0.0, 10.0), (0.334, 0.0, 10.0), (0.35, 0.27, 10.0), (0.365, 0.643, 10.0), (0.37, 0.72, 10.0), (0.38, 0.83, 10.0), (0.39, 0.88, 10.0), (0.4, 0.924, 10.0), (0.404, 0.938, 10.0), (0.42, 0.957, 10.0), (0.436, 0.97, 10.0), (0.46, 0.98, 10.0), (0.5, 0.99, 10.0), (0.546, 0.996, 10.0), (0.58, 0.997, 10.0), (0.62, 0.997, 10.0), (0.66, 0.997, 10.0), (0.7, 0.998, 10.0), (1.06, 0.997, 10.0), (1.529, 0.992, 10.0), (1.97, 0.968, 10.0), (2.325, 0.808, 10.0)], 1.828944, -1.0, -1.0, 0, 37.007308, 0, 4.43, 0)
const S_TIM2M = Glass(GlassID(AGF, 923), 1, 2.535373, -0.01040568, 0.02209869, 0.0009396727, -6.499788e-5, 9.894408e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, 3.044e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, 0.0051, 1.0, -1.0, 8.1, 2.0, 0, 1.0, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.0, 10.0), (0.3, 0.0, 10.0), (0.31, 0.0, 10.0), (0.32, 0.0, 10.0), (0.334, 0.0, 10.0), (0.35, 0.0, 10.0), (0.365, 0.267, 10.0), (0.37, 0.44, 10.0), (0.38, 0.73, 10.0), (0.39, 0.87, 10.0), (0.4, 0.942, 10.0), (0.404, 0.955, 10.0), (0.42, 0.978, 10.0), (0.436, 0.986, 10.0), (0.46, 0.99, 10.0), (0.5, 0.994, 10.0), (0.546, 0.997, 10.0), (0.58, 0.997, 10.0), (0.62, 0.996, 10.0), (0.66, 0.996, 10.0), (0.7, 0.997, 10.0), (1.06, 0.999, 10.0), (1.529, 0.996, 10.0), (1.97, 0.973, 10.0), (2.325, 0.917, 10.0)], 1.613318, -1.0, -1.0, 0, 36.219537, 0, 2.69, 0)
const S_FSL5M = Glass(GlassID(AGF, 924), 1, 2.181284, -0.009793636, 0.007745517, 0.0005928784, -7.340057e-5, 4.012184e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, -5.246e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, 0.0022, 2.0, -1.0, 9.0, 1.5, 0, 3.0, [(0.27, 0.0, 10.0), (0.28, 0.13, 10.0), (0.29, 0.43, 10.0), (0.3, 0.73, 10.0), (0.31, 0.89, 10.0), (0.32, 0.961, 10.0), (0.334, 0.988, 10.0), (0.35, 0.995, 10.0), (0.365, 0.997, 10.0), (0.37, 0.998, 10.0), (0.38, 0.998, 10.0), (0.39, 0.999, 10.0), (0.4, 0.999, 10.0), (0.404, 0.999, 10.0), (0.42, 0.999, 10.0), (0.436, 0.999, 10.0), (0.46, 0.999, 10.0), (0.5, 0.999, 10.0), (0.546, 0.999, 10.0), (0.58, 0.999, 10.0), (0.62, 0.999, 10.0), (0.66, 0.999, 10.0), (0.7, 0.999, 10.0), (1.06, 0.998, 10.0), (1.529, 0.989, 10.0), (1.97, 0.974, 10.0), (2.325, 0.864, 10.0)], 1.484524, -1.0, -1.0, 0, 69.75631, 0, 2.46, 0)
const L_LAM69M = Glass(GlassID(AGF, 925), 1, 2.909506, -0.01373872, 0.02464585, 0.001285182, -9.119209e-5, 8.193186e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, 3.17e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0031, 3.1, -1.0, 8.6, 2.0, 0, 52.2, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.0, 10.0), (0.3, 0.0, 10.0), (0.31, 0.0, 10.0), (0.32, 0.0, 10.0), (0.334, 0.0, 10.0), (0.35, 0.22, 10.0), (0.365, 0.612, 10.0), (0.37, 0.7, 10.0), (0.38, 0.81, 10.0), (0.39, 0.87, 10.0), (0.4, 0.912, 10.0), (0.404, 0.925, 10.0), (0.42, 0.95, 10.0), (0.436, 0.962, 10.0), (0.46, 0.974, 10.0), (0.5, 0.989, 10.0), (0.546, 0.996, 10.0), (0.58, 0.995, 10.0), (0.62, 0.994, 10.0), (0.66, 0.996, 10.0), (0.7, 0.998, 10.0), (1.06, 0.0, 10.0), (1.529, 0.0, 10.0), (1.97, 0.0, 10.0), (2.325, 0.0, 10.0)], 1.727801, -1.0, -1.0, 0, 40.364781, 0, 3.24, 0)
const K_VC79M = Glass(GlassID(AGF, 926), 1, 2.537275, -0.01000404, 0.01614788, -8.966925e-5, 4.332545e-5, -1.820481e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 1.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0076, -1.0, -1.0, 9.6, 2.0, 0, -1.0, [(0.27, 0.022, 10.0), (0.28, 0.103, 10.0), (0.29, 0.26, 10.0), (0.3, 0.378, 10.0), (0.31, 0.669, 10.0), (0.32, 0.799, 10.0), (0.334, 0.891, 10.0), (0.35, 0.964, 10.0), (0.365, 0.978, 10.0), (0.37, 0.982, 10.0), (0.38, 0.985, 10.0), (0.39, 0.985, 10.0), (0.4, 0.985, 10.0), (0.404, 0.985, 10.0), (0.42, 0.985, 10.0), (0.436, 0.988, 10.0), (0.46, 0.99, 10.0), (0.5, 0.992, 10.0), (0.546, 0.994, 10.0), (0.58, 0.994, 10.0), (0.62, 0.994, 10.0), (0.66, 0.997, 10.0), (0.7, 0.997, 10.0), (1.06, 0.998, 10.0), (1.529, 0.997, 10.0), (1.97, 0.978, 10.0), (2.325, 0.0, 10.0)], 1.606477, -1.0, -1.0, 0, 57.60776, 0, 3.09, 0)
const S_BAL42M = Glass(GlassID(AGF, 927), 1, 2.460903, -0.01005747, 0.01237562, 0.0007373934, -7.773189e-5, 4.239549e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, 4.545e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.002, 1.0, -1.0, 6.6, 1.5, 0, 1.2, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.03, 10.0), (0.3, 0.15, 10.0), (0.31, 0.36, 10.0), (0.32, 0.58, 10.0), (0.334, 0.8, 10.0), (0.35, 0.932, 10.0), (0.365, 0.973, 10.0), (0.37, 0.979, 10.0), (0.38, 0.986, 10.0), (0.39, 0.991, 10.0), (0.4, 0.993, 10.0), (0.404, 0.994, 10.0), (0.42, 0.995, 10.0), (0.436, 0.995, 10.0), (0.46, 0.996, 10.0), (0.5, 0.998, 10.0), (0.546, 0.999, 10.0), (0.58, 0.998, 10.0), (0.62, 0.998, 10.0), (0.66, 0.998, 10.0), (0.7, 0.998, 10.0), (1.06, 0.997, 10.0), (1.529, 0.993, 10.0), (1.97, 0.978, 10.0), (2.325, 0.881, 10.0)], 1.580467, -1.0, -1.0, 0, 59.002542, 0, 3.19, 0)
const S_LAH53M = Glass(GlassID(AGF, 928), 1, 3.156971, -0.01480577, 0.02790549, 0.00158408, -0.00012293, 9.591655e-6, 0.0, 0.0, 0.0, 0.0, 0.365, 2.325, 8.688e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 25.0, -0.0052, 2.0, -1.0, 5.9, 1.0, 0, 4.2, [(0.27, 0.0, 10.0), (0.28, 0.0, 10.0), (0.29, 0.0, 10.0), (0.3, 0.0, 10.0), (0.31, 0.0, 10.0), (0.32, 0.0, 10.0), (0.334, 0.0, 10.0), (0.35, 0.38, 10.0), (0.365, 0.741, 10.0), (0.37, 0.8, 10.0), (0.38, 0.88, 10.0), (0.39, 0.925, 10.0), (0.4, 0.947, 10.0), (0.404, 0.954, 10.0), (0.42, 0.969, 10.0), (0.436, 0.977, 10.0), (0.46, 0.985, 10.0), (0.5, 0.993, 10.0), (0.546, 0.996, 10.0), (0.58, 0.996, 10.0), (0.62, 0.996, 10.0), (0.66, 0.997, 10.0), (0.7, 0.998, 10.0), (1.06, 0.999, 10.0), (1.529, 0.996, 10.0), (1.97, 0.973, 10.0), (2.325, 0.813, 10.0)], 1.801019, -1.0, -1.0, 0, 40.721498, 0, 4.43, 0)
end # module