diff --git a/src/common_models/add_to_expression.jl b/src/common_models/add_to_expression.jl index 68ced9de..bb137fc7 100644 --- a/src/common_models/add_to_expression.jl +++ b/src/common_models/add_to_expression.jl @@ -1971,7 +1971,7 @@ function add_to_expression!( W <: AbstractReservesFormulation, } service_name = PSY.get_name(service) - variable = get_variable(container, U, X) + variable = get_variable(container, U, IOM.ComponentPairKey{V, X}) if !has_container_key(container, T, V) add_expressions!(container, T, devices, model) end @@ -2410,7 +2410,7 @@ function add_to_expression!( W <: AbstractReservesFormulation, } service_name = PSY.get_name(service) - variable = get_variable(container, U, X) + variable = get_variable(container, U, IOM.ComponentPairKey{V, X}) if !has_container_key(container, T, V) add_expressions!(container, T, devices, model) end @@ -2444,7 +2444,7 @@ function add_to_expression!( W <: AbstractReservesFormulation, } service_name = PSY.get_name(service) - variable = get_variable(container, U, X) + variable = get_variable(container, U, IOM.ComponentPairKey{V, X}) if !has_container_key(container, T, V) add_expressions!(container, T, devices, model) end @@ -2478,7 +2478,7 @@ function add_to_expression!( W <: AbstractReservesFormulation, } service_name = PSY.get_name(service) - variable = get_variable(container, U, X) + variable = get_variable(container, U, IOM.ComponentPairKey{V, X}) if !has_container_key(container, T, V) add_expressions!(container, T, devices, model) end diff --git a/src/core/reserve_traits.jl b/src/core/reserve_traits.jl index eb174e6e..b849b707 100644 --- a/src/core/reserve_traits.jl +++ b/src/core/reserve_traits.jl @@ -209,13 +209,13 @@ function _offline_reserve_awards( ) where {V <: PSY.Device} offline = Tuple{String, IOM.JuMPArray, Set{String}, Bool}[] for sm in get_services(model) - _is_offline_reserve(get_component_type(sm)) || continue - variable = - get_variable(container, ActivePowerReserveVariable, get_component_type(sm)) + S = get_component_type(sm) + _is_offline_reserve(S) || continue only_off = something(get_attribute(sm, "offline_only"), false) for (service_name, dev_map) in get_contributing_devices_map(sm) members = get(dev_map, V, nothing) isnothing(members) && continue + variable = _reserve_variable(container, V, S) push!(offline, (service_name, variable, Set(PSY.get_name.(members)), only_off)) end end diff --git a/src/energy_storage_models/storage_models.jl b/src/energy_storage_models/storage_models.jl index 0bfe4ef8..cfb6b40b 100644 --- a/src/energy_storage_models/storage_models.jl +++ b/src/energy_storage_models/storage_models.jl @@ -726,7 +726,7 @@ function add_to_expression!( W <: AbstractReservesFormulation, } s_name = PSY.get_name(service) - variable = get_variable(container, U, V) + variable = get_variable(container, U, IOM.ComponentPairKey{UV, V}) for d in devices name = PSY.get_name(d) expression = get_expression(container, T, UV, _service_container_meta(service)) diff --git a/src/hybrid_system_models/hybrid_systems.jl b/src/hybrid_system_models/hybrid_systems.jl index 9e405398..4f49f026 100644 --- a/src/hybrid_system_models/hybrid_systems.jl +++ b/src/hybrid_system_models/hybrid_systems.jl @@ -758,7 +758,7 @@ function add_to_expression!( W <: AbstractReservesFormulation, } s_name = PSY.get_name(service) - variable = get_variable(container, U, V) + variable = get_variable(container, U, IOM.ComponentPairKey{UV, V}) for d in devices name = PSY.get_name(d) expression = get_expression(container, T, UV, _service_container_meta(service)) @@ -2079,7 +2079,7 @@ function add_constraints!( names, time_steps; meta = "$(s_type)_$s_name") # System-level reserve variable for this service, keyed `(service, device, time)`. - sys_reserve = get_variable(container, ActivePowerReserveVariable, s_type) + sys_reserve = _reserve_variable(container, V, s_type) # Per-hybrid reserve variables for this service r_out = get_variable( container, diff --git a/src/services_models/reserve_group.jl b/src/services_models/reserve_group.jl index eb1fe928..0752fa13 100644 --- a/src/services_models/reserve_group.jl +++ b/src/services_models/reserve_group.jl @@ -118,10 +118,13 @@ function check_activeservice_variables( ) where {T <: PSY.Service} for service in contributing_services service_name = PSY.get_name(service) - variable = get_variable(container, ActivePowerReserveVariable, typeof(service)) - # The container is keyed `(service_name, device_name, time)` and shared by the whole - # service type, so check for this service's own entries, not just that it exists. - any(k -> k[1] == service_name, keys(variable.data)) || error( + # Containers are keyed `(service_name, device_name, time)` and shared by the whole + # service type, so check for this service's own entries, not just that one exists. + has_entries = any( + variable -> any(k -> k[1] == service_name, keys(variable.data)), + _reserve_variables(container, typeof(service)), + ) + has_entries || error( "The contributing service $service_name has no ActivePowerReserveVariable \ entries; it must be modeled before the group reserve that references it.", ) @@ -202,8 +205,8 @@ end # Collect the group's contributing reserve variables into one bucket per time step, so the # constraint loop above indexes straight in rather than re-scanning per `(group, t)`. Services -# of the same type share one `(service_name, device_name, time)` container, so each container -# is scanned once. +# of the same type share their `(service_name, device_name, time)` containers, so each service +# type is scanned once. function _group_member_variables( container::OptimizationContainer, contributing_services::Vector{<:PSY.Service}, @@ -211,16 +214,32 @@ function _group_member_variables( ) member_names = Set(PSY.get_name(r) for r in contributing_services) index = [JuMP.VariableRef[] for _ in time_steps] - scanned = Set{DataType}() + scanned = Set{Type}() for r in contributing_services - rtype = typeof(r) + rtype = IOM.canonical_component_type(typeof(r)) rtype in scanned && continue push!(scanned, rtype) - reserve_variable = get_variable(container, ActivePowerReserveVariable, rtype) - for (key, var) in reserve_variable.data - key[1] in member_names || continue - push!(index[key[3]], var) + for reserve_variable in _reserve_variables(container, rtype) + for (key, var) in reserve_variable.data + key[1] in member_names || continue + push!(index[key[3]], var) + end end end return index end + +# Award containers of every contributing device type for service type `SR`. Groups only know +# their member services, not the members' contributing device types. Keys store the service +# type with its unit parameter stripped, so match on that form. +function _reserve_variables( + container::OptimizationContainer, + ::Type{SR}, +) where {SR <: PSY.Service} + S = IOM.canonical_component_type(SR) + return [ + variable for (key, variable) in IOM.get_variables(container) if + IOM.get_entry_type(key) === ActivePowerReserveVariable && + get_component_type(key) <: IOM.ComponentPairKey{<:PSY.Component, S} + ] +end diff --git a/src/services_models/reserve_offers.jl b/src/services_models/reserve_offers.jl index e60dad28..f1a2bfd0 100644 --- a/src/services_models/reserve_offers.jl +++ b/src/services_models/reserve_offers.jl @@ -22,7 +22,7 @@ _cost_offers_reserve(cost::Union{PSY.MarketBidCost, PSY.MarketBidTimeSeriesCost} _cost_offers_reserve(::PSY.OperationalCost, service) = false # Price every contributing device that offers into `service` by its offer curve; returns the set of -# device names so priced (the flat-cost pass skips them). +# `(device type, device name)` so priced (the flat-cost pass skips them). # A group has no contributing devices, so it can carry no per-device offers; with # `GroupReserve <: AbstractReserve` the generic method below would otherwise accept it. # Offers live on the group's contributing services and are priced by their own models. @@ -45,19 +45,19 @@ function add_reserve_offer_costs!( model::ServiceModel{SR, T}, ) where {SR <: PSY.AbstractReserve, T <: AbstractReservesFormulation} service_name = PSY.get_name(service) - award = get_variable(container, ActivePowerReserveVariable, SR) time_steps = get_time_steps(container) resolution = get_resolution(container) dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR base_p = get_model_base_power(container) initial_time = IOM.get_initial_time(container) jump_model = get_jump_model(container) - offered = Set{String}() + offered = Set{Tuple{DataType, String}}() for (device_type, devices) in get_contributing_devices_map(model, service_name) offering = [d for d in devices if _has_reserve_offer(d, service)] isempty(offering) && continue names = [PSY.get_name(d) for d in offering] + award = _reserve_variable(container, device_type, SR) # Block var keyed `(service, device, segment, time)` via # `IOM.sparse_variable_key_type(PiecewiseLinearBlockReserveOffer)`. Segments vary per # device and time, so they are filled sparsely below. @@ -99,7 +99,7 @@ function add_reserve_offer_costs!( add_to_objective_invariant_expression!( container, get_pwl_cost_expression_delta(pwl_vars, slopes, dt)) end - push!(offered, dev_name) + push!(offered, (device_type, dev_name)) end end return offered diff --git a/src/services_models/reserves.jl b/src/services_models/reserves.jl index 56b887cb..143c779e 100644 --- a/src/services_models/reserves.jl +++ b/src/services_models/reserves.jl @@ -205,46 +205,77 @@ function add_reserve_variables!( return end -# Sum the reserve provision of one service across its contributing devices at time `t`, -# reading the service type's sparse container keyed `(service_name, device_name, time)`. +_reserve_variable( + container::OptimizationContainer, + ::Type{D}, + ::Type{SR}, +) where {D <: PSY.Component, SR <: PSY.Service} = + get_variable(container, ActivePowerReserveVariable, IOM.ComponentPairKey{D, SR}) + +# Per time step, the sum of one service's awards across all its contributing device types. function _sum_service_reserves( - reserve_variable::SparseAxisArray, + container::OptimizationContainer, + ::Type{SR}, service_name::String, - contributing_devices::U, - t::Int, + contributing_devices::AbstractDict, extra::Int, -) where { - U <: Vector{D}, -} where {D <: PSY.Component} - acc = IOM.get_hinted_aff_expr(length(contributing_devices) + extra) - for d in contributing_devices - JuMP.add_to_expression!(acc, reserve_variable[(service_name, PSY.get_name(d), t)]) +) where {SR <: PSY.Service} + n_terms = sum(length, values(contributing_devices); init = 0) + extra + acc = [IOM.get_hinted_aff_expr(n_terms) for _ in get_time_steps(container)] + for (device_type, devices) in contributing_devices + _sum_service_reserves!( + acc, + _reserve_variable(container, device_type, SR), + service_name, + devices, + ) end return acc end +function _sum_service_reserves!( + acc::Vector{JuMP.AffExpr}, + reserve_variable::SparseAxisArray, + service_name::String, + devices::Vector{D}, +) where {D <: PSY.Component} + for d in devices, t in eachindex(acc) + JuMP.add_to_expression!( + acc[t], + reserve_variable[(service_name, PSY.get_name(d), t)], + ) + end + return +end + ################################## Reserve Requirement Constraint ########################## function add_constraints!( container::OptimizationContainer, T::Type{RequirementConstraint}, service::SR, - contributing_devices::U, + contributing_devices::AbstractDict, model::ServiceModel{SR, V}, -) where { - SR <: PSY.AbstractReserve, - V <: AbstractReservesFormulation, - U <: Vector{D}, -} where {D <: PSY.Component} +) where {SR <: PSY.AbstractReserve, V <: AbstractReservesFormulation} time_steps = get_time_steps(container) service_name = PSY.get_name(service) # Dense container keyed `[service_name, time]`, built per type; fill this service's row. constraint = get_constraint(container, T, SR) - reserve_variable = get_variable(container, ActivePowerReserveVariable, SR) use_slacks = get_use_slacks(model) - use_slacks && (slack_vars = get_variable(container, ReserveRequirementSlack, SR)) requirement = _get_requirement(service) jump_model = get_jump_model(container) - extra = use_slacks ? 1 : 0 + resource_expression = _sum_service_reserves( + container, + SR, + service_name, + contributing_devices, + use_slacks ? 1 : 0, + ) + if use_slacks + slack_vars = get_variable(container, ReserveRequirementSlack, SR) + for t in time_steps + JuMP.add_to_expression!(resource_expression[t], slack_vars[service_name, t]) + end + end # A static reserve gets a scalar requirement RHS; a time-varying one scales it by an # attached requirement series (resolved by the model-configured name). @@ -254,20 +285,10 @@ function add_constraints!( get_parameter(container, RequirementTimeSeriesParameter, SR) param = get_parameter_column_refs(param_container, service_name) for t in time_steps - resource_expression = - _sum_service_reserves(reserve_variable, service_name, - contributing_devices, - t, extra) - use_slacks && - JuMP.add_to_expression!( - resource_expression, - slack_vars[service_name, t], - ) - constraint[service_name, t] = - JuMP.@constraint( - jump_model, - resource_expression >= param[t] * requirement - ) + constraint[service_name, t] = JuMP.@constraint( + jump_model, + resource_expression[t] >= param[t] * requirement + ) end else ts_vector = IOM.get_time_series( @@ -277,31 +298,16 @@ function add_constraints!( interval = get_interval(get_settings(container)), ) for t in time_steps - resource_expression = - _sum_service_reserves(reserve_variable, service_name, - contributing_devices, - t, extra) - use_slacks && - JuMP.add_to_expression!( - resource_expression, - slack_vars[service_name, t], - ) constraint[service_name, t] = JuMP.@constraint( jump_model, - resource_expression >= ts_vector[t] * requirement + resource_expression[t] >= ts_vector[t] * requirement ) end end else for t in time_steps - resource_expression = - _sum_service_reserves(reserve_variable, service_name, contributing_devices, - t, - extra) - use_slacks && - JuMP.add_to_expression!(resource_expression, slack_vars[service_name, t]) constraint[service_name, t] = - JuMP.@constraint(jump_model, resource_expression >= requirement) + JuMP.@constraint(jump_model, resource_expression[t] >= requirement) end end return @@ -311,13 +317,9 @@ function add_constraints!( container::OptimizationContainer, T::Type{ParticipationFractionConstraint}, service::SR, - contributing_devices::U, + contributing_devices::Vector{D}, model::ServiceModel{SR, V}, -) where { - SR <: PSY.AbstractReserve, - V <: AbstractReservesFormulation, - U <: Vector{D}, -} where {D <: PSY.Device} +) where {SR <: PSY.AbstractReserve, V <: AbstractReservesFormulation, D <: PSY.Component} max_participation_factor = PSY.get_max_participation_factor(service) if max_participation_factor >= 1.0 @@ -327,13 +329,16 @@ function add_constraints!( time_steps = get_time_steps(container) service_name = PSY.get_name(service) # Sparse constraint container keyed `(service_name, device_name, time)`. - cons = lazy_container_addition!(container, T, SR, - [service_name], - [PSY.get_name(d) for d in contributing_devices], - time_steps; + cons = lazy_container_addition!( + container, + T, + IOM.ComponentPairKey{D, SR}, + String[], + String[], + Int[]; sparse = true, ) - var_r = get_variable(container, ActivePowerReserveVariable, SR) + var_r = _reserve_variable(container, D, SR) jump_model = get_jump_model(container) requirement = _get_requirement(service) cap = requirement * max_participation_factor @@ -387,11 +392,11 @@ function add_to_objective_function!( # Devices that submitted a reserve OFFER are priced by their offer curve; the rest keep the # flat DEFAULT_RESERVE_COST. offered = add_reserve_offer_costs!(container, service, model) - contributing_names = - [PSY.get_name(d) for d in get_contributing_devices(model, PSY.get_name(service))] - add_reserves_proportional_cost!( - container, ActivePowerReserveVariable, service, T, contributing_names; - skip_devices = offered) + for devices in values(get_contributing_devices_map(model, PSY.get_name(service))) + add_reserves_proportional_cost!( + container, ActivePowerReserveVariable, service, T, devices; + skip_devices = offered) + end return end @@ -399,32 +404,22 @@ function add_constraints!( container::OptimizationContainer, T::Type{RequirementConstraint}, service::SR, - contributing_devices::U, + contributing_devices::AbstractDict, ::ServiceModel{SR, StepwiseCostReserve}, -) where { - SR <: PSY.AbstractReserve, - U <: Vector{D}, -} where {D <: PSY.Component} +) where {SR <: PSY.AbstractReserve} time_steps = get_time_steps(container) service_name = PSY.get_name(service) # Dense container keyed `[service_name, time]`, built per type; fill this service's row. constraint = get_constraint(container, T, SR) - reserve_variable = get_variable(container, ActivePowerReserveVariable, SR) requirement_variable = get_variable(container, ServiceRequirementVariable, SR) jump_model = get_jump_model(container) + resource_expression = + _sum_service_reserves(container, SR, service_name, contributing_devices, 0) for t in time_steps - resource_expression = - _sum_service_reserves( - reserve_variable, - service_name, - contributing_devices, - t, - 0, - ) constraint[service_name, t] = JuMP.@constraint( jump_model, - resource_expression >= requirement_variable[service_name, t] + resource_expression[t] >= requirement_variable[service_name, t] ) end @@ -457,45 +452,10 @@ function _get_ramp_constraint_contributing_devices( return filtered_device end -function add_constraints!( - container::OptimizationContainer, - T::Type{RampConstraint}, - service::SR, - contributing_devices::Vector{D}, - ::ServiceModel{SR, V}, -) where { - SR <: PSY.Reserve{PSY.ReserveUp}, - V <: AbstractReservesFormulation, - D <: PSY.Component, -} - ramp_devices = _get_ramp_constraint_contributing_devices(service, contributing_devices) - service_name = PSY.get_name(service) - if !isempty(ramp_devices) - jump_model = get_jump_model(container) - time_steps = get_time_steps(container) - time_frame = PSY.get_time_frame(service) - variable = get_variable(container, ActivePowerReserveVariable, SR) - device_name_set = [PSY.get_name(d) for d in ramp_devices] - con_up = lazy_container_addition!(container, T, - SR, - [service_name], - device_name_set, - time_steps; - sparse = true, - ) - for d in ramp_devices, t in time_steps - name = PSY.get_name(d) - ramp_limits = PSY.get_ramp_limits(d, PSY.SU / u"minute") - con_up[(service_name, name, t)] = JuMP.@constraint( - jump_model, - variable[(service_name, name, t)] <= ramp_limits.up * time_frame - ) - end - else - @warn "Data doesn't contain contributing devices with ramp limits for service $service_name, consider adjusting your formulation" - end - return -end +_directional_ramp_limit(ramp_limits, ::Type{<:PSY.Reserve{PSY.ReserveUp}}) = + ramp_limits.up +_directional_ramp_limit(ramp_limits, ::Type{<:PSY.Reserve{PSY.ReserveDown}}) = + ramp_limits.down function add_constraints!( container::OptimizationContainer, @@ -504,35 +464,36 @@ function add_constraints!( contributing_devices::Vector{D}, ::ServiceModel{SR, V}, ) where { - SR <: PSY.Reserve{PSY.ReserveDown}, + SR <: Union{PSY.Reserve{PSY.ReserveUp}, PSY.Reserve{PSY.ReserveDown}}, V <: AbstractReservesFormulation, D <: PSY.Component, } ramp_devices = _get_ramp_constraint_contributing_devices(service, contributing_devices) service_name = PSY.get_name(service) - if !isempty(ramp_devices) - jump_model = get_jump_model(container) - time_steps = get_time_steps(container) - time_frame = PSY.get_time_frame(service) - variable = get_variable(container, ActivePowerReserveVariable, SR) - device_name_set = [PSY.get_name(d) for d in ramp_devices] - con_down = lazy_container_addition!(container, T, - SR, - [service_name], - device_name_set, - time_steps; - sparse = true, + if isempty(ramp_devices) + @warn "Contributing $(D) devices on service $service_name have no binding ramp limits; no ramp constraints are added for them." + return + end + jump_model = get_jump_model(container) + time_steps = get_time_steps(container) + time_frame = PSY.get_time_frame(service) + variable = _reserve_variable(container, D, SR) + cons = lazy_container_addition!( + container, + T, + IOM.ComponentPairKey{D, SR}, + String[], + String[], + Int[]; + sparse = true, + ) + for d in ramp_devices, t in time_steps + name = PSY.get_name(d) + limit = _directional_ramp_limit(PSY.get_ramp_limits(d, PSY.SU / u"minute"), SR) + cons[(service_name, name, t)] = JuMP.@constraint( + jump_model, + variable[(service_name, name, t)] <= limit * time_frame ) - for d in ramp_devices, t in time_steps - name = PSY.get_name(d) - ramp_limits = PSY.get_ramp_limits(d, PSY.SU / u"minute") - con_down[(service_name, name, t)] = JuMP.@constraint( - jump_model, - variable[(service_name, name, t)] <= ramp_limits.down * time_frame - ) - end - else - @warn "Data doesn't contain contributing devices with ramp limits for service $service_name, consider adjusting your formulation" end return end @@ -541,13 +502,9 @@ function add_constraints!( container::OptimizationContainer, T::Type{ReservePowerConstraint}, service::SR, - contributing_devices::U, + contributing_devices::Vector{D}, ::ServiceModel{SR, V}, -) where { - SR <: PSY.OfflineReserve, - V <: AbstractReservesFormulation, - U <: Vector{D}, -} where {D <: PSY.Component} +) where {SR <: PSY.OfflineReserve, V <: AbstractReservesFormulation, D <: PSY.Component} time_steps = get_time_steps(container) resolution = get_resolution(container) if resolution > Dates.Minute(1) @@ -557,62 +514,37 @@ function add_constraints!( minutes_per_period = Dates.value(Dates.Second(resolution)) / 60 end service_name = PSY.get_name(service) - cons = lazy_container_addition!(container, T, - SR, - [service_name], - [PSY.get_name(d) for d in contributing_devices], - time_steps; + cons = lazy_container_addition!( + container, + T, + IOM.ComponentPairKey{D, SR}, + String[], + String[], + Int[]; sparse = true, ) - var_r = get_variable(container, ActivePowerReserveVariable, SR) + var_r = _reserve_variable(container, D, SR) + varstatus = get_variable(container, OnVariable, D) reserve_response_time = PSY.get_time_frame(service) jump_model = get_jump_model(container) for d in contributing_devices - # Function barrier: `contributing_devices` may have an abstract element type, so the - # callee specializes on the concrete types and dispatches once per device rather than - # once per timestep. - varstatus = get_variable(container, OnVariable, typeof(d)) - _add_reserve_power_constraint_device!( - cons, - var_r, - varstatus, - d, - service_name, - reserve_response_time, - minutes_per_period, - jump_model, - time_steps, - ) - end - return -end - -function _add_reserve_power_constraint_device!( - cons, - var_r, - varstatus, - d::D, - service_name::String, - reserve_response_time, - minutes_per_period, - jump_model, - time_steps, -) where {D <: PSY.Component} - name = PSY.get_name(d) - startup_time = PSY.get_time_limits(d).up - ramp_limits = _get_ramp_limits(d) - if reserve_response_time > startup_time - reserve_limit = - PSY.get_active_power_limits(d, PSY.SU).min + - (reserve_response_time - startup_time) * minutes_per_period * ramp_limits.up - else - reserve_limit = 0.0 - end - for t in time_steps - cons[(service_name, name, t)] = JuMP.@constraint( - jump_model, - var_r[(service_name, name, t)] <= (1 - varstatus[name, t]) * reserve_limit - ) + name = PSY.get_name(d) + startup_time = PSY.get_time_limits(d).up + ramp_limits = _get_ramp_limits(d) + if reserve_response_time > startup_time + reserve_limit = + PSY.get_active_power_limits(d, PSY.SU).min + + (reserve_response_time - startup_time) * minutes_per_period * + ramp_limits.up + else + reserve_limit = 0.0 + end + for t in time_steps + cons[(service_name, name, t)] = JuMP.@constraint( + jump_model, + var_r[(service_name, name, t)] <= (1 - varstatus[name, t]) * reserve_limit + ) + end end return end @@ -738,27 +670,26 @@ function process_stepwise_cost_reserve_parameters!( return end +# `skip_devices` are priced by their offer curve in `add_reserve_offer_costs!` instead. function add_reserves_proportional_cost!( container::OptimizationContainer, ::Type{U}, service::T, ::Type{V}, - contributing_names::Vector{String}; - skip_devices = Set{String}(), + contributing_devices::Vector{D}; + skip_devices = Set{Tuple{DataType, String}}(), ) where { T <: PSY.AbstractReserve, U <: ActivePowerReserveVariable, V <: AbstractReservesFormulation, + D <: PSY.Component, } - base_p = get_model_base_power(container) service_name = PSY.get_name(service) - reserve_variable = get_variable(container, U, T) - # Index this service's slice of the `(service, device, time)` container by its contributing - # device names, so each provision is priced once without scanning the whole container. - # `skip_devices` are priced by their offer curve in `add_reserve_offer_costs!` instead. - cost = DEFAULT_RESERVE_COST / base_p - for name in contributing_names - name in skip_devices && continue + reserve_variable = get_variable(container, U, IOM.ComponentPairKey{D, T}) + cost = DEFAULT_RESERVE_COST / get_model_base_power(container) + for d in contributing_devices + name = PSY.get_name(d) + (D, name) in skip_devices && continue for t in get_time_steps(container) add_to_objective_invariant_expression!( container, diff --git a/src/services_models/services_constructor.jl b/src/services_models/services_constructor.jl index 3ef917c4..3f663eba 100644 --- a/src/services_models/services_constructor.jl +++ b/src/services_models/services_constructor.jl @@ -1,9 +1,10 @@ # One `ServiceModel` per service TYPE (like `DeviceModel`). `construct_service!` runs once # per type: it gets all services of the type via `get_available_components(model, sys)`, # reads each service's contributing devices from the nested per-service map -# (`get_contributing_devices(model, service_name)`), and builds. Reserve variable and -# constraint containers are shared per `(entry type, service type)`, with each service -# filling its own slice. Group formulations are deferred to last (their members must exist). +# (`get_contributing_devices_map(model, service_name)`), and builds. Reserve award containers +# are shared per `(device type, service type)` and constraint containers per +# `(entry type, service type)`, with each service filling its own slice. Group formulations +# are deferred to last (their members must exist). # # TODO(services stability): See issue #216. @@ -202,15 +203,14 @@ function construct_service!( ts_services = [s for s in demand_services if _has_ts_requirement(model, s)] isempty(ts_services) || add_parameters!(container, RequirementTimeSeriesParameter, ts_services, model) + add_service_variables!( + container, + ActivePowerReserveVariable, + services, + model, + RangeReserve, + ) for service in services - contributing_devices = get_contributing_devices(model, PSY.get_name(service)) - add_service_variables!( - container, - ActivePowerReserveVariable, - service, - contributing_devices, - RangeReserve, - ) add_to_expression!( container, ActivePowerReserveVariable, @@ -252,7 +252,7 @@ function construct_service!( get_use_slacks(model) && add_reserve_slacks!(container, SR, demand_names) end for service in services - contributing_devices = get_contributing_devices(model, PSY.get_name(service)) + contributing_devices = get_contributing_devices_map(model, PSY.get_name(service)) if _has_reserve_demand(model, service) add_constraints!( container, @@ -261,13 +261,15 @@ function construct_service!( contributing_devices, model, ) - add_constraints!( - container, - ParticipationFractionConstraint, - service, - contributing_devices, - model, - ) + for devices in values(contributing_devices) + add_constraints!( + container, + ParticipationFractionConstraint, + service, + devices, + model, + ) + end add_to_objective_function!(container, service, model) else # Supply-only: no requirement of its own (it may serve a GroupReserve). Price any @@ -309,15 +311,14 @@ function construct_service!( # Slope/breakpoint PWL cost params for the time-series-backed ORDCs (no-op otherwise). process_stepwise_cost_reserve_parameters!(container, model, demand_services) end + add_service_variables!( + container, + ActivePowerReserveVariable, + services, + model, + StepwiseCostReserve, + ) for service in services - contributing_devices = get_contributing_devices(model, PSY.get_name(service)) - add_service_variables!( - container, - ActivePowerReserveVariable, - service, - contributing_devices, - StepwiseCostReserve, - ) add_to_expression!( container, ActivePowerReserveVariable, @@ -352,7 +353,7 @@ function construct_service!( ) end for service in services - contributing_devices = get_contributing_devices(model, PSY.get_name(service)) + contributing_devices = get_contributing_devices_map(model, PSY.get_name(service)) if _has_reserve_demand(model, service) add_constraints!( container, @@ -600,15 +601,14 @@ function construct_service!( ts_services = [s for s in services if _has_ts_requirement(model, s)] isempty(ts_services) || add_parameters!(container, RequirementTimeSeriesParameter, ts_services, model) + add_service_variables!( + container, + ActivePowerReserveVariable, + services, + model, + RampReserve, + ) for service in services - contributing_devices = get_contributing_devices(model, PSY.get_name(service)) - add_service_variables!( - container, - ActivePowerReserveVariable, - service, - contributing_devices, - RampReserve, - ) add_to_expression!( container, ActivePowerReserveVariable, @@ -643,7 +643,7 @@ function construct_service!( ) get_use_slacks(model) && add_reserve_slacks!(container, SR, service_names) for service in services - contributing_devices = get_contributing_devices(model, PSY.get_name(service)) + contributing_devices = get_contributing_devices_map(model, PSY.get_name(service)) add_constraints!( container, RequirementConstraint, @@ -651,14 +651,16 @@ function construct_service!( contributing_devices, model, ) - add_constraints!(container, RampConstraint, service, contributing_devices, model) - add_constraints!( - container, - ParticipationFractionConstraint, - service, - contributing_devices, - model, - ) + for devices in values(contributing_devices) + add_constraints!(container, RampConstraint, service, devices, model) + add_constraints!( + container, + ParticipationFractionConstraint, + service, + devices, + model, + ) + end add_to_objective_function!(container, service, model) add_feedforward_constraints!(container, model, service) end @@ -682,15 +684,14 @@ function construct_service!( ts_services = [s for s in services if _has_ts_requirement(model, s)] isempty(ts_services) || add_parameters!(container, RequirementTimeSeriesParameter, ts_services, model) + add_service_variables!( + container, + ActivePowerReserveVariable, + services, + model, + NonSpinningReserve, + ) for service in services - contributing_devices = get_contributing_devices(model, PSY.get_name(service)) - add_service_variables!( - container, - ActivePowerReserveVariable, - service, - contributing_devices, - NonSpinningReserve, - ) add_feedforward_arguments!(container, model, service) end return @@ -718,7 +719,7 @@ function construct_service!( ) get_use_slacks(model) && add_reserve_slacks!(container, SR, service_names) for service in services - contributing_devices = get_contributing_devices(model, PSY.get_name(service)) + contributing_devices = get_contributing_devices_map(model, PSY.get_name(service)) add_constraints!( container, RequirementConstraint, @@ -726,20 +727,16 @@ function construct_service!( contributing_devices, model, ) - add_constraints!( - container, - ReservePowerConstraint, - service, - contributing_devices, - model, - ) - add_constraints!( - container, - ParticipationFractionConstraint, - service, - contributing_devices, - model, - ) + for devices in values(contributing_devices) + add_constraints!(container, ReservePowerConstraint, service, devices, model) + add_constraints!( + container, + ParticipationFractionConstraint, + service, + devices, + model, + ) + end add_to_objective_function!(container, service, model) add_feedforward_constraints!(container, model, service) end diff --git a/src/static_injector_models/hydro_generation.jl b/src/static_injector_models/hydro_generation.jl index 7444cf73..595def13 100644 --- a/src/static_injector_models/hydro_generation.jl +++ b/src/static_injector_models/hydro_generation.jl @@ -2805,7 +2805,8 @@ function add_to_expression!( isa(service, PSY.Reserve{PSY.ReserveUp}) || continue service_name = PSY.get_name(service) fractions = deployed_fraction_values(container, service_model, service) - variable = get_variable(container, U, typeof(service)) + variable = + get_variable(container, U, IOM.ComponentPairKey{V, typeof(service)}) for t in get_time_steps(container) add_proportional_to_jump_expression!( expression[name, t], @@ -2844,7 +2845,8 @@ function add_to_expression!( isa(service, PSY.Reserve{PSY.ReserveDown}) || continue service_name = PSY.get_name(service) fractions = deployed_fraction_values(container, service_model, service) - variable = get_variable(container, U, typeof(service)) + variable = + get_variable(container, U, IOM.ComponentPairKey{V, typeof(service)}) for t in get_time_steps(container) add_proportional_to_jump_expression!( expression[name, t], diff --git a/test/test_device_hydro_constructors.jl b/test/test_device_hydro_constructors.jl index c2483723..5b7d836a 100644 --- a/test/test_device_hydro_constructors.jl +++ b/test/test_device_hydro_constructors.jl @@ -1677,7 +1677,11 @@ end container = IOM.get_optimization_container(model) expr = IOM.get_expression(container, HydroServedReserveUpExpression, HydroDispatch) - var = IOM.get_variable(container, ActivePowerReserveVariable, typeof(reserve_up)) + var = IOM.get_variable( + container, + ActivePowerReserveVariable, + IOM.ComponentPairKey{HydroDispatch, typeof(reserve_up)}, + ) hy_name = get_name(only(get_components(HydroDispatch, c_sys5_hy))) up_name = get_name(reserve_up) time_steps = IOM.get_time_steps(container) diff --git a/test/test_device_reserve_offers.jl b/test/test_device_reserve_offers.jl index c5b878aa..254b5bcd 100644 --- a/test/test_device_reserve_offers.jl +++ b/test/test_device_reserve_offers.jl @@ -11,6 +11,18 @@ # (instead of the flat `DEFAULT_RESERVE_COST`). These tests pin the DATA MODEL and assert the # consumer builds the 4D block variable, the award-linking constraint, and the offer-slope cost. +# Awards are stored per (device type, service type): join every device type's WIDE frame for +# `service_type` (the encoded service type, e.g. "OnlineReserve__ReserveUp"). +function _read_awards(res, service_type::String) + frames = [ + read_variable(res, key; table_format = TableFormat.WIDE) for + key in list_variable_names(res) if + startswith(key, "ActivePowerReserveVariable__") && + endswith(key, "__" * service_type) + ] + return reduce((a, b) -> innerjoin(a, b; on = :DateTime), frames) +end + # Give every contributing thermal device of `reserve` a MarketBidCost with an energy offer and a # per-device reserve OFFER curve (PiecewiseStepData, NaturalUnit) named after the service. function add_device_reserve_offers!( @@ -86,9 +98,12 @@ end @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED container = get_optimization_container(model) - # The reserve award exists, keyed by service type. + # The reserve award exists, keyed by (device type, service type). @test IOM.has_container_key( - container, ActivePowerReserveVariable, OnlineReserve{ReserveUp}) + container, + ActivePowerReserveVariable, + IOM.ComponentPairKey{ThermalStandard, OnlineReserve{ReserveUp}}, + ) # The per-device reserve OFFER is now consumed: a 4D block variable keyed # (service, device, segment, time) exists for the contributing device type, plus the @@ -99,8 +114,11 @@ end container, POM.ReserveOfferLinkingConstraint, ThermalStandard) blk = IOM.get_variable(container, POM.PiecewiseLinearBlockReserveOffer, ThermalStandard) cons = IOM.get_constraint(container, POM.ReserveOfferLinkingConstraint, ThermalStandard) - award = - IOM.get_variable(container, ActivePowerReserveVariable, OnlineReserve{ReserveUp}) + award = IOM.get_variable( + container, + ActivePowerReserveVariable, + IOM.ComponentPairKey{ThermalStandard, OnlineReserve{ReserveUp}}, + ) @test !isempty(blk) sname = PSY.get_name(reserve) @@ -221,9 +239,7 @@ end @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED res = IOM.OptimizationProblemOutputs(model) - awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; - table_format = TableFormat.WIDE) + awards = _read_awards(res, "OnlineReserve__ReserveUp") # WIDE columns are "__"; values are per-hour reserve awards in MW. col = "$(PSY.get_name(ordc))__$(PSY.get_name(g1))" # The linking constraint caps g1's award at the offered MW each hour; in a dummy hour that cap @@ -277,9 +293,7 @@ end @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED res = IOM.OptimizationProblemOutputs(model) - awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; - table_format = TableFormat.WIDE) + awards = _read_awards(res, "OnlineReserve__ReserveUp") sname = PSY.get_name(ordc) order = sort(collect(keys(base_slope)); by = n -> base_slope[n]) cheapest, priciest = first(order), last(order) @@ -466,10 +480,7 @@ end res, "ServiceRequirementVariable__GroupReserve__ReserveUp"; table_format = TableFormat.WIDE, ) - awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; - table_format = TableFormat.WIDE, - ) + awards = _read_awards(res, "OnlineReserve__ReserveUp") sub_cols = [c for c in names(awards) if startswith(c, "GROUP_SUB_")] load_col = "GROUP_SUB_A__$(_MKT_LOAD)" @test load_col in names(awards) @@ -526,10 +537,7 @@ end res, "ServiceRequirementVariable__OfflineReserve"; table_format = TableFormat.WIDE, ) - awards = read_variable( - res, "ActivePowerReserveVariable__OfflineReserve"; - table_format = TableFormat.WIDE, - ) + awards = _read_awards(res, "OfflineReserve") load_col = "NSPIN__$(_MKT_LOAD)" @test load_col in names(awards) for t in 1:24 @@ -626,7 +634,11 @@ function _check_offline_band( ) con = IOM.get_constraint(container, POM.OfflineReserveBandConstraint, device_type) varbin = IOM.get_variable(container, POM.OnVariable, device_type) - awards = IOM.get_variable(container, POM.ActivePowerReserveVariable, OfflineReserve) + awards = IOM.get_variable( + container, + POM.ActivePowerReserveVariable, + IOM.ComponentPairKey{device_type, OfflineReserve}, + ) checked = 0 for (idx, c) in con.data name, t = idx @@ -671,14 +683,8 @@ end res = IOM.OptimizationProblemOutputs(model) on = read_variable(res, OnVariable, ThermalStandard; table_format = TableFormat.WIDE) - nspin_awards = read_variable( - res, "ActivePowerReserveVariable__OfflineReserve"; - table_format = TableFormat.WIDE, - ) - spin_awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; - table_format = TableFormat.WIDE, - ) + nspin_awards = _read_awards(res, "OfflineReserve") + spin_awards = _read_awards(res, "OnlineReserve__ReserveUp") # Standard UC: the band row is `p + online + offline <= pmax` for every t, so the # coefficient on `u` is exactly zero. This gates the row's existence and its RHS - # the surviving solve assertions below do not, since each award is separately capped @@ -769,14 +775,8 @@ end res, PowerAboveMinimumVariable, ThermalStandard; table_format = TableFormat.WIDE, ) - nspin_awards = read_variable( - res, "ActivePowerReserveVariable__OfflineReserve"; - table_format = TableFormat.WIDE, - ) - spin_awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; - table_format = TableFormat.WIDE, - ) + nspin_awards = _read_awards(res, "OfflineReserve") + spin_awards = _read_awards(res, "OnlineReserve__ReserveUp") off_name = PSY.get_name(offunit) total_off_award = 0.0 @@ -884,10 +884,7 @@ end res, PowerAboveMinimumVariable, PSY.ThermalMultiStart; table_format = TableFormat.WIDE, ) - awards = read_variable( - res, "ActivePowerReserveVariable__OfflineReserve"; - table_format = TableFormat.WIDE, - ) + awards = _read_awards(res, "OfflineReserve") # Committed multistart units honour the compact band in the solved solution. committed = 0 for d in multistarts @@ -965,10 +962,7 @@ end res, "ActivePowerVariable__InterruptiblePowerLoad"; table_format = TableFormat.WIDE, ) - awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; - table_format = TableFormat.WIDE, - ) + awards = _read_awards(res, "OnlineReserve__ReserveUp") total_award = 0.0 for t in 1:24 awarded = sum(awards[t, c] for c in _il_cols(awards)) @@ -989,10 +983,7 @@ end res, "ActivePowerVariable__InterruptiblePowerLoad"; table_format = TableFormat.WIDE, ) - awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveDown"; - table_format = TableFormat.WIDE, - ) + awards = _read_awards(res, "OnlineReserve__ReserveDown") hsl = read_parameter( res, "ActivePowerTimeSeriesParameter__InterruptiblePowerLoad"; table_format = TableFormat.WIDE, @@ -1026,14 +1017,8 @@ end res, "ActivePowerVariable__InterruptiblePowerLoad"; table_format = TableFormat.WIDE, ) - up = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; - table_format = TableFormat.WIDE, - ) - dn = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveDown"; - table_format = TableFormat.WIDE, - ) + up = _read_awards(res, "OnlineReserve__ReserveUp") + dn = _read_awards(res, "OnlineReserve__ReserveDown") hsl = read_parameter( res, "ActivePowerTimeSeriesParameter__InterruptiblePowerLoad"; table_format = TableFormat.WIDE, @@ -1101,10 +1086,7 @@ end res, "ActivePowerVariable__InterruptiblePowerLoad"; table_format = TableFormat.WIDE, ) - awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; - table_format = TableFormat.WIDE, - ) + awards = _read_awards(res, "OnlineReserve__ReserveUp") combined_total = 0.0 for t in 1:24 # One shared LB expression: a per-service headroom bug would allow up to 2*P. @@ -1154,10 +1136,7 @@ end container, POM.PiecewiseLinearBlockReserveOffer, PSY.InterruptiblePowerLoad, ) res = IOM.OptimizationProblemOutputs(model) - awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; - table_format = TableFormat.WIDE, - ) + awards = _read_awards(res, "OnlineReserve__ReserveUp") col = "$(PSY.get_name(ordc))__$(_IL_NAME)" total = 0.0 for t in 1:24 @@ -1192,11 +1171,7 @@ function _solve_offline_ordc(attributes::Dict{String, Any}) @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED res = IOM.OptimizationProblemOutputs(model) on = read_variable(res, OnVariable, ThermalStandard; table_format = TableFormat.WIDE) - awards = read_variable( - res, - "ActivePowerReserveVariable__OfflineReserve"; - table_format = TableFormat.WIDE, - ) + awards = _read_awards(res, "OfflineReserve") service = PSY.get_name(only(get_components(OfflineReserve, sys))) committed_offline = 0.0 for g in get_components(ThermalStandard, sys), t in 1:24 @@ -1253,11 +1228,7 @@ end IOM.ModelBuildStatus.BUILT @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED res = IOM.OptimizationProblemOutputs(model) - awards = read_variable( - res, - "ActivePowerReserveVariable__OfflineReserve"; - table_format = TableFormat.WIDE, - ) + awards = _read_awards(res, "OfflineReserve") service = PSY.get_name(only(get_components(OfflineReserve, sys))) mustrun_name = PSY.get_name(mustrun) for t in 1:24 @@ -1316,11 +1287,7 @@ function _solve_hydro_offline!(model; on = nothing) res = IOM.OptimizationProblemOutputs(model) wide = TableFormat.WIDE commitment = read_variable(res, OnVariable, HydroDispatch; table_format = wide) - award = read_variable( - res, - "ActivePowerReserveVariable__OfflineReserve"; - table_format = wide, - ) + award = _read_awards(res, "OfflineReserve") limit = read_parameter( res, ActivePowerTimeSeriesParameter, diff --git a/test/test_model_decision.jl b/test/test_model_decision.jl index 335ae2a0..f8c0865f 100644 --- a/test/test_model_decision.jl +++ b/test/test_model_decision.jl @@ -412,21 +412,29 @@ end # This test needs to be reviewed # @test isapprox(get_objective_value(res), 256937.0; atol = 10000.0) vars = res.variable_values - # Reserve variables of a type share one container keyed + # Reserve variables share one container per (device type, service type), keyed # `(service_name, device_name, time)`. - service_key = IOM.VariableKey( - ActivePowerReserveVariable, - PSY.OfflineReserve, - ) - @test service_key in keys(vars) - # That container flattens to `"service_name__device_name"` result columns + S = IOM.get_component_type( + IOM.VariableKey(ActivePowerReserveVariable, PSY.OfflineReserve), + ) + service_keys = [ + k for k in keys(vars) if + IOM.get_entry_type(k) === ActivePowerReserveVariable && + IOM.get_component_type(k) <: IOM.ComponentPairKey{<:PSY.Component, S} + ] + @test !isempty(service_keys) + # Each container flattens to `"service_name__device_name"` result columns # (WIDE format one column per flattened pair). - result = read_variable( - res, - "ActivePowerReserveVariable__OfflineReserve"; - table_format = TableFormat.WIDE, - ) - @test any(startswith(string(n), "NonSpinningReserve__") for n in names(result)) + result_columns = [ + n for k in service_keys for n in names( + read_variable( + res, + IOM.encode_key_as_string(k); + table_format = TableFormat.WIDE, + ), + ) + ] + @test any(startswith(string(n), "NonSpinningReserve__") for n in result_columns) end @testset "Test serialization/deserialization of DecisionModel outputs" begin diff --git a/test/test_services_constructor.jl b/test/test_services_constructor.jl index b50de575..1e6a9115 100644 --- a/test/test_services_constructor.jl +++ b/test/test_services_constructor.jl @@ -112,7 +112,7 @@ end end @testset "Per-type reserve container isolates services of the same type" begin - # Two OnlineReserve{ReserveUp} services share one + # Two OnlineReserve{ReserveUp} services share one per-device-type # `(service, device, time)` ActivePowerReserveVariable container. Verify (a) each # service's requirement constraint sums only its own device variables (no # cross-service leakage) and (b) the proportional reserve cost prices each variable @@ -129,7 +129,11 @@ end IOM.ModelBuildStatus.BUILT container = get_optimization_container(model) - rv = IOM.get_variable(container, ActivePowerReserveVariable, OnlineReserve{ReserveUp}) + rv = IOM.get_variable( + container, + ActivePowerReserveVariable, + IOM.ComponentPairKey{ThermalStandard, OnlineReserve{ReserveUp}}, + ) con = IOM.get_constraint( container, RequirementConstraint, @@ -373,7 +377,11 @@ end @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED container = get_optimization_container(model) - rv = IOM.get_variable(container, ActivePowerReserveVariable, OnlineReserve{ReserveUp}) + rv = IOM.get_variable( + container, + ActivePowerReserveVariable, + IOM.ComponentPairKey{ThermalStandard, OnlineReserve{ReserveUp}}, + ) con = IOM.get_constraint( container, RequirementConstraint, @@ -1333,7 +1341,11 @@ end IOM.ModelBuildStatus.BUILT container = get_optimization_container(model) - rv = IOM.get_variable(container, ActivePowerReserveVariable, OnlineReserve{ReserveUp}) + rv = IOM.get_variable( + container, + ActivePowerReserveVariable, + IOM.ComponentPairKey{ThermalStandard, OnlineReserve{ReserveUp}}, + ) con = IOM.get_constraint( container, RequirementConstraint, @@ -1384,7 +1396,11 @@ end IOM.ModelBuildStatus.BUILT container = get_optimization_container(model) - rv = IOM.get_variable(container, ActivePowerReserveVariable, OnlineReserve{ReserveUp}) + rv = IOM.get_variable( + container, + ActivePowerReserveVariable, + IOM.ComponentPairKey{ThermalStandard, OnlineReserve{ReserveUp}}, + ) con = IOM.get_constraint( container, RequirementConstraint, @@ -1462,7 +1478,11 @@ end @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED container = get_optimization_container(model) - rv = IOM.get_variable(container, ActivePowerReserveVariable, OnlineReserve{ReserveUp}) + rv = IOM.get_variable( + container, + ActivePowerReserveVariable, + IOM.ComponentPairKey{ThermalStandard, OnlineReserve{ReserveUp}}, + ) for t in IOM.get_time_steps(container) provided = sum( @@ -1763,7 +1783,7 @@ end table_format = TableFormat.WIDE, ) awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; + res, "ActivePowerReserveVariable__ThermalStandard__OnlineReserve__ReserveUp"; table_format = TableFormat.WIDE, ) sub_cols = _sub_cols(awards, "GROUP_SUB_") @@ -1778,7 +1798,7 @@ end model = _solve_group_model(sys) res = IOM.OptimizationProblemOutputs(model) awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; + res, "ActivePowerReserveVariable__ThermalStandard__OnlineReserve__ReserveUp"; table_format = TableFormat.WIDE, ) sub_a_total = sum(awards[1, c] for c in _sub_cols(awards, "GROUP_SUB_A")) @@ -1824,7 +1844,7 @@ end model = _solve_group_model(sys; include_group = false) res = IOM.OptimizationProblemOutputs(model) awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; + res, "ActivePowerReserveVariable__ThermalStandard__OnlineReserve__ReserveUp"; table_format = TableFormat.WIDE, ) for t in 1:24, c in _sub_cols(awards, "GROUP_SUB_") @@ -1880,7 +1900,7 @@ end table_format = TableFormat.WIDE, ) awards = read_variable( - res, "ActivePowerReserveVariable__OnlineReserve__ReserveUp"; + res, "ActivePowerReserveVariable__ThermalStandard__OnlineReserve__ReserveUp"; table_format = TableFormat.WIDE, ) sub_cols = _sub_cols(awards, "GROUP_SUB_") @@ -2179,7 +2199,10 @@ end reserve = IOM.get_variable( container, ActivePowerReserveVariable, - PSY.OnlineReserve{PSY.ReserveUp}, + IOM.ComponentPairKey{ + PSY.InterruptiblePowerLoad, + PSY.OnlineReserve{PSY.ReserveUp}, + }, ) for service_name in ("R1", "R2") devices = if nested @@ -2254,16 +2277,18 @@ end container = IOM.get_optimization_container(model) jump_model = IOM.get_jump_model(container) on = IOM.get_variable(container, OnVariable, PSY.InterruptiblePowerLoad) - r_up = - IOM.get_variable( - container, - ActivePowerReserveVariable, - PSY.OnlineReserve{PSY.ReserveUp}, - ) + r_up = IOM.get_variable( + container, + ActivePowerReserveVariable, + IOM.ComponentPairKey{PSY.InterruptiblePowerLoad, PSY.OnlineReserve{PSY.ReserveUp}}, + ) r_dn = IOM.get_variable( container, ActivePowerReserveVariable, - PSY.OnlineReserve{PSY.ReserveDown}, + IOM.ComponentPairKey{ + PSY.InterruptiblePowerLoad, + PSY.OnlineReserve{PSY.ReserveDown}, + }, ) for t in IOM.get_time_steps(container)