diff --git a/src/HybridSystemsSimulations.jl b/src/HybridSystemsSimulations.jl index e5ca2e24..b81b05c1 100644 --- a/src/HybridSystemsSimulations.jl +++ b/src/HybridSystemsSimulations.jl @@ -24,6 +24,10 @@ export TotalReserve # Auxiliary variables +# FeedForward +export CyclingChargeLimitFeedforward +export CyclingDischargeLimitFeedforward + # Constraints export OptConditionRenewablePower export OptConditionBatteryCharge diff --git a/src/add_aux_variables.jl b/src/add_aux_variables.jl index 75e4fb7c..21afb6e0 100644 --- a/src/add_aux_variables.jl +++ b/src/add_aux_variables.jl @@ -9,8 +9,6 @@ function PSI.calculate_aux_variable_value!( resolution = PSI.get_resolution(container) fraction_of_hour = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR charge_var = PSI.get_variable(container, BatteryCharge(), T) - ch_served_reg_up = PSI.get_expression(container, ChargeServedReserveUpExpression(), T) - ch_served_reg_dn = PSI.get_expression(container, ChargeServedReserveDownExpression(), T) aux_variable_container = PSI.get_aux_variable(container, CumulativeCyclingCharge(), T) for d in devices name = PSY.get_name(d) @@ -23,6 +21,10 @@ function PSI.calculate_aux_variable_value!( fraction_of_hour * sum(PSI.jump_value(charge_var[name, k]) for k in 1:t) else + ch_served_reg_up = + PSI.get_expression(container, ChargeServedReserveUpExpression(), T) + ch_served_reg_dn = + PSI.get_expression(container, ChargeServedReserveDownExpression(), T) aux_variable_container[name, t] = efficiency.in * fraction_of_hour * @@ -49,10 +51,6 @@ function PSI.calculate_aux_variable_value!( resolution = PSI.get_resolution(container) fraction_of_hour = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR discharge_var = PSI.get_variable(container, BatteryDischarge(), T) - ds_served_reg_up = - PSI.get_expression(container, DischargeServedReserveUpExpression(), T) - ds_served_reg_dn = - PSI.get_expression(container, DischargeServedReserveDownExpression(), T) aux_variable_container = PSI.get_aux_variable(container, CumulativeCyclingDischarge(), T) for d in devices @@ -66,6 +64,10 @@ function PSI.calculate_aux_variable_value!( fraction_of_hour * sum(PSI.jump_value(discharge_var[name, k]) for k in 1:t) else + ds_served_reg_up = + PSI.get_expression(container, DischargeServedReserveUpExpression(), T) + ds_served_reg_dn = + PSI.get_expression(container, DischargeServedReserveDownExpression(), T) aux_variable_container[name, t] = (1.0 / efficiency.out) * fraction_of_hour * diff --git a/src/add_constraints.jl b/src/add_constraints.jl index 493fabdf..fdb4356e 100644 --- a/src/add_constraints.jl +++ b/src/add_constraints.jl @@ -935,6 +935,20 @@ function _add_constraints_cyclingcharge_withreserves!( ci_name = PSY.get_name(device) storage = PSY.get_storage(device) efficiency = PSY.get_efficiency(storage) + E_max = PSY.get_state_of_charge_limits(storage).max + cycles_per_day = PSY.get_cycle_limits(storage) + cycles_in_horizon = + cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY + con_cycling_ch[ci_name] = JuMP.@constraint( + PSI.get_jump_model(container), + efficiency.in * + fraction_of_hour * + sum( + charge_var[ci_name, :] + ch_served_reg_dn[ci_name, :] - + ch_served_reg_up[ci_name, :], + ) <= cycles_in_horizon * E_max + ) + #= if PSI.built_for_recurrent_solves(container) param_value = PSI.get_parameter_array(container, CyclingChargeLimitParameter(), D)[ci_name] @@ -962,6 +976,7 @@ function _add_constraints_cyclingcharge_withreserves!( ) <= cycles_in_horizon * E_max ) end + =# end return end @@ -1087,6 +1102,20 @@ function _add_constraints_cyclingdischarge_withreserves!( ci_name = PSY.get_name(device) storage = PSY.get_storage(device) efficiency = PSY.get_efficiency(storage) + E_max = PSY.get_state_of_charge_limits(storage).max + cycles_per_day = PSY.get_cycle_limits(storage) + cycles_in_horizon = + cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY + con_cycling_ds[ci_name] = JuMP.@constraint( + PSI.get_jump_model(container), + (1.0 / efficiency.out) * + fraction_of_hour * + sum( + discharge_var[ci_name, :] + ds_served_reg_up[ci_name, :] - + ds_served_reg_dn[ci_name, :], + ) <= cycles_in_horizon * E_max + ) + #= if PSI.built_for_recurrent_solves(container) param_value = PSI.get_parameter_array(container, CyclingDischargeLimitParameter(), D)[ci_name] @@ -1114,6 +1143,7 @@ function _add_constraints_cyclingdischarge_withreserves!( ) <= cycles_in_horizon * E_max ) end + =# end return end @@ -1221,11 +1251,10 @@ function PSI.add_constraints!( names = [PSY.get_name(x) for x in devices] time_steps = PSI.get_time_steps(container) reg_var = PSI.get_variable(container, ChargeRegularizationVariable(), V) - ch_served_reg_up = PSI.get_expression(container, ChargeServedReserveUpExpression(), V) - ch_served_reg_dn = PSI.get_expression(container, ChargeServedReserveDownExpression(), V) powerin_var = PSI.get_variable(container, BatteryCharge(), V) has_services = PSI.has_service_model(model) - + if has_services + end constraint_ub = PSI.add_constraints_container!( container, ChargeRegularizationConstraint(), @@ -1245,10 +1274,10 @@ function PSI.add_constraints!( ) if has_services - services = Set() - for d in devices - union!(services, PSY.get_services(d)) - end + ch_served_reg_up = + PSI.get_expression(container, ChargeServedReserveUpExpression(), V) + ch_served_reg_dn = + PSI.get_expression(container, ChargeServedReserveDownExpression(), V) for device in devices ci_name = PSY.get_name(device) constraint_ub[ci_name, 1] = @@ -1317,10 +1346,6 @@ function PSI.add_constraints!( time_steps = PSI.get_time_steps(container) reg_var = PSI.get_variable(container, DischargeRegularizationVariable(), V) powerout_var = PSI.get_variable(container, BatteryDischarge(), V) - ds_served_reg_up = - PSI.get_expression(container, DischargeServedReserveUpExpression(), V) - ds_served_reg_dn = - PSI.get_expression(container, DischargeServedReserveDownExpression(), V) has_services = PSI.has_service_model(model) constraint_ub = PSI.add_constraints_container!( @@ -1342,10 +1367,10 @@ function PSI.add_constraints!( ) if has_services - services = Set() - for d in devices - union!(services, PSY.get_services(d)) - end + ds_served_reg_up = + PSI.get_expression(container, DischargeServedReserveUpExpression(), V) + ds_served_reg_dn = + PSI.get_expression(container, DischargeServedReserveDownExpression(), V) for device in devices ci_name = PSY.get_name(device) constraint_ub[ci_name, 1] = diff --git a/src/core/constraints.jl b/src/core/constraints.jl index 5e296b15..11b38b1b 100644 --- a/src/core/constraints.jl +++ b/src/core/constraints.jl @@ -68,6 +68,13 @@ struct DischargeRegularizationConstraint <: PSI.ConstraintType end struct StateofChargeTargetConstraint <: PSI.ConstraintType end struct RenewableActivePowerLimitConstraint <: PSI.ConstraintType end +################### +### Feedforwards ### +################### + +struct FeedForwardCyclingChargeConstraint <: PSI.ConstraintType end +struct FeedForwardCyclingDischargeConstraint <: PSI.ConstraintType end + ############################################## ### Dual Optimality Conditions Constraints ### ############################################## diff --git a/src/core/variables.jl b/src/core/variables.jl index 63001686..bdc9d8f8 100644 --- a/src/core/variables.jl +++ b/src/core/variables.jl @@ -142,3 +142,4 @@ struct ComplementarySlackVarCyclingDischarge <: MerchantModelComplementarySlackV # implement below #PSI.convert_result_to_natural_units() = false +PSI.should_write_resulting_value(::Type{TotalReserve}) = false diff --git a/src/feedforwards.jl b/src/feedforwards.jl index faa3a79a..89937f39 100644 --- a/src/feedforwards.jl +++ b/src/feedforwards.jl @@ -11,9 +11,9 @@ struct CyclingChargeLimitFeedforward <: PSI.AbstractAffectFeedforward penalty_cost::Float64, meta=PSI.CONTAINER_KEY_EMPTY_META, ) where {T} - values_vector = Vector{PSI.VariableKey}(undef, length(affected_values)) + values_vector = Vector{PSI.ParameterKey}(undef, length(affected_values)) for (ix, v) in enumerate(affected_values) - if v <: PSI.VariableType + if v <: PSI.ParameterType values_vector[ix] = PSI.get_optimization_container_key(v(), component_type, meta) else @@ -48,9 +48,9 @@ struct CyclingDischargeLimitFeedforward <: PSI.AbstractAffectFeedforward penalty_cost::Float64, meta=PSI.CONTAINER_KEY_EMPTY_META, ) where {T} - values_vector = Vector{PSI.VariableKey}(undef, length(affected_values)) + values_vector = Vector{PSI.ParameterKey}(undef, length(affected_values)) for (ix, v) in enumerate(affected_values) - if v <: PSI.VariableType + if v <: PSI.ParameterType values_vector[ix] = PSI.get_optimization_container_key(v(), component_type, meta) else @@ -72,6 +72,168 @@ PSI.get_default_parameter_type(::CyclingDischargeLimitFeedforward, _) = PSI.get_optimization_container_key(ff::CyclingDischargeLimitFeedforward) = ff.optimization_container_key +#= +function PSI.add_feedforward_arguments!( + container::PSI.OptimizationContainer, + model::PSI.DeviceModel, + devices::Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, +) where {D <: PSY.HybridSystem} + for ff in PSI.get_feedforwards(model) + PSI._add_feedforward_arguments!(container, model, devices, ff) + end + return +end +=# + +function PSI._add_feedforward_arguments!( + container::PSI.OptimizationContainer, + model::PSI.DeviceModel, + devices::Vector{D}, + ff::U, +) where { + D <: PSY.HybridSystem, + U <: Union{CyclingChargeLimitFeedforward, CyclingDischargeLimitFeedforward}, +} + parameter_type = PSI.get_default_parameter_type(ff, D) + PSI.add_parameters!(container, parameter_type, devices, model) + return +end + +function PSI.add_feedforward_constraints!( + container::PSI.OptimizationContainer, + model::PSI.DeviceModel, + devices::Vector{V}, +) where {V <: PSY.HybridSystem} + for ff in PSI.get_feedforwards(model) + PSI.add_feedforward_constraints!(container, model, devices, ff) + end + return +end + +function PSI.add_feedforward_constraints!( + container::PSI.OptimizationContainer, + device_model::PSI.DeviceModel, + devices::Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, + ff::CyclingChargeLimitFeedforward, +) where {D <: PSY.HybridSystem} + if PSI.get_attribute(device_model, "cycling") + throw( + IS.ConflictingInputsError( + "Cycling Attribute not allowed with Cycling Limit Feedforwards", + ), + ) + end + time_steps = PSI.get_time_steps(container) + resolution = PSI.get_resolution(container) + fraction_of_hour = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR + names = [PSY.get_name(d) for d in devices] + charge_var = PSI.get_variable(container, BatteryCharge(), D) + ch_served_reg_up = PSI.get_expression(container, ChargeServedReserveUpExpression(), D) + ch_served_reg_dn = PSI.get_expression(container, ChargeServedReserveDownExpression(), D) + T = FeedForwardCyclingChargeConstraint + con_cycling_ch = PSI.add_constraints_container!(container, T(), D, names) + for device in devices + ci_name = PSY.get_name(device) + storage = PSY.get_storage(device) + efficiency = PSY.get_efficiency(storage) + E_max = PSY.get_state_of_charge_limits(storage).max + cycles_per_day = PSY.get_cycle_limits(storage) + cycles_in_horizon = + cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY + if PSI.built_for_recurrent_solves(container) + param_value = + PSI.get_parameter_array(container, CyclingChargeLimitParameter(), D)[ci_name] + con_cycling_ch[ci_name] = JuMP.@constraint( + PSI.get_jump_model(container), + efficiency.in * + fraction_of_hour * + sum( + charge_var[ci_name, :] + ch_served_reg_dn[ci_name, :] - + ch_served_reg_up[ci_name, :], + ) <= param_value + ) + else + E_max = PSY.get_state_of_charge_limits(storage).max + cycles_per_day = PSY.get_cycle_limits(storage) + cycles_in_horizon = + cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY + con_cycling_ch[ci_name] = JuMP.@constraint( + PSI.get_jump_model(container), + efficiency.in * + fraction_of_hour * + sum( + charge_var[ci_name, :] + ch_served_reg_dn[ci_name, :] - + ch_served_reg_up[ci_name, :], + ) <= cycles_in_horizon * E_max + ) + end + end + return +end + +function PSI.add_feedforward_constraints!( + container::PSI.OptimizationContainer, + device_model::PSI.DeviceModel, + devices::Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, + ff::CyclingDischargeLimitFeedforward, +) where {D <: PSY.HybridSystem} + if PSI.get_attribute(device_model, "cycling") + throw( + IS.ConflictingInputsError( + "Cycling Attribute not allowed with Cycling Limit Feedforwards", + ), + ) + end + time_steps = PSI.get_time_steps(container) + resolution = PSI.get_resolution(container) + fraction_of_hour = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR + names = [PSY.get_name(d) for d in devices] + discharge_var = PSI.get_variable(container, BatteryDischarge(), D) + ds_served_reg_up = + PSI.get_expression(container, DischargeServedReserveUpExpression(), D) + ds_served_reg_dn = + PSI.get_expression(container, DischargeServedReserveDownExpression(), D) + T = FeedForwardCyclingDischargeConstraint + con_cycling_ds = PSI.add_constraints_container!(container, T(), D, names) + for device in devices + ci_name = PSY.get_name(device) + storage = PSY.get_storage(device) + efficiency = PSY.get_efficiency(storage) + E_max = PSY.get_state_of_charge_limits(storage).max + cycles_per_day = PSY.get_cycle_limits(storage) + cycles_in_horizon = + cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY + if PSI.built_for_recurrent_solves(container) + param_value = + PSI.get_parameter_array(container, CyclingDischargeLimitParameter(), D)[ci_name] + con_cycling_ds[ci_name] = JuMP.@constraint( + PSI.get_jump_model(container), + (1.0 / efficiency.out) * + fraction_of_hour * + sum( + discharge_var[ci_name, :] + ds_served_reg_up[ci_name, :] - + ds_served_reg_dn[ci_name, :], + ) <= param_value + ) + else + E_max = PSY.get_state_of_charge_limits(storage).max + cycles_per_day = PSY.get_cycle_limits(storage) + cycles_in_horizon = + cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY + con_cycling_ds[ci_name] = JuMP.@constraint( + PSI.get_jump_model(container), + (1.0 / efficiency.out) * + fraction_of_hour * + sum( + discharge_var[ci_name, :] + ds_served_reg_up[ci_name, :] - + ds_served_reg_dn[ci_name, :], + ) <= cycles_in_horizon * E_max + ) + end + end + return +end + function PSI.update_parameter_values!( model::PSI.DecisionModel, key::PSI.ParameterKey{T, U},