From d6a5756eff508e00d641a1fd37d7e450916b9047 Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:06:29 -0600 Subject: [PATCH 01/17] Add `incremental_initial_input`, `decremental_initial_input` --- src/models/cost_functions/MarketBidCost.jl | 51 ++++++++++++++++------ 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index a26751f9c2..65f55987b5 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -11,7 +11,7 @@ Compatible with most US Market bidding mechanisms that support demand and genera """ @kwdef mutable struct MarketBidCost <: OperationalCost "No load cost" - no_load_cost::Union{TimeSeriesKey, Float64} + no_load_cost::Union{TimeSeriesKey, Nothing, Float64} = nothing "Start-up cost at different stages of the thermal cycle as the unit cools after a shutdown (e.g., *hot*, *warm*, or *cold* starts). Warm is also referred to as intermediate in some markets. Can also accept a single value if there is only one @@ -19,20 +19,24 @@ Compatible with most US Market bidding mechanisms that support demand and genera start_up::Union{TimeSeriesKey, StartUpStages} "Shut-down cost" shut_down::Float64 - "Sell Offer Curves data, which can be a time series or a [`CostCurve`](@ref) using - [`PiecewiseIncrementalCurve`](@ref)" + "Sell Offer Curves data, which can be a time series of [`PiecewiseStepData`](@ref) or a + [`CostCurve`](@ref) of [`PiecewiseIncrementalCurve`](@ref)" incremental_offer_curves::Union{ Nothing, - TimeSeriesKey, + TimeSeriesKey, # piecewise step data CostCurve{PiecewiseIncrementalCurve}, } = nothing - "Buy Offer Curves data, can be a time series or a [`CostCurve`](@ref) using - [`PiecewiseIncrementalCurve`](@ref)" + "Buy Offer Curves data, which can be a time series of [`PiecewiseStepData`](@ref) or a + [`CostCurve`](@ref) of [`PiecewiseIncrementalCurve`](@ref)" decremental_offer_curves::Union{ Nothing, TimeSeriesKey, CostCurve{PiecewiseIncrementalCurve}, } = nothing + "If using a time series for incremental_offer_curves, this is a time series of `Float64` representing the `initial_input`" + incremental_initial_input::Union{Nothing, TimeSeriesKey} = nothing + "If using a time series for decremental_offer_curves, this is a time series of `Float64` representing the `initial_input`" + decremental_initial_input::Union{Nothing, TimeSeriesKey} = nothing "Bids for the ancillary services" ancillary_service_offers::Vector{Service} = Vector{Service}() end @@ -54,6 +58,23 @@ MarketBidCost( ancillary_service_offers, ) +MarketBidCost( + no_load_cost::Float64, + start_up::Union{TimeSeriesKey, StartUpStages}, + shut_down, + incremental_offer_curves, + decremental_offer_curves, + ancillary_service_offers, +) = + MarketBidCost(; + no_load_cost = no_load_cost, + start_up = start_up, + shut_down = shut_down, + incremental_offer_curves = incremental_offer_curves, + decremental_offer_curves = decremental_offer_curves, + ancillary_service_offers = ancillary_service_offers + ) + # Constructor for demo purposes; non-functional. function MarketBidCost(::Nothing) MarketBidCost(; @@ -73,18 +94,22 @@ function MarketBidCost( shut_down, incremental_offer_curves = nothing, decremental_offer_curves = nothing, + incremental_initial_input = nothing, + decremental_initial_input = nothing, ancillary_service_offers = Vector{Service}(), ) # Intended for use with generators that are not multi-start (e.g. ThermalStandard). # Operators use `hot` when they don’t have multiple stages. start_up_multi = (hot = Float64(start_up), warm = 0.0, cold = 0.0) - return MarketBidCost( - no_load_cost, - start_up_multi, - shut_down, - incremental_offer_curves, - decremental_offer_curves, - ancillary_service_offers, + return MarketBidCost(; + no_load_cost = no_load_cost, + start_up = start_up_multi, + shut_down = shut_down, + incremental_offer_curves = incremental_offer_curves, + decremental_offer_curves = decremental_offer_curves, + incremental_initial_input = incremental_initial_input, + decremental_initial_input = decremental_initial_input, + ancillary_service_offers = ancillary_service_offers, ) end From 52321a00c9f67c0fd081f4ac08e20e86b06ac3f6 Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:11:22 -0600 Subject: [PATCH 02/17] First draft of support for `incremental_initial_input` WIP -- not fully tested and probably bad in a number of ways --- src/models/cost_function_timeseries.jl | 122 +++++++++++++++++---- src/models/cost_functions/MarketBidCost.jl | 46 ++++++-- test/test_cost_functions.jl | 98 +++++++++++++---- 3 files changed, 214 insertions(+), 52 deletions(-) diff --git a/src/models/cost_function_timeseries.jl b/src/models/cost_function_timeseries.jl index dbd2bf0db3..287e1dbf5f 100644 --- a/src/models/cost_function_timeseries.jl +++ b/src/models/cost_function_timeseries.jl @@ -4,7 +4,6 @@ function _validate_market_bid_cost(cost, context) StackTraces.stacktrace()[2].func, context, MarketBidCost, cost)) end -# VALIDATORS function _validate_reserve_demand_curve(cost, name) !(cost isa CostCurve{PiecewiseIncrementalCurve}) && throw( ArgumentError( @@ -94,12 +93,6 @@ Helper function for cost getters. - `start_time`: as in `get_time_series` - `len`: as in `get_time_series` """ -_process_get_cost(_, _, cost::Nothing, _, _, _, _) = throw( - ArgumentError( - "This cost component is empty, please use the corresponding setter to add cost data.", - ), -) - function _process_get_cost(::Type{T}, _, cost::T, transform_fn, start_time::Union{Nothing, Dates.DateTime}, len::Union{Nothing, Int}, @@ -123,18 +116,59 @@ end # GETTER IMPLEMENTATIONS """ -Retrieve the variable cost bid for a `StaticInjection` device with a `MarketBidCost`. If -this field is a time series, the user may specify `start_time` and `len` and the function -returns a `TimeArray` of `CostCurve`s; if the field is not a time series, the function -returns a single `CostCurve`. +Retrieve the variable cost bid for a `StaticInjection` device with a `MarketBidCost`. If any +of the relevant fields (`incremental_offer_curves`, `initial_input`, `no_load_cost`) are +time series, the user may specify `start_time` and `len` and the function returns a +`TimeArray` of `CostCurve`s; if the field is not a time series, the function returns a +single `CostCurve`. """ -get_variable_cost( +function get_variable_cost( device::StaticInjection, cost::MarketBidCost; start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, -) = _process_get_cost(CostCurve{PiecewiseIncrementalCurve}, device, - get_incremental_offer_curves(cost), make_market_bid_curve, start_time, len) +) + function_data = if (get_incremental_offer_curves(cost) isa TimeSeriesKey) + get_incremental_offer_curves(device, cost; start_time = start_time, len = len) + else + get_incremental_offer_curves(device, cost) + end + initial_input = if (get_incremental_initial_input(cost) isa TimeSeriesKey) + get_incremental_initial_input(device, cost; start_time = start_time, len = len) + else + get_incremental_initial_input(device, cost) + end + input_at_zero = if (get_no_load_cost(cost) isa TimeSeriesKey) + get_no_load_cost(device, cost; start_time = start_time, len = len) + else + get_no_load_cost(device, cost) + end + params::Vector{Any} = [function_data, initial_input, input_at_zero] + first_time_series = findfirst(isa.(params, TimeSeries.TimeArray)) + if !isnothing(first_time_series) + timestamps = TimeSeries.timestamp(params[first_time_series]) + for (i, param) in enumerate(params) + if !(param isa TimeSeries.TimeArray) + params[i] = + TimeSeries.TimeArray(timestamps, fill(param, length(timestamps))) + end + end + !allequal(TimeSeries.timestamp.(params)) && + throw( + ArgumentError( + "Time series mismatch between incremental_offer_curves, incremental_initial_input, and no_load_cost", + ), + ) + @show collect(zip(collect.(TimeSeries.values.(params))...)) |> length + @show first(collect(zip(collect.(TimeSeries.values.(params))...))) + return TimeSeries.TimeArray(TimeSeries.timestamp(function_data), + [ + make_market_bid_curve(fd, ii, iaz) for + (fd, ii, iaz) in collect(zip(collect.(TimeSeries.values.(params))...)) + ]) + end + return make_market_bid_curve(input_at_zero, initial_input, function_data) +end """ Retrieve the variable cost data for a `ReserveDemandCurve`. The user may specify @@ -188,22 +222,51 @@ function get_fuel_cost(component::StaticInjection; ) end +""" +Retrieve the `incremental_offer_curves` for a `StaticInjection` device with a +`MarketBidCost`. If this field is a time series, the user may specify `start_time` and `len` +and the function returns a `TimeArray` of `Float64`s; if the field is not a time series, the +function returns a single `Float64` or `Nothing`. +""" +get_incremental_offer_curves( + device::StaticInjection, + cost::MarketBidCost; + start_time::Union{Nothing, Dates.DateTime} = nothing, + len::Union{Nothing, Int} = nothing, +) = _process_get_cost(Union{PiecewiseStepData, CostCurve{PiecewiseIncrementalCurve}}, + device, get_incremental_offer_curves(cost), nothing, start_time, len) + +# TODO decremental + """ Retrieve the no-load cost data for a `StaticInjection` device with a `MarketBidCost`. If this field is a time series, the user may specify `start_time` and `len` and the function returns a `TimeArray` of `Float64`s; if the field is not a time series, the function -returns a single `Float64`. +returns a single `Float64` or `Nothing`. """ get_no_load_cost( device::StaticInjection, cost::MarketBidCost; start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, -) = _process_get_cost(Float64, device, +) = _process_get_cost(Union{Nothing, Float64}, device, get_no_load_cost(cost), nothing, start_time, len) """ -Retrieve the no-load cost data for a `StaticInjection` device with a `MarketBidCost`. If +Retrieve the `incremental_initial_input` for a `StaticInjection` device with a `MarketBidCost`. +""" +get_incremental_initial_input( + device::StaticInjection, + cost::MarketBidCost; + start_time::Union{Nothing, Dates.DateTime} = nothing, + len::Union{Nothing, Int} = nothing, +) = _process_get_cost(Union{Nothing, Float64}, device, + get_incremental_initial_input(cost), nothing, start_time, len) + +# TODO decremental + +""" +Retrieve the startup cost data for a `StaticInjection` device with a `MarketBidCost`. If this field is a time series, the user may specify `start_time` and `len` and the function returns a `TimeArray` of `Float64`s; if the field is not a time series, the function returns a single `Float64`. @@ -323,7 +386,7 @@ function set_fuel_cost!( end """ -Set the no-load cost for a `StaticInjection` device with a `MarketBidCost` to either a single number or a time series. +Set the no-load cost for a `StaticInjection` device with a `MarketBidCost` to either a scalar or a time series. # Arguments - `sys::System`: PowerSystem System @@ -337,10 +400,31 @@ function set_no_load_cost!( ) market_bid_cost = get_operation_cost(component) _validate_market_bid_cost(market_bid_cost, "get_operation_cost(component)") - to_set = _process_set_cost(Float64, Float64, sys, component, data) + to_set = _process_set_cost(Union{Float64, Nothing}, Float64, sys, component, data) set_no_load_cost!(market_bid_cost, to_set) end +""" +Set the `incremental_initial_input` for a `StaticInjection` device with a `MarketBidCost` to either a scalar or a time series. + +# Arguments +- `sys::System`: PowerSystem System +- `component::StaticInjection`: Static injection device +- `time_series_data::Union{Float64, IS.TimeSeriesData},`: the data. If a time series, must be of eltype `Float64`. +""" +function set_incremental_initial_input!( + sys::System, + component::StaticInjection, + data::Union{Float64, IS.TimeSeriesData}, +) + market_bid_cost = get_operation_cost(component) + _validate_market_bid_cost(market_bid_cost, "get_operation_cost(component)") + to_set = _process_set_cost(Union{Float64, Nothing}, Float64, sys, component, data) + set_incremental_initial_input!(market_bid_cost, to_set) +end + +# TODO decremental + """ Set the startup cost for a `StaticInjection` device with a `MarketBidCost` to either a single `StartUpStages` or a time series. diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index 65f55987b5..decda78778 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -35,8 +35,7 @@ Compatible with most US Market bidding mechanisms that support demand and genera } = nothing "If using a time series for incremental_offer_curves, this is a time series of `Float64` representing the `initial_input`" incremental_initial_input::Union{Nothing, TimeSeriesKey} = nothing - "If using a time series for decremental_offer_curves, this is a time series of `Float64` representing the `initial_input`" - decremental_initial_input::Union{Nothing, TimeSeriesKey} = nothing + # TODO decremental "Bids for the ancillary services" ancillary_service_offers::Vector{Service} = Vector{Service}() end @@ -72,13 +71,13 @@ MarketBidCost( shut_down = shut_down, incremental_offer_curves = incremental_offer_curves, decremental_offer_curves = decremental_offer_curves, - ancillary_service_offers = ancillary_service_offers + ancillary_service_offers = ancillary_service_offers, ) # Constructor for demo purposes; non-functional. function MarketBidCost(::Nothing) MarketBidCost(; - no_load_cost = 0.0, + no_load_cost = nothing, start_up = (hot = START_COST, warm = START_COST, cold = START_COST), shut_down = 0.0, ) @@ -95,7 +94,6 @@ function MarketBidCost( incremental_offer_curves = nothing, decremental_offer_curves = nothing, incremental_initial_input = nothing, - decremental_initial_input = nothing, ancillary_service_offers = Vector{Service}(), ) # Intended for use with generators that are not multi-start (e.g. ThermalStandard). @@ -108,7 +106,6 @@ function MarketBidCost( incremental_offer_curves = incremental_offer_curves, decremental_offer_curves = decremental_offer_curves, incremental_initial_input = incremental_initial_input, - decremental_initial_input = decremental_initial_input, ancillary_service_offers = ancillary_service_offers, ) end @@ -121,8 +118,11 @@ get_start_up(value::MarketBidCost) = value.start_up get_shut_down(value::MarketBidCost) = value.shut_down """Get [`MarketBidCost`](@ref) `incremental_offer_curves`.""" get_incremental_offer_curves(value::MarketBidCost) = value.incremental_offer_curves -"""Get [`MarketBidCost`](@ref) `incremental_offer_curves`.""" +"""Get [`MarketBidCost`](@ref) `decremental_offer_curves`.""" get_decremental_offer_curves(value::MarketBidCost) = value.incremental_offer_curves +"""Get [`MarketBidCost`](@ref) `incremental_initial_input`.""" +get_incremental_initial_input(value::MarketBidCost) = value.incremental_initial_input +# TODO decremental """Get [`MarketBidCost`](@ref) `ancillary_service_offers`.""" get_ancillary_service_offers(value::MarketBidCost) = value.ancillary_service_offers @@ -135,6 +135,10 @@ set_shut_down!(value::MarketBidCost, val) = value.shut_down = val """Set [`MarketBidCost`](@ref) `incremental_offer_curves`.""" set_incremental_offer_curves!(value::MarketBidCost, val) = value.incremental_offer_curves = val +"""Set [`MarketBidCost`](@ref) `incremental_initial_input`.""" +set_incremental_initial_input!(value::MarketBidCost, val) = + value.incremental_initial_input = val +# TODO decremental """Set [`MarketBidCost`](@ref) `incremental_offer_curves`.""" set_decremental_offer_curves!(value::MarketBidCost, val) = value.decremental_offer_curves = val @@ -146,10 +150,7 @@ set_ancillary_service_offers!(value::MarketBidCost, val) = # curves in MarketBidCost) is a CostCurve{PiecewiseIncrementalCurve} with NaN initial input # and first x-coordinate function is_market_bid_curve(curve::ProductionVariableCostCurve) - (curve isa CostCurve{PiecewiseIncrementalCurve}) || return false - value_curve = get_value_curve(curve) - return isnan(get_initial_input(value_curve)) && - isnan(first(get_x_coords(get_function_data(value_curve)))) + return (curve isa CostCurve{PiecewiseIncrementalCurve}) end """ @@ -166,6 +167,18 @@ function make_market_bid_curve(powers::Vector{Float64}, return make_market_bid_curve(fd; power_units = power_units) end +make_market_bid_curve(initial_input::Union{Nothing, Real}, x_coords::Vector, slopes::Vector; + power_units::UnitSystem = UnitSystem.NATURAL_UNITS) = + CostCurve(PiecewiseIncrementalCurve(initial_input, x_coords, slopes), power_units) + +make_market_bid_curve(input_at_zero::Union{Nothing, Real}, + initial_input::Union{Nothing, Real}, x_coords::Vector, slopes::Vector; + power_units::UnitSystem = UnitSystem.NATURAL_UNITS) = + CostCurve( + PiecewiseIncrementalCurve(input_at_zero, initial_input, x_coords, slopes), + power_units, + ) + """ Make a CostCurve{PiecewiseIncrementalCurve} suitable for inclusion in a MarketBidCost from the FunctionData that might be used to store such a cost curve in a time series. @@ -177,7 +190,16 @@ function make_market_bid_curve(data::PiecewiseStepData; "The first x-coordinate in the PiecewiseStepData representation must be NaN", ), ) - cc = CostCurve(IncrementalCurve(data, NaN), power_units) + cc = CostCurve(IncrementalCurve(data, nothing), power_units) @assert is_market_bid_curve(cc) return cc end + +make_market_bid_curve(fd::PiecewiseStepData, initial_input::Union{Nothing, Real}; + power_units::UnitSystem = UnitSystem.NATURAL_UNITS) = + CostCurve(IncrementalCurve(fd, initial_input), power_units) + +make_market_bid_curve(fd::PiecewiseStepData, initial_input::Union{Nothing, Real}, + input_at_zero::Union{Nothing, Real}; + power_units::UnitSystem = UnitSystem.NATURAL_UNITS) = + CostCurve(IncrementalCurve(fd, initial_input, input_at_zero), power_units) diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index 9a73427109..66bd4d3dcf 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -311,13 +311,27 @@ end @test is_market_bid_curve(make_market_bid_curve(get_function_data(mbc))) @test_throws ArgumentError make_market_bid_curve( [100.0, 105.0, 120.0, 130.0], [26.0, 28.0, 30.0]) + + mbc2 = make_market_bid_curve(20.0, [1.0, 2.0, 3.0], [4.0, 6.0]) + @test is_market_bid_curve(mbc2) + @test is_market_bid_curve( + make_market_bid_curve(get_function_data(mbc2), get_initial_input(mbc2)), + ) + + mbc3 = make_market_bid_curve(18.0, 20.0, [1.0, 2.0, 3.0], [4.0, 6.0]) + @test is_market_bid_curve(mbc3) + @test is_market_bid_curve( + make_market_bid_curve(get_function_data(mbc2), get_initial_input(mbc3)), + ) end test_costs = Dict( CostCurve{QuadraticCurve} => repeat([CostCurve(QuadraticCurve(999.0, 2.0, 1.0))], 24), - CostCurve{PiecewiseIncrementalCurve} => + PiecewiseStepData => repeat([make_market_bid_curve([2.0, 3.0], [4.0, 6.0])], 24), + PiecewiseIncrementalCurve => + repeat([make_market_bid_curve(18.0, 20.0, [1.0, 2.0, 3.0], [4.0, 6.0])], 24), Float64 => collect(11.0:34.0), PSY.StartUpStages => @@ -338,15 +352,15 @@ test_costs = Dict( generator = get_component(ThermalStandard, sys, "322_CT_6") market_bid = MarketBidCost(nothing) set_operation_cost!(generator, market_bid) - forecast = IS.Deterministic( + forecast_fd = IS.Deterministic( "variable_cost", Dict(k => get_function_data.(v) for (k, v) in pairs(data_quadratic)), resolution, ) - @test_throws TypeError set_variable_cost!(sys, generator, forecast) + @test_throws TypeError set_variable_cost!(sys, generator, forecast_fd) for s in generator.services - forecast = IS.Deterministic(get_name(s), service_data, resolution) - @test_throws TypeError set_service_bid!(sys, generator, s, forecast) + forecast_fd = IS.Deterministic(get_name(s), service_data, resolution) + @test_throws TypeError set_service_bid!(sys, generator, s, forecast_fd) end end @@ -355,27 +369,30 @@ end resolution = Dates.Hour(1) name = "test" horizon = 24 - data_pwl = SortedDict(initial_time => test_costs[CostCurve{PiecewiseIncrementalCurve}]) + data_pwl = SortedDict(initial_time => test_costs[PiecewiseStepData]) service_data = data_pwl sys = PSB.build_system(PSITestSystems, "test_RTS_GMLC_sys") generator = get_component(ThermalStandard, sys, "322_CT_6") market_bid = MarketBidCost(nothing) set_operation_cost!(generator, market_bid) - forecast = IS.Deterministic( + forecast_fd = IS.Deterministic( "variable_cost", Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), resolution, ) - set_variable_cost!(sys, generator, forecast) + set_variable_cost!(sys, generator, forecast_fd) + for s in generator.services - forecast = IS.Deterministic( + forecast_fd = IS.Deterministic( get_name(s), Dict(k => get_function_data.(v) for (k, v) in pairs(service_data)), resolution, ) - set_service_bid!(sys, generator, s, forecast) + set_service_bid!(sys, generator, s, forecast_fd) end + iocs = get_incremental_offer_curves(generator, market_bid) + isequal(first(TimeSeries.values(iocs)), first(data_pwl[initial_time])) cost_forecast = get_variable_cost(generator, market_bid; start_time = initial_time) @test isequal(first(TimeSeries.values(cost_forecast)), first(data_pwl[initial_time])) @@ -388,6 +405,45 @@ end end end +@testset "Test MarketBidCost with PiecewiseLinearData Cost Timeseries, initial_input, and no_load_cost" begin + initial_time = Dates.DateTime("2020-01-01") + resolution = Dates.Hour(1) + name = "test" + horizon = 24 + data_pwl = SortedDict(initial_time => test_costs[PiecewiseIncrementalCurve]) + service_data = data_pwl + sys = PSB.build_system(PSITestSystems, "test_RTS_GMLC_sys") + generator = get_component(ThermalStandard, sys, "322_CT_6") + market_bid = MarketBidCost(nothing) + set_operation_cost!(generator, market_bid) + forecast_fd = IS.Deterministic( + "variable_cost_function_data", + Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), + resolution, + ) + set_variable_cost!(sys, generator, forecast_fd) + + forecast_ii = IS.Deterministic( + "variable_cost_initial_input", + Dict(k => get_initial_input.(get_value_curve.(v)) for (k, v) in pairs(data_pwl)), + resolution, + ) + PSY.set_incremental_initial_input!(sys, generator, forecast_ii) + + forecast_iaz = IS.Deterministic( + "variable_cost_input_at_zero", + Dict(k => get_input_at_zero.(get_value_curve.(v)) for (k, v) in pairs(data_pwl)), + resolution, + ) + set_no_load_cost!(sys, generator, forecast_iaz) + + iocs = get_incremental_offer_curves(generator, market_bid) + @show iocs + isequal(first(TimeSeries.values(iocs)), first(data_pwl[initial_time])) + cost_forecast = get_variable_cost(generator, market_bid; start_time = initial_time) + @test isequal(first(TimeSeries.values(cost_forecast)), first(data_pwl[initial_time])) +end + @testset "Test MarketBidCost with single `start_up::Number` value" begin expected = (hot = 1.0, warm = 0.0, cold = 0.0) # should only be used for the `hot` value. cost = MarketBidCost(; start_up = 1, no_load_cost = rand(), shut_down = rand()) @@ -400,17 +456,17 @@ end other_time = initial_time + resolution name = "test" horizon = 24 - data_pwl = SortedDict(initial_time => test_costs[CostCurve{PiecewiseIncrementalCurve}], - other_time => test_costs[CostCurve{PiecewiseIncrementalCurve}]) + data_pwl = SortedDict(initial_time => test_costs[PiecewiseStepData], + other_time => test_costs[PiecewiseStepData]) sys = System(100.0) reserve = ReserveDemandCurve{ReserveUp}(nothing) add_component!(sys, reserve) - forecast = IS.Deterministic( + forecast_fd = IS.Deterministic( "variable_cost", Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), resolution, ) - set_variable_cost!(sys, reserve, forecast) + set_variable_cost!(sys, reserve, forecast_fd) cost_forecast = get_variable_cost(reserve; start_time = initial_time) @test isequal(first(TimeSeries.values(cost_forecast)), first(data_pwl[initial_time])) end @@ -433,8 +489,8 @@ end resolution = Dates.Hour(1) horizon = 24 data_float = SortedDict(initial_time => test_costs[Float64]) - forecast = IS.Deterministic("fuel_cost", data_float, resolution) - set_fuel_cost!(sys, generator, forecast) + forecast_fd = IS.Deterministic("fuel_cost", data_float, resolution) + set_fuel_cost!(sys, generator, forecast_fd) fuel_forecast = get_fuel_cost(generator; start_time = initial_time) @test first(TimeSeries.values(fuel_forecast)) == first(data_float[initial_time]) fuel_forecast = get_fuel_cost(generator) # missing start_time filled in with initial time @@ -448,7 +504,7 @@ end set_operation_cost!(generator, market_bid) op_cost = get_operation_cost(generator) - @test get_no_load_cost(generator, op_cost) == 0.0 + @test get_no_load_cost(generator, op_cost) === nothing set_no_load_cost!(sys, generator, 1.23) @test get_no_load_cost(generator, op_cost) == 1.23 @@ -457,9 +513,9 @@ end resolution = Dates.Hour(1) horizon = 24 data_float = SortedDict(initial_time => test_costs[Float64]) - forecast = IS.Deterministic("no_load_cost", data_float, resolution) + forecast_fd = IS.Deterministic("no_load_cost", data_float, resolution) - set_no_load_cost!(sys, generator, forecast) + set_no_load_cost!(sys, generator, forecast_fd) @test first(TimeSeries.values(get_no_load_cost(generator, op_cost))) == first(data_float[initial_time]) end @@ -482,13 +538,13 @@ end resolution = Dates.Hour(1) horizon = 24 data_sus = SortedDict(initial_time => test_costs[PSY.StartUpStages]) - forecast = IS.Deterministic( + forecast_fd = IS.Deterministic( "start_up", Dict(k => Tuple.(v) for (k, v) in pairs(data_sus)), resolution, ) - set_start_up!(sys, generator, forecast) + set_start_up!(sys, generator, forecast_fd) @test first(TimeSeries.values(get_start_up(generator, op_cost))) == first(data_sus[initial_time]) end From 1dcabd535d62dac2ab1fb5e94e6adf5dbcfd5b2d Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Wed, 25 Sep 2024 22:52:36 -0700 Subject: [PATCH 03/17] [WIP] Update MarketBidCost interface --- src/PowerSystems.jl | 1 + src/models/cost_function_timeseries.jl | 231 ++++++++++++++++++++- src/models/cost_functions/MarketBidCost.jl | 94 +++++---- 3 files changed, 279 insertions(+), 47 deletions(-) diff --git a/src/PowerSystems.jl b/src/PowerSystems.jl index b311860c42..dfe79ca746 100644 --- a/src/PowerSystems.jl +++ b/src/PowerSystems.jl @@ -388,6 +388,7 @@ export get_decremental_offer_curves, set_decremental_offer_curves! export get_ancillary_service_offers, set_ancillary_service_offers! export get_services_bid export set_variable_cost! +export set_incremental_variable_cost!, set_decremental_variable_cost! export set_service_bid! export iterate_windows export get_window diff --git a/src/models/cost_function_timeseries.jl b/src/models/cost_function_timeseries.jl index 287e1dbf5f..3e803db417 100644 --- a/src/models/cost_function_timeseries.jl +++ b/src/models/cost_function_timeseries.jl @@ -128,6 +128,9 @@ function get_variable_cost( start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, ) + if typeof(get_incremental_offer_curves(cost)) <: CostCurve + return get_incremental_offer_curves(cost) + end function_data = if (get_incremental_offer_curves(cost) isa TimeSeriesKey) get_incremental_offer_curves(device, cost; start_time = start_time, len = len) else @@ -159,15 +162,102 @@ function get_variable_cost( "Time series mismatch between incremental_offer_curves, incremental_initial_input, and no_load_cost", ), ) - @show collect(zip(collect.(TimeSeries.values.(params))...)) |> length - @show first(collect(zip(collect.(TimeSeries.values.(params))...))) + #@show collect(zip(collect.(TimeSeries.values.(params))...)) |> length + #@show first(collect(zip(collect.(TimeSeries.values.(params))...))) return TimeSeries.TimeArray(TimeSeries.timestamp(function_data), [ - make_market_bid_curve(fd, ii, iaz) for + _make_market_bid_curve(fd, ii; input_at_zero = iaz) for (fd, ii, iaz) in collect(zip(collect.(TimeSeries.values.(params))...)) ]) end - return make_market_bid_curve(input_at_zero, initial_input, function_data) + return make_market_bid_curve( + function_data, + initial_input; + input_at_zero = input_at_zero, + ) +end + +""" +Retrieve the variable cost bid for a `StaticInjection` device with a `MarketBidCost`. If any +of the relevant fields (`incremental_offer_curves`, `initial_input`, `no_load_cost`) are +time series, the user may specify `start_time` and `len` and the function returns a +`TimeArray` of `CostCurve`s; if the field is not a time series, the function returns a +single `CostCurve`. +""" +function get_incremental_variable_cost( + device::StaticInjection, + cost::MarketBidCost; + start_time::Union{Nothing, Dates.DateTime} = nothing, + len::Union{Nothing, Int} = nothing, +) + return get_variable_cost( + device, + cost; + start_time = start_time, + len = len, + ) +end + +""" +Retrieve the variable cost bid for a `StaticInjection` device with a `MarketBidCost`. If any +of the relevant fields (`decremental_offer_curves`, `initial_input`, `no_load_cost`) are +time series, the user may specify `start_time` and `len` and the function returns a +`TimeArray` of `CostCurve`s; if the field is not a time series, the function returns a +single `CostCurve`. +""" +function get_decremental_variable_cost( + device::StaticInjection, + cost::MarketBidCost; + start_time::Union{Nothing, Dates.DateTime} = nothing, + len::Union{Nothing, Int} = nothing, +) + if typeof(get_decremental_offer_curves(cost)) <: CostCurve + return get_decremental_offer_curves(cost) + end + function_data = if (get_decremental_offer_curves(cost) isa TimeSeriesKey) + get_decremental_offer_curves(device, cost; start_time = start_time, len = len) + else + get_decremental_offer_curves(device, cost) + end + initial_input = if (get_decremental_initial_input(cost) isa TimeSeriesKey) + get_decremental_initial_input(device, cost; start_time = start_time, len = len) + else + get_decremental_initial_input(device, cost) + end + input_at_zero = if (get_no_load_cost(cost) isa TimeSeriesKey) + get_no_load_cost(device, cost; start_time = start_time, len = len) + else + get_no_load_cost(device, cost) + end + params::Vector{Any} = [function_data, initial_input, input_at_zero] + first_time_series = findfirst(isa.(params, TimeSeries.TimeArray)) + if !isnothing(first_time_series) + timestamps = TimeSeries.timestamp(params[first_time_series]) + for (i, param) in enumerate(params) + if !(param isa TimeSeries.TimeArray) + params[i] = + TimeSeries.TimeArray(timestamps, fill(param, length(timestamps))) + end + end + !allequal(TimeSeries.timestamp.(params)) && + throw( + ArgumentError( + "Time series mismatch between incremental_offer_curves, incremental_initial_input, and no_load_cost", + ), + ) + #@show collect(zip(collect.(TimeSeries.values.(params))...)) |> length + #@show first(collect(zip(collect.(TimeSeries.values.(params))...))) + return TimeSeries.TimeArray(TimeSeries.timestamp(function_data), + [ + _make_market_bid_curve(fd, ii; input_at_zero = iaz) for + (fd, ii, iaz) in collect(zip(collect.(TimeSeries.values.(params))...)) + ]) + end + return make_market_bid_curve( + function_data, + initial_input; + input_at_zero = input_at_zero, + ) end """ @@ -236,7 +326,19 @@ get_incremental_offer_curves( ) = _process_get_cost(Union{PiecewiseStepData, CostCurve{PiecewiseIncrementalCurve}}, device, get_incremental_offer_curves(cost), nothing, start_time, len) -# TODO decremental +""" +Retrieve the `decremental_offer_curves` for a `StaticInjection` device with a +`MarketBidCost`. If this field is a time series, the user may specify `start_time` and `len` +and the function returns a `TimeArray` of `Float64`s; if the field is not a time series, the +function returns a single `Float64` or `Nothing`. +""" +get_decremental_offer_curves( + device::StaticInjection, + cost::MarketBidCost; + start_time::Union{Nothing, Dates.DateTime} = nothing, + len::Union{Nothing, Int} = nothing, +) = _process_get_cost(Union{PiecewiseStepData, CostCurve{PiecewiseIncrementalCurve}}, + device, get_decremental_offer_curves(cost), nothing, start_time, len) """ Retrieve the no-load cost data for a `StaticInjection` device with a `MarketBidCost`. If @@ -263,7 +365,16 @@ get_incremental_initial_input( ) = _process_get_cost(Union{Nothing, Float64}, device, get_incremental_initial_input(cost), nothing, start_time, len) -# TODO decremental +""" +Retrieve the `decremental_initial_input` for a `StaticInjection` device with a `MarketBidCost`. +""" +get_decremental_initial_input( + device::StaticInjection, + cost::MarketBidCost; + start_time::Union{Nothing, Dates.DateTime} = nothing, + len::Union{Nothing, Int} = nothing, +) = _process_get_cost(Union{Nothing, Float64}, device, + get_decremental_initial_input(cost), nothing, start_time, len) """ Retrieve the startup cost data for a `StaticInjection` device with a `MarketBidCost`. If @@ -309,7 +420,7 @@ end # SETTER IMPLEMENTATIONS """ -Set the variable cost bid for a `StaticInjection` device with a `MarketBidCost`. +Set the incremental variable cost bid for a `StaticInjection` device with a `MarketBidCost`. # Arguments - `sys::System`: PowerSystem System @@ -317,14 +428,27 @@ Set the variable cost bid for a `StaticInjection` device with a `MarketBidCost`. - `time_series_data::Union{Nothing, IS.TimeSeriesData, CostCurve{PiecewiseIncrementalCurve}},`: the data. If a time series, must be of eltype `PiecewiseStepData`. +- `power_units::UnitSystem`: Units to be used for data. Must be NATURAL_UNITS for """ function set_variable_cost!( sys::System, component::StaticInjection, data::Union{Nothing, IS.TimeSeriesData, CostCurve{PiecewiseIncrementalCurve}}, + power_units::UnitSystem, ) market_bid_cost = get_operation_cost(component) _validate_market_bid_cost(market_bid_cost, "get_operation_cost(component)") + if (typeof(data) <: CostCurve{PiecewiseIncrementalCurve}) && + (data.power_units != power_units) + throw( + ArgumentError( + "Units specified in CostCurve data differs from the units specified in the set cost.", + ), + ) + end + if (typeof(data) <: IS.TimeSeriesData) && (power_units != UnitSystem.NATURAL_UNITS) + throw(ArgumentError("Time Series data for MarketBidCost must be in NATURAL_UNITS.")) + end to_set = _process_set_cost( CostCurve{PiecewiseIncrementalCurve}, PiecewiseStepData, @@ -332,7 +456,73 @@ function set_variable_cost!( component, data, ) + set_incremental_offer_curves!(market_bid_cost, to_set) + return +end + +""" +Set the incremental variable cost bid for a `StaticInjection` device with a `MarketBidCost`. + +# Arguments +- `sys::System`: PowerSystem System +- `component::StaticInjection`: Static injection device +- `time_series_data::Union{Nothing, IS.TimeSeriesData, + CostCurve{PiecewiseIncrementalCurve}},`: the data. If a time series, must be of eltype + `PiecewiseStepData`. +- `power_units::UnitSystem`: Units to be used for data. +""" +function set_incremental_variable_cost!( + sys::System, + component::StaticInjection, + data::Union{Nothing, IS.TimeSeriesData, CostCurve{PiecewiseIncrementalCurve}}, + power_units::UnitSystem, +) + set_variable_cost!(sys, component, data, power_units) + return +end + +""" +Set the decremental variable cost bid for a `StaticInjection` device with a `MarketBidCost`. + +# Arguments +- `sys::System`: PowerSystem System +- `component::StaticInjection`: Static injection device +- `time_series_data::Union{Nothing, IS.TimeSeriesData, + CostCurve{PiecewiseIncrementalCurve}},`: the data. If a time series, must be of eltype + `PiecewiseStepData`. +- `power_units::UnitSystem`: Units to be used for data. +""" +function set_decremental_variable_cost!( + sys::System, + component::StaticInjection, + data::Union{Nothing, IS.TimeSeriesData, CostCurve{PiecewiseIncrementalCurve}}, + power_units::UnitSystem, +) + market_bid_cost = get_operation_cost(component) + _validate_market_bid_cost(market_bid_cost, "get_operation_cost(component)") + + if (typeof(data) <: CostCurve{PiecewiseIncrementalCurve}) && + (data.power_units != power_units) + throw( + ArgumentError( + "Units specified in CostCurve data differs from the units specified in the set cost.", + ), + ) + end + if (typeof(data) <: IS.TimeSeriesData) && (power_units != UnitSystem.NATURAL_UNITS) + throw(ArgumentError("Time Series data for MarketBidCost must be in NATURAL_UNITS.")) + end + to_set = _process_set_cost( + CostCurve{PiecewiseIncrementalCurve}, + PiecewiseStepData, + sys, + component, + data, + ) + + set_decremental_offer_curves!(market_bid_cost, to_set) + return end """ @@ -423,7 +613,24 @@ function set_incremental_initial_input!( set_incremental_initial_input!(market_bid_cost, to_set) end -# TODO decremental +""" +Set the `decremental_initial_input` for a `StaticInjection` device with a `MarketBidCost` to either a scalar or a time series. + +# Arguments +- `sys::System`: PowerSystem System +- `component::StaticInjection`: Static injection device +- `time_series_data::Union{Float64, IS.TimeSeriesData},`: the data. If a time series, must be of eltype `Float64`. +""" +function set_decremental_initial_input!( + sys::System, + component::StaticInjection, + data::Union{Float64, IS.TimeSeriesData}, +) + market_bid_cost = get_operation_cost(component) + _validate_market_bid_cost(market_bid_cost, "get_operation_cost(component)") + to_set = _process_set_cost(Union{Float64, Nothing}, Float64, sys, component, data) + set_decremental_initial_input!(market_bid_cost, to_set) +end """ Set the startup cost for a `StaticInjection` device with a `MarketBidCost` to either a single `StartUpStages` or a time series. @@ -458,6 +665,7 @@ function set_service_bid!( component::StaticInjection, service::Service, time_series_data::IS.TimeSeriesData, + power_units::UnitSystem, ) data_type = IS.eltype_data(time_series_data) !(data_type <: PiecewiseStepData) && @@ -471,6 +679,13 @@ function set_service_bid!( "Name provided in the TimeSeries Data $(get_name(time_series_data)), doesn't match the Service $(get_name(service)).", ) end + if power_units != UnitSystem.NATURAL_UNITS + throw( + ArgumentError( + "Power Unit specified for service market bids must be NATURAL_UNITS", + ), + ) + end verify_device_eligibility(sys, component, service) add_time_series!(sys, component, time_series_data) ancillary_service_offers = get_ancillary_service_offers(get_operation_cost(component)) diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index decda78778..eb60b415df 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -35,7 +35,8 @@ Compatible with most US Market bidding mechanisms that support demand and genera } = nothing "If using a time series for incremental_offer_curves, this is a time series of `Float64` representing the `initial_input`" incremental_initial_input::Union{Nothing, TimeSeriesKey} = nothing - # TODO decremental + "If using a time series for decremental_offer_curves, this is a time series of `Float64` representing the `initial_input`" + decremental_initial_input::Union{Nothing, TimeSeriesKey} = nothing "Bids for the ancillary services" ancillary_service_offers::Vector{Service} = Vector{Service}() end @@ -94,6 +95,7 @@ function MarketBidCost( incremental_offer_curves = nothing, decremental_offer_curves = nothing, incremental_initial_input = nothing, + decremental_initial_input = nothing, ancillary_service_offers = Vector{Service}(), ) # Intended for use with generators that are not multi-start (e.g. ThermalStandard). @@ -106,6 +108,7 @@ function MarketBidCost( incremental_offer_curves = incremental_offer_curves, decremental_offer_curves = decremental_offer_curves, incremental_initial_input = incremental_initial_input, + dencremental_initial_input = decremental_initial_input, ancillary_service_offers = ancillary_service_offers, ) end @@ -122,7 +125,8 @@ get_incremental_offer_curves(value::MarketBidCost) = value.incremental_offer_cur get_decremental_offer_curves(value::MarketBidCost) = value.incremental_offer_curves """Get [`MarketBidCost`](@ref) `incremental_initial_input`.""" get_incremental_initial_input(value::MarketBidCost) = value.incremental_initial_input -# TODO decremental +"""Get [`MarketBidCost`](@ref) `decremental_initial_input`.""" +get_decremental_initial_input(value::MarketBidCost) = value.decremental_initial_input """Get [`MarketBidCost`](@ref) `ancillary_service_offers`.""" get_ancillary_service_offers(value::MarketBidCost) = value.ancillary_service_offers @@ -138,14 +142,22 @@ set_incremental_offer_curves!(value::MarketBidCost, val) = """Set [`MarketBidCost`](@ref) `incremental_initial_input`.""" set_incremental_initial_input!(value::MarketBidCost, val) = value.incremental_initial_input = val -# TODO decremental """Set [`MarketBidCost`](@ref) `incremental_offer_curves`.""" set_decremental_offer_curves!(value::MarketBidCost, val) = value.decremental_offer_curves = val +"""Set [`MarketBidCost`](@ref) `decremental_initial_input`.""" +set_decremental_initial_input!(value::MarketBidCost, val) = + value.decremental_initial_input = val """Set [`MarketBidCost`](@ref) `ancillary_service_offers`.""" set_ancillary_service_offers!(value::MarketBidCost, val) = value.ancillary_service_offers = val +"""Auxiliary Method for setting up start up that are not multi-start""" +function set_start_up!(value::MarketBidCost, val::Real) + start_up_multi = (hot = Float64(val), warm = 0.0, cold = 0.0) + set_start_up!(value, start_up_multi) +end + # Each market bid curve (the elements that make up the incremental and decremental offer # curves in MarketBidCost) is a CostCurve{PiecewiseIncrementalCurve} with NaN initial input # and first x-coordinate @@ -155,51 +167,55 @@ end """ Make a CostCurve{PiecewiseIncrementalCurve} suitable for inclusion in a MarketBidCost from a -vector of power values, a vector of marginal costs, and an optional units system. The -minimum power, and cost at minimum power, are not represented. +vector of power values, a vector of marginal costs, a float of initial input, and an optional units system and input at zero. + +# Examples +```julia +mbc = make_market_bid_curve([0.0, 100.0, 105.0, 120.0, 130.0], [25.0, 26.0, 28.0, 30.0], 10.0) +mbc2 = make_market_bid_curve([0.0, 100.0, 105.0, 120.0, 130.0], [25.0, 26.0, 28.0, 30.0], 10.0; input_at_zero = 10.0) +mbc3 = make_market_bid_curve([0.0, 100.0, 105.0, 120.0, 130.0], [25.0, 26.0, 28.0, 30.0], 10.0; power_inputs = UnitSystem.NATURAL_UNITS) +``` """ function make_market_bid_curve(powers::Vector{Float64}, - marginal_costs::Vector{Float64}; - power_units::UnitSystem = UnitSystem.NATURAL_UNITS) - (length(powers) != length(marginal_costs)) && - throw(ArgumentError("Must specify an equal number of powers and marginal_costs")) - fd = PiecewiseStepData(vcat(NaN, powers), marginal_costs) - return make_market_bid_curve(fd; power_units = power_units) + marginal_costs::Vector{Float64}, + initial_input::Float64; + power_units::UnitSystem = UnitSystem.NATURAL_UNITS, + input_at_zero::Union{Nothing, Float64} = nothing) + if length(powers) == length(marginal_costs) + 1 + fd = PiecewiseStepData(powers, marginal_costs) + return make_market_bid_curve( + fd, + initial_input; + power_units = power_units, + input_at_zero, + ) + else + throw( + ArgumentError( + "Must specify exactly one more number of powers ($(length(powers))) than marginal_costs ($(length(marginal_costs)))", + ), + ) + end end -make_market_bid_curve(initial_input::Union{Nothing, Real}, x_coords::Vector, slopes::Vector; - power_units::UnitSystem = UnitSystem.NATURAL_UNITS) = - CostCurve(PiecewiseIncrementalCurve(initial_input, x_coords, slopes), power_units) - -make_market_bid_curve(input_at_zero::Union{Nothing, Real}, - initial_input::Union{Nothing, Real}, x_coords::Vector, slopes::Vector; - power_units::UnitSystem = UnitSystem.NATURAL_UNITS) = - CostCurve( - PiecewiseIncrementalCurve(input_at_zero, initial_input, x_coords, slopes), - power_units, - ) - """ Make a CostCurve{PiecewiseIncrementalCurve} suitable for inclusion in a MarketBidCost from the FunctionData that might be used to store such a cost curve in a time series. """ -function make_market_bid_curve(data::PiecewiseStepData; - power_units::UnitSystem = UnitSystem.NATURAL_UNITS) - !isnan(first(get_x_coords(data))) && throw( - ArgumentError( - "The first x-coordinate in the PiecewiseStepData representation must be NaN", - ), - ) - cc = CostCurve(IncrementalCurve(data, nothing), power_units) +function make_market_bid_curve(data::PiecewiseStepData, + initial_input::Float64; + power_units::UnitSystem = UnitSystem.NATURAL_UNITS, + input_at_zero::Union{Nothing, Float64} = nothing) + cc = CostCurve(IncrementalCurve(data, initial_input, input_at_zero), power_units) @assert is_market_bid_curve(cc) return cc end -make_market_bid_curve(fd::PiecewiseStepData, initial_input::Union{Nothing, Real}; - power_units::UnitSystem = UnitSystem.NATURAL_UNITS) = - CostCurve(IncrementalCurve(fd, initial_input), power_units) - -make_market_bid_curve(fd::PiecewiseStepData, initial_input::Union{Nothing, Real}, - input_at_zero::Union{Nothing, Real}; - power_units::UnitSystem = UnitSystem.NATURAL_UNITS) = - CostCurve(IncrementalCurve(fd, initial_input, input_at_zero), power_units) +function _make_market_bid_curve(data::PiecewiseStepData, + initial_input::Union{Nothing, Float64}; + power_units::UnitSystem = UnitSystem.NATURAL_UNITS, + input_at_zero::Union{Nothing, Float64} = nothing) + cc = CostCurve(IncrementalCurve(data, initial_input, input_at_zero), power_units) + @assert is_market_bid_curve(cc) + return cc +end From d67bdcc3921792d9f58ed2507e1b80148e0f81fa Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Wed, 25 Sep 2024 22:52:45 -0700 Subject: [PATCH 04/17] [WIP] Update test cost functions --- test/test_cost_functions.jl | 97 ++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 18 deletions(-) diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index b9c81a5b5d..9ccc242607 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -12,33 +12,80 @@ "FuelCurve with power_units UnitSystem.NATURAL_UNITS = 2, fuel_cost 4.0, vom_cost LinearCurve(0.0, 0.0), and value_curve:\n QuadraticCurve (a type of InputOutputCurve) where function is: f(x) = 1.0 x^2 + 2.0 x + 3.0" end -@testset "Test market bid cost interface" begin - mbc = make_market_bid_curve([100.0, 105.0, 120.0, 130.0], [25.0, 26.0, 28.0, 30.0]) +@testset "Test MarketBidCost direct struct creation" begin + sys = PSB.build_system(PSITestSystems, "test_RTS_GMLC_sys") + generator = get_component(ThermalStandard, sys, "322_CT_6") + #Update generator cost to MarketBidCost using Natural Units + powers = [22.0, 33.0, 44.0, 55.0] # MW + marginal_costs = [25.0, 26.0, 28.0] # $/MWh + initial_input = 50.0 # $/h + mbc = MarketBidCost(; + start_up = 0.0, + shut_down = 0.0, + incremental_offer_curves = CostCurve( + PiecewiseIncrementalCurve( + initial_input, + powers, + marginal_costs, + ), + ), + ) + set_operation_cost!(generator, mbc) + @test get_operation_cost(generator) isa MarketBidCost +end + +@testset "Test Make market bid curve interface" begin + mbc = make_market_bid_curve( + [0.0, 100.0, 105.0, 120.0, 130.0], + [25.0, 26.0, 28.0, 30.0], + 10.0, + ) @test is_market_bid_curve(mbc) @test is_market_bid_curve(make_market_bid_curve(get_function_data(mbc))) @test_throws ArgumentError make_market_bid_curve( - [100.0, 105.0, 120.0, 130.0], [26.0, 28.0, 30.0]) + [100.0, 105.0, 120.0, 130.0], [26.0, 28.0, 30.0, 40.0]) - mbc2 = make_market_bid_curve(20.0, [1.0, 2.0, 3.0], [4.0, 6.0]) + mbc2 = make_market_bid_curve([1.0, 2.0, 3.0], [4.0, 6.0], 10.0; input_at_zero = 2.0) @test is_market_bid_curve(mbc2) @test is_market_bid_curve( make_market_bid_curve(get_function_data(mbc2), get_initial_input(mbc2)), ) - mbc3 = make_market_bid_curve(18.0, 20.0, [1.0, 2.0, 3.0], [4.0, 6.0]) - @test is_market_bid_curve(mbc3) - @test is_market_bid_curve( - make_market_bid_curve(get_function_data(mbc2), get_initial_input(mbc3)), - ) + sys = PSB.build_system(PSITestSystems, "test_RTS_GMLC_sys") + generator = get_component(ThermalStandard, sys, "322_CT_6") + market_bid = MarketBidCost(nothing) + mbc3 = make_market_bid_curve([22.0, 33.0, 44.0, 55.0], [25.0, 26.0, 28.0], 50.0) + set_incremental_offer_curves!(market_bid, mbc3) + set_start_up!(market_bid, 0.0) + set_operation_cost!(generator, market_bid) + @test get_operation_cost(generator) isa MarketBidCost end test_costs = Dict( CostCurve{QuadraticCurve} => repeat([CostCurve(QuadraticCurve(999.0, 2.0, 1.0))], 24), PiecewiseStepData => - repeat([make_market_bid_curve([2.0, 3.0], [4.0, 6.0])], 24), + repeat( + [ + PSY._make_market_bid_curve( + PiecewiseStepData([0.0, 2.0, 3.0], [4.0, 6.0]), + nothing, + ), + ], + 24, + ), PiecewiseIncrementalCurve => - repeat([make_market_bid_curve(18.0, 20.0, [1.0, 2.0, 3.0], [4.0, 6.0])], 24), + repeat( + [ + make_market_bid_curve( + [1.0, 2.0, 3.0], + [4.0, 6.0], + 18.0; + input_at_zero = 20.0, + ), + ], + 24, + ), Float64 => collect(11.0:34.0), PSY.StartUpStages => @@ -64,10 +111,11 @@ test_costs = Dict( Dict(k => get_function_data.(v) for (k, v) in pairs(data_quadratic)), resolution, ) - @test_throws TypeError set_variable_cost!(sys, generator, forecast_fd) + power_units = UnitSystem.NATURAL_UNITS + @test_throws TypeError set_variable_cost!(sys, generator, forecast_fd, power_units) for s in generator.services forecast_fd = IS.Deterministic(get_name(s), service_data, resolution) - @test_throws TypeError set_service_bid!(sys, generator, s, forecast_fd) + @test_throws TypeError set_service_bid!(sys, generator, s, forecast_fd, power_units) end end @@ -76,26 +124,40 @@ end resolution = Dates.Hour(1) name = "test" horizon = 24 + power_units = UnitSystem.NATURAL_UNITS data_pwl = SortedDict(initial_time => test_costs[PiecewiseStepData]) service_data = data_pwl sys = PSB.build_system(PSITestSystems, "test_RTS_GMLC_sys") generator = get_component(ThermalStandard, sys, "322_CT_6") market_bid = MarketBidCost(nothing) set_operation_cost!(generator, market_bid) - forecast_fd = IS.Deterministic( + forecast_fd = Deterministic( "variable_cost", Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), resolution, ) - set_variable_cost!(sys, generator, forecast_fd) + @test_throws ArgumentError set_variable_cost!( + sys, + generator, + forecast_fd, + UnitSystem.SYSTEM_BASE, + ) + set_variable_cost!(sys, generator, forecast_fd, power_units) for s in generator.services - forecast_fd = IS.Deterministic( + forecast_fd = Deterministic( get_name(s), Dict(k => get_function_data.(v) for (k, v) in pairs(service_data)), resolution, ) - set_service_bid!(sys, generator, s, forecast_fd) + @test_throws ArgumentError set_service_bid!( + sys, + generator, + s, + forecast_fd, + UnitSystem.SYSTEM_BASE, + ) + set_service_bid!(sys, generator, s, forecast_fd, power_units) end iocs = get_incremental_offer_curves(generator, market_bid) @@ -145,7 +207,6 @@ end set_no_load_cost!(sys, generator, forecast_iaz) iocs = get_incremental_offer_curves(generator, market_bid) - @show iocs isequal(first(TimeSeries.values(iocs)), first(data_pwl[initial_time])) cost_forecast = get_variable_cost(generator, market_bid; start_time = initial_time) @test isequal(first(TimeSeries.values(cost_forecast)), first(data_pwl[initial_time])) From 1b73b6501998cd0cb0247c8e1c5dd64aa3b79e53 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Thu, 26 Sep 2024 18:54:04 -0700 Subject: [PATCH 05/17] update auxiliary functions --- src/models/cost_functions/MarketBidCost.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index eb60b415df..d7a25c6acb 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -91,7 +91,7 @@ Accepts a single `start_up` value to use as the `hot` value, with `warm` and `co function MarketBidCost( no_load_cost, start_up::Real, - shut_down, + shut_down; incremental_offer_curves = nothing, decremental_offer_curves = nothing, incremental_initial_input = nothing, @@ -108,7 +108,7 @@ function MarketBidCost( incremental_offer_curves = incremental_offer_curves, decremental_offer_curves = decremental_offer_curves, incremental_initial_input = incremental_initial_input, - dencremental_initial_input = decremental_initial_input, + decremental_initial_input = decremental_initial_input, ancillary_service_offers = ancillary_service_offers, ) end @@ -211,8 +211,11 @@ function make_market_bid_curve(data::PiecewiseStepData, return cc end -function _make_market_bid_curve(data::PiecewiseStepData, - initial_input::Union{Nothing, Float64}; +""" +Auxiliary make market bid curve for timeseries with nothing inputs. +""" +function _make_market_bid_curve(data::PiecewiseStepData; + initial_input::Union{Nothing, Float64} = nothing, power_units::UnitSystem = UnitSystem.NATURAL_UNITS, input_at_zero::Union{Nothing, Float64} = nothing) cc = CostCurve(IncrementalCurve(data, initial_input, input_at_zero), power_units) From 4e569356fd0a9671a022d49beffc74bd0ab5ab95 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Thu, 26 Sep 2024 18:54:17 -0700 Subject: [PATCH 06/17] use auxiliary method for getters --- src/models/cost_function_timeseries.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/cost_function_timeseries.jl b/src/models/cost_function_timeseries.jl index 3e803db417..8f1af801f3 100644 --- a/src/models/cost_function_timeseries.jl +++ b/src/models/cost_function_timeseries.jl @@ -166,7 +166,7 @@ function get_variable_cost( #@show first(collect(zip(collect.(TimeSeries.values.(params))...))) return TimeSeries.TimeArray(TimeSeries.timestamp(function_data), [ - _make_market_bid_curve(fd, ii; input_at_zero = iaz) for + _make_market_bid_curve(fd; initial_input = ii, input_at_zero = iaz) for (fd, ii, iaz) in collect(zip(collect.(TimeSeries.values.(params))...)) ]) end @@ -249,7 +249,7 @@ function get_decremental_variable_cost( #@show first(collect(zip(collect.(TimeSeries.values.(params))...))) return TimeSeries.TimeArray(TimeSeries.timestamp(function_data), [ - _make_market_bid_curve(fd, ii; input_at_zero = iaz) for + _make_market_bid_curve(fd; initial_input = ii, input_at_zero = iaz) for (fd, ii, iaz) in collect(zip(collect.(TimeSeries.values.(params))...)) ]) end @@ -269,7 +269,7 @@ get_variable_cost( start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, ) = _process_get_cost(CostCurve{PiecewiseIncrementalCurve}, service, get_variable(service), - make_market_bid_curve, start_time, len) + _make_market_bid_curve, start_time, len) """ Return service bid time series data for a `StaticInjection` device with a `MarketBidCost`. @@ -292,7 +292,7 @@ function get_services_bid( len = len, count = 1, ) - converted = read_and_convert_ts(ts, service, start_time, len, make_market_bid_curve) + converted = read_and_convert_ts(ts, service, start_time, len, _make_market_bid_curve) return converted end From 729f7cb5d27bc2562392e1b737ae406b45b9d378 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Thu, 26 Sep 2024 18:54:21 -0700 Subject: [PATCH 07/17] update tests --- test/test_cost_functions.jl | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index 9ccc242607..ff04b3752d 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -20,7 +20,7 @@ end marginal_costs = [25.0, 26.0, 28.0] # $/MWh initial_input = 50.0 # $/h mbc = MarketBidCost(; - start_up = 0.0, + start_up = (hot = 0.0, warm = 0.0, cold = 0.0), shut_down = 0.0, incremental_offer_curves = CostCurve( PiecewiseIncrementalCurve( @@ -41,9 +41,11 @@ end 10.0, ) @test is_market_bid_curve(mbc) - @test is_market_bid_curve(make_market_bid_curve(get_function_data(mbc))) + @test is_market_bid_curve( + make_market_bid_curve(get_function_data(mbc), get_initial_input(mbc)), + ) @test_throws ArgumentError make_market_bid_curve( - [100.0, 105.0, 120.0, 130.0], [26.0, 28.0, 30.0, 40.0]) + [100.0, 105.0, 120.0, 130.0], [26.0, 28.0, 30.0, 40.0], 10.0) mbc2 = make_market_bid_curve([1.0, 2.0, 3.0], [4.0, 6.0], 10.0; input_at_zero = 2.0) @test is_market_bid_curve(mbc2) @@ -69,7 +71,6 @@ test_costs = Dict( [ PSY._make_market_bid_curve( PiecewiseStepData([0.0, 2.0, 3.0], [4.0, 6.0]), - nothing, ), ], 24, @@ -179,6 +180,7 @@ end resolution = Dates.Hour(1) name = "test" horizon = 24 + power_units = UnitSystem.NATURAL_UNITS data_pwl = SortedDict(initial_time => test_costs[PiecewiseIncrementalCurve]) service_data = data_pwl sys = PSB.build_system(PSITestSystems, "test_RTS_GMLC_sys") @@ -190,7 +192,7 @@ end Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), resolution, ) - set_variable_cost!(sys, generator, forecast_fd) + set_variable_cost!(sys, generator, forecast_fd, power_units) forecast_ii = IS.Deterministic( "variable_cost_initial_input", @@ -214,7 +216,10 @@ end @testset "Test MarketBidCost with single `start_up::Number` value" begin expected = (hot = 1.0, warm = 0.0, cold = 0.0) # should only be used for the `hot` value. - cost = MarketBidCost(; start_up = 1, no_load_cost = rand(), shut_down = rand()) + no_load_cost = rand() + start_up = 1.0 + shut_down = rand() + cost = MarketBidCost(no_load_cost, start_up, shut_down) @test get_start_up(cost) == expected end From 7dbb0f87217c4f4ecaa528b49b87ef04eef73ca7 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Thu, 26 Sep 2024 18:54:27 -0700 Subject: [PATCH 08/17] update exports --- src/PowerSystems.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PowerSystems.jl b/src/PowerSystems.jl index dfe79ca746..f330eced7c 100644 --- a/src/PowerSystems.jl +++ b/src/PowerSystems.jl @@ -385,6 +385,8 @@ export get_start_up export get_shut_down export get_incremental_offer_curves, set_incremental_offer_curves! export get_decremental_offer_curves, set_decremental_offer_curves! +export get_incremental_initial_input, set_incremental_initial_input! +export get_decremental_initial_input, set_decremental_initial_input! export get_ancillary_service_offers, set_ancillary_service_offers! export get_services_bid export set_variable_cost! From 41232cfddea53c2c5091c601762f11af32624b9d Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Fri, 27 Sep 2024 14:04:13 -0700 Subject: [PATCH 09/17] fix test serialization typo --- test/test_serialization.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_serialization.jl b/test/test_serialization.jl index 50fdce8724..e30624695c 100644 --- a/test/test_serialization.jl +++ b/test/test_serialization.jl @@ -106,7 +106,8 @@ end add_component!(sys, gen) ta = TimeSeries.TimeArray(dates, data) time_series = IS.SingleTimeSeries(; name = "variable_cost", data = ta) - set_variable_cost!(sys, gen, time_series) + power_units = UnitSystem.NATURAL_UNITS + set_variable_cost!(sys, gen, time_series, power_units) service = ConstantReserve{ReserveDown}(; name = "init_$i", available = false, @@ -125,6 +126,7 @@ end gen, service, IS.SingleTimeSeries(; name = "init_$i", data = ta), + power_units, ) end _, result = validate_serialization(sys) From 42ac087cdc7963b0e027d514eebde723586787b8 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Fri, 27 Sep 2024 14:56:07 -0700 Subject: [PATCH 10/17] decremental tests --- test/test_cost_functions.jl | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index ff04b3752d..2698571c0d 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -214,6 +214,46 @@ end @test isequal(first(TimeSeries.values(cost_forecast)), first(data_pwl[initial_time])) end +@testset "Test MarketBidCost with Decremental PiecewiseLinearData Cost Timeseries, initial_input, and no_load_cost" begin + initial_time = Dates.DateTime("2020-01-01") + resolution = Dates.Hour(1) + name = "test" + horizon = 24 + power_units = UnitSystem.NATURAL_UNITS + data_pwl = SortedDict(initial_time => test_costs[PiecewiseIncrementalCurve]) + service_data = data_pwl + sys = PSB.build_system(PSITestSystems, "test_RTS_GMLC_sys") + generator = get_component(ThermalStandard, sys, "322_CT_6") + market_bid = MarketBidCost(nothing) + set_operation_cost!(generator, market_bid) + forecast_fd = IS.Deterministic( + "decremental_variable_cost_function_data", + Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), + resolution, + ) + set_decremental_variable_cost!(sys, generator, forecast_fd, power_units) + + forecast_ii = IS.Deterministic( + "decremental_variable_cost_initial_input", + Dict(k => get_initial_input.(get_value_curve.(v)) for (k, v) in pairs(data_pwl)), + resolution, + ) + PSY.set_decremental_initial_input!(sys, generator, forecast_ii) + + forecast_iaz = IS.Deterministic( + "variable_cost_input_at_zero", + Dict(k => get_input_at_zero.(get_value_curve.(v)) for (k, v) in pairs(data_pwl)), + resolution, + ) + set_no_load_cost!(sys, generator, forecast_iaz) + + iocs = get_decremental_offer_curves(generator, market_bid) + isequal(first(TimeSeries.values(iocs)), first(data_pwl[initial_time])) + cost_forecast = + get_decremental_variable_cost(generator, market_bid; start_time = initial_time) + @test isequal(first(TimeSeries.values(cost_forecast)), first(data_pwl[initial_time])) +end + @testset "Test MarketBidCost with single `start_up::Number` value" begin expected = (hot = 1.0, warm = 0.0, cold = 0.0) # should only be used for the `hot` value. no_load_cost = rand() From 790846a66b4dbeb107a4a99e227800b50c38b9fc Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Fri, 27 Sep 2024 14:56:18 -0700 Subject: [PATCH 11/17] update auxiliary methods --- src/models/cost_function_timeseries.jl | 15 +++++++++------ src/models/cost_functions/MarketBidCost.jl | 14 +++++++++++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/models/cost_function_timeseries.jl b/src/models/cost_function_timeseries.jl index 8f1af801f3..2a1c2709e4 100644 --- a/src/models/cost_function_timeseries.jl +++ b/src/models/cost_function_timeseries.jl @@ -426,8 +426,9 @@ Set the incremental variable cost bid for a `StaticInjection` device with a `Mar - `sys::System`: PowerSystem System - `component::StaticInjection`: Static injection device - `time_series_data::Union{Nothing, IS.TimeSeriesData, - CostCurve{PiecewiseIncrementalCurve}},`: the data. If a time series, must be of eltype - `PiecewiseStepData`. + CostCurve{PiecewiseIncrementalCurve}},`: the data. If using a time series, must be of eltype + `PiecewiseStepData`. `PiecewiseIncrementalCurve` is only accepted for single CostCurve and + not accepted for time series data. - `power_units::UnitSystem`: Units to be used for data. Must be NATURAL_UNITS for """ function set_variable_cost!( @@ -468,8 +469,9 @@ Set the incremental variable cost bid for a `StaticInjection` device with a `Mar - `sys::System`: PowerSystem System - `component::StaticInjection`: Static injection device - `time_series_data::Union{Nothing, IS.TimeSeriesData, - CostCurve{PiecewiseIncrementalCurve}},`: the data. If a time series, must be of eltype - `PiecewiseStepData`. + CostCurve{PiecewiseIncrementalCurve}},`: the data. If using a time series, must be of eltype + `PiecewiseStepData`. `PiecewiseIncrementalCurve` is only accepted for single CostCurve and + not accepted for time series data. - `power_units::UnitSystem`: Units to be used for data. """ function set_incremental_variable_cost!( @@ -489,8 +491,9 @@ Set the decremental variable cost bid for a `StaticInjection` device with a `Mar - `sys::System`: PowerSystem System - `component::StaticInjection`: Static injection device - `time_series_data::Union{Nothing, IS.TimeSeriesData, - CostCurve{PiecewiseIncrementalCurve}},`: the data. If a time series, must be of eltype - `PiecewiseStepData`. + CostCurve{PiecewiseIncrementalCurve}},`: the data. If using a time series, must be of eltype + `PiecewiseStepData`. `PiecewiseIncrementalCurve` is only accepted for single CostCurve and + not accepted for time series data. - `power_units::UnitSystem`: Units to be used for data. """ function set_decremental_variable_cost!( diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index d7a25c6acb..d5753171e6 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -19,14 +19,14 @@ Compatible with most US Market bidding mechanisms that support demand and genera start_up::Union{TimeSeriesKey, StartUpStages} "Shut-down cost" shut_down::Float64 - "Sell Offer Curves data, which can be a time series of [`PiecewiseStepData`](@ref) or a + "Sell Offer Curves data, which can be a time series of `PiecewiseStepData` or a [`CostCurve`](@ref) of [`PiecewiseIncrementalCurve`](@ref)" incremental_offer_curves::Union{ Nothing, TimeSeriesKey, # piecewise step data CostCurve{PiecewiseIncrementalCurve}, } = nothing - "Buy Offer Curves data, which can be a time series of [`PiecewiseStepData`](@ref) or a + "Buy Offer Curves data, which can be a time series of `PiecewiseStepData` or a [`CostCurve`](@ref) of [`PiecewiseIncrementalCurve`](@ref)" decremental_offer_curves::Union{ Nothing, @@ -47,6 +47,8 @@ MarketBidCost( shut_down, incremental_offer_curves, decremental_offer_curves, + incremental_initial_input, + decremental_initial_input, ancillary_service_offers, ) = MarketBidCost( @@ -55,6 +57,8 @@ MarketBidCost( shut_down, incremental_offer_curves, decremental_offer_curves, + incremental_initial_input, + decremental_initial_input, ancillary_service_offers, ) @@ -64,6 +68,8 @@ MarketBidCost( shut_down, incremental_offer_curves, decremental_offer_curves, + incremental_initial_input, + decremental_initial_input, ancillary_service_offers, ) = MarketBidCost(; @@ -72,6 +78,8 @@ MarketBidCost( shut_down = shut_down, incremental_offer_curves = incremental_offer_curves, decremental_offer_curves = decremental_offer_curves, + incremental_initial_input = incremental_initial_input, + decremental_initial_input = decremental_initial_input, ancillary_service_offers = ancillary_service_offers, ) @@ -122,7 +130,7 @@ get_shut_down(value::MarketBidCost) = value.shut_down """Get [`MarketBidCost`](@ref) `incremental_offer_curves`.""" get_incremental_offer_curves(value::MarketBidCost) = value.incremental_offer_curves """Get [`MarketBidCost`](@ref) `decremental_offer_curves`.""" -get_decremental_offer_curves(value::MarketBidCost) = value.incremental_offer_curves +get_decremental_offer_curves(value::MarketBidCost) = value.decremental_offer_curves """Get [`MarketBidCost`](@ref) `incremental_initial_input`.""" get_incremental_initial_input(value::MarketBidCost) = value.incremental_initial_input """Get [`MarketBidCost`](@ref) `decremental_initial_input`.""" From 985783022c1286216ecd04d7e439f0c59ec549ac Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Fri, 27 Sep 2024 14:56:23 -0700 Subject: [PATCH 12/17] update exports --- src/PowerSystems.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PowerSystems.jl b/src/PowerSystems.jl index f330eced7c..103d62df3a 100644 --- a/src/PowerSystems.jl +++ b/src/PowerSystems.jl @@ -380,6 +380,7 @@ export get_data export iterate_components export get_time_series_multiple export get_variable_cost +export get_incremental_variable_cost, get_decremental_variable_cost export get_no_load_cost export get_start_up export get_shut_down From df835896b2014be9bf85839d0c31a87f2ad5f8e3 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Fri, 27 Sep 2024 15:07:38 -0700 Subject: [PATCH 13/17] remove extra auxiliary function --- src/models/cost_functions/MarketBidCost.jl | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index d5753171e6..6d6b5ac2ff 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -62,27 +62,6 @@ MarketBidCost( ancillary_service_offers, ) -MarketBidCost( - no_load_cost::Float64, - start_up::Union{TimeSeriesKey, StartUpStages}, - shut_down, - incremental_offer_curves, - decremental_offer_curves, - incremental_initial_input, - decremental_initial_input, - ancillary_service_offers, -) = - MarketBidCost(; - no_load_cost = no_load_cost, - start_up = start_up, - shut_down = shut_down, - incremental_offer_curves = incremental_offer_curves, - decremental_offer_curves = decremental_offer_curves, - incremental_initial_input = incremental_initial_input, - decremental_initial_input = decremental_initial_input, - ancillary_service_offers = ancillary_service_offers, - ) - # Constructor for demo purposes; non-functional. function MarketBidCost(::Nothing) MarketBidCost(; From e197a5b2920304ed3731df03436da4e2f50fe7f0 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Fri, 27 Sep 2024 15:32:01 -0700 Subject: [PATCH 14/17] add auxiliary constructor --- src/models/cost_functions/MarketBidCost.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index 6d6b5ac2ff..7d9f0907c8 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -41,6 +41,7 @@ Compatible with most US Market bidding mechanisms that support demand and genera ancillary_service_offers::Vector{Service} = Vector{Service}() end +"Auxiliary Constructor for Deserialization with Integer at no load cost" MarketBidCost( no_load_cost::Integer, start_up::Union{TimeSeriesKey, StartUpStages}, @@ -62,6 +63,25 @@ MarketBidCost( ancillary_service_offers, ) +"""Auxiliary Constructor for TestData""" +MarketBidCost( + no_load_cost::Float64, + start_up::Union{TimeSeriesKey, StartUpStages}, + shut_down, + incremental_offer_curves, + ancillary_service_offers, +) = + MarketBidCost( + Float64(no_load_cost), + start_up, + shut_down, + incremental_offer_curves, + nothing, + nothing, + nothing, + ancillary_service_offers, + ) + # Constructor for demo purposes; non-functional. function MarketBidCost(::Nothing) MarketBidCost(; From 22fbec5c2aae2ac0a10eafcfd9de4a7b1207ac81 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Fri, 27 Sep 2024 15:50:46 -0700 Subject: [PATCH 15/17] update auxiliary method --- src/models/cost_functions/MarketBidCost.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index 7d9f0907c8..b184ee0fe5 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -69,6 +69,7 @@ MarketBidCost( start_up::Union{TimeSeriesKey, StartUpStages}, shut_down, incremental_offer_curves, + decremental_offer_curves, ancillary_service_offers, ) = MarketBidCost( @@ -76,7 +77,7 @@ MarketBidCost( start_up, shut_down, incremental_offer_curves, - nothing, + decremental_offer_curves, nothing, nothing, ancillary_service_offers, From af89171e09f2b5efb3bdd1340eece6465e81ad5a Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Sun, 29 Sep 2024 20:05:44 -0700 Subject: [PATCH 16/17] add docs --- docs/src/how_to/market_bid_cost.md | 101 +++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 26 deletions(-) diff --git a/docs/src/how_to/market_bid_cost.md b/docs/src/how_to/market_bid_cost.md index 4a1bd8ce13..8ca855df06 100644 --- a/docs/src/how_to/market_bid_cost.md +++ b/docs/src/how_to/market_bid_cost.md @@ -4,9 +4,61 @@ A [`MarketBidCost`](@ref) is an `OperationalCost` data structure that allows the cost model that is very similar to most US electricity market auctions with bids for energy and ancillary services jointly. This page showcases how to create data for this cost function. -## Adding Energy bids to MarketBidCost +## Adding a Single Incremental Energy bids to MarketBidCost -### Step 1: Constructiong device with MarketBidCost +### Construct directly the MarketBidCost using the `make_market_bid_curve` method. + +The `make_market_bid_curve` creates an incremental or decremental offer curve from a vector of `n` power values, a vector of `n-1` marginal costs and single initial input. For example, the following code creates an incremental offer curve: + +```@repl market_bid_cost +using PowerSystems, Dates +proposed_offer_curve = + make_market_bid_curve([0.0, 100.0, 105.0, 120.0, 130.0], [25.0, 26.0, 28.0, 30.0], 10.0) +``` + +Then a device with MarketBidCost can be directly instantiated using: + +```@repl market_bid_cost +using PowerSystems, Dates +bus = ACBus(1, "nodeE", "REF", 0, 1.0, (min = 0.9, max = 1.05), 230, nothing, nothing) + +generator = ThermalStandard(; + name = "Brighton", + available = true, + status = true, + bus = bus, + active_power = 6.0, + reactive_power = 1.50, + rating = 0.75, + prime_mover_type = PrimeMovers.ST, + fuel = ThermalFuels.COAL, + active_power_limits = (min = 0.0, max = 6.0), + reactive_power_limits = (min = -4.50, max = 4.50), + time_limits = (up = 0.015, down = 0.015), + ramp_limits = (up = 5.0, down = 3.0), + operation_cost = MarketBidCost(; + no_load_cost = 0.0, + start_up = (hot = 0.0, warm = 0.0, cold = 0.0), + shut_down = 0.0, + incremental_offer_curves = proposed_offer_curve, + ), + base_power = 100.0, +) +``` + +Similarly, a decremental offer curve can also be created directly using the same helper method: + +```@repl market_bid_cost +using PowerSystems, Dates +decremental_offer = + make_market_bid_curve([0.0, 100.0, 105.0, 120.0, 130.0], [30.0, 28.0, 26.0, 25.0], 50.0) +``` + +and can be added to a `MarketBidCost` using the field `decremental_offer_curves`. + +## Adding Time Series Energy bids to MarketBidCost + +### Step 1: Constructing device with MarketBidCost When using [`MarketBidCost`](@ref), the user can add the cost struct to the device specifying only certain elements, at this point the actual energy cost bids don't need to be populated/passed. @@ -43,19 +95,22 @@ generator = ThermalStandard(; ### Step 2: Creating the `TimeSeriesData` for the Market Bid The user is expected to pass the `TimeSeriesData` that holds the energy bid data which can be -of any type (i.e. `SingleTimeSeries` or `Deterministic`) and data can be `Array{Float64}`, -`Array{Tuple{Float64, Float64}}` or `Array{Array{Tuple{Float64,Float64}}`. If the data is -just floats then the cost in the optimization is seen as a constant variable cost, but if -data is a Tuple or `Array{Tuple}` then the model expects the tuples to be cost & power-point -pairs (cost in $/p.u-hr & power-point in p.u-hr), which is modeled same as TwoPartCost or -ThreePartCost. Code below shows an example of how to build a TimeSeriesData. +of any type (i.e. `SingleTimeSeries` or `Deterministic`) and data must be `PiecewiseStepData`. +This data type is created by specifying a vector of `n` powers, and `n-1` marginal costs. +The data must be specified in natural units, that is power in MW and marginal cost in $/MWh +or it will not be accepted when adding to the system. +Code below shows an example of how to build a Deterministic TimeSeries. ```@repl market_bid_cost +initial_time = Dates.DateTime("2020-01-01") +psd1 = PiecewiseStepData([5.0, 7.33, 9.67, 12.0], [2.901, 5.8272, 8.941]) +psd2 = PiecewiseStepData([5.0, 7.33, 9.67, 12.0], [3.001, 6.0072, 9.001]) data = Dict( - Dates.DateTime("2020-01-01") => [ - [(0.0, 0.05), (290.1, 0.0733), (582.72, 0.0967), (894.1, 0.120)], - [(0.0, 0.05), (300.1, 0.0733), (600.72, 0.0967), (900.1, 0.120)]], + initial_time => [ + psd1, + psd2, + ], ) time_series_data = Deterministic(; name = "variable_cost", @@ -64,20 +119,6 @@ time_series_data = Deterministic(; ) ``` -**NOTE:** Due to [limitations in DataStructures.jl](https://github.com/JuliaCollections/DataStructures.jl/issues/239), -in `PowerSystems.jl` when creating Forecasts or TimeSeries for your MarketBidCost, you need -to define your data as in the example or with a very explicit container. Otherwise, it won't -discern the types properly in the constructor and will return `SortedDict{Any,Any,Base.Order.ForwardOrdering}` which causes the constructor in `PowerSystems.jl` to fail. For instance, you need to define -the `Dict` with the data as follows: - -```julia -# Very verbose dict definition -data = Dict{DateTime, Array{Array{Tuple{Float64, Float64}, 1}, 1}}() -for t in range(initial_time_sys; step = Hour(1), length = window_count) - data[t] = MY_BID_DATA -end -``` - ### Step 3a: Adding Energy Bid TimeSeriesData to the device To add energy market bids time-series to the `MarketBidCost`, use `set_variable_cost!`. The @@ -86,12 +127,20 @@ arguments for `set_variable_cost!` are: - `sys::System`: PowerSystem System - `component::StaticInjection`: Static injection device - `time_series_data::TimeSeriesData`: TimeSeriesData + - `power_units::UnitSystem`: UnitSystem + +Currently, time series data only supports natural units for time series data, i.e. MW for power and $/MWh for marginal costs. ```@repl market_bid_cost sys = System(100.0, [bus], [generator]) -set_variable_cost!(sys, generator, time_series_data) +set_variable_cost!(sys, generator, time_series_data, UnitSystem.NATURAL_ITEMS) ``` +**Note:** `set_variable_cost!` add curves to the `incremental_offer_curves` in the MarketBidCost. +Similarly, `set_incremental_variable_cost!` can be used to add curves to the `incremental_offer_curves`. +On the other hand, `set_decremental_variable_cost!` must be used to decremental curves (usually for storage or demand). +The creation of the TimeSeriesData is similar to Step 2, using `PiecewiseStepData` + ### Step 3b: Adding Service Bid TimeSeriesData to the device Similar to adding energy market bids, for adding bids for ancillary services, use From ee432111bdc3113adb7caa9c8e3424e4fc0c0d73 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Mon, 30 Sep 2024 16:57:53 -0600 Subject: [PATCH 17/17] add additional setter --- src/models/cost_function_timeseries.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/models/cost_function_timeseries.jl b/src/models/cost_function_timeseries.jl index 2a1c2709e4..2e83a81cd2 100644 --- a/src/models/cost_function_timeseries.jl +++ b/src/models/cost_function_timeseries.jl @@ -429,7 +429,7 @@ Set the incremental variable cost bid for a `StaticInjection` device with a `Mar CostCurve{PiecewiseIncrementalCurve}},`: the data. If using a time series, must be of eltype `PiecewiseStepData`. `PiecewiseIncrementalCurve` is only accepted for single CostCurve and not accepted for time series data. -- `power_units::UnitSystem`: Units to be used for data. Must be NATURAL_UNITS for +- `power_units::UnitSystem`: Units to be used for data. Must be NATURAL_UNITS. """ function set_variable_cost!( sys::System, @@ -462,6 +462,16 @@ function set_variable_cost!( return end +function set_variable_cost!( + sys::System, + component::StaticInjection, + data::Union{Nothing, IS.TimeSeriesData, CostCurve{PiecewiseIncrementalCurve}}, +) + @warn "Variable Cost UnitSystem not specificied for $(get_name(component)). set_variable_cost! assumes data is in UnitSystem.NATURAL_UNITS" + set_variable_cost!(sys, component, data, UnitSystem.NATURAL_UNITS) + return +end + """ Set the incremental variable cost bid for a `StaticInjection` device with a `MarketBidCost`.