Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/common_models/add_to_expression.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/core/reserve_traits.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/energy_storage_models/storage_models.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions src/hybrid_system_models/hybrid_systems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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,
Expand Down
43 changes: 31 additions & 12 deletions src/services_models/reserve_group.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)
Expand Down Expand Up @@ -202,25 +205,41 @@ 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},
time_steps::UnitRange{Int},
)
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
8 changes: 4 additions & 4 deletions src/services_models/reserve_offers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading