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
2 changes: 1 addition & 1 deletion ext/PowerFlowsExt/PowerFlowsExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module PowerFlowsExt

using InfrastructureOptimizationModels
using PowerFlows
import InfrastructureOptimizationModels: IS, PNM, PSY
import InfrastructureOptimizationModels: IS
import InfrastructureOptimizationModels:
OptimizationContainerKey,
AbstractPowerFlowEvaluationData
Expand Down
12 changes: 7 additions & 5 deletions src/PowerOperationsModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ import InfrastructureOptimizationModels:
get_default_time_series_names,
# proportional cost
proportional_cost,
is_time_variant_term,
is_time_variant_proportional,
add_proportional_cost!,
add_proportional_cost_maybe_time_variant!,
skip_proportional_cost,
Expand Down Expand Up @@ -134,7 +134,6 @@ import InfrastructureOptimizationModels:

# Market bid cost: import IOM functions that POM extends with device-specific methods
import InfrastructureOptimizationModels:
_has_market_bid_cost,
_consider_parameter,
validate_occ_component,
_include_min_gen_power_in_constraint,
Expand All @@ -143,7 +142,6 @@ import InfrastructureOptimizationModels:
_vom_offer_direction,
add_pwl_constraint_delta!,
add_pwl_term_delta!,
get_output_offer_curves,
# Internal utilities used by market bid overrides and proportional_cost
is_time_variant,
apply_maybe_across_time_series,
Expand All @@ -154,7 +152,6 @@ import InfrastructureOptimizationModels:
has_service_model,
IncrementalOffer,
DecrementalOffer,
get_input_offer_curves,
add_constraint_dual!,
assign_dual_variable!,
_calculate_dual_variable_value!,
Expand Down Expand Up @@ -191,6 +188,7 @@ using InfrastructureOptimizationModels # TODO: use explicit imports.
#################################################################################
include("core/definitions.jl")
include("core/interfaces.jl")
include("core/default_interface_methods.jl")
include("core/physical_constant_definitions.jl")
include("core/variables.jl")
include("core/expressions.jl")
Expand All @@ -213,6 +211,10 @@ include("common_models/add_parameters.jl")
include("common_models/make_system_expressions.jl")
include("common_models/reserve_range_constraints.jl")

# Market bid cost plumbing (PSY orchestration moved out of IOM). Must be included
# before device-specific files that reference MBC_TYPES / IEC_TYPES.
include("common_models/market_bid_plumbing.jl")

# Initial Conditions
include("initial_conditions/add_initial_condition.jl")
include("initial_conditions/device_initial_conditions.jl")
Expand All @@ -237,7 +239,7 @@ include("static_injector_models/hydrogeneration_constructor.jl")
include("energy_storage_models/storage_models.jl")
include("energy_storage_models/storage_constructor.jl")

# Market bid cost: device-specific overloads for IOM's generic market_bid.jl
# POM market bid cost overrides (plumbing is included earlier, before device files)
include("common_models/market_bid_overrides.jl")

# AC Transmission Models
Expand Down
53 changes: 42 additions & 11 deletions src/common_models/add_parameters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ function _add_time_series_parameters!(
for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, D)
reduction_entry = all_branch_maps_by_type[reduction][D][arc]
device_with_time_series =
get_device_with_time_series(reduction_entry, ts_type, ts_name)
IOM.get_branch_with_time_series(reduction_entry, ts_type, ts_name)
if device_with_time_series === nothing
continue
end
Expand Down Expand Up @@ -307,25 +307,56 @@ end
_get_time_series_name(::T, ::PSY.Component, model::DeviceModel) where {T <: ParameterType} =
get_time_series_names(model)[T]

_get_time_series_name(::StartupCostParameter, device::PSY.Component, ::DeviceModel) =
IS.get_name(PSY.get_start_up(PSY.get_operation_cost(device)))
# The fact that we're seeing these parameters means that we should
# have a time-varying MBC/IEC, so the `get_time_series_key` call should be valid.

_get_time_series_name(::ShutdownCostParameter, device::PSY.Component, ::DeviceModel) =
IS.get_name(PSY.get_shut_down(PSY.get_operation_cost(device)))
function _get_time_series_name(
::StartupCostParameter,
device::PSY.Component,
::DeviceModel,
)
op_cost = PSY.get_operation_cost(device)
IS.@assert_op op_cost isa TS_OFFER_CURVE_COST_TYPES
return IS.get_name(IS.get_time_series_key(PSY.get_start_up(op_cost)))
end

function _get_time_series_name(
::ShutdownCostParameter,
device::PSY.Component,
::DeviceModel,
)
op_cost = PSY.get_operation_cost(device)
IS.@assert_op op_cost isa TS_OFFER_CURVE_COST_TYPES
return IS.get_name(IS.get_time_series_key(PSY.get_shut_down(op_cost)))
end

_get_time_series_name(
function _get_time_series_name(
::IncrementalCostAtMinParameter,
device::PSY.Device,
::DeviceModel,
) =
IS.get_name(PSY.get_incremental_initial_input(PSY.get_operation_cost(device)))
)
op_cost = PSY.get_operation_cost(device)
IS.@assert_op op_cost isa TS_OFFER_CURVE_COST_TYPES
return IS.get_name(
IS.get_initial_input(
PSY.get_value_curve(PSY.get_incremental_offer_curves(op_cost)),
),
)
end

_get_time_series_name(
function _get_time_series_name(
::DecrementalCostAtMinParameter,
device::PSY.Device,
::DeviceModel,
) =
IS.get_name(PSY.get_decremental_initial_input(PSY.get_operation_cost(device)))
)
op_cost = PSY.get_operation_cost(device)
IS.@assert_op op_cost isa TS_OFFER_CURVE_COST_TYPES
return IS.get_name(
IS.get_initial_input(
PSY.get_value_curve(PSY.get_decremental_offer_curves(op_cost)),
),
)
end

#################################################################################
# _get_expected_time_series_eltype — for ObjectiveFunctionParameter
Expand Down
117 changes: 85 additions & 32 deletions src/common_models/market_bid_overrides.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,49 @@
_has_market_bid_cost(::PSY.RenewableNonDispatch) = false
_has_market_bid_cost(::PSY.PowerLoad) = false
_has_market_bid_cost(device::PSY.ControllableLoad) =
PSY.get_operation_cost(device) isa PSY.MarketBidCost
PSY.get_operation_cost(device) isa MBC_TYPES

#################################################################################
# Section 1b: Generic MarketBidCost OnVariable proportional cost
#
# Shared between thermals, hydros, and interruptible loads. The OnVariable cost
# for MBC is the offer curve's `initial_input` (cost at minimum generation). The
# only per-device variation is whether that comes from the incremental side
# (generators) or the decremental side (controllable loads). Direction is set
# by the `_onvar_offer_direction` trait.
#################################################################################

_onvar_offer_direction(::PSY.Generator) = IncrementalOffer()
_onvar_offer_direction(::PSY.ControllableLoad) = DecrementalOffer()

_cost_at_min_param(::IncrementalOffer) = IncrementalCostAtMinParameter()
_cost_at_min_param(::DecrementalOffer) = DecrementalCostAtMinParameter()

# Static MarketBidCost: read initial_input directly from the offer curve.
proportional_cost(
::OptimizationContainer,
::PSY.MarketBidCost,
::Type{OnVariable},
comp::Union{PSY.Generator, PSY.ControllableLoad},
::Type{<:AbstractDeviceFormulation},
::Int,
) = IOM.get_initial_input(_onvar_offer_direction(comp), comp)

# Time-series MarketBidCost: read from parameter container populated by add_parameters!.
function proportional_cost(
container::OptimizationContainer,
::PSY.MarketBidTimeSeriesCost,
::Type{OnVariable},
comp::T,
::Type{<:AbstractDeviceFormulation},
t::Int,
) where {T <: Union{PSY.Generator, PSY.ControllableLoad}}
param = _cost_at_min_param(_onvar_offer_direction(comp))
name = get_name(comp)
param_arr = get_parameter_array(container, param, T)
param_mult = get_parameter_multiplier_array(container, param, T)
return param_arr[name, t] * param_mult[name, t]
end

#################################################################################
# Section 2: _consider_parameter — compact commitment startup
Expand All @@ -32,14 +74,16 @@ _consider_parameter(
# Section 3: Device-specific validate_occ_component
#################################################################################

# ThermalMultiStart: accept NTuple{3, Float64} and StartUpStages without warning
function validate_occ_component(
# ThermalMultiStart: accept NTuple{3, Float64} and PSY.StartUpStages without warning
function IOM.validate_occ_component(
::Type{StartupCostParameter},
device::PSY.ThermalMultiStart,
)
startup = PSY.get_start_up(PSY.get_operation_cost(device))
# TupleTimeSeries{PSY.StartUpStages} guarantees NTuple{3, Float64} values at construction
startup isa IS.TupleTimeSeries && return
_validate_eltype(
Union{Float64, NTuple{3, Float64}, StartUpStages},
Union{Float64, NTuple{3, Float64}, PSY.StartUpStages},
device,
startup,
" startup cost",
Expand All @@ -48,55 +92,55 @@ end

# Renewable / Storage: warn on nonzero startup, shutdown, and no-load costs

function validate_occ_component(
function IOM.validate_occ_component(
::Type{StartupCostParameter},
device::Union{PSY.RenewableDispatch, PSY.Storage},
)
startup = PSY.get_start_up(PSY.get_operation_cost(device))
apply_maybe_across_time_series(device, startup) do x
if x != PSY.single_start_up_to_stages(0.0)
# x may be Float64 (TGC), PSY.StartUpStages (static MBC), or NTuple{3, Float64}
# (TupleTimeSeries elements). `values` normalizes both NamedTuple and Tuple.
if any(!iszero, x isa Number ? (x,) : values(x))
Comment thread
luke-kiernan marked this conversation as resolved.
@warn "Nonzero startup cost detected for renewable generation or storage device $(get_name(device))."
end
end
end

function validate_occ_component(
# LinearCurve (static) and TimeSeriesLinearCurve (TS) are the only types carried in
# MBC/ImportExportCost shutdown and no-load fields. Only the static case is meaningfully
# comparable to zero at validation time — for TS we'd need to iterate the series, which
# the time-series store may not even have populated yet.
# FIXME better solution?
_scalar_if_static(x::IS.LinearCurve) = IS.get_proportional_term(x)
_scalar_if_static(::IS.TimeSeriesLinearCurve) = nothing

function IOM.validate_occ_component(
::Type{ShutdownCostParameter},
device::Union{PSY.RenewableDispatch, PSY.Storage},
)
shutdown = PSY.get_shut_down(PSY.get_operation_cost(device))
apply_maybe_across_time_series(device, shutdown) do x
if x != 0.0
@warn "Nonzero shutdown cost detected for renewable generation or storage device $(get_name(device))."
end
x = _scalar_if_static(PSY.get_shut_down(PSY.get_operation_cost(device)))
if !isnothing(x) && x != 0.0
@warn "Nonzero shutdown cost detected for renewable generation or storage device $(get_name(device))."
end
end

function validate_occ_component(
function IOM.validate_occ_component(
::Type{IncrementalCostAtMinParameter},
device::Union{PSY.RenewableDispatch, PSY.Storage},
)
no_load_cost = PSY.get_no_load_cost(PSY.get_operation_cost(device))
if !isnothing(no_load_cost)
apply_maybe_across_time_series(device, no_load_cost) do x
if x != 0.0
@warn "Nonzero no-load cost detected for renewable generation or storage device $(get_name(device))."
end
end
x = _scalar_if_static(PSY.get_no_load_cost(PSY.get_operation_cost(device)))
if !isnothing(x) && x != 0.0
@warn "Nonzero no-load cost detected for renewable generation or storage device $(get_name(device))."
end
end

function validate_occ_component(
function IOM.validate_occ_component(
::Type{DecrementalCostAtMinParameter},
device::PSY.Storage,
)
no_load_cost = PSY.get_no_load_cost(PSY.get_operation_cost(device))
if !isnothing(no_load_cost)
apply_maybe_across_time_series(device, no_load_cost) do x
if x != 0.0
@warn "Nonzero no-load cost detected for storage device $(get_name(device))."
end
end
x = _scalar_if_static(PSY.get_no_load_cost(PSY.get_operation_cost(device)))
if !isnothing(x) && x != 0.0
@warn "Nonzero no-load cost detected for storage device $(get_name(device))."
end
end

Expand Down Expand Up @@ -167,11 +211,16 @@ _include_constant_min_gen_power_in_constraint(
# Section 6: Source ImportExport — both incremental and decremental offers
#################################################################################

# FIXME behavior change: we now always add PWL terms for both import and export. The
# previous `isnothing(...)` guard is dead in the new PSY (offer curves default to
# `ZERO_OFFER_CURVE`, not nothing), and we don't yet have a way to introspect TS-backed
# curves to decide "trivially empty". Skipping when the curve is trivial (one-directional
# source) would be the better behavior — revisit once we have a cheap emptiness check.
function add_variable_cost_to_objective!(
container::OptimizationContainer,
::Type{ActivePowerOutVariable},
component::PSY.Source,
cost_function::PSY.ImportExportCost,
cost_function::IEC_TYPES,
::Type{ImportExportSourceModel},
)
isnothing(get_output_offer_curves(cost_function)) && return
Expand All @@ -190,7 +239,7 @@ function add_variable_cost_to_objective!(
container::OptimizationContainer,
::Type{ActivePowerInVariable},
component::PSY.Source,
cost_function::PSY.ImportExportCost,
cost_function::IEC_TYPES,
::Type{ImportExportSourceModel},
)
isnothing(get_input_offer_curves(cost_function)) && return
Expand Down Expand Up @@ -218,8 +267,12 @@ function add_variable_cost_to_objective!(
) where {T <: VariableType, U <: AbstractControllablePowerLoadFormulation}
component_name = PSY.get_name(component)
@debug "Market Bid" _group = LOG_GROUP_COST_FUNCTIONS component_name
if !(isnothing(get_output_offer_curves(cost_function)))
error("Component $(component_name) is not allowed to participate as a supply.")
if IOM.is_nontrivial_offer(get_output_offer_curves(cost_function))
throw(
ArgumentError(
"Component $(component_name) is not allowed to participate as a supply.",
),
)
end
add_pwl_term_delta!(
DecrementalOffer(),
Expand Down
Loading