Complete the device time-varying market-bid offer-curve path#141
Conversation
The time-varying PWL market-bid machinery was wired for ORDC services only; the device (ThermalStandard MarketBidTimeSeriesCost) path was missing its counterparts, so build! cascaded through several gaps. Add the device-side methods: - _get_time_series_name for incremental/decremental PWL slope+breakpoint params (read the offer-curve time-series key from the device's cost). - calc_additional_axes for those params (batch-max tranche axis), so the PWL parameter container is sized with its tranche axis instead of empty. - _get_cost_if_exists(::MarketBidTimeSeriesCost) = nothing (not a fuel curve), mirroring the static MarketBidCost method.
There was a problem hiding this comment.
Pull request overview
This PR completes the device-side plumbing for time-varying market-bid offer curves so that devices carrying MarketBidTimeSeriesCost can successfully build! and solve with time-varying PWL offer curves.
Changes:
- Adds device-side
_get_time_series_nameoverloads for incremental/decremental PWL slope & breakpoint parameters. - Adds device-side
calc_additional_axesoverloads for tranche-axis sizing of time-varying PWL slope & breakpoint parameters. - Fixes fuel-curve detection path by treating
MarketBidTimeSeriesCostas non-fuel-curve (likeMarketBidCost).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/common_models/add_parameters.jl |
Adds missing device-side time-series name resolution and tranche-axis sizing for time-varying PWL offer-curve parameters. |
src/common_models/add_expressions.jl |
Prevents MarketBidTimeSeriesCost from falling into the fuel-curve accessor path by returning nothing for _get_cost_if_exists. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function _get_time_series_name( | ||
| ::Type{ | ||
| <:Union{ | ||
| IncrementalPiecewiseLinearSlopeParameter, | ||
| IncrementalPiecewiseLinearBreakpointParameter, | ||
| }, | ||
| }, | ||
| 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_value_curve(PSY.get_incremental_offer_curves(op_cost)), | ||
| ), | ||
| ) | ||
| end |
| function _get_time_series_name( | ||
| ::Type{ | ||
| <:Union{ | ||
| DecrementalPiecewiseLinearSlopeParameter, | ||
| DecrementalPiecewiseLinearBreakpointParameter, | ||
| }, | ||
| }, | ||
| 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_value_curve(PSY.get_decremental_offer_curves(op_cost)), | ||
| ), | ||
| ) | ||
| end |
| function _device_offer_curve_ts_key(::Type{T}, device::PSY.Component) where {T <: ParameterType} | ||
| op_cost = PSY.get_operation_cost(device) | ||
| IS.@assert_op op_cost isa TS_OFFER_CURVE_COST_TYPES | ||
| curve = if T <: Union{ | ||
| IncrementalPiecewiseLinearSlopeParameter, | ||
| IncrementalPiecewiseLinearBreakpointParameter, | ||
| } | ||
| PSY.get_incremental_offer_curves(op_cost) | ||
| else | ||
| PSY.get_decremental_offer_curves(op_cost) | ||
| end | ||
| return IS.get_time_series_key(PSY.get_value_curve(curve)) | ||
| end |
| function calc_additional_axes( | ||
| ::OptimizationContainer, | ||
| ::Type{T}, | ||
| devices::U, | ||
| ::DeviceModel{D, W}, | ||
| ) where { | ||
| T <: AbstractPiecewiseLinearSlopeParameter, | ||
| U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, | ||
| W <: AbstractDeviceFormulation, | ||
| } where {D <: PSY.Component} | ||
| max_tranches = maximum(d -> get_max_tranches(d, _device_offer_curve_ts_key(T, d)), devices) | ||
| return (IOM.make_tranche_axis(max_tranches),) | ||
| end | ||
|
|
||
| function calc_additional_axes( | ||
| ::OptimizationContainer, | ||
| ::Type{T}, | ||
| devices::U, | ||
| ::DeviceModel{D, W}, | ||
| ) where { | ||
| T <: AbstractPiecewiseLinearBreakpointParameter, | ||
| U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, | ||
| W <: AbstractDeviceFormulation, | ||
| } where {D <: PSY.Component} | ||
| max_tranches = maximum(d -> get_max_tranches(d, _device_offer_curve_ts_key(T, d)), devices) | ||
| return (IOM.make_tranche_axis(max_tranches + 1),) # one more breakpoint than tranches | ||
| end |
| ) | ||
| end | ||
|
|
||
| function _get_time_series_name( |
There was a problem hiding this comment.
We can combine these with a helper.
There was a problem hiding this comment.
There are also other _get_time_series_name methods with similar logic so maybe this could use a larger refactor.
| function _device_offer_curve_ts_key(::Type{T}, device::PSY.Component) where {T <: ParameterType} | ||
| op_cost = PSY.get_operation_cost(device) | ||
| IS.@assert_op op_cost isa TS_OFFER_CURVE_COST_TYPES | ||
| curve = if T <: Union{ |
There was a problem hiding this comment.
We should dispatch on this instead.
| ) | ||
| end | ||
|
|
||
| # Time-varying device offer curves: the incremental/decremental PWL slope & |
There was a problem hiding this comment.
I think this comment is excessive, the other methods don't use documentation.
Summary
Completes the device time-varying market-bid offer-curve path. The time-varying PWL market-bid machinery was wired for ORDC services only; a
ThermalStandard(or other device) carrying aMarketBidTimeSeriesCostcascaded through several missing device-side methods duringbuild!. This adds the device counterparts so a 24h time-varying offer-curve UC builds and solves.Pairs with Sienna-Platform/InfrastructureOptimizationModels.jl#
rh/scalar-ts-cost-unwrap(scalarunwrap_for_param), which fixes the first crash in the chain.What was missing (each surfaced as a separate
build!failure)_get_time_series_nameforIncremental/DecrementalPiecewiseLinearSlope/BreakpointParameteron a device — the genericget_time_series_names(model)[T]has no entry →KeyError. Added device methods reading the offer-curve time-series key from the device'sMarketBidTimeSeriesCost(analogous to the existing ORDC-service method).calc_additional_axesfor those PWL params on a device — the default device method returns(), so the parameter container was built with no tranche axis and the PWL element fell throughunwrap_for_param→size(::PiecewiseStepData). Added device methods that size the container to the batch-wide max tranches (viaget_max_tranches), padding shorter per-hour curves._get_cost_if_exists(::MarketBidTimeSeriesCost) = nothing— the generic falls toget_variable(cost)(a service/fuel accessor) →MethodError. Mirrors the existing staticMarketBidCostmethod (TS bids aren't fuel curves).All additions are small and additive, mirroring the existing static-
MarketBidCost/ ORDC-service handling; no changes to existing methods.Verification
A 24h copper-plate UC with
ThermalStandardgens (and anInterruptiblePowerLoadbid) carrying time-varying offer curves nowbuild!s andsolve!s, with the per-hour JuMP block-offer objective coefficients matching the per-hour offer-curve slopes exactly. Verified on both synthetic data and ~1 month of real ERCOT DAM offer curves (per-hourSingleTimeSeries→transform_single_time_series!→ 24hDeterministicwindow).Note: building a one-sided device cost (supply-only gen / demand-only load) requires marking the unused side with an empty-name time-series key (the placeholder the validation keys off). PSY exposes no helper for this — worth a follow-up to add one, analogous to the static
ZERO_OFFER_CURVE.🤖 Generated with Claude Code