#! format: off

requires_initialization(::AbstractThermalFormulation) = false
requires_initialization(::AbstractThermalUnitCommitment) = true
requires_initialization(::ThermalStandardDispatch) = true
requires_initialization(::ThermalBasicCompactUnitCommitment) = false
requires_initialization(::ThermalBasicUnitCommitment) = false

get_variable_multiplier(_, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = 1.0
get_variable_multiplier(::OnVariable, d::PSY.ThermalGen, ::Union{AbstractCompactUnitCommitment, ThermalCompactDispatch}) = PSY.get_active_power_limits(d).min
get_expression_type_for_reserve(::ActivePowerReserveVariable, ::Type{<:PSY.ThermalGen}, ::Type{<:PSY.Reserve{PSY.ReserveUp}}) = ActivePowerRangeExpressionUB
get_expression_type_for_reserve(::ActivePowerReserveVariable, ::Type{<:PSY.ThermalGen}, ::Type{<:PSY.Reserve{PSY.ReserveDown}}) = ActivePowerRangeExpressionLB

############## ActivePowerVariable, ThermalGen ####################
get_variable_binary(::ActivePowerVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false
get_variable_warm_start_value(::ActivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power(d)
get_variable_lower_bound(::ActivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).min
get_variable_lower_bound(::ActivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalUnitCommitment) = 0.0
get_variable_upper_bound(::ActivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).max
get_variable_lower_bound(::ActivePowerVariable, d::PSY.ThermalGen, ::ThermalDispatchNoMin) = 0.0

############## PowerAboveMinimumVariable, ThermalGen ####################
get_variable_binary(::PowerAboveMinimumVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false
get_variable_warm_start_value(::PowerAboveMinimumVariable, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = max(0.0, PSY.get_active_power(d) - PSY.get_active_power_limits(d).min)
get_variable_lower_bound(::PowerAboveMinimumVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0
get_variable_upper_bound(::PowerAboveMinimumVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).max - PSY.get_active_power_limits(d).min

############## ReactivePowerVariable, ThermalGen ####################
get_variable_binary(::ReactivePowerVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false
get_variable_warm_start_value(::ReactivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_reactive_power(d)
get_variable_lower_bound(::ReactivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).min
get_variable_upper_bound(::ReactivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).max

############## OnVariable, ThermalGen ####################
get_variable_binary(::OnVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = true
get_variable_warm_start_value(::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_status(d) ? 1.0 : 0.0

############## StopVariable, ThermalGen ####################
get_variable_binary(::StopVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = true
get_variable_lower_bound(::StopVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0
get_variable_upper_bound(::StopVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0

############## StartVariable, ThermalGen ####################
get_variable_binary(::StartVariable, d::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = true
get_variable_lower_bound(::StartVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0
get_variable_upper_bound(::StartVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0

############## ColdStartVariable, WarmStartVariable, HotStartVariable ############
get_variable_binary(::Union{ColdStartVariable, WarmStartVariable, HotStartVariable}, ::Type{PSY.ThermalMultiStart}, ::AbstractThermalFormulation) = true

########################### Parameter related set functions ################################
get_parameter_multiplier(::VariableValueParameter, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0
get_initial_parameter_value(::VariableValueParameter, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0
get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionUB, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).max
get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionLB, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).min
get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionUB, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = PSY.get_active_power_limits(d).max - PSY.get_active_power_limits(d).min
get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionLB, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = 0.0
get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionUB, d::PSY.ThermalGen, ::ThermalCompactDispatch) = PSY.get_active_power_limits(d).max - PSY.get_active_power_limits(d).min
get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionLB, d::PSY.ThermalGen, ::ThermalCompactDispatch) = 0.0
get_expression_multiplier(::OnStatusParameter, ::ActivePowerBalance, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).min

#################### Initial Conditions for models ###############
initial_condition_default(::DeviceStatus, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_status(d)
initial_condition_variable(::DeviceStatus, d::PSY.ThermalGen, ::AbstractThermalFormulation) = OnVariable()
initial_condition_default(::DevicePower, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power(d)
initial_condition_variable(::DevicePower, d::PSY.ThermalGen, ::AbstractThermalFormulation) = ActivePowerVariable()
initial_condition_default(::DeviceAboveMinPower, d::PSY.ThermalGen, ::AbstractThermalFormulation) = max(0.0, PSY.get_active_power(d) - PSY.get_active_power_limits(d).min)
initial_condition_variable(::DeviceAboveMinPower, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = PowerAboveMinimumVariable()
initial_condition_variable(::DeviceAboveMinPower, d::PSY.ThermalGen, ::ThermalCompactDispatch) = PowerAboveMinimumVariable()
initial_condition_default(::InitialTimeDurationOn, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_status(d) ? PSY.get_time_at_status(d) : 0.0
initial_condition_variable(::InitialTimeDurationOn, d::PSY.ThermalGen, ::AbstractThermalFormulation) = OnVariable()
initial_condition_default(::InitialTimeDurationOff, d::PSY.ThermalGen, ::AbstractThermalFormulation) = !PSY.get_status(d) ? PSY.get_time_at_status(d) : 0.0
initial_condition_variable(::InitialTimeDurationOff, d::PSY.ThermalGen, ::AbstractThermalFormulation) = OnVariable()

########################Objective Function##################################################
proportional_cost(cost::PSY.OperationalCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_fixed(cost)
proportional_cost(cost::PSY.OperationalCost, S::OnVariable, T::PSY.ThermalGen, U::AbstractCompactUnitCommitment) = no_load_cost(cost, S, T, U)  + PSY.get_fixed(cost)
proportional_cost(cost::PSY.MarketBidCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_no_load(cost)
proportional_cost(cost::PSY.MarketBidCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractCompactUnitCommitment)=PSY.get_no_load(cost)
proportional_cost(cost::PSY.MultiStartCost, ::OnVariable, ::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=PSY.get_fixed(cost) + PSY.get_no_load(cost)

has_multistart_variables(::PSY.ThermalGen, ::AbstractThermalFormulation)=false
has_multistart_variables(::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=true

objective_function_multiplier(::VariableType, ::AbstractThermalFormulation)=OBJECTIVE_FUNCTION_POSITIVE

shut_down_cost(cost::PSY.OperationalCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_shut_down(cost)
shut_down_cost(cost::PSY.TwoPartCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=0.0

sos_status(::PSY.ThermalGen, ::AbstractThermalDispatchFormulation)=SOSStatusVariable.NO_VARIABLE
sos_status(::PSY.ThermalGen, ::AbstractThermalUnitCommitment)=SOSStatusVariable.VARIABLE
sos_status(::PSY.ThermalMultiStart, ::AbstractStandardUnitCommitment)=SOSStatusVariable.VARIABLE
sos_status(::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=SOSStatusVariable.VARIABLE

start_up_cost(cost::PSY.OperationalCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_start_up(cost)
start_up_cost(cost::PSY.TwoPartCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=0.0
start_up_cost(cost::PSY.MultiStartCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=maximum(PSY.get_start_up(cost))
start_up_cost(cost::PSY.MultiStartCost, ::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=PSY.get_start_up(cost)
start_up_cost(cost::PSY.MarketBidCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=maximum(PSY.get_start_up(cost))
start_up_cost(cost::PSY.MarketBidCost, ::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=PSY.get_start_up(cost)
# If the formulation used ignores start up costs, the model ignores that data.
start_up_cost(cost::PSY.MarketBidCost, ::PSY.ThermalMultiStart, ::AbstractThermalFormulation)=maximum(PSY.get_start_up(cost))

uses_compact_power(::PSY.ThermalGen, ::AbstractThermalFormulation)=false
uses_compact_power(::PSY.ThermalGen, ::AbstractCompactUnitCommitment )=true
uses_compact_power(::PSY.ThermalGen, ::ThermalCompactDispatch)=true

variable_cost(cost::PSY.OperationalCost, ::ActivePowerVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_variable(cost)
variable_cost(cost::PSY.OperationalCost, ::PowerAboveMinimumVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_variable(cost)

no_load_cost(cost::PSY.MultiStartCost, ::OnVariable, ::PSY.ThermalMultiStart, U::AbstractThermalFormulation) = PSY.get_no_load(cost)
function no_load_cost(cost::Union{PSY.ThreePartCost, PSY.TwoPartCost}, S::OnVariable, T::PSY.ThermalGen, U::AbstractThermalFormulation)
    return no_load_cost(PSY.get_variable(cost), S, T, U)
end
no_load_cost(cost::PSY.VariableCost{Vector{NTuple{2, Float64}}}, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation) = first(PSY.get_cost(cost))[1]
no_load_cost(cost::PSY.VariableCost{Float64}, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_cost(cost) * PSY.get_active_power_limits(d).min * PSY.get_base_power(d)
function no_load_cost(cost::PSY.VariableCost{Tuple{Float64, Float64}}, ::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation)
    return (PSY.get_cost(cost)[1] * (PSY.get_active_power_limits(d).min)^2 + PSY.get_cost(cost)[2] * PSY.get_active_power_limits(d).min)* PSY.get_base_power(d)
end
    #! format: on
function get_initial_conditions_device_model(
    model::OperationModel,
    ::DeviceModel{T, D},
) where {T <: PSY.ThermalGen, D <: AbstractThermalDispatchFormulation}
    if supports_milp(get_optimization_container(model))
        return DeviceModel(T, ThermalBasicUnitCommitment)
    else
        return DeviceModel(T, ThermalBasicDispatch)
    end
end

function get_initial_conditions_device_model(
    ::OperationModel,
    ::DeviceModel{T, D},
) where {T <: PSY.ThermalGen, D <: ThermalDispatchNoMin}
    return DeviceModel(T, ThermalDispatchNoMin)
end

function get_initial_conditions_device_model(
    ::OperationModel,
    ::DeviceModel{T, D},
) where {T <: PSY.ThermalGen, D <: AbstractThermalUnitCommitment}
    return DeviceModel(T, ThermalBasicUnitCommitment)
end

function get_initial_conditions_device_model(
    ::OperationModel,
    ::DeviceModel{T, D},
) where {T <: PSY.ThermalGen, D <: AbstractCompactUnitCommitment}
    return DeviceModel(T, ThermalBasicCompactUnitCommitment)
end

function get_default_time_series_names(
    ::Type{U},
    ::Type{V},
) where {U <: PSY.ThermalGen, V <: Union{FixedOutput, AbstractThermalFormulation}}
    return Dict{Type{<:TimeSeriesParameter}, String}()
end

function get_default_attributes(
    ::Type{U},
    ::Type{V},
) where {U <: PSY.ThermalGen, V <: Union{FixedOutput, AbstractThermalFormulation}}
    return Dict{String, Any}()
end

######## THERMAL GENERATION CONSTRAINTS ############

# active power limits of generators when there are no CommitmentVariables
"""
Min and max active power limits of generators for thermal dispatch formulations
"""
function get_min_max_limits(
    device,
    ::Type{ActivePowerVariableLimitsConstraint},
    ::Type{<:AbstractThermalDispatchFormulation},
)
    return PSY.get_active_power_limits(device)
end

# active power limits of generators when there are CommitmentVariables
"""
Min and max active power limits of generators for thermal unit commitment formulations
"""
function get_min_max_limits(
    device,
    ::Type{ActivePowerVariableLimitsConstraint},
    ::Type{<:AbstractThermalUnitCommitment},
)
    return PSY.get_active_power_limits(device)
end

"""
Range constraints for thermal compact dispatch
"""
function add_constraints!(
    container::OptimizationContainer,
    T::Type{<:PowerVariableLimitsConstraint},
    U::Type{<:Union{PowerAboveMinimumVariable, ExpressionType}},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    X::Type{<:PM.AbstractPowerModel},
) where {V <: PSY.ThermalGen, W <: ThermalCompactDispatch}
    if !has_semicontinuous_feedforward(model, PowerAboveMinimumVariable)
        add_range_constraints!(container, T, U, devices, model, X)
    end
    return
end

"""
Min and max active power limits of generators for thermal dispatch compact formulations
"""
function get_min_max_limits(
    device,
    ::Type{ActivePowerVariableLimitsConstraint},
    ::Type{ThermalCompactDispatch},
)
    return (
        min=0.0,
        max=PSY.get_active_power_limits(device).max -
            PSY.get_active_power_limits(device).min,
    )
end

"""
Min and max active power limits of generators for thermal dispatch no minimum formulations
"""
function get_min_max_limits(
    device,
    ::Type{ActivePowerVariableLimitsConstraint},
    ::Type{ThermalDispatchNoMin},
)
    return (min=0.0, max=PSY.get_active_power_limits(device).max)
end

"""
Semicontinuous range constraints for thermal dispatch formulations
"""
function add_constraints!(
    container::OptimizationContainer,
    T::Type{<:PowerVariableLimitsConstraint},
    U::Type{<:Union{VariableType, ExpressionType}},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    X::Type{<:PM.AbstractPowerModel},
) where {V <: PSY.ThermalGen, W <: AbstractThermalDispatchFormulation}
    if !has_semicontinuous_feedforward(model, U)
        add_range_constraints!(container, T, U, devices, model, X)
    end
    return
end

"""
Min and max active power limits for multi-start unit commitment formulations
"""
function get_min_max_limits(
    device,
    ::Type{ActivePowerVariableLimitsConstraint},
    ::Type{<:ThermalMultiStartUnitCommitment},
) #  -> Union{Nothing, NamedTuple{(:startup, :shutdown), Tuple{Float64, Float64}}}
    return (
        min=0.0,
        max=PSY.get_active_power_limits(device).max -
            PSY.get_active_power_limits(device).min,
    )
end

"""
Semicontinuous range constraints for unit commitment formulations
"""
function add_constraints!(
    container::OptimizationContainer,
    T::Type{<:PowerVariableLimitsConstraint},
    U::Type{<:Union{VariableType, ExpressionType}},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    X::Type{<:PM.AbstractPowerModel},
) where {V <: PSY.ThermalGen, W <: AbstractThermalUnitCommitment}
    add_semicontinuous_range_constraints!(container, T, U, devices, model, X)
    return
end

"""
Startup and shutdown active power limits for Compact Unit Commitment
"""
function get_startup_shutdown_limits(
    device,
    ::Type{ActivePowerVariableLimitsConstraint},
    ::Type{<:ThermalMultiStartUnitCommitment},
)
    return PSY.get_power_trajectory(device)
end

"""
Min and Max active power limits for Compact Unit Commitment
"""
function get_min_max_limits(
    device,
    ::Type{ActivePowerVariableLimitsConstraint},
    ::Type{<:AbstractCompactUnitCommitment},
) #  -> Union{Nothing, NamedTuple{(:startup, :shutdown), Tuple{Float64, Float64}}}
    return (
        min=0,
        max=PSY.get_active_power_limits(device).max -
            PSY.get_active_power_limits(device).min,
    )
end

"""
Startup shutdown limits for Compact Unit Commitment
"""
function get_startup_shutdown_limits(
    device,
    ::Type{ActivePowerVariableLimitsConstraint},
    ::Type{<:AbstractCompactUnitCommitment},
)
    return (
        startup=PSY.get_active_power_limits(device).max,
        shutdown=PSY.get_active_power_limits(device).max,
    )
end

function _get_data_for_range_ic(
    initial_conditions_power::Vector{<:InitialCondition},
    initial_conditions_status::Vector{<:InitialCondition},
)
    lenght_devices_power = length(initial_conditions_power)
    lenght_devices_status = length(initial_conditions_status)
    IS.@assert_op lenght_devices_power == lenght_devices_status
    ini_conds = Matrix{InitialCondition}(undef, lenght_devices_power, 2)
    idx = 0
    for (ix, ic) in enumerate(initial_conditions_power)
        g = get_component(ic)
        IS.@assert_op g == get_component(initial_conditions_status[ix])
        idx += 1
        ini_conds[idx, 1] = ic
        ini_conds[idx, 2] = initial_conditions_status[ix]
    end
    return ini_conds
end

"""
This function adds range constraint for the first time period. Constraint (10) from PGLIB formulation
"""
function add_constraints!(
    container::OptimizationContainer,
    T::Type{<:ActivePowerVariableLimitsConstraint},
    U::Type{<:VariableType},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    X::Type{<:PM.AbstractPowerModel},
) where {V <: PSY.ThermalMultiStart, W <: ThermalMultiStartUnitCommitment}
    time_steps = get_time_steps(container)
    constraint_type = T()
    variable_type = U()
    component_type = V
    varp = get_variable(container, variable_type, component_type)
    varstatus = get_variable(container, OnVariable(), component_type)
    varon = get_variable(container, StartVariable(), component_type)
    varoff = get_variable(container, StopVariable(), component_type)

    names = [PSY.get_name(x) for x in devices]
    con_on = add_constraints_container!(
        container,
        constraint_type,
        component_type,
        names,
        time_steps,
        meta="on",
    )
    con_off = add_constraints_container!(
        container,
        constraint_type,
        component_type,
        names,
        time_steps[1:(end - 1)],
        meta="off",
    )
    con_lb = add_constraints_container!(
        container,
        constraint_type,
        component_type,
        names,
        time_steps,
        meta="lb",
    )

    for device in devices, t in time_steps
        name = PSY.get_name(device)
        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type
        startup_shutdown_limits = get_startup_shutdown_limits(device, T, W)
        if JuMP.has_lower_bound(varp[name, t])
            JuMP.set_lower_bound(varp[name, t], 0.0)
        end

        con_on[name, t] = JuMP.@constraint(
            container.JuMPmodel,
            varp[name, t] <=
            (limits.max - limits.min) * varstatus[name, t] -
            max(limits.max - startup_shutdown_limits.startup, 0.0) * varon[name, t]
        )

        con_lb[name, t] = JuMP.@constraint(container.JuMPmodel, varp[name, t] >= 0.0)

        if t == length(time_steps)
            continue
        else
            con_off[name, t] = JuMP.@constraint(
                container.JuMPmodel,
                varp[name, t] <=
                (limits.max - limits.min) * varstatus[name, t] -
                max(limits.max - startup_shutdown_limits.shutdown, 0.0) *
                varoff[name, t + 1]
            )
        end
    end
    return
end

function add_constraints!(
    container::OptimizationContainer,
    T::Type{<:ActivePowerVariableLimitsConstraint},
    U::Type{ActivePowerRangeExpressionLB},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    X::Type{<:PM.AbstractPowerModel},
) where {V <: PSY.ThermalMultiStart, W <: ThermalMultiStartUnitCommitment}
    time_steps = get_time_steps(container)
    constraint_type = T()
    expression_type = U()
    component_type = V
    expression_products = get_expression(container, expression_type, component_type)
    varp = get_variable(container, PowerAboveMinimumVariable(), component_type)

    names = [PSY.get_name(x) for x in devices]
    con_lb = add_constraints_container!(
        container,
        constraint_type,
        component_type,
        names,
        time_steps,
        meta="lb",
    )

    for device in devices, t in time_steps
        name = PSY.get_name(device)
        if JuMP.has_lower_bound(varp[name, t])
            JuMP.set_lower_bound(varp[name, t], 0.0)
        end
        con_lb[name, t] =
            JuMP.@constraint(container.JuMPmodel, expression_products[name, t] >= 0)
    end
    return
end

function add_constraints!(
    container::OptimizationContainer,
    T::Type{<:ActivePowerVariableLimitsConstraint},
    U::Type{ActivePowerRangeExpressionUB},
    devices::IS.FlattenIteratorWrapper{V},
    model::DeviceModel{V, W},
    X::Type{<:PM.AbstractPowerModel},
) where {V <: PSY.ThermalMultiStart, W <: ThermalMultiStartUnitCommitment}
    time_steps = get_time_steps(container)
    constraint_type = T()
    expression_type = U()
    component_type = V
    expression_products = get_expression(container, expression_type, component_type)
    varstatus = get_variable(container, OnVariable(), component_type)
    varon = get_variable(container, StartVariable(), component_type)
    varoff = get_variable(container, StopVariable(), component_type)
    varp = get_variable(container, PowerAboveMinimumVariable(), component_type)

    names = [PSY.get_name(x) for x in devices]
    con_on = add_constraints_container!(
        container,
        constraint_type,
        component_type,
        names,
        time_steps,
        meta="ubon",
    )
    con_off = add_constraints_container!(
        container,
        constraint_type,
        component_type,
        names,
        time_steps[1:(end - 1)],
        meta="uboff",
    )

    for device in devices, t in time_steps
        name = PSY.get_name(device)
        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type
        startup_shutdown_limits = get_startup_shutdown_limits(device, T, W)
        if JuMP.has_lower_bound(varp[name, t])
            JuMP.set_lower_bound(varp[name, t], 0.0)
        end
        con_on[name, t] = JuMP.@constraint(
            container.JuMPmodel,
            expression_products[name, t] <=
            (limits.max - limits.min) * varstatus[name, t] -
            max(limits.max - startup_shutdown_limits.startup, 0) * varon[name, t]
        )
        if t == length(time_steps)
            continue
        else
            con_off[name, t] = JuMP.@constraint(
                container.JuMPmodel,
                expression_products[name, t] <=
                (limits.max - limits.min) * varstatus[name, t] -
                max(limits.max - startup_shutdown_limits.shutdown, 0) * varoff[name, t + 1]
            )
        end
    end
    return
end

function add_constraints!(
    container::OptimizationContainer,
    ::Type{ActiveRangeICConstraint},
    devices::IS.FlattenIteratorWrapper{T},
    model::DeviceModel{T, S},
    W::Type{<:PM.AbstractPowerModel},
) where {T <: PSY.ThermalGen, S <: AbstractCompactUnitCommitment}
    initial_conditions_power = get_initial_condition(container, DeviceAboveMinPower(), T)
    initial_conditions_status = get_initial_condition(container, DeviceStatus(), T)
    ini_conds = _get_data_for_range_ic(initial_conditions_power, initial_conditions_status)

    if !isempty(ini_conds)
        varstop = get_variable(container, StopVariable(), T)
        set_name = [PSY.get_name(d) for d in devices]
        con = add_constraints_container!(container, ActiveRangeICConstraint(), T, set_name)

        for (ix, ic) in enumerate(ini_conds[:, 1])
            name = get_component_name(ic)
            device = get_component(ic)
            limits = PSY.get_active_power_limits(device)
            lag_ramp_limits = PSY.get_power_trajectory(device)
            val = max(limits.max - lag_ramp_limits.shutdown, 0)
            con[name] = JuMP.@constraint(
                container.JuMPmodel,
                val * varstop[name, 1] <=
                ini_conds[ix, 2].value * (limits.max - limits.min) - get_value(ic)
            )
        end
    else
        @warn "Data doesn't contain generators with ramp limits, consider adjusting your formulation"
    end
    return
end

"""
Reactive power limits of generators for all dispatch formulations
"""
function get_min_max_limits(
    device,
    ::Type{ReactivePowerVariableLimitsConstraint},
    ::Type{<:AbstractThermalDispatchFormulation},
)
    return PSY.get_reactive_power_limits(device)
end

"""
Reactive power limits of generators when there CommitmentVariables
"""
function get_min_max_limits(
    device,
    ::Type{ReactivePowerVariableLimitsConstraint},
    ::Type{<:AbstractThermalUnitCommitment},
)
    return PSY.get_reactive_power_limits(device)
end

function add_constraints!(
    container::OptimizationContainer,
    T::Type{CommitmentConstraint},
    devices::IS.FlattenIteratorWrapper{U},
    model::DeviceModel{U, V},
    W::Type{<:PM.AbstractPowerModel},
) where {U <: PSY.ThermalGen, V <: AbstractThermalUnitCommitment}
    time_steps = get_time_steps(container)
    varstart = get_variable(container, StartVariable(), U)
    varstop = get_variable(container, StopVariable(), U)
    varon = get_variable(container, OnVariable(), U)
    names = axes(varstart, 1)
    initial_conditions = get_initial_condition(container, DeviceStatus(), U)
    constraint =
        add_constraints_container!(container, CommitmentConstraint(), U, names, time_steps)
    aux_constraint = add_constraints_container!(
        container,
        CommitmentConstraint(),
        U,
        names,
        time_steps,
        meta="aux",
    )

    for ic in initial_conditions
        name = PSY.get_name(get_component(ic))
        constraint[name, 1] = JuMP.@constraint(
            container.JuMPmodel,
            varon[name, 1] == get_value(ic) + varstart[name, 1] - varstop[name, 1]
        )
        aux_constraint[name, 1] = JuMP.@constraint(
            container.JuMPmodel,
            varstart[name, 1] + varstop[name, 1] <= 1.0
        )
    end

    for t in time_steps[2:end], ic in initial_conditions
        name = get_component_name(ic)
        constraint[name, t] = JuMP.@constraint(
            container.JuMPmodel,
            varon[name, t] == varon[name, t - 1] + varstart[name, t] - varstop[name, t]
        )
        aux_constraint[name, t] = JuMP.@constraint(
            container.JuMPmodel,
            varstart[name, t] + varstop[name, t] <= 1.0
        )
    end
    return
end

########################## Make initial Conditions for a Model #############################
function initial_conditions!(
    container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    formulation::AbstractThermalUnitCommitment,
) where {T <: PSY.ThermalGen}
    add_initial_condition!(container, devices, formulation, DeviceStatus())
    add_initial_condition!(container, devices, formulation, DevicePower())
    add_initial_condition!(container, devices, formulation, InitialTimeDurationOn())
    add_initial_condition!(container, devices, formulation, InitialTimeDurationOff())

    return
end

function initial_conditions!(
    container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    formulation::AbstractCompactUnitCommitment,
) where {T <: PSY.ThermalGen}
    add_initial_condition!(container, devices, formulation, DeviceStatus())
    add_initial_condition!(container, devices, formulation, DeviceAboveMinPower())
    add_initial_condition!(container, devices, formulation, InitialTimeDurationOn())
    add_initial_condition!(container, devices, formulation, InitialTimeDurationOff())

    return
end

function initial_conditions!(
    container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    formulation::Union{ThermalBasicUnitCommitment, ThermalBasicCompactUnitCommitment},
) where {T <: PSY.ThermalGen}
    add_initial_condition!(container, devices, formulation, DeviceStatus())
    return
end

function initial_conditions!(
    container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    formulation::AbstractThermalDispatchFormulation,
) where {T <: PSY.ThermalGen}
    add_initial_condition!(container, devices, formulation, DevicePower())
    return
end

function initial_conditions!(
    container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    formulation::ThermalCompactDispatch,
) where {T <: PSY.ThermalGen}
    add_initial_condition!(container, devices, formulation, DeviceAboveMinPower())
    return
end
############################ Auxiliary Variables Calculation ################################
function calculate_aux_variable_value!(
    container::OptimizationContainer,
    ::AuxVarKey{TimeDurationOn, T},
    ::PSY.System,
) where {T <: PSY.ThermalGen}
    on_variable_results = get_variable(container, OnVariable(), T)
    aux_variable_container = get_aux_variable(container, TimeDurationOn(), T)
    ini_cond = get_initial_condition(container, InitialTimeDurationOn(), T)

    time_steps = get_time_steps(container)

    for ix in eachindex(JuMP.axes(aux_variable_container)[1])
        IS.@assert_op JuMP.axes(aux_variable_container)[1][ix] ==
                      JuMP.axes(on_variable_results)[1][ix]
        IS.@assert_op JuMP.axes(aux_variable_container)[1][ix] ==
                      get_component_name(ini_cond[ix])
        on_var = jump_value.(on_variable_results.data[ix, :])
        ini_cond_value = get_condition(ini_cond[ix])
        aux_variable_container.data[ix, :] .= ini_cond_value
        sum_on_var = sum(on_var)
        if sum_on_var == time_steps[end] # Unit was always on
            aux_variable_container.data[ix, :] += time_steps
        elseif sum_on_var == 0.0 # Unit was always off
            aux_variable_container.data[ix, :] .= 0.0
        else
            previous_condition = ini_cond_value
            for (t, v) in enumerate(on_var)
                if v < 0.99 # Unit turn off
                    time_value = 0.0
                elseif isapprox(v, 1.0; atol=ABSOLUTE_TOLERANCE) # Unit is on
                    time_value = previous_condition + 1.0
                else
                    error("Binary condition returned $v")
                end
                previous_condition = aux_variable_container.data[ix, t] = time_value
            end
        end
    end

    return
end

function calculate_aux_variable_value!(
    container::OptimizationContainer,
    ::AuxVarKey{TimeDurationOff, T},
    ::PSY.System,
) where {T <: PSY.ThermalGen}
    on_variable_results = get_variable(container, OnVariable(), T)
    aux_variable_container = get_aux_variable(container, TimeDurationOff(), T)
    ini_cond = get_initial_condition(container, InitialTimeDurationOff(), T)

    time_steps = get_time_steps(container)
    for ix in eachindex(JuMP.axes(aux_variable_container)[1])
        IS.@assert_op JuMP.axes(aux_variable_container)[1][ix] ==
                      JuMP.axes(on_variable_results)[1][ix]
        IS.@assert_op JuMP.axes(aux_variable_container)[1][ix] ==
                      get_component_name(ini_cond[ix])
        on_var = jump_value.(on_variable_results.data[ix, :])
        ini_cond_value = get_condition(ini_cond[ix])
        aux_variable_container.data[ix, :] .= ini_cond_value
        sum_on_var = sum(on_var)
        if sum_on_var == time_steps[end] # Unit was always on
            aux_variable_container.data[ix, :] .= 0.0
        elseif sum_on_var == 0.0 # Unit was always off
            aux_variable_container.data[ix, :] += time_steps
        else
            previous_condition = ini_cond_value
            for (t, v) in enumerate(on_var)
                if v < 0.99 # Unit turn off
                    time_value = previous_condition + 1.0
                elseif isapprox(v, 1.0; atol=ABSOLUTE_TOLERANCE) # Unit is on
                    time_value = 0.0
                else
                    error("Binary condition returned $v")
                end
                previous_condition = aux_variable_container.data[ix, t] = time_value
            end
        end
    end

    return
end

function calculate_aux_variable_value!(
    container::OptimizationContainer,
    ::AuxVarKey{PowerOutput, T},
    system::PSY.System,
) where {T <: PSY.ThermalGen}
    devices = get_available_components(T, system)
    time_steps = get_time_steps(container)
    if has_container_key(container, OnVariable, T)
        on_variable_results = get_variable(container, OnVariable(), T)
    elseif has_container_key(container, OnStatusParameter, T)
        on_variable_results = get_parameter_array(container, OnStatusParameter(), T)
    else
        error(
            "$T formulation is NOT supported without a Feedforward for CommitmentDecisions,
      please consider changing your simulation setup or adding a SemiContinuousFeedforward.",
        )
    end
    p_variable_results = get_variable(container, PowerAboveMinimumVariable(), T)
    aux_variable_container = get_aux_variable(container, PowerOutput(), T)
    for d in devices, t in time_steps
        name = PSY.get_name(d)
        min = PSY.get_active_power_limits(d).min
        aux_variable_container[name, t] =
            jump_value(on_variable_results[name, t]) * min +
            jump_value(p_variable_results[name, t])
    end

    return
end
########################### Ramp/Rate of Change Constraints ################################
"""
This function gets the data for the generators for ramping constraints of thermal generators
"""
_get_initial_condition_type(
    ::Type{RampConstraint},
    ::Type{<:PSY.ThermalGen},
    ::Type{<:AbstractThermalFormulation},
) = DevicePower
_get_initial_condition_type(
    ::Type{RampConstraint},
    ::Type{<:PSY.ThermalGen},
    ::Type{<:AbstractCompactUnitCommitment},
) = DeviceAboveMinPower
_get_initial_condition_type(
    ::Type{RampConstraint},
    ::Type{<:PSY.ThermalGen},
    ::Type{ThermalCompactDispatch},
) = DeviceAboveMinPower

"""
This function adds the ramping limits of generators when there are CommitmentVariables
"""
function add_constraints!(
    container::OptimizationContainer,
    T::Type{RampConstraint},
    devices::IS.FlattenIteratorWrapper{U},
    model::DeviceModel{U, V},
    W::Type{<:PM.AbstractPowerModel},
) where {U <: PSY.ThermalGen, V <: AbstractThermalUnitCommitment}
    add_semicontinuous_ramp_constraints!(
        container,
        T,
        ActivePowerVariable,
        devices,
        model,
        W,
    )
    return
end

function add_constraints!(
    container::OptimizationContainer,
    T::Type{RampConstraint},
    devices::IS.FlattenIteratorWrapper{U},
    model::DeviceModel{U, V},
    W::Type{<:PM.AbstractPowerModel},
) where {U <: PSY.ThermalGen, V <: AbstractCompactUnitCommitment}
    add_semicontinuous_ramp_constraints!(
        container,
        T,
        PowerAboveMinimumVariable,
        devices,
        model,
        W,
    )
    return
end

function add_constraints!(
    container::OptimizationContainer,
    T::Type{RampConstraint},
    devices::IS.FlattenIteratorWrapper{U},
    model::DeviceModel{U, ThermalCompactDispatch},
    W::Type{<:PM.AbstractPowerModel},
) where {U <: PSY.ThermalGen}
    add_linear_ramp_constraints!(container, T, PowerAboveMinimumVariable, devices, model, W)
    return
end

function add_constraints!(
    container::OptimizationContainer,
    T::Type{RampConstraint},
    devices::IS.FlattenIteratorWrapper{U},
    model::DeviceModel{U, V},
    W::Type{<:PM.AbstractPowerModel},
) where {U <: PSY.ThermalGen, V <: AbstractThermalDispatchFormulation}
    add_linear_ramp_constraints!(container, T, ActivePowerVariable, devices, model, W)
    return
end

function add_constraints!(
    container::OptimizationContainer,
    T::Type{RampConstraint},
    devices::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart},
    model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment},
    W::Type{<:PM.AbstractPowerModel},
)
    add_linear_ramp_constraints!(container, T, PowerAboveMinimumVariable, devices, model, W)
    return
end

########################### start up trajectory constraints ######################################

function _convert_hours_to_timesteps(
    start_times_hr::StartUpStages,
    resolution::Dates.TimePeriod,
)
    _start_times_ts = (
        round((hr * MINUTES_IN_HOUR) / Dates.value(Dates.Minute(resolution)), RoundUp) for
        hr in start_times_hr
    )
    start_times_ts = StartUpStages(_start_times_ts)
    return start_times_ts
end

@doc raw"""
Constructs contraints for different types of starts based on generator down-time

# Equations
for t in time_limits[s+1]:T

``` var_starts[name, s, t] <= sum( var_stop[name, t-i] for i in time_limits[s]:(time_limits[s+1]-1)  ```

# LaTeX

``  δ^{s}(t)  \leq \sum_{i=TS^{s}_{g}}^{TS^{s+1}_{g}} x^{stop}(t-i) ``
"""
function add_constraints!(
    container::OptimizationContainer,
    ::Type{StartupTimeLimitTemperatureConstraint},
    devices::IS.FlattenIteratorWrapper{T},
    model::DeviceModel{T, ThermalMultiStartUnitCommitment},
    ::Type{<:PM.AbstractPowerModel},
) where {T <: PSY.ThermalMultiStart}
    resolution = get_resolution(container)
    time_steps = get_time_steps(container)
    start_vars = [
        get_variable(container, HotStartVariable(), T),
        get_variable(container, WarmStartVariable(), T),
    ]
    varstop = get_variable(container, StopVariable(), T)

    names = [PSY.get_name(d) for d in devices]

    con = [
        add_constraints_container!(
            container,
            StartupTimeLimitTemperatureConstraint(),
            T,
            names,
            time_steps;
            sparse=true,
            meta="hot",
        ),
        add_constraints_container!(
            container,
            StartupTimeLimitTemperatureConstraint(),
            T,
            names,
            time_steps;
            sparse=true,
            meta="warm",
        ),
    ]

    for t in time_steps, d in devices
        name = PSY.get_name(d)
        startup_types = PSY.get_start_types(d)
        time_limits = _convert_hours_to_timesteps(PSY.get_start_time_limits(d), resolution)
        for ix in 1:(startup_types - 1)
            if t >= time_limits[ix + 1]
                con[ix][name, t] = JuMP.@constraint(
                    container.JuMPmodel,
                    start_vars[ix][name, t] <= sum(
                        varstop[name, t - i] for i in UnitRange{Int}(
                            Int(time_limits[ix]),
                            Int(time_limits[ix + 1] - 1),
                        )
                    )
                )
            end
        end
    end
    return
end

@doc raw"""

Constructs contraints that restricts devices to one type of start at a time

# Equations

``` sum(var_starts[name, s, t] for s in starts) = var_start[name, t]  ```

# LaTeX

``  \sum^{S_g}_{s=1} δ^{s}(t)  \eq  x^{start}(t) ``

"""
function add_constraints!(
    container::OptimizationContainer,
    ::Type{StartTypeConstraint},
    devices::IS.FlattenIteratorWrapper{T},
    model::DeviceModel{T, ThermalMultiStartUnitCommitment},
    ::Type{<:PM.AbstractPowerModel},
) where {T <: PSY.ThermalMultiStart}
    time_steps = get_time_steps(container)
    varstart = get_variable(container, StartVariable(), T)
    start_vars = [
        get_variable(container, HotStartVariable(), T),
        get_variable(container, WarmStartVariable(), T),
        get_variable(container, ColdStartVariable(), T),
    ]

    set_name = [PSY.get_name(d) for d in devices]
    con = add_constraints_container!(
        container,
        StartTypeConstraint(),
        T,
        set_name,
        time_steps,
    )

    for t in time_steps, d in devices
        name = PSY.get_name(d)
        startup_types = PSY.get_start_types(d)
        con[name, t] = JuMP.@constraint(
            container.JuMPmodel,
            varstart[name, t] == sum(start_vars[ix][name, t] for ix in 1:(startup_types))
        )
    end
    return
end

@doc raw"""
Constructs contraints that restricts devices to one type of start at a time

# Equations
ub:
``` (time_limits[st+1]-1)*δ^{s}(t) + (1 - δ^{s}(t)) * M_VALUE >= sum(1-varbin[name, i]) for i in 1:t) + initial_condition_offtime  ```
lb:
``` (time_limits[st]-1)*δ^{s}(t) =< sum(1-varbin[name, i]) for i in 1:t) + initial_condition_offtime  ```

# LaTeX

`` TS^{s+1}_{g} δ^{s}(t) + (1-δ^{s}(t)) M_VALUE   \geq  \sum^{t}_{i=1} x^{status}(i)  +  DT_{g}^{0}  \forall t in \{1, \ldots,  TS^{s+1}_{g}``

`` TS^{s}_{g} δ^{s}(t) \leq  \sum^{t}_{i=1} x^{status}(i)  +  DT_{g}^{0}  \forall t in \{1, \ldots,  TS^{s+1}_{g}``

"""
function add_constraints!(
    container::OptimizationContainer,
    ::Type{StartupInitialConditionConstraint},
    devices::IS.FlattenIteratorWrapper{T},
    model::DeviceModel{T, ThermalMultiStartUnitCommitment},
    ::Type{<:PM.AbstractPowerModel},
) where {T <: PSY.ThermalMultiStart}
    resolution = get_resolution(container)
    initial_conditions_offtime =
        get_initial_condition(container, InitialTimeDurationOff(), PSY.ThermalMultiStart)

    time_steps = get_time_steps(container)
    set_name = [get_component_name(ic) for ic in initial_conditions_offtime]
    varbin = get_variable(container, OnVariable(), T)
    varstarts = [
        get_variable(container, HotStartVariable(), T),
        get_variable(container, WarmStartVariable(), T),
    ]

    con_ub = add_constraints_container!(
        container,
        StartupInitialConditionConstraint(),
        T,
        set_name,
        time_steps,
        1:(MAX_START_STAGES - 1);
        sparse=true,
        meta="ub",
    )
    con_lb = add_constraints_container!(
        container,
        StartupInitialConditionConstraint(),
        T,
        set_name,
        time_steps,
        1:(MAX_START_STAGES - 1);
        sparse=true,
        meta="lb",
    )

    for t in time_steps, (ix, ic) in enumerate(initial_conditions_offtime)
        name = PSY.get_name(get_component(ic))
        startup_types = PSY.get_start_types(get_component(ic))
        time_limits = _convert_hours_to_timesteps(
            PSY.get_start_time_limits(get_component(ic)),
            resolution,
        )
        ic = initial_conditions_offtime[ix]
        for st in 1:(startup_types - 1)
            var = varstarts[st]
            if t < (time_limits[st + 1] - 1)
                con_ub[name, t, st] = JuMP.@constraint(
                    container.JuMPmodel,
                    (time_limits[st + 1] - 1) * var[name, t] +
                    (1 - var[name, t]) * M_VALUE >=
                    sum((1 - varbin[name, i]) for i in 1:t) + get_value(ic)
                )
                con_lb[name, t, st] = JuMP.@constraint(
                    container.JuMPmodel,
                    time_limits[st] * var[name, t] <=
                    sum((1 - varbin[name, i]) for i in 1:t) + get_value(ic)
                )
            end
        end
    end

    return
end

"""
This function creates constraints that keep must run devices online
"""
function add_constraints!(
    container::OptimizationContainer,
    ::Type{MustRunConstraint},
    devices::IS.FlattenIteratorWrapper{T},
    model::DeviceModel{T, S},
    W::Type{<:PM.AbstractPowerModel},
) where {T <: PSY.ThermalGen, S <: AbstractThermalUnitCommitment}
    time_steps = get_time_steps(container)
    varon = get_variable(container, OnVariable(), T)
    names = [PSY.get_name(d) for d in devices if PSY.get_must_run(d)]
    constraint =
        add_constraints_container!(container, MustRunConstraint(), T, names, time_steps)

    for name in names, t in time_steps
        constraint[name, t] = JuMP.@constraint(container.JuMPmodel, varon[name, t] >= 1.0)
    end
    return
end

########################### time duration constraints ######################################
"""
If the fraction of hours that a generator has a duration constraint is less than
the fraction of hours that a single time_step represents then it is not binding.
"""
function _get_data_for_tdc(
    initial_conditions_on::Vector{T},
    initial_conditions_off::Vector{U},
    resolution::Dates.TimePeriod,
) where {T <: InitialCondition, U <: InitialCondition}
    steps_per_hour = 60 / Dates.value(Dates.Minute(resolution))
    fraction_of_hour = 1 / steps_per_hour
    lenght_devices_on = length(initial_conditions_on)
    lenght_devices_off = length(initial_conditions_off)
    IS.@assert_op lenght_devices_off == lenght_devices_on
    time_params = Vector{UpDown}(undef, lenght_devices_on)
    ini_conds = Matrix{InitialCondition}(undef, lenght_devices_on, 2)
    idx = 0
    for (ix, ic) in enumerate(initial_conditions_on)
        g = get_component(ic)
        IS.@assert_op g == get_component(initial_conditions_off[ix])
        time_limits = PSY.get_time_limits(g)
        name = PSY.get_name(g)
        if time_limits !== nothing
            if (time_limits.up <= fraction_of_hour) & (time_limits.down <= fraction_of_hour)
                @debug "Generator $(name) has a nonbinding time limits. Constraints Skipped"
                continue
            else
                idx += 1
            end
            ini_conds[idx, 1] = ic
            ini_conds[idx, 2] = initial_conditions_off[ix]
            up_val = round(time_limits.up * steps_per_hour, RoundUp)
            down_val = round(time_limits.down * steps_per_hour, RoundUp)
            time_params[idx] = time_params[idx] = (up=up_val, down=down_val)
        end
    end
    if idx < lenght_devices_on
        ini_conds = ini_conds[1:idx, :]
        deleteat!(time_params, (idx + 1):lenght_devices_on)
    end
    return ini_conds, time_params
end

function add_constraints!(
    container::OptimizationContainer,
    ::Type{DurationConstraint},
    ::IS.FlattenIteratorWrapper{U},
    ::DeviceModel{U, V},
    ::Type{<:PM.AbstractPowerModel},
) where {U <: PSY.ThermalGen, V <: AbstractThermalUnitCommitment}
    parameters = built_for_recurrent_solves(container)
    resolution = get_resolution(container)
    # Use getter functions that don't require creating the keys here
    initial_conditions_on = get_initial_condition(container, InitialTimeDurationOn(), U)
    initial_conditions_off = get_initial_condition(container, InitialTimeDurationOff(), U)
    ini_conds, time_params =
        _get_data_for_tdc(initial_conditions_on, initial_conditions_off, resolution)
    if !(isempty(ini_conds))
        if parameters
            device_duration_parameters!(
                container,
                time_params,
                ini_conds,
                DurationConstraint(),
                (OnVariable(), StartVariable(), StopVariable()),
                U,
            )
        else
            device_duration_retrospective!(
                container,
                time_params,
                ini_conds,
                DurationConstraint(),
                (OnVariable(), StartVariable(), StopVariable()),
                U,
            )
        end
    else
        @warn "Data doesn't contain generators with time-up/down limits, consider adjusting your formulation"
    end
    return
end

function add_constraints!(
    container::OptimizationContainer,
    ::Type{DurationConstraint},
    devices::IS.FlattenIteratorWrapper{U},
    model::DeviceModel{U, ThermalMultiStartUnitCommitment},
    W::Type{<:PM.AbstractPowerModel},
) where {U <: PSY.ThermalGen}
    parameters = built_for_recurrent_solves(container)
    resolution = get_resolution(container)
    initial_conditions_on = get_initial_condition(container, InitialTimeDurationOn(), U)
    initial_conditions_off = get_initial_condition(container, InitialTimeDurationOff(), U)
    ini_conds, time_params =
        _get_data_for_tdc(initial_conditions_on, initial_conditions_off, resolution)
    if !(isempty(ini_conds))
        if parameters
            device_duration_parameters!(
                container,
                time_params,
                ini_conds,
                DurationConstraint(),
                (OnVariable(), StartVariable(), StopVariable()),
                U,
            )
        else
            device_duration_compact_retrospective!(
                container,
                time_params,
                ini_conds,
                DurationConstraint(),
                (OnVariable(), StartVariable(), StopVariable()),
                U,
            )
        end
    else
        @warn "Data doesn't contain generators with time-up/down limits, consider adjusting your formulation"
    end
    return
end

########################### Objective Function Calls#############################################
# These functions are custom implementations of the cost data. In the file objective_functions.jl there are default implementations. Define these only if needed.

function objective_function!(
    container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    ::DeviceModel{T, U},
    ::Type{<:PM.AbstractPowerModel},
) where {T <: PSY.ThermalGen, U <: AbstractThermalUnitCommitment}
    add_variable_cost!(container, ActivePowerVariable(), devices, U())
    add_start_up_cost!(container, StartVariable(), devices, U())
    add_shut_down_cost!(container, StopVariable(), devices, U())
    add_proportional_cost!(container, OnVariable(), devices, U())
    return
end

function objective_function!(
    container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    ::DeviceModel{T, U},
    ::Type{<:PM.AbstractPowerModel},
) where {T <: PSY.ThermalGen, U <: AbstractCompactUnitCommitment}
    add_variable_cost!(container, PowerAboveMinimumVariable(), devices, U())
    add_start_up_cost!(container, StartVariable(), devices, U())
    add_shut_down_cost!(container, StopVariable(), devices, U())
    add_proportional_cost!(container, OnVariable(), devices, U())
    return
end

function objective_function!(
    container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart},
    ::DeviceModel{PSY.ThermalMultiStart, U},
    ::Type{<:PM.AbstractPowerModel},
) where {U <: ThermalMultiStartUnitCommitment}
    add_variable_cost!(container, PowerAboveMinimumVariable(), devices, U())
    for var_type in START_VARIABLES
        add_start_up_cost!(container, var_type(), devices, U())
    end
    add_shut_down_cost!(container, StopVariable(), devices, U())
    add_proportional_cost!(container, OnVariable(), devices, U())
    return
end

function objective_function!(
    container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    ::DeviceModel{T, U},
    ::Type{<:PM.AbstractPowerModel},
) where {T <: PSY.ThermalGen, U <: AbstractThermalDispatchFormulation}
    add_variable_cost!(container, ActivePowerVariable(), devices, U())
    return
end

function objective_function!(
    container::OptimizationContainer,
    devices::IS.FlattenIteratorWrapper{T},
    ::DeviceModel{T, U},
    ::Type{<:PM.AbstractPowerModel},
) where {T <: PSY.ThermalGen, U <: ThermalCompactDispatch}
    add_variable_cost!(container, PowerAboveMinimumVariable(), devices, U())
    return
end

function objective_function!(
    ::OptimizationContainer,
    ::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart},
    ::DeviceModel{PSY.ThermalMultiStart, ThermalDispatchNoMin},
    ::Type{<:PM.AbstractPowerModel},
)
    throw(
        IS.ConflictingInputsError(
            "ThermalDispatchNoMin cost function is not compatible with ThermalMultiStart Devices.",
        ),
    )
end


"""
This module provides utilities to build neural networks with Flux,
along with a library of standard architectures.
"""
module GraphNeuralNetworkLib

export SimpleGNN, SimpleGraphNetHP

using ..AlphaZero

using CUDA
using Base: @kwdef

import Flux

CUDA.allowscalar(false)
array_on_gpu(::Array) = false
array_on_gpu(::CuArray) = true
array_on_gpu(arr) = error("Usupported array type: ", typeof(arr))

using Flux: relu, softmax, flatten
using Flux: Chain, Dense, Conv, BatchNorm, SkipConnection
import Zygote

# include("graph_network.jl")

#####
##### Flux Networks
#####

"""
    FluxNetwork <: AbstractNetwork

Abstract type for neural networks implemented using the _Flux_ framework.

The `regularized_params_` function must be overrided for all layers containing
parameters that are subject to regularization.

Provided that the above holds, `FluxNetwork` implements the full
network interface with the following exceptions:
[`Network.HyperParams`](@ref), [`Network.hyperparams`](@ref),
[`Network.forward`](@ref) and [`Network.on_gpu`](@ref).
"""
abstract type FluxGNN <: GraphNetwork end

function Base.copy(nn::Net) where Net <: FluxGNN
  #new = Net(Network.hyperparams(nn))
  #Flux.loadparams!(new, Flux.params(nn))
  #return new
  return Base.deepcopy(nn)
end

Network.to_cpu(nn::FluxGNN) = Flux.cpu(nn)

function Network.to_gpu(nn::FluxGNN)
  CUDA.allowscalar(false)
  return Flux.gpu(nn)
end

function Network.set_test_mode!(nn::FluxGNN, mode)
  Flux.testmode!(nn, mode)
end

Network.convert_input(nn::FluxGNN, x) =
  Network.on_gpu(nn) ? Flux.gpu(x) : x

Network.convert_output(nn::FluxGNN, x) = Flux.cpu(x)

Network.params(nn::FluxGNN) = Flux.params(nn)

# This should be included in Flux
function lossgrads(f, args...)
  val, back = Zygote.pullback(f, args...)
  grad = back(Zygote.sensitivity(val))
  return val, grad
end

function Network.train!(callback, nn::FluxGNN, opt::Adam, loss, data, n)
  optimiser = Flux.ADAM(opt.lr)
  params = Flux.params(nn)
  for (i, d) in enumerate(data)
    l, grads = lossgrads(params) do
      loss(d...)
    end
    Flux.update!(optimiser, params, grads)
    callback(i, l)
  end
end

function Network.train!(
    callback, nn::FluxGNN, opt::CyclicNesterov, loss, data, n)
  lr = CyclicSchedule(
    opt.lr_base,
    opt.lr_high,
    opt.lr_low, n=n)
  momentum = CyclicSchedule(
    opt.momentum_high,
    opt.momentum_low,
    opt.momentum_high, n=n)
  optimiser = Flux.Nesterov(opt.lr_low, opt.momentum_high)
  params = Flux.params(nn)
  for (i, d) in enumerate(data)
    l, grads = lossgrads(params) do
      loss(d...)
    end
    Flux.update!(optimiser, params, grads)
    optimiser.eta = lr[i]
    optimiser.rho = momentum[i]
    callback(i, l)
  end
end

regularized_params_(l) = []
regularized_params_(l::Flux.Dense) = [l.weight]
regularized_params_(l::Flux.Conv) = [l.weight]

function Network.regularized_params(net::GraphNetwork)
  return (w for l in Flux.modules(net) for w in regularized_params_(l))
end

function Network.gc(::GraphNetwork)
  GC.gc(true)
  # CUDA.reclaim()
end

# Flux.@functor does not work with abstract types
function Flux.functor(nn::Net) where Net <: FluxGNN
  children = (nn.common, nn.vhead, nn.phead, )
  constructor = cs -> Net(nn.gspec, nn.hyper, cs...)
  return (children, constructor)
end

Network.hyperparams(nn::FluxGNN) = nn.hyper

Network.game_spec(nn::FluxGNN) = nn.gspec

Network.on_gpu(nn::FluxGNN) = array_on_gpu(nn.common[end].bias)

#####
##### Include networks library
#####

include("architectures/demographnet.jl")

end


# features specifically related to Flux.jl
@info "RoMEPlotting is adding Flux related functionality."

@eval PlotTypesPose2 = Union{Type{Pose2Pose2}, Type{Pose2Point2BearingRange}, Type{Pose2Point2Range}, Type{Pose2Point2Bearing}, Type{MixtureFluxPose2Pose2}}

@eval ExtendedPose2Pose2Types = Union{Pose2Pose2, MixtureFluxPose2Pose2}

# rerun since a few functions need to be reloaded with Flux
include("PlotFactorsReload.jl")

#


using DeepQLearning
using POMDPModels
using POMDPSimulators
using POMDPPolicies
using POMDPs
using POMDPModelTools
using Flux
using Random
using StaticArrays
using Test
import CommonRLInterface

RL = CommonRLInterface

Random.seed!(7)
GLOBAL_RNG = MersenneTwister(1) # for test consistency

function evaluate(mdp::Union{MDP,POMDP}, policy, rng, n_ep=100, max_steps=100)
    avg_r = 0.
    sim = RolloutSimulator(rng=rng, max_steps=max_steps)
    for i=1:n_ep
        DeepQLearning.resetstate!(policy)
        avg_r += simulate(sim, mdp, policy)
    end
    return avg_r/=n_ep
end

function evaluate(env::RL.AbstractEnv, policy, rng, n_ep=100, max_steps=100)
    avg_r = 0.
    for i=1:n_ep
        DeepQLearning.resetstate!(policy)
        r = 0.0
        step = 0
        RL.reset!(env)
        while !RL.terminated(env) && step < max_steps
            a = action(policy, RL.observe(env))
            r += RL.act!(env, a)
            step += 1
        end
        avg_r += r
    end
    return avg_r/=n_ep
end

# @testset "TigerPOMDP DDRQN" begin
    pomdp = TigerPOMDP(0.01, -1.0, 0.1, 0.8, 0.95);
    input_dims = reduce(*, size(convert_o(Vector{Float64}, first(observations(pomdp)), pomdp))) + reduce(*, size(convert_a(Vector{Float64}, first(actions(pomdp)), pomdp)))
    @show input_dims
    model = Chain(x->flattenbatch(x), LSTM(input_dims, 4), Dense(4, length(actions(pomdp))))
    max_steps = 10000
    exploration = EpsGreedyPolicy(pomdp, LinearDecaySchedule(start=1.0, stop=0.01, steps=max_steps/2),
                                  rng=GLOBAL_RNG)
    solver = DeepQLearningSolver(qnetwork = model, prioritized_replay=false, max_steps=max_steps,
                             learning_rate=0.0001, exploration_policy = exploration,
                             log_freq=500, target_update_freq = 1000,
                             recurrence=true,trace_length=10, double_q=true, dueling=true, max_episode_length=100)

    policy = solve(solver, pomdp)
    @test size(actionvalues(policy, true)) == (length(actions(pomdp)),)
# end


@inline function writeshortest(buf::Vector{UInt8}, pos, x::T,
    plus=false, space=false, hash=true,
    precision=-1, expchar=UInt8('e'), padexp=false, decchar=UInt8('.'), typed=false) where {T}
    @assert 0 < pos <= length(buf)
    neg = signbit(x)
    # special cases
    if x == 0
        if neg
            buf[pos] = UInt8('-')
            pos += 1
        elseif plus
            buf[pos] = UInt8('+')
            pos += 1
        elseif space
            buf[pos] = UInt8(' ')
            pos += 1
        end
        buf[pos] = UInt8('0')
        pos += 1
        if hash
            buf[pos] = decchar
            pos += 1
        end
        if precision == -1
            buf[pos] = UInt8('0')
            pos += 1
            if typed && x isa Float32
                buf[pos] = UInt8('f')
                buf[pos + 1] = UInt8('0')
                pos += 2
            end
            return pos
        end
        while precision > 1
            buf[pos] = UInt8('0')
            pos += 1
            precision -= 1
        end
        if typed && x isa Float32
            buf[pos] = UInt8('f')
            buf[pos + 1] = UInt8('0')
            pos += 2
        end
        return pos
    elseif isnan(x)
        buf[pos] = UInt8('N')
        buf[pos + 1] = UInt8('a')
        buf[pos + 2] = UInt8('N')
        if typed
            if x isa Float32
                buf[pos + 3] = UInt8('3')
                buf[pos + 4] = UInt8('2')
            elseif x isa Float16
                buf[pos + 3] = UInt8('1')
                buf[pos + 4] = UInt8('6')
            end
        end
        return pos + 3 + (typed && x isa Union{Float32, Float16} ? 2 : 0)
    elseif !isfinite(x)
        if neg
            buf[pos] = UInt8('-')
        end
        buf[pos + neg] = UInt8('I')
        buf[pos + neg + 1] = UInt8('n')
        buf[pos + neg + 2] = UInt8('f')
        if typed
            if x isa Float32
                buf[pos + neg + 3] = UInt8('3')
                buf[pos + neg + 4] = UInt8('2')
            elseif x isa Float16
                buf[pos + neg + 3] = UInt8('1')
                buf[pos + neg + 4] = UInt8('6')
            end
        end
        return pos + neg + 3 + (typed && x isa Union{Float32, Float16} ? 2 : 0)
    end

    bits = uint(x)
    mant = bits & (oftype(bits, 1) << mantissabits(T) - oftype(bits, 1))
    exp = Int((bits >> mantissabits(T)) & ((Int64(1) << exponentbits(T)) - 1))
    m2 = oftype(bits, Int64(1) << mantissabits(T)) | mant
    e2 = exp - bias(T) - mantissabits(T)
    fraction = m2 & ((oftype(bits, 1) << -e2) - 1)
    if e2 > 0 || e2 < -52 || fraction != 0
        if exp == 0
            e2 = 1 - bias(T) - mantissabits(T) - 2
            m2 = mant
        else
            e2 -= 2
        end
        even = (m2 & 1) == 0
        mv = oftype(m2, 4 * m2)
        mp = oftype(m2, mv + 2)
        mmShift = mant != 0 || exp <= 1
        mm = oftype(m2, mv - 1 - mmShift)
        vmIsTrailingZeros = false
        vrIsTrailingZeros = false
        lastRemovedDigit = 0x00
        if e2 >= 0
            q = log10pow2(e2) - (T == Float64 ? (e2 > 3) : 0)
            e10 = q
            k = pow5_inv_bitcount(T) + pow5bits(q) - 1
            i = -e2 + q + k
            vr, vp, vm = mulshiftinvsplit(T, mv, mp, mm, q, i)
            if T == Float32 || T == Float16
                if q != 0 && div(vp - 1, 10) <= div(vm, 10)
                    l = pow5_inv_bitcount(T) + pow5bits(q - 1) - 1
                    mul = T == Float32 ? FLOAT_POW5_INV_SPLIT[q] : HALF_POW5_INV_SPLIT[q]
                    lastRemovedDigit = (mulshift(mv, mul, -e2 + q - 1 + l) % 10) % UInt8
                end
            end
            if q <= qinvbound(T)
                if ((mv % UInt32) - 5 * div(mv, 5)) == 0
                    vrIsTrailingZeros = pow5(mv, q)
                elseif even
                    vmIsTrailingZeros = pow5(mm, q)
                else
                    vp -= pow5(mp, q)
                end
            end
        else
            q = log10pow5(-e2) - (T == Float64 ? (-e2 > 1) : 0)
            e10 = q + e2
            i = -e2 - q
            k = pow5bits(i) - pow5_bitcount(T)
            j = q - k
            vr, vp, vm = mulshiftsplit(T, mv, mp, mm, i, j)
            if T == Float32 || T == Float16
                if q != 0 && div(vp - 1, 10) <= div(vm, 10)
                    j = q - 1 - (pow5bits(i + 1) - pow5_bitcount(T))
                    mul = T == Float32 ? FLOAT_POW5_SPLIT[i + 2] : HALF_POW5_SPLIT[i + 2]
                    lastRemovedDigit = (mulshift(mv, mul, j) % 10) % UInt8
                end
            end
            if q <= 1
                vrIsTrailingZeros = true
                if even
                    vmIsTrailingZeros = mmShift
                else
                    vp -= 1
                end
            elseif q < qbound(T)
                vrIsTrailingZeros = pow2(mv, q - (T != Float64))
            end
        end
        removed = 0
        if vmIsTrailingZeros || vrIsTrailingZeros
            while true
                vpDiv10 = div(vp, 10)
                vmDiv10 = div(vm, 10)
                vpDiv10 <= vmDiv10 && break
                vmMod10 = (vm % UInt32) - UInt32(10) * (vmDiv10 % UInt32)
                vrDiv10 = div(vr, 10)
                vrMod10 = (vr % UInt32) - UInt32(10) * (vrDiv10 % UInt32)
                vmIsTrailingZeros &= vmMod10 == 0
                vrIsTrailingZeros &= lastRemovedDigit == 0
                lastRemovedDigit = vrMod10 % UInt8
                vr = vrDiv10
                vp = vpDiv10
                vm = vmDiv10
                removed += 1
            end
            if vmIsTrailingZeros
                while true
                    vmDiv10 = div(vm, 10)
                    vmMod10 = (vm % UInt32) - UInt32(10) * (vmDiv10 % UInt32)
                    vmMod10 != 0 && break
                    vpDiv10 = div(vp, 10)
                    vrDiv10 = div(vr, 10)
                    vrMod10 = (vr % UInt32) - UInt32(10) * (vrDiv10 % UInt32)
                    vrIsTrailingZeros &= lastRemovedDigit == 0
                    lastRemovedDigit = vrMod10 % UInt8
                    vr = vrDiv10
                    vp = vpDiv10
                    vm = vmDiv10
                    removed += 1
                end
            end
            if vrIsTrailingZeros && lastRemovedDigit == 5 && vr % 2 == 0
                lastRemovedDigit = UInt8(4)
            end
            output = vr + ((vr == vm && (!even || !vmIsTrailingZeros)) || lastRemovedDigit >= 5)
        else
            roundUp = false
            vpDiv100 = div(vp, 100)
            vmDiv100 = div(vm, 100)
            if vpDiv100 > vmDiv100
                vrDiv100 = div(vr, 100)
                vrMod100 = (vr % UInt32) - UInt32(100) * (vrDiv100 % UInt32)
                roundUp = vrMod100 >= 50
                vr = vrDiv100
                vp = vpDiv100
                vm = vmDiv100
                removed += 2
            end
            while true
                vpDiv10 = div(vp, 10)
                vmDiv10 = div(vm, 10)
                vpDiv10 <= vmDiv10 && break
                vrDiv10 = div(vr, 10)
                vrMod10 = (vr % UInt32) - UInt32(10) * (vrDiv10 % UInt32)
                roundUp = vrMod10 >= 5
                vr = vrDiv10
                vp = vpDiv10
                vm = vmDiv10
                removed += 1
            end
            output = vr + (vr == vm || roundUp || lastRemovedDigit >= 5)
        end
        nexp = e10 + removed
    else
        output = m2 >> -e2
        nexp = 0
        while true
            q = div(output, 10)
            r = (output % UInt32) - UInt32(10) * (q % UInt32)
            r != 0 && break
            output = q
            nexp += 1
        end
    end

    if typed && x isa Float16
        buf[pos] = UInt8('F')
        buf[pos + 1] = UInt8('l')
        buf[pos + 2] = UInt8('o')
        buf[pos + 3] = UInt8('a')
        buf[pos + 4] = UInt8('t')
        buf[pos + 5] = UInt8('1')
        buf[pos + 6] = UInt8('6')
        buf[pos + 7] = UInt8('(')
        pos += 8
    end
    if neg
        buf[pos] = UInt8('-')
        pos += 1
    elseif plus
        buf[pos] = UInt8('+')
        pos += 1
    elseif space
        buf[pos] = UInt8(' ')
        pos += 1
    end

    olength = decimallength(output)
    exp_form = true
    pt = nexp + olength
    if -4 < pt <= (precision == -1 ? (T == Float16 ? 5 : 6) : precision)
        exp_form = false
        if pt <= 0
            buf[pos] = UInt8('0')
            pos += 1
            buf[pos] = decchar
            pos += 1
            for _ = 1:abs(pt)
                buf[pos] = UInt8('0')
                pos += 1
            end
        # elseif pt >= olength
            # nothing to do at this point
        # else
            # nothing to do at this point
        end
    else
        pos += 1
    end
    i = 0
    ptr = pointer(buf)
    ptr2 = pointer(DIGIT_TABLE)
    if (output >> 32) != 0
        q = output ÷ 100000000
        output2 = (output % UInt32) - UInt32(100000000) * (q % UInt32)
        output = q

        c = output2 % UInt32(10000)
        output2 = div(output2, UInt32(10000))
        d = output2 % UInt32(10000)
        c0 = (c % 100) << 1
        c1 = (c ÷ 100) << 1
        d0 = (d % 100) << 1
        d1 = (d ÷ 100) << 1
        memcpy(ptr, pos + olength - 2, ptr2, c0 + 1, 2)
        memcpy(ptr, pos + olength - 4, ptr2, c1 + 1, 2)
        memcpy(ptr, pos + olength - 6, ptr2, d0 + 1, 2)
        memcpy(ptr, pos + olength - 8, ptr2, d1 + 1, 2)
        i += 8
    end
    output2 = output % UInt32
    while output2 >= 10000
        c = output2 % UInt32(10000)
        output2 = div(output2, UInt32(10000))
        c0 = (c % 100) << 1
        c1 = (c ÷ 100) << 1
        memcpy(ptr, pos + olength - i - 2, ptr2, c0 + 1, 2)
        memcpy(ptr, pos + olength - i - 4, ptr2, c1 + 1, 2)
        i += 4
    end
    if output2 >= 100
        c = (output2 % UInt32(100)) << 1
        output2 = div(output2, UInt32(100))
        memcpy(ptr, pos + olength - i - 2, ptr2, c + 1, 2)
        i += 2
    end
    if output2 >= 10
        c = output2 << 1
        buf[pos + 1] = DIGIT_TABLE[c + 2]
        buf[pos - exp_form] = DIGIT_TABLE[c + 1]
    else
        buf[pos - exp_form] = UInt8('0') + (output2 % UInt8)
    end

    if !exp_form
        if pt <= 0
            pos += olength
            precision -= olength
            while hash && precision > 0
                buf[pos] = UInt8('0')
                pos += 1
                precision -= 1
            end
        elseif pt >= olength
            pos += olength
            precision -= olength
            for _ = 1:nexp
                buf[pos] = UInt8('0')
                pos += 1
                precision -= 1
            end
            if hash
                buf[pos] = decchar
                pos += 1
                if precision < 0
                    buf[pos] = UInt8('0')
                    pos += 1
                end
                while precision > 0
                    buf[pos] = UInt8('0')
                    pos += 1
                    precision -= 1
                end
            end
        else
            pointoff = olength - abs(nexp)
            memmove(ptr, pos + pointoff + 1, ptr, pos + pointoff, olength - pointoff + 1)
            buf[pos + pointoff] = decchar
            pos += olength + 1
            precision -= olength
            while hash && precision > 0
                buf[pos] = UInt8('0')
                pos += 1
                precision -= 1
            end
        end
        if typed && x isa Float32
            buf[pos] = UInt8('f')
            buf[pos + 1] = UInt8('0')
            pos += 2
        end
    else
        if olength > 1 || hash
            buf[pos] = decchar
            pos += olength
            precision -= olength
        end
        if hash && olength == 1
            buf[pos] = UInt8('0')
            pos += 1
        end
        while hash && precision > 0
            buf[pos] = UInt8('0')
            pos += 1
            precision -= 1
        end

        buf[pos] = expchar
        pos += 1
        exp2 = nexp + olength - 1
        if exp2 < 0
            buf[pos] = UInt8('-')
            pos += 1
            exp2 = -exp2
        elseif padexp
            buf[pos] = UInt8('+')
            pos += 1
        end

        if exp2 >= 100
            c = exp2 % 10
            memcpy(ptr, pos, ptr2, 2 * div(exp2, 10) + 1, 2)
            buf[pos + 2] = UInt8('0') + (c % UInt8)
            pos += 3
        elseif exp2 >= 10
            memcpy(ptr, pos, ptr2, 2 * exp2 + 1, 2)
            pos += 2
        else
            if padexp
                buf[pos] = UInt8('0')
                pos += 1
            end
            buf[pos] = UInt8('0') + (exp2 % UInt8)
            pos += 1
        end
    end
    if typed && x isa Float16
        buf[pos] = UInt8(')')
        pos += 1
    end

    return pos
end


module SrtmKgb23
  # KURUCZ
  sfluxref = [53.2101 , 51.4143, 49.3348, 45.4612    ,40.8294 , 35.1801, 28.6947, 21.5751    ,14.6388 , 1.59111, 1.31860, 1.04018    ,0.762140,0.484214,0.182275, 2.54948e-02]

  #     Rayleigh extinction coefficient at all v
  rayl = [5.94837e-08,5.70593e-08,6.27845e-08,5.56602e-08,5.25571e-08,4.73388e-08,4.17466e-08,3.98097e-08,4.00786e-08,3.67478e-08,3.45186e-08,3.46156e-08,3.32155e-08,3.23642e-08,2.72590e-08,2.96813e-08]

  #     Average Giver et al. correction factor for this band.
  givfac = 1.029

  layreffr = 6

  #     ------------------------------------------------------------------

  #     The array KA contains absorption coefs at the 16 chosen g-values
  #     for a range of pressure levels> ~100mb:and binary:temperatures
  #     species parameters (see taumol.f for definition).  The first
  #     index in the array, JS, runs from 1 to 9, and corresponds to
  #     different values of the binary species parameter.  For instance,
  #     JS=1 refers to dry air, JS = 2 corresponds to the paramter value 1/8,
  #     JS = 3 corresponds to the parameter value 2/8, etc.  The second index
  #     in the array, JT, which runs from 1 to 5, corresponds to different
  #     temperatures.  More specifically, JT = 3 means that the data are for
  #     the reference temperature TREF for this  pressure level:JT = 2 refers
  #     to TREF-15, JT = 1 is for TREF-30:and JT = 5:JT = 4 is for TREF+15
  #     is for TREF+30.  The third index:runs from 1 to 13 and refers:JP
  #     to the JPth reference pressure level (see taumol.f for these levels
  #     in mb).  The fourth index, IG, goes from 1 to 16, and indicates
  #     which g-interval the absorption coefficients are for.
  #     -----------------------------------------------------------------
  ka = zeros(5,13,16)
  ka[:, 1, 1] = [0.33078e-07,0.34034e-07,0.35124e-07,0.34187e-07,0.34744e-07]
  ka[:, 2, 1] = [0.25544e-07,0.25873e-07,0.26742e-07,0.27512e-07,0.27504e-07]
  ka[:, 3, 1] = [0.18549e-07,0.19611e-07,0.20840e-07,0.22548e-07,0.23069e-07]
  ka[:, 4, 1] = [0.28794e-07,0.30837e-07,0.32679e-07,0.34307e-07,0.36901e-07]
  ka[:, 5, 1] = [0.36776e-07,0.39144e-07,0.41300e-07,0.43264e-07,0.46626e-07]
  ka[:, 6, 1] = [0.59710e-07,0.62941e-07,0.65500e-07,0.67353e-07,0.68774e-07]
  ka[:, 7, 1] = [0.12143e-06,0.12932e-06,0.13250e-06,0.13526e-06,0.13849e-06]
  ka[:, 8, 1] = [0.12531e-06,0.13241e-06,0.13939e-06,0.14705e-06,0.15465e-06]
  ka[:, 9, 1] = [0.20209e-06,0.21134e-06,0.22163e-06,0.23098e-06,0.24004e-06]
  ka[:,10, 1] = [0.10750e-05,0.11204e-05,0.11575e-05,0.11923e-05,0.12227e-05]
  ka[:,11, 1] = [0.27782e-05,0.28204e-05,0.28406e-05,0.28380e-05,0.28440e-05]
  ka[:,12, 1] = [0.38510e-05,0.39934e-05,0.40697e-05,0.41102e-05,0.41571e-05]
  ka[:,13, 1] = [0.43157e-05,0.44488e-05,0.45799e-05,0.46585e-05,0.47223e-05]
  ka[:, 1, 2] = [0.84637e-06,0.86989e-06,0.90697e-06,0.90000e-06,0.91373e-06]
  ka[:, 2, 2] = [0.67062e-06,0.68649e-06,0.72334e-06,0.73645e-06,0.72978e-06]
  ka[:, 3, 2] = [0.52317e-06,0.53924e-06,0.55425e-06,0.58347e-06,0.57813e-06]
  ka[:, 4, 2] = [0.39868e-06,0.41431e-06,0.42761e-06,0.43892e-06,0.45982e-06]
  ka[:, 5, 2] = [0.32074e-06,0.33452e-06,0.34754e-06,0.35582e-06,0.37378e-06]
  ka[:, 6, 2] = [0.42465e-06,0.44058e-06,0.45605e-06,0.47192e-06,0.48493e-06]
  ka[:, 7, 2] = [0.47581e-06,0.50000e-06,0.52487e-06,0.54192e-06,0.55955e-06]
  ka[:, 8, 2] = [0.10592e-05,0.11093e-05,0.11483e-05,0.11923e-05,0.12169e-05]
  ka[:, 9, 2] = [0.50835e-05,0.51710e-05,0.52329e-05,0.52644e-05,0.52818e-05]
  ka[:,10, 2] = [0.77867e-05,0.82156e-05,0.86002e-05,0.89664e-05,0.92394e-05]
  ka[:,11, 2] = [0.89031e-05,0.93573e-05,0.97686e-05,0.10236e-04,0.10591e-04]
  ka[:,12, 2] = [0.98068e-05,0.10238e-04,0.10700e-04,0.11108e-04,0.11471e-04]
  ka[:,13, 2] = [0.11145e-04,0.11697e-04,0.12123e-04,0.12482e-04,0.12856e-04]
  ka[:, 1, 3] = [0.66049e-05,0.67547e-05,0.70104e-05,0.69745e-05,0.70687e-05]
  ka[:, 2, 3] = [0.54104e-05,0.55245e-05,0.57623e-05,0.58532e-05,0.58099e-05]
  ka[:, 3, 3] = [0.43608e-05,0.44784e-05,0.45778e-05,0.47665e-05,0.47295e-05]
  ka[:, 4, 3] = [0.35399e-05,0.36454e-05,0.37356e-05,0.38107e-05,0.39586e-05]
  ka[:, 5, 3] = [0.28576e-05,0.29552e-05,0.30325e-05,0.31028e-05,0.32276e-05]
  ka[:, 6, 3] = [0.21017e-05,0.21836e-05,0.22554e-05,0.23121e-05,0.23486e-05]
  ka[:, 7, 3] = [0.19384e-05,0.19914e-05,0.20428e-05,0.20879e-05,0.21191e-05]
  ka[:, 8, 3] = [0.32672e-05,0.33753e-05,0.34694e-05,0.35273e-05,0.35984e-05]
  ka[:, 9, 3] = [0.82257e-05,0.84975e-05,0.86969e-05,0.88995e-05,0.90123e-05]
  ka[:,10, 3] = [0.35363e-04,0.36204e-04,0.36916e-04,0.37708e-04,0.38294e-04]
  ka[:,11, 3] = [0.48837e-04,0.50952e-04,0.52710e-04,0.54347e-04,0.55976e-04]
  ka[:,12, 3] = [0.56059e-04,0.58301e-04,0.60100e-04,0.61776e-04,0.63179e-04]
  ka[:,13, 3] = [0.57871e-04,0.59505e-04,0.60930e-04,0.62289e-04,0.63278e-04]
  ka[:, 1, 4] = [0.27624e-04,0.28017e-04,0.29078e-04,0.28567e-04,0.28724e-04]
  ka[:, 2, 4] = [0.23107e-04,0.23464e-04,0.24357e-04,0.24519e-04,0.24024e-04]
  ka[:, 3, 4] = [0.19113e-04,0.19400e-04,0.19615e-04,0.20420e-04,0.19962e-04]
  ka[:, 4, 4] = [0.15873e-04,0.16138e-04,0.16349e-04,0.16528e-04,0.17176e-04]
  ka[:, 5, 4] = [0.13198e-04,0.13437e-04,0.13641e-04,0.13810e-04,0.14393e-04]
  ka[:, 6, 4] = [0.10951e-04,0.11172e-04,0.11352e-04,0.11506e-04,0.11631e-04]
  ka[:, 7, 4] = [0.86121e-05,0.88300e-05,0.90149e-05,0.91565e-05,0.92594e-05]
  ka[:, 8, 4] = [0.71478e-05,0.72918e-05,0.74035e-05,0.74959e-05,0.75566e-05]
  ka[:, 9, 4] = [0.16458e-04,0.17092e-04,0.17686e-04,0.17967e-04,0.18273e-04]
  ka[:,10, 4] = [0.47953e-04,0.49663e-04,0.51524e-04,0.52800e-04,0.54192e-04]
  ka[:,11, 4] = [0.94263e-04,0.95557e-04,0.96513e-04,0.97430e-04,0.97733e-04]
  ka[:,12, 4] = [0.12087e-03,0.12152e-03,0.12240e-03,0.12318e-03,0.12403e-03]
  ka[:,13, 4] = [0.12781e-03,0.12897e-03,0.13049e-03,0.13171e-03,0.13337e-03]
  ka[:, 1, 5] = [0.82859e-04,0.84817e-04,0.89056e-04,0.88057e-04,0.89410e-04]
  ka[:, 2, 5] = [0.70937e-04,0.72685e-04,0.76796e-04,0.77955e-04,0.76735e-04]
  ka[:, 3, 5] = [0.59876e-04,0.61448e-04,0.62837e-04,0.66293e-04,0.64996e-04]
  ka[:, 4, 5] = [0.50598e-04,0.52054e-04,0.53293e-04,0.54332e-04,0.57333e-04]
  ka[:, 5, 5] = [0.42742e-04,0.44035e-04,0.45164e-04,0.46134e-04,0.48935e-04]
  ka[:, 6, 5] = [0.35769e-04,0.36975e-04,0.38038e-04,0.38917e-04,0.39681e-04]
  ka[:, 7, 5] = [0.29747e-04,0.30824e-04,0.31756e-04,0.32589e-04,0.33314e-04]
  ka[:, 8, 5] = [0.21994e-04,0.22945e-04,0.23786e-04,0.24517e-04,0.25155e-04]
  ka[:, 9, 5] = [0.22298e-04,0.22688e-04,0.23127e-04,0.23803e-04,0.24335e-04]
  ka[:,10, 5] = [0.88898e-04,0.91280e-04,0.93333e-04,0.95224e-04,0.96298e-04]
  ka[:,11, 5] = [0.12299e-03,0.12407e-03,0.12536e-03,0.12642e-03,0.12813e-03]
  ka[:,12, 5] = [0.14539e-03,0.14851e-03,0.15022e-03,0.15157e-03,0.15204e-03]
  ka[:,13, 5] = [0.15949e-03,0.16239e-03,0.16467e-03,0.16667e-03,0.16801e-03]
  ka[:, 1, 6] = [0.25339e-03,0.25995e-03,0.27170e-03,0.26963e-03,0.27413e-03]
  ka[:, 2, 6] = [0.21908e-03,0.22404e-03,0.23522e-03,0.23902e-03,0.23717e-03]
  ka[:, 3, 6] = [0.18611e-03,0.19076e-03,0.19518e-03,0.20503e-03,0.20222e-03]
  ka[:, 4, 6] = [0.15769e-03,0.16210e-03,0.16597e-03,0.16948e-03,0.17817e-03]
  ka[:, 5, 6] = [0.13402e-03,0.13792e-03,0.14153e-03,0.14434e-03,0.15267e-03]
  ka[:, 6, 6] = [0.11390e-03,0.11743e-03,0.12065e-03,0.12316e-03,0.12543e-03]
  ka[:, 7, 6] = [0.96417e-04,0.99612e-04,0.10233e-03,0.10453e-03,0.10652e-03]
  ka[:, 8, 6] = [0.81395e-04,0.84205e-04,0.86346e-04,0.88406e-04,0.90122e-04]
  ka[:, 9, 6] = [0.47776e-04,0.48971e-04,0.49736e-04,0.49917e-04,0.50289e-04]
  ka[:,10, 6] = [0.10698e-03,0.10815e-03,0.10817e-03,0.10799e-03,0.10851e-03]
  ka[:,11, 6] = [0.20220e-03,0.20727e-03,0.21241e-03,0.21675e-03,0.21989e-03]
  ka[:,12, 6] = [0.23474e-03,0.23601e-03,0.23974e-03,0.24383e-03,0.24876e-03]
  ka[:,13, 6] = [0.23410e-03,0.23809e-03,0.24185e-03,0.24554e-03,0.24952e-03]
  ka[:, 1, 7] = [0.67024e-03,0.68026e-03,0.70419e-03,0.70159e-03,0.71089e-03]
  ka[:, 2, 7] = [0.58729e-03,0.59778e-03,0.62097e-03,0.62912e-03,0.62423e-03]
  ka[:, 3, 7] = [0.50967e-03,0.51900e-03,0.52765e-03,0.54794e-03,0.54266e-03]
  ka[:, 4, 7] = [0.44167e-03,0.45006e-03,0.45793e-03,0.46469e-03,0.48335e-03]
  ka[:, 5, 7] = [0.38096e-03,0.38881e-03,0.39576e-03,0.40259e-03,0.42086e-03]
  ka[:, 6, 7] = [0.32818e-03,0.33539e-03,0.34192e-03,0.34829e-03,0.35405e-03]
  ka[:, 7, 7] = [0.28259e-03,0.28946e-03,0.29584e-03,0.30203e-03,0.30742e-03]
  ka[:, 8, 7] = [0.24273e-03,0.24912e-03,0.25546e-03,0.26110e-03,0.26607e-03]
  ka[:, 9, 7] = [0.19937e-03,0.20653e-03,0.21314e-03,0.21968e-03,0.22520e-03]
  ka[:,10, 7] = [0.13306e-03,0.13331e-03,0.13393e-03,0.13538e-03,0.13616e-03]
  ka[:,11, 7] = [0.16236e-03,0.16154e-03,0.16187e-03,0.16113e-03,0.16209e-03]
  ka[:,12, 7] = [0.17872e-03,0.18355e-03,0.18612e-03,0.18792e-03,0.18745e-03]
  ka[:,13, 7] = [0.18970e-03,0.19384e-03,0.19773e-03,0.20261e-03,0.20377e-03]
  ka[:, 1, 8] = [0.18130e-02,0.18305e-02,0.18716e-02,0.18655e-02,0.18814e-02]
  ka[:, 2, 8] = [0.16420e-02,0.16600e-02,0.17006e-02,0.17156e-02,0.17108e-02]
  ka[:, 3, 8] = [0.14687e-02,0.14870e-02,0.15042e-02,0.15423e-02,0.15376e-02]
  ka[:, 4, 8] = [0.13068e-02,0.13248e-02,0.13421e-02,0.13592e-02,0.13960e-02]
  ka[:, 5, 8] = [0.11574e-02,0.11753e-02,0.11928e-02,0.12097e-02,0.12467e-02]
  ka[:, 6, 8] = [0.10167e-02,0.10342e-02,0.10515e-02,0.10681e-02,0.10840e-02]
  ka[:, 7, 8] = [0.88992e-03,0.90662e-03,0.92299e-03,0.93844e-03,0.95347e-03]
  ka[:, 8, 8] = [0.78445e-03,0.80031e-03,0.81639e-03,0.83131e-03,0.84631e-03]
  ka[:, 9, 8] = [0.69812e-03,0.71312e-03,0.72801e-03,0.74222e-03,0.75605e-03]
  ka[:,10, 8] = [0.32521e-03,0.33835e-03,0.35194e-03,0.36381e-03,0.37594e-03]
  ka[:,11, 8] = [0.31406e-03,0.32013e-03,0.32456e-03,0.33261e-03,0.33741e-03]
  ka[:,12, 8] = [0.28132e-03,0.28732e-03,0.29674e-03,0.30509e-03,0.31393e-03]
  ka[:,13, 8] = [0.25704e-03,0.26316e-03,0.27195e-03,0.27732e-03,0.28905e-03]
  ka[:, 1, 9] = [0.67370e-02,0.67873e-02,0.68896e-02,0.68819e-02,0.69268e-02]
  ka[:, 2, 9] = [0.63111e-02,0.63622e-02,0.64623e-02,0.65060e-02,0.65027e-02]
  ka[:, 3, 9] = [0.58834e-02,0.59361e-02,0.59858e-02,0.60811e-02,0.60806e-02]
  ka[:, 4, 9] = [0.54753e-02,0.55309e-02,0.55823e-02,0.56306e-02,0.57229e-02]
  ka[:, 5, 9] = [0.50781e-02,0.51373e-02,0.51895e-02,0.52391e-02,0.53328e-02]
  ka[:, 6, 9] = [0.46791e-02,0.47408e-02,0.47949e-02,0.48470e-02,0.48993e-02]
  ka[:, 7, 9] = [0.42724e-02,0.43381e-02,0.43951e-02,0.44503e-02,0.45061e-02]
  ka[:, 8, 9] = [0.38568e-02,0.39254e-02,0.39848e-02,0.40431e-02,0.41005e-02]
  ka[:, 9, 9] = [0.35657e-02,0.36362e-02,0.36994e-02,0.37608e-02,0.38215e-02]
  ka[:,10, 9] = [0.33774e-02,0.34501e-02,0.35171e-02,0.35811e-02,0.36450e-02]
  ka[:,11, 9] = [0.23923e-02,0.24623e-02,0.25263e-02,0.25890e-02,0.26517e-02]
  ka[:,12, 9] = [0.16959e-02,0.17542e-02,0.18077e-02,0.18614e-02,0.19187e-02]
  ka[:,13, 9] = [0.11732e-02,0.12232e-02,0.12712e-02,0.13221e-02,0.13677e-02]
  ka[:, 1,10] = [0.19604e-01,0.19698e-01,0.19938e-01,0.19854e-01,0.19950e-01]
  ka[:, 2,10] = [0.18714e-01,0.18803e-01,0.19035e-01,0.19131e-01,0.19102e-01]
  ka[:, 3,10] = [0.17676e-01,0.17785e-01,0.17904e-01,0.18189e-01,0.18156e-01]
  ka[:, 4,10] = [0.16662e-01,0.16773e-01,0.16908e-01,0.17056e-01,0.17303e-01]
  ka[:, 5,10] = [0.15655e-01,0.15775e-01,0.15942e-01,0.16103e-01,0.16359e-01]
  ka[:, 6,10] = [0.14694e-01,0.14838e-01,0.15029e-01,0.15200e-01,0.15320e-01]
  ka[:, 7,10] = [0.13797e-01,0.13959e-01,0.14174e-01,0.14350e-01,0.14471e-01]
  ka[:, 8,10] = [0.12902e-01,0.13089e-01,0.13313e-01,0.13489e-01,0.13626e-01]
  ka[:, 9,10] = [0.11897e-01,0.12105e-01,0.12333e-01,0.12512e-01,0.12656e-01]
  ka[:,10,10] = [0.11834e-01,0.12072e-01,0.12331e-01,0.12548e-01,0.12721e-01]
  ka[:,11,10] = [0.11416e-01,0.11699e-01,0.11968e-01,0.12141e-01,0.12311e-01]
  ka[:,12,10] = [0.10776e-01,0.11070e-01,0.11309e-01,0.11507e-01,0.11702e-01]
  ka[:,13,10] = [0.99577e-02,0.10263e-01,0.10492e-01,0.10719e-01,0.10951e-01]
  ka[:, 1,11] = [0.29783e-01,0.29883e-01,0.30210e-01,0.30141e-01,0.30248e-01]
  ka[:, 2,11] = [0.28562e-01,0.28743e-01,0.29186e-01,0.29339e-01,0.29258e-01]
  ka[:, 3,11] = [0.27212e-01,0.27429e-01,0.27654e-01,0.28083e-01,0.28023e-01]
  ka[:, 4,11] = [0.25949e-01,0.26197e-01,0.26424e-01,0.26623e-01,0.27069e-01]
  ka[:, 5,11] = [0.24686e-01,0.24942e-01,0.25176e-01,0.25403e-01,0.25908e-01]
  ka[:, 6,11] = [0.23430e-01,0.23686e-01,0.23923e-01,0.24158e-01,0.24433e-01]
  ka[:, 7,11] = [0.22171e-01,0.22424e-01,0.22653e-01,0.22909e-01,0.23203e-01]
  ka[:, 8,11] = [0.20928e-01,0.21171e-01,0.21407e-01,0.21699e-01,0.22018e-01]
  ka[:, 9,11] = [0.19076e-01,0.19320e-01,0.19548e-01,0.19858e-01,0.20150e-01]
  ka[:,10,11] = [0.19537e-01,0.19788e-01,0.20064e-01,0.20391e-01,0.20843e-01]
  ka[:,11,11] = [0.19137e-01,0.19444e-01,0.19793e-01,0.20268e-01,0.20695e-01]
  ka[:,12,11] = [0.18393e-01,0.18715e-01,0.19143e-01,0.19652e-01,0.20037e-01]
  ka[:,13,11] = [0.17255e-01,0.17680e-01,0.18170e-01,0.18641e-01,0.19010e-01]
  ka[:, 1,12] = [0.46641e-01,0.46796e-01,0.47107e-01,0.46977e-01,0.47093e-01]
  ka[:, 2,12] = [0.46819e-01,0.46956e-01,0.47337e-01,0.47426e-01,0.47339e-01]
  ka[:, 3,12] = [0.46276e-01,0.46366e-01,0.46462e-01,0.46935e-01,0.46862e-01]
  ka[:, 4,12] = [0.44986e-01,0.45103e-01,0.45287e-01,0.45535e-01,0.46152e-01]
  ka[:, 5,12] = [0.43367e-01,0.43552e-01,0.43761e-01,0.44035e-01,0.44674e-01]
  ka[:, 6,12] = [0.41584e-01,0.41793e-01,0.42020e-01,0.42344e-01,0.42701e-01]
  ka[:, 7,12] = [0.39785e-01,0.40007e-01,0.40269e-01,0.40635e-01,0.41003e-01]
  ka[:, 8,12] = [0.37918e-01,0.38155e-01,0.38468e-01,0.38839e-01,0.39210e-01]
  ka[:, 9,12] = [0.35060e-01,0.35328e-01,0.35698e-01,0.36105e-01,0.36551e-01]
  ka[:,10,12] = [0.35216e-01,0.35547e-01,0.35872e-01,0.36327e-01,0.36542e-01]
  ka[:,11,12] = [0.35158e-01,0.35374e-01,0.35869e-01,0.36242e-01,0.36691e-01]
  ka[:,12,12] = [0.34264e-01,0.34598e-01,0.35106e-01,0.35505e-01,0.36021e-01]
  ka[:,13,12] = [0.32716e-01,0.33195e-01,0.33830e-01,0.34228e-01,0.34925e-01]
  ka[:, 1,13] = [0.76084e-01,0.76052e-01,0.76051e-01,0.75851e-01,0.75753e-01]
  ka[:, 2,13] = [0.79580e-01,0.79564e-01,0.79664e-01,0.79619e-01,0.79414e-01]
  ka[:, 3,13] = [0.82218e-01,0.82302e-01,0.82367e-01,0.82543e-01,0.82246e-01]
  ka[:, 4,13] = [0.83613e-01,0.83740e-01,0.83824e-01,0.83866e-01,0.84103e-01]
  ka[:, 5,13] = [0.83913e-01,0.84123e-01,0.84289e-01,0.84403e-01,0.84840e-01]
  ka[:, 6,13] = [0.83159e-01,0.83442e-01,0.83718e-01,0.83891e-01,0.83993e-01]
  ka[:, 7,13] = [0.81401e-01,0.81826e-01,0.82202e-01,0.82451e-01,0.82656e-01]
  ka[:, 8,13] = [0.78949e-01,0.79505e-01,0.79978e-01,0.80351e-01,0.80690e-01]
  ka[:, 9,13] = [0.76002e-01,0.76671e-01,0.77279e-01,0.77752e-01,0.78257e-01]
  ka[:,10,13] = [0.69777e-01,0.70425e-01,0.71054e-01,0.71706e-01,0.72167e-01]
  ka[:,11,13] = [0.72929e-01,0.73732e-01,0.74323e-01,0.75246e-01,0.75786e-01]
  ka[:,12,13] = [0.73007e-01,0.74429e-01,0.74922e-01,0.75916e-01,0.76947e-01]
  ka[:,13,13] = [0.71376e-01,0.72507e-01,0.73710e-01,0.74716e-01,0.75702e-01]
  ka[:, 1,14] = [0.12585e+00,0.12569e+00,0.12576e+00,0.12571e+00,0.12570e+00]
  ka[:, 2,14] = [0.13868e+00,0.13853e+00,0.13849e+00,0.13824e+00,0.13803e+00]
  ka[:, 3,14] = [0.15142e+00,0.15135e+00,0.15112e+00,0.15093e+00,0.15074e+00]
  ka[:, 4,14] = [0.16359e+00,0.16359e+00,0.16339e+00,0.16311e+00,0.16285e+00]
  ka[:, 5,14] = [0.17462e+00,0.17472e+00,0.17456e+00,0.17421e+00,0.17393e+00]
  ka[:, 6,14] = [0.18403e+00,0.18447e+00,0.18450e+00,0.18424e+00,0.18395e+00]
  ka[:, 7,14] = [0.19179e+00,0.19239e+00,0.19256e+00,0.19253e+00,0.19252e+00]
  ka[:, 8,14] = [0.19772e+00,0.19863e+00,0.19901e+00,0.19923e+00,0.19947e+00]
  ka[:, 9,14] = [0.20154e+00,0.20279e+00,0.20355e+00,0.20417e+00,0.20473e+00]
  ka[:,10,14] = [0.18853e+00,0.19028e+00,0.19160e+00,0.19253e+00,0.19393e+00]
  ka[:,11,14] = [0.18013e+00,0.18167e+00,0.18320e+00,0.18375e+00,0.18507e+00]
  ka[:,12,14] = [0.19011e+00,0.19027e+00,0.19283e+00,0.19402e+00,0.19478e+00]
  ka[:,13,14] = [0.19594e+00,0.19738e+00,0.19911e+00,0.20124e+00,0.20282e+00]
  ka[:, 1,15] = [0.22369e+00,0.22259e+00,0.22155e+00,0.22059e+00,0.21997e+00]
  ka[:, 2,15] = [0.25602e+00,0.25478e+00,0.25377e+00,0.25306e+00,0.25237e+00]
  ka[:, 3,15] = [0.29258e+00,0.29107e+00,0.28998e+00,0.28920e+00,0.28830e+00]
  ka[:, 4,15] = [0.33067e+00,0.32888e+00,0.32753e+00,0.32646e+00,0.32566e+00]
  ka[:, 5,15] = [0.37114e+00,0.36880e+00,0.36713e+00,0.36598e+00,0.36499e+00]
  ka[:, 6,15] = [0.41494e+00,0.41167e+00,0.40935e+00,0.40779e+00,0.40636e+00]
  ka[:, 7,15] = [0.46115e+00,0.45729e+00,0.45455e+00,0.45230e+00,0.45004e+00]
  ka[:, 8,15] = [0.50906e+00,0.50463e+00,0.50137e+00,0.49843e+00,0.49550e+00]
  ka[:, 9,15] = [0.55829e+00,0.55330e+00,0.54936e+00,0.54557e+00,0.54168e+00]
  ka[:,10,15] = [0.60814e+00,0.60274e+00,0.59806e+00,0.59326e+00,0.58833e+00]
  ka[:,11,15] = [0.62954e+00,0.62588e+00,0.62076e+00,0.61665e+00,0.61184e+00]
  ka[:,12,15] = [0.62585e+00,0.62437e+00,0.61807e+00,0.61303e+00,0.60869e+00]
  ka[:,13,15] = [0.64856e+00,0.64505e+00,0.63861e+00,0.63277e+00,0.62702e+00]
  ka[:, 1,16] = [0.33327e+00,0.33385e+00,0.33538e+00,0.33638e+00,0.33736e+00]
  ka[:, 2,16] = [0.40916e+00,0.40842e+00,0.40848e+00,0.40865e+00,0.40854e+00]
  ka[:, 3,16] = [0.50099e+00,0.49888e+00,0.49727e+00,0.49588e+00,0.49422e+00]
  ka[:, 4,16] = [0.60389e+00,0.60029e+00,0.59704e+00,0.59367e+00,0.59071e+00]
  ka[:, 5,16] = [0.71868e+00,0.71337e+00,0.70835e+00,0.70318e+00,0.69852e+00]
  ka[:, 6,16] = [0.84815e+00,0.84138e+00,0.83446e+00,0.82728e+00,0.82023e+00]
  ka[:, 7,16] = [0.99512e+00,0.98644e+00,0.97698e+00,0.96695e+00,0.95712e+00]
  ka[:, 8,16] = [0.11606e+01,0.11485e+01,0.11354e+01,0.11218e+01,0.11077e+01]
  ka[:, 9,16] = [0.13444e+01,0.13282e+01,0.13102e+01,0.12917e+01,0.12735e+01]
  ka[:,10,16] = [0.15423e+01,0.15207e+01,0.14970e+01,0.14733e+01,0.14494e+01]
  ka[:,11,16] = [0.17462e+01,0.17138e+01,0.16827e+01,0.16518e+01,0.16199e+01]
  ka[:,12,16] = [0.19577e+01,0.19145e+01,0.18728e+01,0.18320e+01,0.17910e+01]
  ka[:,13,16] = [0.21716e+01,0.21171e+01,0.20639e+01,0.20113e+01,0.19587e+01]

  #     -----------------------------------------------------------------
  forref = zeros(3,16)
  forref[:, 1] = [ 0.315770e-07, 0.671978e-07, 0.440649e-06 ]
  forref[:, 2] = [ 0.313674e-06, 0.285252e-06, 0.421024e-05 ]
  forref[:, 3] = [ 0.135818e-05, 0.145071e-05, 0.611285e-05 ]
  forref[:, 4] = [ 0.534065e-05, 0.586268e-05, 0.933970e-05 ]
  forref[:, 5] = [ 0.964007e-05, 0.107110e-04, 0.104486e-04 ]
  forref[:, 6] = [ 0.302775e-04, 0.357530e-04, 0.340724e-04 ]
  forref[:, 7] = [ 0.102437e-03, 0.108475e-03, 0.105245e-03 ]
  forref[:, 8] = [ 0.146054e-03, 0.141490e-03, 0.133071e-03 ]
  forref[:, 9] = [ 0.163978e-03, 0.150208e-03, 0.142864e-03 ]
  forref[:,10] = [ 0.220412e-03, 0.182943e-03, 0.150941e-03 ]
  forref[:,11] = [ 0.228877e-03, 0.197679e-03, 0.163220e-03 ]
  forref[:,12] = [ 0.234177e-03, 0.217734e-03, 0.185038e-03 ]
  forref[:,13] = [ 0.257187e-03, 0.241570e-03, 0.221178e-03 ]
  forref[:,14] = [ 0.272455e-03, 0.270637e-03, 0.256269e-03 ]
  forref[:,15] = [ 0.339445e-03, 0.300268e-03, 0.286574e-03 ]
  forref[:,16] = [ 0.338841e-03, 0.355428e-03, 0.353794e-03 ]

  #     -----------------------------------------------------------------
  #     The array SELFREF contains the coefficient of the water vapor
  #     self-continuum (including the energy term).  The first index
  #     refers to temperature in 7.2 degree increments.  For instance,
  #     JT = 1 refers to a temperature of 245.6, JT = 2 refers to 252.8,
  #     etc.  The second index runs over the g-channel (1 to 16).
  selfref = zeros(10,16)
  selfref[:, 1] = [0.100945e-04, 0.801113e-05, 0.635771e-05, 0.504554e-05, 0.400419e-05,0.317777e-05, 0.252191e-05, 0.200141e-05, 0.158834e-05, 0.126052e-05]
  selfref[:, 2] = [0.107573e-04, 0.999809e-05, 0.929245e-05, 0.863661e-05, 0.802706e-05,0.746053e-05, 0.693399e-05, 0.644460e-05, 0.598976e-05, 0.556702e-05]
  selfref[:, 3] = [0.350389e-04, 0.319234e-04, 0.290850e-04, 0.264989e-04, 0.241428e-04,0.219962e-04, 0.200404e-04, 0.182586e-04, 0.166351e-04, 0.151560e-04]
  selfref[:, 4] = [0.122993e-03, 0.110885e-03, 0.999691e-04, 0.901277e-04, 0.812551e-04,0.732559e-04, 0.660443e-04, 0.595426e-04, 0.536809e-04, 0.483963e-04]
  selfref[:, 5] = [0.206434e-03, 0.187435e-03, 0.170185e-03, 0.154522e-03, 0.140301e-03,0.127388e-03, 0.115664e-03, 0.105019e-03, 0.953540e-04, 0.865783e-04]
  selfref[:, 6] = [0.590645e-03, 0.533109e-03, 0.481177e-03, 0.434305e-03, 0.391998e-03,0.353812e-03, 0.319346e-03, 0.288238e-03, 0.260160e-03, 0.234817e-03]
  selfref[:, 7] = [0.163029e-02, 0.148773e-02, 0.135763e-02, 0.123891e-02, 0.113057e-02,0.103170e-02, 0.941483e-03, 0.859153e-03, 0.784023e-03, 0.715462e-03]
  selfref[:, 8] = [0.204528e-02, 0.189258e-02, 0.175128e-02, 0.162053e-02, 0.149954e-02,0.138758e-02, 0.128398e-02, 0.118812e-02, 0.109941e-02, 0.101733e-02]
  selfref[:, 9] = [0.210589e-02, 0.197078e-02, 0.184434e-02, 0.172601e-02, 0.161528e-02,0.151164e-02, 0.141466e-02, 0.132390e-02, 0.123896e-02, 0.115947e-02]
  selfref[:,10] = [0.245098e-02, 0.233745e-02, 0.222918e-02, 0.212592e-02, 0.202745e-02,0.193353e-02, 0.184397e-02, 0.175856e-02, 0.167710e-02, 0.159941e-02]
  selfref[:,11] = [0.267460e-02, 0.253325e-02, 0.239936e-02, 0.227255e-02, 0.215244e-02,0.203868e-02, 0.193093e-02, 0.182888e-02, 0.173222e-02, 0.164067e-02]
  selfref[:,12] = [0.304510e-02, 0.283919e-02, 0.264720e-02, 0.246820e-02, 0.230130e-02,0.214568e-02, 0.200059e-02, 0.186531e-02, 0.173918e-02, 0.162157e-02]
  selfref[:,13] = [0.338445e-02, 0.314719e-02, 0.292655e-02, 0.272139e-02, 0.253060e-02,0.235319e-02, 0.218822e-02, 0.203482e-02, 0.189217e-02, 0.175952e-02]
  selfref[:,14] = [0.388649e-02, 0.357018e-02, 0.327961e-02, 0.301269e-02, 0.276750e-02,0.254226e-02, 0.233535e-02, 0.214528e-02, 0.197068e-02, 0.181029e-02]
  selfref[:,15] = [0.412547e-02, 0.387413e-02, 0.363810e-02, 0.341646e-02, 0.320831e-02,0.301285e-02, 0.282930e-02, 0.265693e-02, 0.249506e-02, 0.234305e-02]
  selfref[:,16] = [0.534327e-02, 0.482967e-02, 0.436544e-02, 0.394583e-02, 0.356655e-02,0.322373e-02, 0.291387e-02, 0.263378e-02, 0.238062e-02, 0.215179e-02]
end


# TODO: comment everything
module AsciiMutator
  using Helper

  export mutate
  export Code

  type Code
    str::String
    #
    # Start string should contain 3 symbols: 1 unchanged, 2 empty and 3 unchanged.
    # String should be a correct julia code.
    #
    function Code() new("; ;") end
  end
  #
  # TODO: optimize this method as deep aspossible
  # Produces one add/change/del mutation on code and returns it's modified version.
  # Probabilities array is an array of three items: add,change,del values. e.g.:
  # [1,2,1] means that add and del mutations will be twice less then change one.
  # Character for adding and changing is in visible ASCII range - 32:126. see ASCII
  # table for details. Change mutation is the most frequent, so it should be first.
  # @param code Code we have to modify
  # @param prob Probability array (add,change,del). e.g.: [X,X,X]
  # @return {AsciiMutator.Code}
  #
  function mutate(code::Code, prob::Array{Int})
    pIndex = Helper.getProbIndex(prob)
    len    = length(code.str) - 1
    i      = rand(2:len)

    # 1 - add, 2 - change, 3 - del
    if pIndex === 2 code.str     = string(code.str[1:i-1], Char(rand(32:126)), code.str[i+1:end])
    elseif pIndex === 1 code.str = string(code.str[1:i-1], Char(rand(32:126)), code.str[i:end])
    elseif len > 2 code.str      = string(code.str[1:i-1], code.str[i+1:end]) end
    code
  end
end