diff --git a/src/energy_storage_models/storage_constructor.jl b/src/energy_storage_models/storage_constructor.jl index 3987d1d..3011e04 100644 --- a/src/energy_storage_models/storage_constructor.jl +++ b/src/energy_storage_models/storage_constructor.jl @@ -323,7 +323,7 @@ function construct_device!( add_constraint_dual!(container, sys, model) add_event_constraints!(container, devices, model, network_model) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) return end @@ -444,7 +444,7 @@ function construct_device!( add_feedforward_constraints!(container, model, devices) # TODO issue with time varying MBC. - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return diff --git a/src/energy_storage_models/storage_models.jl b/src/energy_storage_models/storage_models.jl index 2630d76..4010772 100644 --- a/src/energy_storage_models/storage_models.jl +++ b/src/energy_storage_models/storage_models.jl @@ -188,29 +188,10 @@ function add_constraints!( model::DeviceModel{V, W}, ::NetworkModel{X}, ) where { - T <: OutputActivePowerVariableLimitsConstraint, - U <: ActivePowerOutVariable, - V <: PSY.Storage, - W <: AbstractStorageFormulation, - X <: AbstractPowerModel, -} - if get_attribute(model, "reservation") - add_reserve_range_constraints!(container, T, U, devices, model, X) - else - add_range_constraints!(container, T, U, devices, model, X) - end -end - -function add_constraints!( - container::OptimizationContainer, - ::Type{T}, - ::Type{U}, - devices::IS.FlattenIteratorWrapper{V}, - model::DeviceModel{V, W}, - ::NetworkModel{X}, -) where { - T <: InputActivePowerVariableLimitsConstraint, - U <: ActivePowerInVariable, + T <: Union{ + OutputActivePowerVariableLimitsConstraint, InputActivePowerVariableLimitsConstraint, + }, + U <: Union{ActivePowerOutVariable, ActivePowerInVariable}, V <: PSY.Storage, W <: AbstractStorageFormulation, X <: AbstractPowerModel, @@ -220,139 +201,87 @@ function add_constraints!( else add_range_constraints!(container, T, U, devices, model, X) end + return end -function add_reserve_range_constraint_with_deployment!( - container::OptimizationContainer, - ::Type{T}, - ::Type{U}, - devices::IS.FlattenIteratorWrapper{V}, - model::DeviceModel{V, W}, - ::NetworkModel{X}, -) where { - T <: OutputActivePowerVariableLimitsConstraint, - U <: ActivePowerOutVariable, - V <: PSY.Storage, - W <: AbstractStorageFormulation, - X <: AbstractPowerModel, -} - time_steps = get_time_steps(container) - names = [PSY.get_name(x) for x in devices] - powerout_var = get_variable(container, U, V) - ss_var = get_variable(container, ReservationVariable, V) - r_up_ds = get_expression(container, ReserveDeploymentBalanceUpDischarge, V) - r_dn_ds = get_expression(container, ReserveDeploymentBalanceDownDischarge, V) - - constraint = add_constraints_container!(container, T, V, names, time_steps) - - for d in devices, t in time_steps - ci_name = PSY.get_name(d) - constraint[ci_name, t] = JuMP.@constraint( - get_jump_model(container), - powerout_var[ci_name, t] + r_up_ds[ci_name, t] - r_dn_ds[ci_name, t] <= - ss_var[ci_name, t] * PSY.get_output_active_power_limits(d).max - ) - end -end - -function add_reserve_range_constraint_with_deployment!( +# Direction-dependent reserve-deployment expression pairs and max-limit accessors. +# For OutputActivePower (discharge), "effective power" is `P_out + up - down`. +# For InputActivePower (charge), it's `P_in + down - up` — reserves swap roles because +# a charging battery's net power is increased by downward reserves. +_deployment_increasing_expr(::Type{<:OutputActivePowerVariableLimitsConstraint}) = + ReserveDeploymentBalanceUpDischarge +_deployment_decreasing_expr(::Type{<:OutputActivePowerVariableLimitsConstraint}) = + ReserveDeploymentBalanceDownDischarge +_deployment_increasing_expr(::Type{<:InputActivePowerVariableLimitsConstraint}) = + ReserveDeploymentBalanceDownCharge +_deployment_decreasing_expr(::Type{<:InputActivePowerVariableLimitsConstraint}) = + ReserveDeploymentBalanceUpCharge + +# Reservation-binary handling: discharge active when ss=1, charge active when ss=0. +_reservation_factor(::Type{<:OutputActivePowerVariableLimitsConstraint}, ss, name, t) = + ss[name, t] +_reservation_factor(::Type{<:InputActivePowerVariableLimitsConstraint}, ss, name, t) = + 1.0 - ss[name, t] + +function _add_deployment_upper_bound!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, - model::DeviceModel{V, W}, - ::NetworkModel{X}, + model::DeviceModel{V, W}; + with_reservation::Bool, ) where { - T <: InputActivePowerVariableLimitsConstraint, - U <: ActivePowerInVariable, + T <: Union{ + OutputActivePowerVariableLimitsConstraint, InputActivePowerVariableLimitsConstraint, + }, + U <: Union{ActivePowerOutVariable, ActivePowerInVariable}, V <: PSY.Storage, W <: AbstractStorageFormulation, - X <: AbstractPowerModel, } time_steps = get_time_steps(container) names = [PSY.get_name(x) for x in devices] - - powerin_var = get_variable(container, U, V) - ss_var = get_variable(container, ReservationVariable, V) - r_up_ch = get_expression(container, ReserveDeploymentBalanceUpCharge, V) - r_dn_ch = get_expression(container, ReserveDeploymentBalanceDownCharge, V) + jump_model = get_jump_model(container) + power_var = get_variable(container, U, V) + r_inc = get_expression(container, _deployment_increasing_expr(T), V) + r_dec = get_expression(container, _deployment_decreasing_expr(T), V) + ss_var = with_reservation ? get_variable(container, ReservationVariable, V) : nothing constraint = add_constraints_container!(container, T, V, names, time_steps) - for d in devices, t in time_steps ci_name = PSY.get_name(d) - constraint[ci_name, t] = JuMP.@constraint( - get_jump_model(container), - powerin_var[ci_name, t] + r_dn_ch[ci_name, t] - r_up_ch[ci_name, t] <= - (1.0 - ss_var[ci_name, t]) * PSY.get_input_active_power_limits(d).max + effective_power = + power_var[ci_name, t] + r_inc[ci_name, t] - r_dec[ci_name, t] + bound = IOM.get_bound(IOM.UpperBound(), IOM.get_min_max_limits(d, T, W)) + bin = with_reservation ? _reservation_factor(T, ss_var, ci_name, t) : 1.0 + IOM.add_range_bound_constraint!( + IOM.UpperBound(), jump_model, constraint, ci_name, t, + effective_power, bound, bin, ) end + return end -function add_reserve_range_constraint_with_deployment_no_reservation!( +add_reserve_range_constraint_with_deployment!( container::OptimizationContainer, ::Type{T}, ::Type{U}, - devices::IS.FlattenIteratorWrapper{V}, - model::DeviceModel{V, W}, - ::NetworkModel{X}, -) where { - T <: OutputActivePowerVariableLimitsConstraint, - U <: ActivePowerOutVariable, - V <: PSY.Storage, - W <: AbstractStorageFormulation, - X <: AbstractPowerModel, -} - time_steps = get_time_steps(container) - names = [PSY.get_name(x) for x in devices] - powerout_var = get_variable(container, U, V) - r_up_ds = get_expression(container, ReserveDeploymentBalanceUpDischarge, V) - r_dn_ds = get_expression(container, ReserveDeploymentBalanceDownDischarge, V) - - constraint = add_constraints_container!(container, T, V, names, time_steps) - - for d in devices, t in time_steps - ci_name = PSY.get_name(d) - constraint[ci_name, t] = JuMP.@constraint( - get_jump_model(container), - powerout_var[ci_name, t] + r_up_ds[ci_name, t] - r_dn_ds[ci_name, t] <= - PSY.get_output_active_power_limits(d).max - ) - end -end - -function add_reserve_range_constraint_with_deployment_no_reservation!( + devices, + model::DeviceModel, + ::NetworkModel, +) where {T, U} = + _add_deployment_upper_bound!( + container, T, U, devices, model; with_reservation = true) + +add_reserve_range_constraint_with_deployment_no_reservation!( container::OptimizationContainer, ::Type{T}, ::Type{U}, - devices::IS.FlattenIteratorWrapper{V}, - model::DeviceModel{V, W}, - ::NetworkModel{X}, -) where { - T <: InputActivePowerVariableLimitsConstraint, - U <: ActivePowerInVariable, - V <: PSY.Storage, - W <: AbstractStorageFormulation, - X <: AbstractPowerModel, -} - time_steps = get_time_steps(container) - names = [PSY.get_name(x) for x in devices] - - powerin_var = get_variable(container, U, V) - r_up_ch = get_expression(container, ReserveDeploymentBalanceUpCharge, V) - r_dn_ch = get_expression(container, ReserveDeploymentBalanceDownCharge, V) - - constraint = add_constraints_container!(container, T, V, names, time_steps) - - for d in devices, t in time_steps - ci_name = PSY.get_name(d) - constraint[ci_name, t] = JuMP.@constraint( - get_jump_model(container), - powerin_var[ci_name, t] + r_dn_ch[ci_name, t] - r_up_ch[ci_name, t] <= - PSY.get_input_active_power_limits(d).max - ) - end -end + devices, + model::DeviceModel, + ::NetworkModel, +) where {T, U} = + _add_deployment_upper_bound!( + container, T, U, devices, model; with_reservation = false) function add_constraints!( container::OptimizationContainer, @@ -941,91 +870,59 @@ function add_energybalance_without_reserves!( return end +# Reserve-assignment bounds for discharge (Up) / charge (Down): +# UB: power + up_assignment <= max +# LB: power - down_assignment >= min +# Same shape for both directions, parametrized by the "assignment" expression pair and +# power variable/limits; routed through `IOM.add_range_bound_constraint!`. +_reserve_assignment_power_var(::Type{ReserveDischargeConstraint}) = ActivePowerOutVariable +_reserve_assignment_power_var(::Type{ReserveChargeConstraint}) = ActivePowerInVariable +_reserve_assignment_up_expr(::Type{ReserveDischargeConstraint}) = + ReserveAssignmentBalanceUpDischarge +_reserve_assignment_down_expr(::Type{ReserveDischargeConstraint}) = + ReserveAssignmentBalanceDownDischarge +_reserve_assignment_up_expr(::Type{ReserveChargeConstraint}) = + ReserveAssignmentBalanceUpCharge +_reserve_assignment_down_expr(::Type{ReserveChargeConstraint}) = + ReserveAssignmentBalanceDownCharge +_reserve_assignment_limits(::Type{ReserveDischargeConstraint}, d) = + PSY.get_output_active_power_limits(d) +_reserve_assignment_limits(::Type{ReserveChargeConstraint}, d) = + PSY.get_input_active_power_limits(d) + """ -Add Energy Balance Constraints for AbstractStorageFormulation +Reserve-assignment range constraints for discharge (T = ReserveDischargeConstraint) +and charge (T = ReserveChargeConstraint) under `StorageDispatchWithReserves`. """ function add_constraints!( container::OptimizationContainer, - ::Type{ReserveDischargeConstraint}, + ::Type{T}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, StorageDispatchWithReserves}, network_model::NetworkModel{X}, -) where {V <: PSY.Storage, X <: AbstractPowerModel} +) where { + T <: Union{ReserveDischargeConstraint, ReserveChargeConstraint}, + V <: PSY.Storage, + X <: AbstractPowerModel, +} names = String[PSY.get_name(x) for x in devices] time_steps = get_time_steps(container) - powerout_var = get_variable(container, ActivePowerOutVariable, V) - r_up_ds = get_expression(container, ReserveAssignmentBalanceUpDischarge, V) - r_dn_ds = get_expression(container, ReserveAssignmentBalanceDownDischarge, V) - - constraint_ds_ub = add_constraints_container!(container, ReserveDischargeConstraint, - V, - names, - time_steps; - meta = "ub", - ) - - constraint_ds_lb = add_constraints_container!(container, ReserveDischargeConstraint, - V, - names, - time_steps; - meta = "lb", - ) + jump_model = get_jump_model(container) + power_var = get_variable(container, _reserve_assignment_power_var(T), V) + r_up = get_expression(container, _reserve_assignment_up_expr(T), V) + r_dn = get_expression(container, _reserve_assignment_down_expr(T), V) + con_ub = add_constraints_container!(container, T, V, names, time_steps; meta = "ub") + con_lb = add_constraints_container!(container, T, V, names, time_steps; meta = "lb") for d in devices, t in time_steps name = PSY.get_name(d) - constraint_ds_ub[name, t] = JuMP.@constraint( - get_jump_model(container), - powerout_var[name, t] + r_up_ds[name, t] <= - PSY.get_output_active_power_limits(d).max - ) - constraint_ds_lb[name, t] = JuMP.@constraint( - get_jump_model(container), - powerout_var[name, t] - r_dn_ds[name, t] >= - PSY.get_output_active_power_limits(d).min - ) - end - return -end - -function add_constraints!( - container::OptimizationContainer, - ::Type{ReserveChargeConstraint}, - devices::IS.FlattenIteratorWrapper{V}, - model::DeviceModel{V, StorageDispatchWithReserves}, - network_model::NetworkModel{X}, -) where {V <: PSY.Storage, X <: AbstractPowerModel} - names = String[PSY.get_name(x) for x in devices] - time_steps = get_time_steps(container) - powerin_var = get_variable(container, ActivePowerInVariable, V) - r_up_ch = get_expression(container, ReserveAssignmentBalanceUpCharge, V) - r_dn_ch = get_expression(container, ReserveAssignmentBalanceDownCharge, V) - - constraint_ch_ub = add_constraints_container!(container, ReserveChargeConstraint, - V, - names, - time_steps; - meta = "ub", - ) - - constraint_ch_lb = add_constraints_container!(container, ReserveChargeConstraint, - V, - names, - time_steps; - meta = "lb", - ) - - for d in devices, t in get_time_steps(container) - name = PSY.get_name(d) - constraint_ch_ub[name, t] = JuMP.@constraint( - get_jump_model(container), - powerin_var[name, t] + r_dn_ch[name, t] <= - PSY.get_input_active_power_limits(d).max - ) - constraint_ch_lb[name, t] = JuMP.@constraint( - get_jump_model(container), - powerin_var[name, t] - r_up_ch[name, t] >= - PSY.get_input_active_power_limits(d).min - ) + limits = _reserve_assignment_limits(T, d) + IOM.add_range_bound_constraint!( + IOM.UpperBound(), jump_model, con_ub, name, t, + power_var[name, t] + r_up[name, t], limits.max) + IOM.add_range_bound_constraint!( + IOM.LowerBound(), jump_model, con_lb, name, t, + power_var[name, t] - r_dn[name, t], limits.min) end return end @@ -1727,7 +1624,7 @@ end ########################### Objective Function and Costs ###################### # no test coverage -function objective_function!( +function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, U}, @@ -1753,7 +1650,7 @@ function objective_function!( return end -function objective_function!( +function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{PSY.EnergyReservoirStorage}, model::DeviceModel{PSY.EnergyReservoirStorage, T}, @@ -1797,6 +1694,9 @@ function objective_function!( return end +# Storage cycling/energy-target slack penalties are applied only at the final timestep +# (a single horizon-end accumulator), not per-timestep — so we can't delegate to the +# IOM default `add_proportional_cost!`, which loops over all timesteps. # no test coverage function add_proportional_cost!( container::OptimizationContainer, @@ -1804,42 +1704,22 @@ function add_proportional_cost!( devices::IS.FlattenIteratorWrapper{U}, ::Type{F}, ) where { - T <: Union{StorageChargeCyclingSlackVariable, StorageDischargeCyclingSlackVariable}, + T <: Union{ + StorageChargeCyclingSlackVariable, StorageDischargeCyclingSlackVariable, + StorageEnergyShortageVariable, StorageEnergySurplusVariable, + }, U <: PSY.EnergyReservoirStorage, F <: AbstractStorageFormulation, } - time_steps = get_time_steps(container) + t_end = last(get_time_steps(container)) variable = get_variable(container, T, U) for d in devices name = PSY.get_name(d) op_cost_data = PSY.get_operation_cost(d) cost_term = proportional_cost(op_cost_data, T, d, F) - add_to_objective_invariant_expression!( - container, - variable[name, time_steps[end]] * cost_term, - ) - end -end - -function add_proportional_cost!( - container::OptimizationContainer, - ::Type{T}, - devices::IS.FlattenIteratorWrapper{U}, - ::Type{F}, -) where { - T <: Union{StorageEnergyShortageVariable, StorageEnergySurplusVariable}, - U <: PSY.EnergyReservoirStorage, - F <: AbstractStorageFormulation, -} - time_steps = get_time_steps(container) - variable = get_variable(container, T, U) - for d in devices - name = PSY.get_name(d) - op_cost_data = PSY.get_operation_cost(d) - cost_term = proportional_cost(op_cost_data, T, d, F) - add_to_objective_invariant_expression!( - container, - variable[name, time_steps[end]] * cost_term, + IOM.add_cost_term_invariant!( + container, variable[name, t_end], cost_term, + ProductionCostExpression, U, name, t_end, ) end end @@ -1862,57 +1742,3 @@ function calculate_aux_variable_value!( return end - -################## Storage Systems with Market Bid Cost ################### - -function _add_variable_cost_to_objective!( - container::OptimizationContainer, - ::T, - component::PSY.Component, - cost_function::MBC_TYPES, - ::U, -) where { - T <: Union{ActivePowerOutVariable, StorageRegularizationVariableDischarge}, - U <: AbstractStorageFormulation, -} - component_name = PSY.get_name(component) - @debug "Market Bid" _group = LOG_GROUP_COST_FUNCTIONS component_name - incremental_cost_curves = PSY.get_incremental_offer_curves(cost_function) - if !isnothing(incremental_cost_curves) - add_pwl_term_delta!( - IncrementalOffer(), - container, - component, - cost_function, - T(), - U(), - ) - end - return -end - -function _add_variable_cost_to_objective!( - container::OptimizationContainer, - ::T, - component::PSY.Component, - cost_function::MBC_TYPES, - ::U, -) where { - T <: Union{ActivePowerInVariable, StorageRegularizationVariableCharge}, - U <: AbstractStorageFormulation, -} - component_name = PSY.get_name(component) - @debug "Market Bid" _group = LOG_GROUP_COST_FUNCTIONS component_name - decremental_cost_curves = PSY.get_decremental_offer_curves(cost_function) - if !isnothing(decremental_cost_curves) - add_pwl_term_delta!( - DecrementalOffer(), - container, - component, - cost_function, - T(), - U(), - ) - end - return -end diff --git a/src/initial_conditions/update_initial_conditions.jl b/src/initial_conditions/update_initial_conditions.jl index 159be64..48b18c0 100644 --- a/src/initial_conditions/update_initial_conditions.jl +++ b/src/initial_conditions/update_initial_conditions.jl @@ -8,6 +8,7 @@ _ic_variable_type(::Type{DeviceAboveMinPower}) = PowerAboveMinimumVariable() _ic_variable_type(::Type{InitialTimeDurationOn}) = TimeDurationOn() _ic_variable_type(::Type{InitialTimeDurationOff}) = TimeDurationOff() _ic_variable_type(::Type{InitialEnergyLevel}) = EnergyVariable() +_ic_variable_type(::Type{InitialReservoirVolume}) = HydroReservoirVolumeVariable() # Dispatch to the right container getter based on variable vs aux variable type # FIXME we should add something like this to the API. diff --git a/src/static_injector_models/hydro_generation.jl b/src/static_injector_models/hydro_generation.jl index a51884f..b932e81 100644 --- a/src/static_injector_models/hydro_generation.jl +++ b/src/static_injector_models/hydro_generation.jl @@ -1,20 +1,5 @@ #! format: off -# Helper for proportional cost terms in objective function -function _add_proportional_term!( - container::OptimizationContainer, - ::Type{T}, - component::U, - linear_term::Float64, - time_period::Int, -) where {T <: VariableType, U <: PSY.Component} - component_name = PSY.get_name(component) - variable = get_variable(container, T, U)[component_name, time_period] - lin_cost = variable * linear_term - add_to_objective_invariant_expression!(container, lin_cost) - return lin_cost -end - # These methods are defined in PowerSimulations requires_initialization(::AbstractHydroReservoirFormulation) = false requires_initialization(::AbstractHydroUnitCommitment) = true @@ -286,8 +271,7 @@ objective_function_multiplier(::Type{HydroWaterShortageVariable}, ::Type{<:Abstr objective_function_multiplier(::Type{WaterSpillageVariable}, ::Type{<:AbstractHydroReservoirFormulation})=OBJECTIVE_FUNCTION_POSITIVE # objective_function_multiplier(::ActivePowerOutVariable, ::HydroWaterFactorModel)=OBJECTIVE_FUNCTION_POSITIVE -sos_status(::PSY.HydroGen, ::AbstractHydroReservoirFormulation)=SOSStatusVariable.NO_VARIABLE -sos_status(::PSY.HydroGen, ::AbstractHydroUnitCommitment)=SOSStatusVariable.VARIABLE +IOM.uses_commitment_variables(::Type{<:PSY.HydroGen}) = true variable_cost(::Nothing, ::Type{ActivePowerVariable}, ::Type{<:PSY.HydroGen}, ::Type{<:AbstractHydroReservoirFormulation})=0.0 variable_cost(cost::PSY.OperationalCost, ::Type{ActivePowerVariable}, ::Type{<:PSY.HydroGen}, ::Type{<:AbstractHydroFormulation})=PSY.get_variable(cost) @@ -560,13 +544,18 @@ end ############################### Constraints ################################ ############################################################################ -""" -Time series constraints -""" +# `ActivePowerVariableLimitsConstraint` is always called with one of the two +# `ActivePowerRangeExpression{LB,UB}` expression types from the hydro constructors, +# so the U bounds below are tightened to `RangeConstraint{LB,UB}Expressions` +# (PSI-era signatures admitted a raw VariableType via Union; that branch is dead here). + +# HydroDispatchRunOfRiver[Budget]: LB is a plain range constraint; UB additionally adds +# a parameterized upper bound from the ActivePowerTimeSeriesParameter (the run-of-river +# profile). function add_constraints!( container::OptimizationContainer, T::Type{ActivePowerVariableLimitsConstraint}, - U::Type{<:Union{VariableType, ExpressionType}}, + U::Type{<:RangeConstraintLBExpressions}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, @@ -578,22 +567,13 @@ function add_constraints!( if !has_semicontinuous_feedforward(model, U) add_range_constraints!(container, T, U, devices, model, X) end - add_parameterized_upper_bound_range_constraints( - container, - ActivePowerVariableTimeSeriesLimitsConstraint, - U, - ActivePowerTimeSeriesParameter, - devices, - model, - X, - ) return end function add_constraints!( container::OptimizationContainer, T::Type{ActivePowerVariableLimitsConstraint}, - U::Type{<:RangeConstraintLBExpressions}, + U::Type{<:RangeConstraintUBExpressions}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, @@ -605,16 +585,24 @@ function add_constraints!( if !has_semicontinuous_feedforward(model, U) add_range_constraints!(container, T, U, devices, model, X) end + add_parameterized_upper_bound_range_constraints( + container, + ActivePowerVariableTimeSeriesLimitsConstraint, + U, + ActivePowerTimeSeriesParameter, + devices, + model, + X, + ) return end -""" -Add semicontinuous range constraints for [`HydroCommitmentRunOfRiver`](@ref) formulation -""" +# HydroCommitmentRunOfRiver: semicontinuous (OnVariable-gated) range; UB additionally +# adds the parameterized TS upper bound. function add_constraints!( container::OptimizationContainer, T::Type{ActivePowerVariableLimitsConstraint}, - U::Type{<:Union{VariableType, <:RangeConstraintLBExpressions}}, + U::Type{<:RangeConstraintLBExpressions}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, @@ -626,7 +614,7 @@ end function add_constraints!( container::OptimizationContainer, T::Type{ActivePowerVariableLimitsConstraint}, - U::Type{<:Union{VariableType, ExpressionType}}, + U::Type{<:RangeConstraintUBExpressions}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, @@ -644,10 +632,11 @@ function add_constraints!( return end +# HydroTurbine + HydroTurbineEnergyCommitment: plain semicontinuous range, no TS bound. function add_constraints!( container::OptimizationContainer, T::Type{ActivePowerVariableLimitsConstraint}, - U::Type{<:Union{VariableType, ExpressionType}}, + U::Type{<:Union{RangeConstraintLBExpressions, RangeConstraintUBExpressions}}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, @@ -2196,7 +2185,8 @@ function calculate_aux_variable_value!( end ##################################### Hydro generation cost ############################ -function objective_function!( +# generic commitment +function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, U}, @@ -2207,37 +2197,11 @@ function objective_function!( return end -# MarketBidCost proportional_cost args: (container, cost, variable, device, formulation, time) -# HydroGenerationCost proportional_cost args: (cost, variable, device, formulation) -# this ties the two together by ignoring the container and time args -proportional_cost( - ::OptimizationContainer, - cost::PSY.HydroGenerationCost, - ::Type{U}, - comp::PSY.HydroGen, - ::Type{V}, - ::Int, -) where {U <: OnVariable, V <: AbstractHydroUnitCommitment} = - proportional_cost(cost, U, comp, V) - -# HydroGenerationCost uses CostCurves (no FuelCurve path), so the OnVariable -# proportional term's rate is always static — `is_time_variant` on the variable -# curve would be answering a broader question than this trait asks. -IOM.is_time_variant_proportional(::PSY.HydroGenerationCost) = false - +# HydroGenerationCost rate is always static (CostCurve only, no FuelCurve), so the +# static 4-arg `proportional_cost` definition above + IOM's default `add_proportional_cost!` +# handle the OnVariable term. We only need to register the must-run trait. skip_proportional_cost(d::PSY.HydroPumpTurbine) = PSY.get_must_run(d) -add_proportional_cost!( - container::OptimizationContainer, - ::Type{U}, - devices::IS.FlattenIteratorWrapper{T}, - ::Type{V}, -) where {U <: OnVariable, T <: PSY.HydroGen, V <: AbstractHydroUnitCommitment} = - add_proportional_cost_maybe_time_variant!(container, U, devices, V) - -# MarketBidCost (static + time-series) proportional_cost/is_time_variant_proportional are generic — -# see common_models/market_bid_overrides.jl. - # These _include_{constant}_min_gen_power functions are needed for MarketBidCost. # Commitment has an on/off choice, so add OnVariable * breakpoint1 to power constraint. _include_min_gen_power_in_constraint( @@ -2270,7 +2234,8 @@ _include_min_gen_power_in_constraint( ::Type{<:AbstractDeviceFormulation}, ) = false -function objective_function!( +# generic dispatch +function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, U}, @@ -2280,7 +2245,8 @@ function objective_function!( return end -function objective_function!( +# RunOfRiver dispatch +function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, U}, @@ -2293,7 +2259,8 @@ function objective_function!( return end -function objective_function!( +# energy model reservoir +function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, U}, @@ -2309,7 +2276,8 @@ function objective_function!( return end -function objective_function!( +# water model reservoir +function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, U}, @@ -2321,7 +2289,8 @@ function objective_function!( return end -function objective_function!( +# pump turbines +function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, U}, @@ -2332,6 +2301,9 @@ function objective_function!( return end +# Hydro slack/spillage variables are in per-unit; cost data is in $/MW(h), so multiplying +# by `base_power` converts the product to $. Unlike thermal OnVariable (binary) where +# `proportional_cost` is already a $-per-period rate, hydro rates need this scaling. function add_proportional_cost!( container::OptimizationContainer, ::Type{U}, @@ -2339,8 +2311,11 @@ function add_proportional_cost!( ::Type{V}, ) where { T <: PSY.Component, - U <: - Union{HydroEnergySurplusVariable, HydroEnergyShortageVariable, WaterSpillageVariable}, + U <: Union{ + HydroEnergySurplusVariable, HydroEnergyShortageVariable, WaterSpillageVariable, + HydroBalanceShortageVariable, HydroBalanceSurplusVariable, + HydroWaterSurplusVariable, HydroWaterShortageVariable, + }, V <: AbstractDeviceFormulation, } base_p = get_model_base_power(container) @@ -2349,102 +2324,12 @@ function add_proportional_cost!( op_cost_data = PSY.get_operation_cost(d) cost_term = proportional_cost(op_cost_data, U, d, V) iszero(cost_term) && continue + rate = cost_term * multiplier * base_p + name = PSY.get_name(d) for t in get_time_steps(container) - _add_proportional_term!( - container, - U, - d, - cost_term * multiplier * base_p, - t, - ) - end - end - return -end - -function add_proportional_cost!( - container::OptimizationContainer, - ::Type{U}, - devices::IS.FlattenIteratorWrapper{T}, - ::Type{V}, -) where { - T <: PSY.HydroReservoir, - U <: - Union{HydroEnergySurplusVariable, HydroEnergyShortageVariable, WaterSpillageVariable}, - V <: HydroEnergyModelReservoir, -} - base_p = get_model_base_power(container) - multiplier = objective_function_multiplier(U, V) - for d in devices - op_cost_data = PSY.get_operation_cost(d) - cost_term = proportional_cost(op_cost_data, U, d, V) - iszero(cost_term) && continue - for t in get_time_steps(container) - _add_proportional_term!( - container, - U, - d, - cost_term * multiplier * base_p, - t, - ) - end - end - return -end - -function add_proportional_cost!( - container::OptimizationContainer, - ::Type{U}, - devices::IS.FlattenIteratorWrapper{T}, - ::Type{V}, -) where { - T <: PSY.HydroReservoir, - U <: Union{HydroBalanceShortageVariable, HydroBalanceSurplusVariable}, - V <: HydroEnergyModelReservoir, -} - base_p = get_model_base_power(container) - multiplier = objective_function_multiplier(U, V) - for d in devices - op_cost_data = PSY.get_operation_cost(d) - cost_term = proportional_cost(op_cost_data, U, d, V) - iszero(cost_term) && continue - time_steps = get_time_steps(container) - for t in time_steps - _add_proportional_term!( - container, - U, - d, - cost_term * multiplier * base_p, - t, - ) - end - end - return -end - -function add_proportional_cost!( - container::OptimizationContainer, - ::Type{U}, - devices::IS.FlattenIteratorWrapper{T}, - ::Type{V}, -) where { - T <: PSY.HydroReservoir, - U <: Union{HydroWaterSurplusVariable, HydroWaterShortageVariable}, - V <: HydroWaterModelReservoir, -} - base_p = get_model_base_power(container) - multiplier = objective_function_multiplier(U, V) - for d in devices - op_cost_data = PSY.get_operation_cost(d) - cost_term = proportional_cost(op_cost_data, U, d, V) - iszero(cost_term) && continue - for t in get_time_steps(container) - _add_proportional_term!( - container, - U, - d, - cost_term * multiplier * base_p, - t, + variable = get_variable(container, U, T)[name, t] + IOM.add_cost_term_invariant!( + container, variable, rate, ProductionCostExpression, T, name, t, ) end end @@ -2455,31 +2340,6 @@ end ##################### Update Initial Conditions ############################ ############################################################################ -function update_initial_conditions!( - ics::Vector{T}, - store::EmulationModelStore, - ::Dates.Millisecond, -) where { - T <: Union{ - InitialCondition{InitialReservoirVolume, Float64}, - InitialCondition{InitialReservoirVolume, JuMP.VariableRef}, - InitialCondition{InitialReservoirVolume, Nothing}, - }, -} - for ic in ics - var_val = get_variable_value( - store, - HydroReservoirVolumeVariable(), - get_component_type(ic), - ) - set_ic_quantity!( - ic, - get_last_recorded_value(var_val)[get_component_name(ic)], - ) - end - return -end - ##### Pump Turbine Constraints ##### """ @@ -2492,33 +2352,13 @@ function add_constraints!( devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, -) where { - V <: PSY.HydroPumpTurbine, - W <: HydroPumpEnergyDispatch, - X <: AbstractPowerModel, -} +) where {V <: PSY.HydroPumpTurbine, W <: HydroPumpEnergyDispatch, X <: AbstractPowerModel} if !get_attribute(model, "reservation") add_range_constraints!(container, T, U, devices, model, X) else array = get_expression(container, U, V) - reservation = get_variable(container, ReservationVariable, V) - time_steps = get_time_steps(container) - device_names = [PSY.get_name(d) for d in devices] - con_lb = add_constraints_container!(container, T, - V, - device_names, - time_steps; - meta = "lb", - ) - for device in devices, t in time_steps - ci_name = PSY.get_name(device) - limits = get_min_max_limits(device, T, W) - con_lb[ci_name, t] = - JuMP.@constraint( - get_jump_model(container), - array[ci_name, t] >= limits.min * reservation[ci_name, t] - ) - end + IOM.add_reserve_bound_range_constraints!( + container, T, IOM.LowerBound(), array, devices, model, false) end return end @@ -2533,39 +2373,20 @@ function add_constraints!( devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, -) where { - V <: PSY.HydroPumpTurbine, - W <: HydroPumpEnergyDispatch, - X <: AbstractPowerModel, -} +) where {V <: PSY.HydroPumpTurbine, W <: HydroPumpEnergyDispatch, X <: AbstractPowerModel} if !get_attribute(model, "reservation") add_range_constraints!(container, T, U, devices, model, X) else array = get_expression(container, U, V) - reservation = get_variable(container, ReservationVariable, V) - time_steps = get_time_steps(container) - device_names = [PSY.get_name(d) for d in devices] - con_ub = add_constraints_container!(container, T, - V, - device_names, - time_steps; - meta = "ub", - ) - for device in devices, t in time_steps - ci_name = PSY.get_name(device) - limits = get_min_max_limits(device, T, W) - con_ub[ci_name, t] = - JuMP.@constraint( - get_jump_model(container), - array[ci_name, t] <= limits.max * reservation[ci_name, t] - ) - end + IOM.add_reserve_bound_range_constraints!( + container, T, IOM.UpperBound(), array, devices, model, false) end return end """ -Add semicontinuous LB range constraints for [`HydroPumpEnergyCommitment`](@ref) formulation +Add semicontinuous LB range constraints for [`HydroPumpEnergyCommitment`](@ref) formulation. +Reservation path pairs a reservation-keyed bound ("lb") with an OnVariable-keyed bound ("lb_aux"). """ function add_constraints!( container::OptimizationContainer, @@ -2574,51 +2395,22 @@ function add_constraints!( devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, -) where { - V <: PSY.HydroPumpTurbine, - W <: HydroPumpEnergyCommitment, - X <: AbstractPowerModel, -} +) where {V <: PSY.HydroPumpTurbine, W <: HydroPumpEnergyCommitment, X <: AbstractPowerModel} if !get_attribute(model, "reservation") add_semicontinuous_range_constraints!(container, T, U, devices, model, X) else array = get_expression(container, U, V) - reservation = get_variable(container, ReservationVariable, V) - onvar = get_variable(container, OnVariable, V) - time_steps = get_time_steps(container) - device_names = [PSY.get_name(d) for d in devices] - con_lb = add_constraints_container!(container, T, - V, - device_names, - time_steps; - meta = "lb", - ) - con_lb_aux = add_constraints_container!(container, T, - V, - device_names, - time_steps; - meta = "lb_aux", - ) - for device in devices, t in time_steps - ci_name = PSY.get_name(device) - limits = get_min_max_limits(device, T, W) - con_lb[ci_name, t] = - JuMP.@constraint( - get_jump_model(container), - array[ci_name, t] >= limits.min * reservation[ci_name, t] - ) - con_lb_aux[ci_name, t] = - JuMP.@constraint( - get_jump_model(container), - array[ci_name, t] >= limits.min * onvar[ci_name, t] - ) - end + IOM.add_reserve_bound_range_constraints!( + container, T, IOM.LowerBound(), array, devices, model, false) + IOM.add_commitment_bound_range_constraints!( + container, T, IOM.LowerBound(), array, devices, model; meta_suffix = "_aux") end return end """ -Add semicontinuous UB range constraints for [`HydroPumpEnergyCommitment`](@ref) formulation +Add semicontinuous UB range constraints for [`HydroPumpEnergyCommitment`](@ref) formulation. +Reservation path pairs a reservation-keyed bound ("ub") with an OnVariable-keyed bound ("ub_aux"). """ function add_constraints!( container::OptimizationContainer, @@ -2627,45 +2419,15 @@ function add_constraints!( devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, -) where { - V <: PSY.HydroPumpTurbine, - W <: HydroPumpEnergyCommitment, - X <: AbstractPowerModel, -} +) where {V <: PSY.HydroPumpTurbine, W <: HydroPumpEnergyCommitment, X <: AbstractPowerModel} if !get_attribute(model, "reservation") add_semicontinuous_range_constraints!(container, T, U, devices, model, X) else array = get_expression(container, U, V) - reservation = get_variable(container, ReservationVariable, V) - onvar = get_variable(container, OnVariable, V) - time_steps = get_time_steps(container) - device_names = [PSY.get_name(d) for d in devices] - con_ub = add_constraints_container!(container, T, - V, - device_names, - time_steps; - meta = "ub", - ) - con_ub_aux = add_constraints_container!(container, T, - V, - device_names, - time_steps; - meta = "ub_aux", - ) - for device in devices, t in time_steps - ci_name = PSY.get_name(device) - limits = get_min_max_limits(device, T, W) - con_ub[ci_name, t] = - JuMP.@constraint( - get_jump_model(container), - array[ci_name, t] <= limits.max * reservation[ci_name, t] - ) - con_ub_aux[ci_name, t] = - JuMP.@constraint( - get_jump_model(container), - array[ci_name, t] <= limits.max * onvar[ci_name, t] - ) - end + IOM.add_reserve_bound_range_constraints!( + container, T, IOM.UpperBound(), array, devices, model, false) + IOM.add_commitment_bound_range_constraints!( + container, T, IOM.UpperBound(), array, devices, model; meta_suffix = "_aux") end return end @@ -2686,13 +2448,22 @@ function add_constraints!( return end +get_min_max_limits( + x::PSY.HydroPumpTurbine, + ::Type{<:ActivePowerPumpReservationConstraint}, + ::Type{<:AbstractHydroPumpFormulation}, +) = PSY.get_active_power_limits_pump(x) + """ This function defines the constraints for the pump power for the [`PowerSystems.HydroPumpTurbine`](@extref). + +Enforces `power_pump <= pump_max * (1 - reservation)`: the pump can only draw power +when the unit is not reserved for generation. """ function add_constraints!( container::OptimizationContainer, - ::Type{ActivePowerPumpReservationConstraint}, + T::Type{ActivePowerPumpReservationConstraint}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, @@ -2701,28 +2472,9 @@ function add_constraints!( W <: AbstractHydroPumpFormulation, X <: AbstractPowerModel, } - time_steps = get_time_steps(container) - names = PSY.get_name.(devices) - power_var = get_variable(container, ActivePowerPumpVariable, V) - reservation_var = get_variable(container, ReservationVariable, V) - - constraint = - add_constraints_container!(container, ActivePowerPumpReservationConstraint, - V, - names, - time_steps, - ) - - for device in devices - name = PSY.get_name(device) - pump_max = get_variable_upper_bound(ActivePowerPumpVariable, device, W) - for t in time_steps - constraint[name, t] = JuMP.@constraint( - container.JuMPmodel, - power_var[name, t] <= pump_max * (1 - reservation_var[name, t]) - ) - end - end + array = get_variable(container, ActivePowerPumpVariable, V) + IOM.add_reserve_bound_range_constraints!( + container, T, IOM.UpperBound(), array, devices, model, true) return end diff --git a/src/static_injector_models/hydrogeneration_constructor.jl b/src/static_injector_models/hydrogeneration_constructor.jl index d64742d..8f1b8b1 100644 --- a/src/static_injector_models/hydrogeneration_constructor.jl +++ b/src/static_injector_models/hydrogeneration_constructor.jl @@ -207,7 +207,7 @@ function construct_device!( ) add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) @@ -324,7 +324,7 @@ function construct_device!( add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return @@ -470,7 +470,7 @@ function construct_device!( add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return @@ -596,7 +596,7 @@ function construct_device!( add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return @@ -781,7 +781,7 @@ function construct_device!( add_feedforward_constraints!(container, model, devices) # this is erroring when there's a market bid cost. - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return @@ -932,7 +932,7 @@ function construct_device!( add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_constraint_dual!(container, sys, model) return end @@ -1067,7 +1067,7 @@ function construct_device!( ) add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) @@ -1182,7 +1182,7 @@ function construct_device!( add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) @@ -1319,7 +1319,7 @@ function construct_device!( ) add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) @@ -1435,7 +1435,7 @@ function construct_device!( add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) @@ -1638,7 +1638,7 @@ function construct_device!( add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return @@ -1796,7 +1796,7 @@ function construct_device!( end add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) return end @@ -1934,7 +1934,7 @@ function construct_device!( add_feedforward_constraints!(container, model, devices) - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return @@ -2113,7 +2113,7 @@ function construct_device!( ) end - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) @@ -2303,7 +2303,7 @@ function construct_device!( ) end - objective_function!(container, devices, model, S) + add_to_objective_function!(container, devices, model, S) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) diff --git a/src/static_injector_models/thermal_generation.jl b/src/static_injector_models/thermal_generation.jl index 4bee2e8..6582331 100644 --- a/src/static_injector_models/thermal_generation.jl +++ b/src/static_injector_models/thermal_generation.jl @@ -287,9 +287,6 @@ function get_min_max_limits( return PSY.get_active_power_limits(device) end -# removed: add_constraints! for compact formulations. body was identical to -# the AbstractThermalDispatch version. - """ Min and max active power limits of generators for thermal dispatch compact formulations """ @@ -487,6 +484,7 @@ function _get_data_for_range_ic( return ini_conds end +# commitment formulations: time series upper bounds. function add_constraints!( container::OptimizationContainer, ::Type{ActivePowerVariableTimeSeriesLimitsConstraint}, @@ -595,6 +593,7 @@ function add_constraints!( return end +# multistart devices with commitment formulations: lower bound expression. function add_constraints!( container::OptimizationContainer, T::Type{<:ActivePowerVariableLimitsConstraint}, @@ -638,6 +637,7 @@ function add_constraints!( return end +# multistart devices with commitment formulations: upper bound expression. function add_constraints!( container::OptimizationContainer, T::Type{<:ActivePowerVariableLimitsConstraint}, @@ -705,6 +705,7 @@ function add_constraints!( return end +# compact commitment: IC constraints. function add_constraints!( container::OptimizationContainer, ::Type{ActiveRangeICConstraint}, @@ -768,6 +769,7 @@ function get_min_max_limits( return PSY.get_reactive_power_limits(device) end +# commitment formulations: commitment constraints (on/off logic) function add_constraints!( container::OptimizationContainer, T::Type{CommitmentConstraint}, @@ -1029,6 +1031,7 @@ _get_initial_condition_type( ::Type{ThermalCompactDispatch}, ) = DeviceAboveMinPower +# plain commitment ramping limits: semicontinuous with ActivePowerVariable. """ This function adds the ramping limits of generators when there are CommitmentVariables """ @@ -1054,6 +1057,7 @@ function add_constraints!( return end +# compact commitment ramping limits: semicontinuous with PowerAboveMinimumVariable. function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, @@ -1076,6 +1080,7 @@ function add_constraints!( return end +# compact dispatch ramping limits: linear with PowerAboveMinimumVariable. function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, @@ -1087,6 +1092,7 @@ function add_constraints!( return end +# non-compact dispatch ramping limits: linear with ActivePowerVariable. function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, @@ -1102,6 +1108,7 @@ function add_constraints!( return end +# multi-start commitment ramping limits: linear with PowerAboveMinimumVariable. function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, @@ -1372,6 +1379,7 @@ function _get_data_for_tdc( return ini_conds, time_params end +# non-multistart commitment formulations: time duration constraints function add_constraints!( container::OptimizationContainer, ::Type{DurationConstraint}, @@ -1410,6 +1418,7 @@ function add_constraints!( return end +# multi-start unit commitment: time duration constraints function add_constraints!( container::OptimizationContainer, ::Type{DurationConstraint}, @@ -1467,6 +1476,7 @@ add_proportional_cost!( ########################### 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. +# regular commitment function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, @@ -1484,6 +1494,7 @@ function add_to_objective_function!( return end +# compact commitment function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, @@ -1501,6 +1512,7 @@ function add_to_objective_function!( return end +# multi-start commitment function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart}, @@ -1520,6 +1532,7 @@ function add_to_objective_function!( return end +# regular dispatch function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, @@ -1534,6 +1547,7 @@ function add_to_objective_function!( return end +# compact dispatch function add_to_objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, @@ -1548,6 +1562,7 @@ function add_to_objective_function!( return end +# can't have multi-start with ThermalDispatchNoMin. function add_to_objective_function!( ::OptimizationContainer, ::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart}, @@ -1660,13 +1675,15 @@ function IOM._add_semicontinuous_bound_range_constraints_impl!( dir::IOM.BoundDirection, array, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, - ::DeviceModel{V, W}, + ::DeviceModel{V, W}; + meta_suffix::String = "", ) where {T <: ConstraintType, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation} time_steps = IOM.get_time_steps(container) names = IS.get_name.(devices) jump_model = IOM.get_jump_model(container) con = IOM.add_constraints_container!( - container, T, V, names, time_steps; meta = IOM.constraint_meta(dir)) + container, T, V, names, time_steps; + meta = IOM.constraint_meta(dir) * meta_suffix) varbin = IOM.get_variable(container, OnVariable, V) for device in devices