From f28a212d99671d8c7d90b696de3e78e04900566c Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Thu, 9 May 2024 15:12:04 -0700 Subject: [PATCH 1/6] Accommodate `NaN` in cost validation and comparison --- src/models/cost_functions/ValueCurves.jl | 9 ++++----- src/models/cost_functions/variable_cost.jl | 6 ++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/models/cost_functions/ValueCurves.jl b/src/models/cost_functions/ValueCurves.jl index 5137762006..4e1edefba0 100644 --- a/src/models/cost_functions/ValueCurves.jl +++ b/src/models/cost_functions/ValueCurves.jl @@ -54,12 +54,11 @@ end get_initial_input(curve::Union{IncrementalCurve, AverageRateCurve}) = curve.initial_input # BASE METHODS -Base.:(==)(a::InputOutputCurve, b::InputOutputCurve) = - (get_function_data(a) == get_function_data(b)) +Base.:(==)(a::T, b::T) where T <: ValueCurve = IS.double_equals_from_fields(a, b) -Base.:(==)(a::T, b::T) where {T <: Union{IncrementalCurve, AverageRateCurve}} = - (get_function_data(a) == get_function_data(b)) && - (get_initial_input(a) == get_initial_input(b)) +Base.isequal(a::T, b::T) where T <: ValueCurve = IS.isequal_from_fields(a, b) + +Base.hash(a::ValueCurve) = IS.hash_from_fields(a) "Get an `InputOutputCurve` representing `f(x) = 0`" Base.zero(::Union{InputOutputCurve, Type{InputOutputCurve}}) = diff --git a/src/models/cost_functions/variable_cost.jl b/src/models/cost_functions/variable_cost.jl index e40c696a15..5523f3344e 100644 --- a/src/models/cost_functions/variable_cost.jl +++ b/src/models/cost_functions/variable_cost.jl @@ -14,6 +14,12 @@ get_initial_input(cost::ProductionVariableCost) = get_initial_input(get_value_cu "Calculate the convexity of the underlying data" is_convex(cost::ProductionVariableCost) = is_convex(get_value_curve(cost)) +Base.:(==)(a::T, b::T) where T <: ProductionVariableCost = IS.double_equals_from_fields(a, b) + +Base.isequal(a::T, b::T) where T <: ProductionVariableCost = IS.isequal_from_fields(a, b) + +Base.hash(a::ProductionVariableCost) = IS.hash_from_fields(a) + """ Direct representation of the variable operation cost of a power plant in currency. Composed of a [`ValueCurve`][@ref] that may represent input-output, incremental, or average rate From 23e46fd38dd36625ea348d2622158618401e43fd Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Thu, 9 May 2024 15:15:21 -0700 Subject: [PATCH 2/6] Add market bid curve validator and factory function --- src/models/cost_functions/MarketBidCost.jl | 36 ++++++++++++++++++++++ test/test_cost_functions.jl | 8 +++++ 2 files changed, 44 insertions(+) diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index 5afa2c1896..36a09578e6 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -104,3 +104,39 @@ set_decremental_offer_curves!(value::MarketBidCost, val) = """Set [`MarketBidCost`](@ref) `ancillary_service_offers`.""" set_ancillary_service_offers!(value::MarketBidCost, val) = value.ancillary_service_offers = val + +# 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 +function is_market_bid_curve(curve::ProductionVariableCost) + (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)))) +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. +""" +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) +end + +""" +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, NaN), power_units) + @assert is_market_bid_curve(cc) + return cc +end diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index 4db8a4578b..3d886d3d77 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -134,6 +134,14 @@ end UnitSystem.DEVICE_BASE 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]) + @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]) +end + test_costs = Dict( QuadraticFunctionData => repeat([QuadraticFunctionData(999.0, 2.0, 1.0)], 24), From 0d87770bf4456e0b94bc0b56f887e9aada23c465 Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Thu, 9 May 2024 15:17:23 -0700 Subject: [PATCH 3/6] Renovate market bid curve time series interface --- src/PowerSystems.jl | 1 + src/models/cost_function_timeseries.jl | 12 ++++------ test/test_cost_functions.jl | 33 ++++++++++++-------------- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/PowerSystems.jl b/src/PowerSystems.jl index 69b5e5dbe8..ef6cc60d66 100644 --- a/src/PowerSystems.jl +++ b/src/PowerSystems.jl @@ -62,6 +62,7 @@ export OperationalCost, MarketBidCost, LoadCost, StorageCost export HydroGenerationCost, RenewableGenerationCost, ThermalGenerationCost export get_function_data, get_initial_input, get_value_curve, get_power_units export get_fuel_cost, set_fuel_cost! +export is_market_bid_curve, make_market_bid_curve export Generator export HydroGen diff --git a/src/models/cost_function_timeseries.jl b/src/models/cost_function_timeseries.jl index 46f0a2c53d..d61062bee1 100644 --- a/src/models/cost_function_timeseries.jl +++ b/src/models/cost_function_timeseries.jl @@ -1,7 +1,3 @@ -# MarketBidCost has two variable costs, here we mean the incremental one -get_generation_variable_cost(cost::MarketBidCost) = get_incremental_offer_curves(cost) -# get_generation_variable_cost(cost::OperationalCost) = get_variable_cost(cost) - function _validate_time_series_variable_cost( time_series_data::IS.TimeSeriesData; desired_type::Type = PiecewiseStepData, @@ -39,7 +35,7 @@ function get_variable_cost( end data = IS.get_time_series_array(component, ts, start_time; len = len) time_stamps = TimeSeries.timestamp(data) - return data + return TimeSeries.TimeArray(time_stamps, map(make_market_bid_curve, TimeSeries.values(data))) end """ @@ -53,11 +49,11 @@ Returns variable cost bids time-series data for MarketBidCost. """ function get_variable_cost( device::StaticInjection, - cost::OperationalCost; + cost::MarketBidCost; start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, ) - time_series_key = get_generation_variable_cost(cost) + time_series_key = get_incremental_offer_curves(cost) if isnothing(time_series_key) error( "Cost component is empty, please use `set_variable_cost!` to add variable cost forecast.", @@ -123,7 +119,7 @@ function get_services_bid( start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, ) - variable_ts_key = get_generation_variable_cost(cost) + variable_ts_key = get_incremental_offer_curves(cost) raw_data = get_time_series( variable_ts_key.time_series_type, device, diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index 3d886d3d77..4c698b1470 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -143,13 +143,10 @@ end end test_costs = Dict( - QuadraticFunctionData => - repeat([QuadraticFunctionData(999.0, 2.0, 1.0)], 24), - PiecewiseLinearData => - repeat( - [PiecewiseStepData([1.0, 2.0, 3.0], [4.0, 6.0])], - 24, - ), + CostCurve{QuadraticCurve} => + repeat([CostCurve(QuadraticCurve(999.0, 2.0, 1.0))], 24), + CostCurve{PiecewiseIncrementalCurve} => + repeat([make_market_bid_curve([2.0, 3.0], [4.0, 6.0])], 24), Float64 => collect(11.0:34.0), ) @@ -163,12 +160,12 @@ test_costs = Dict( horizon = 24 service_data = Dict(initial_time => rand(horizon)) data_quadratic = - SortedDict(initial_time => test_costs[QuadraticFunctionData]) + SortedDict(initial_time => test_costs[CostCurve{QuadraticCurve}]) 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("variable_cost", data_quadratic, resolution) + forecast = 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) for s in generator.services forecast = IS.Deterministic(get_name(s), service_data, resolution) @@ -181,25 +178,25 @@ end resolution = Dates.Hour(1) name = "test" horizon = 24 - data_pwl = SortedDict(initial_time => test_costs[PiecewiseLinearData]) + data_pwl = SortedDict(initial_time => test_costs[CostCurve{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 = IS.Deterministic("variable_cost", data_pwl, resolution) + forecast = IS.Deterministic("variable_cost", Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), resolution) set_variable_cost!(sys, generator, forecast) for s in generator.services - forecast = IS.Deterministic(get_name(s), service_data, resolution) + forecast = 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) end cost_forecast = get_variable_cost(generator, market_bid; start_time = initial_time) - @test first(TimeSeries.values(cost_forecast)) == first(data_pwl[initial_time]) + @test isequal(first(TimeSeries.values(cost_forecast)), first(data_pwl[initial_time])) for s in generator.services service_cost = get_services_bid(generator, market_bid, s; start_time = initial_time) - @test first(TimeSeries.values(service_cost)) == first(service_data[initial_time]) + @test isequal(first(TimeSeries.values(service_cost)), first(service_data[initial_time])) end end @@ -215,15 +212,15 @@ end other_time = initial_time + resolution name = "test" horizon = 24 - data_pwl = SortedDict(initial_time => test_costs[PiecewiseLinearData], - other_time => test_costs[PiecewiseLinearData]) + data_pwl = SortedDict(initial_time => test_costs[CostCurve{PiecewiseIncrementalCurve}], + other_time => test_costs[CostCurve{PiecewiseIncrementalCurve}]) sys = System(100.0) reserve = ReserveDemandCurve{ReserveUp}(nothing) add_component!(sys, reserve) - forecast = IS.Deterministic("variable_cost", data_pwl, resolution) + forecast = IS.Deterministic("variable_cost", Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), resolution) set_variable_cost!(sys, reserve, forecast) cost_forecast = get_variable_cost(reserve; start_time = initial_time) - @test first(TimeSeries.values(cost_forecast)) == first(data_pwl[initial_time]) + @test isequal(first(TimeSeries.values(cost_forecast)), first(data_pwl[initial_time])) end @testset "Test fuel cost (scalar and time series)" begin From 229a27c196245114ece1c875ce11109975e83686 Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Thu, 9 May 2024 15:42:23 -0700 Subject: [PATCH 4/6] Run formatter --- src/models/cost_function_timeseries.jl | 5 ++- src/models/cost_functions/MarketBidCost.jl | 8 +++- src/models/cost_functions/ValueCurves.jl | 4 +- src/models/cost_functions/variable_cost.jl | 5 ++- test/test_cost_functions.jl | 43 ++++++++++++++++------ 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/models/cost_function_timeseries.jl b/src/models/cost_function_timeseries.jl index d61062bee1..bbc1a0dda8 100644 --- a/src/models/cost_function_timeseries.jl +++ b/src/models/cost_function_timeseries.jl @@ -35,7 +35,10 @@ function get_variable_cost( end data = IS.get_time_series_array(component, ts, start_time; len = len) time_stamps = TimeSeries.timestamp(data) - return TimeSeries.TimeArray(time_stamps, map(make_market_bid_curve, TimeSeries.values(data))) + return TimeSeries.TimeArray( + time_stamps, + map(make_market_bid_curve, TimeSeries.values(data)), + ) end """ diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index 36a09578e6..7b795ddcb5 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -112,7 +112,7 @@ function is_market_bid_curve(curve::ProductionVariableCost) (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)))) + isnan(first(get_x_coords(get_function_data(value_curve)))) end """ @@ -135,7 +135,11 @@ 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")) + !isnan(first(get_x_coords(data))) && throw( + ArgumentError( + "The first x-coordinate in the PiecewiseStepData representation must be NaN", + ), + ) cc = CostCurve(IncrementalCurve(data, NaN), power_units) @assert is_market_bid_curve(cc) return cc diff --git a/src/models/cost_functions/ValueCurves.jl b/src/models/cost_functions/ValueCurves.jl index 4e1edefba0..897f802f5e 100644 --- a/src/models/cost_functions/ValueCurves.jl +++ b/src/models/cost_functions/ValueCurves.jl @@ -54,9 +54,9 @@ end get_initial_input(curve::Union{IncrementalCurve, AverageRateCurve}) = curve.initial_input # BASE METHODS -Base.:(==)(a::T, b::T) where T <: ValueCurve = IS.double_equals_from_fields(a, b) +Base.:(==)(a::T, b::T) where {T <: ValueCurve} = IS.double_equals_from_fields(a, b) -Base.isequal(a::T, b::T) where T <: ValueCurve = IS.isequal_from_fields(a, b) +Base.isequal(a::T, b::T) where {T <: ValueCurve} = IS.isequal_from_fields(a, b) Base.hash(a::ValueCurve) = IS.hash_from_fields(a) diff --git a/src/models/cost_functions/variable_cost.jl b/src/models/cost_functions/variable_cost.jl index 5523f3344e..0e92f1b972 100644 --- a/src/models/cost_functions/variable_cost.jl +++ b/src/models/cost_functions/variable_cost.jl @@ -14,9 +14,10 @@ get_initial_input(cost::ProductionVariableCost) = get_initial_input(get_value_cu "Calculate the convexity of the underlying data" is_convex(cost::ProductionVariableCost) = is_convex(get_value_curve(cost)) -Base.:(==)(a::T, b::T) where T <: ProductionVariableCost = IS.double_equals_from_fields(a, b) +Base.:(==)(a::T, b::T) where {T <: ProductionVariableCost} = + IS.double_equals_from_fields(a, b) -Base.isequal(a::T, b::T) where T <: ProductionVariableCost = IS.isequal_from_fields(a, b) +Base.isequal(a::T, b::T) where {T <: ProductionVariableCost} = IS.isequal_from_fields(a, b) Base.hash(a::ProductionVariableCost) = IS.hash_from_fields(a) diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index 4c698b1470..680a1eec46 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -135,17 +135,17 @@ end 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]) - @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]) + mbc = make_market_bid_curve([100.0, 105.0, 120.0, 130.0], [25.0, 26.0, 28.0, 30.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]) end test_costs = Dict( - CostCurve{QuadraticCurve} => + CostCurve{QuadraticCurve} => repeat([CostCurve(QuadraticCurve(999.0, 2.0, 1.0))], 24), - CostCurve{PiecewiseIncrementalCurve} => + CostCurve{PiecewiseIncrementalCurve} => repeat([make_market_bid_curve([2.0, 3.0], [4.0, 6.0])], 24), Float64 => collect(11.0:34.0), @@ -165,7 +165,11 @@ test_costs = Dict( generator = get_component(ThermalStandard, sys, "322_CT_6") market_bid = MarketBidCost(nothing) set_operation_cost!(generator, market_bid) - forecast = IS.Deterministic("variable_cost", Dict(k => get_function_data.(v) for (k, v) in pairs(data_quadratic)), resolution) + forecast = 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) for s in generator.services forecast = IS.Deterministic(get_name(s), service_data, resolution) @@ -184,10 +188,18 @@ end generator = get_component(ThermalStandard, sys, "322_CT_6") market_bid = MarketBidCost(nothing) set_operation_cost!(generator, market_bid) - forecast = IS.Deterministic("variable_cost", Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), resolution) + forecast = IS.Deterministic( + "variable_cost", + Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), + resolution, + ) set_variable_cost!(sys, generator, forecast) for s in generator.services - forecast = IS.Deterministic(get_name(s), Dict(k => get_function_data.(v) for (k, v) in pairs(service_data)), resolution) + forecast = 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) end @@ -196,7 +208,10 @@ end for s in generator.services service_cost = get_services_bid(generator, market_bid, s; start_time = initial_time) - @test isequal(first(TimeSeries.values(service_cost)), first(service_data[initial_time])) + @test isequal( + first(TimeSeries.values(service_cost)), + first(service_data[initial_time]), + ) end end @@ -217,7 +232,11 @@ end sys = System(100.0) reserve = ReserveDemandCurve{ReserveUp}(nothing) add_component!(sys, reserve) - forecast = IS.Deterministic("variable_cost", Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), resolution) + forecast = IS.Deterministic( + "variable_cost", + Dict(k => get_function_data.(v) for (k, v) in pairs(data_pwl)), + resolution, + ) set_variable_cost!(sys, reserve, forecast) cost_forecast = get_variable_cost(reserve; start_time = initial_time) @test isequal(first(TimeSeries.values(cost_forecast)), first(data_pwl[initial_time])) From ce9802be3927e29c64ff39e839d11e7fd308679d Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Fri, 10 May 2024 09:41:31 -0700 Subject: [PATCH 5/6] Rewrite `cost_function_timeseries.jl` for reusability --- src/models/cost_function_timeseries.jl | 353 ++++++++++++------------- 1 file changed, 172 insertions(+), 181 deletions(-) diff --git a/src/models/cost_function_timeseries.jl b/src/models/cost_function_timeseries.jl index bbc1a0dda8..6f5bc32076 100644 --- a/src/models/cost_function_timeseries.jl +++ b/src/models/cost_function_timeseries.jl @@ -1,119 +1,131 @@ -function _validate_time_series_variable_cost( - time_series_data::IS.TimeSeriesData; - desired_type::Type = PiecewiseStepData, -) - data_type = IS.eltype_data(time_series_data) - (data_type <: desired_type) || throw( - TypeError( - StackTraces.stacktrace()[2].func, "time series data", desired_type, - data_type), - ) -end - +# VALIDATORS function _validate_market_bid_cost(cost, context) (cost isa MarketBidCost) || throw(TypeError( StackTraces.stacktrace()[2].func, context, MarketBidCost, cost)) end +function _validate_fuel_curve(component::Component) + op_cost = get_operation_cost(component) + var_cost = get_variable(op_cost) + !(var_cost isa FuelCurve) && throw( + ArgumentError( + "Variable cost of type $(typeof(var_cost)) cannot represent a fuel cost, use FuelCurve instead", + ), + ) + return var_cost +end + """ -Returns variable cost bids time-series data. +Validates if a device is eligible to contribute to a service. # Arguments -- `ts::IS.TimeSeriesData`:TimeSeriesData -- `component::Component`: Component -- `start_time::Union{Nothing, Dates.DateTime} = nothing`: Time when the time-series data starts -- `len::Union{Nothing, Int} = nothing`: Length of the time-series to be returned +- `sys::System`: PowerSystem System +- `component::StaticInjection`: Static injection device +- `service::Service,`: Service for which the device is eligible to contribute """ -function get_variable_cost( +function verify_device_eligibility( + sys::System, + component::StaticInjection, + service::Service, +) + if !has_service(component, service) + error( + "Device $(get_name(component)) isn't eligible to contribute to service $(get_name(service)).", + ) + end + return +end + +# GETTER HELPER FUNCTIONS +""" +Call get_time_series_array on the given time series and return a TimeArray of the results, +values mapped by `transform_fn` if it is not nothing +""" +function read_and_convert_ts( ts::IS.TimeSeriesData, component::Component, start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, + transform_fn = nothing, ) - if start_time === nothing - start_time = IS.get_initial_timestamp(ts) - end + isnothing(start_time) && (start_time = IS.get_initial_timestamp(ts)) + isnothing(transform_fn) && (transform_fn = (x -> x)) data = IS.get_time_series_array(component, ts, start_time; len = len) time_stamps = TimeSeries.timestamp(data) return TimeSeries.TimeArray( time_stamps, - map(make_market_bid_curve, TimeSeries.values(data)), + map(transform_fn, TimeSeries.values(data)), ) end """ -Returns variable cost bids time-series data for MarketBidCost. +Helper function for cost getters. # Arguments -- `device::StaticInjection`: Static injection device -- `cost::MarketBidCost`: Operations Cost -- `start_time::Union{Nothing, Dates.DateTime} = nothing`: Time when the time-series data starts -- `len::Union{Nothing, Int} = nothing`: Length of the time-series to be returned +- `T`: type/eltype we expect +- `component::Component`: the component +- `cost`: the data: either a single element of type `T` or a `TimeSeriesKey` +- `transform_fn`: a function to apply to the elements of the time series +- `start_time`: as in `get_time_series` +- `len`: as in `get_time_series` """ -function get_variable_cost( - device::StaticInjection, - cost::MarketBidCost; - start_time::Union{Nothing, Dates.DateTime} = nothing, - len::Union{Nothing, Int} = nothing, +_process_get_cost(_, _, cost::Nothing, _, _, _, _) = throw( + ArgumentError( + "This cost component is empty, please use the corresponding setter to add cost data.", + ), ) - time_series_key = get_incremental_offer_curves(cost) - if isnothing(time_series_key) - error( - "Cost component is empty, please use `set_variable_cost!` to add variable cost forecast.", - ) - end - raw_data = get_time_series( - get_time_series_type(time_series_key), - device, - get_name(time_series_key); - start_time = start_time, - len = len, - count = 1, - ) - cost = get_variable_cost(raw_data, device, start_time, len) + +function _process_get_cost(::Type{T}, _, cost::T, transform_fn, + start_time::Union{Nothing, Dates.DateTime}, + len::Union{Nothing, Int}, +) where {T} + !isnothing(start_time) && + throw(ArgumentError("Got non-nothing start_time but this cost is a scalar")) + !isnothing(len) && + throw(ArgumentError("Got non-nothing len but this cost is a scalar")) return cost end +function _process_get_cost(::Type{T}, component::Component, cost::TimeSeriesKey, + transform_fn, + start_time::Union{Nothing, Dates.DateTime}, + len::Union{Nothing, Int}, +) where {T} + ts = get_time_series(component, cost, start_time, len, 1) + converted = read_and_convert_ts(ts, component, start_time, len, transform_fn) + return converted +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`. """ -Returns variable cost time-series data for a ReserveDemandCurve. +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) -# Arguments -- `service::ReserveDemandCurve`: ReserveDemandCurve -- `start_time::Union{Nothing, Dates.DateTime} = nothing`: Time when the time-series data starts -- `len::Union{Nothing, Int} = nothing`: Length of the time-series to be returned """ -function get_variable_cost( +Retrieve the variable cost data for a `ReserveDemandCurve`. The user may specify +`start_time` and `len` and the function returns a `TimeArray` of `CostCurve`s. +""" +get_variable_cost( service::ReserveDemandCurve; start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, -) - time_series_key = get_variable(service) - if isnothing(time_series_key) - error( - "Cost component has a `nothing` stored in field `variable`, Please use `set_variable_cost!` to add variable cost forecast.", - ) - end - raw_data = get_time_series( - get_time_series_type(time_series_key), - service, - get_name(time_series_key); - start_time = start_time, - len = len, - count = 1, - ) - cost = get_variable_cost(raw_data, service, start_time, len) - return cost -end +) = _process_get_cost(CostCurve{PiecewiseIncrementalCurve}, service, get_variable(service), + make_market_bid_curve, start_time, len) """ -Returns service bids time-series data for a device that has MarketBidCost. - -# Arguments -- `sys::System`: PowerSystem System -- `cost::MarketBidCost`: Operations Cost -- `service::Service`: Service -- `start_time::Union{Nothing, Dates.DateTime} = nothing`: Time when the time-series data starts -- `len::Union{Nothing, Int} = nothing`: Length of the time-series to be returned +Return service bid time series data for a `StaticInjection` device with a `MarketBidCost`. +The user may specify `start_time` and `len` and the function returns a `TimeArray` of +`CostCurve`s. """ function get_services_bid( device::StaticInjection, @@ -123,7 +135,7 @@ function get_services_bid( len::Union{Nothing, Int} = nothing, ) variable_ts_key = get_incremental_offer_curves(cost) - raw_data = get_time_series( + ts = get_time_series( variable_ts_key.time_series_type, device, get_name(service); @@ -131,96 +143,79 @@ function get_services_bid( len = len, count = 1, ) - cost = get_variable_cost(raw_data, device, start_time, len) - return cost + converted = read_and_convert_ts(ts, service, start_time, len, make_market_bid_curve) + return converted end +"Get the fuel cost of the component's variable cost, which must be a `FuelCurve`." +function get_fuel_cost(component::StaticInjection; + start_time::Union{Nothing, Dates.DateTime} = nothing, + len::Union{Nothing, Int} = nothing, +) + var_cost = _validate_fuel_curve(component) + return _process_get_cost( + Float64, + component, + get_fuel_cost(var_cost), + nothing, + start_time, + len, + ) +end + +# SETTER HELPER FUNCTIONS """ -Adds energy market bid time series to the component's operation cost, which must be a MarketBidCost. +Helper function for cost setters. # Arguments -- `sys::System`: PowerSystem System -- `component::StaticInjection`: Static injection device -- `time_series_data::IS.TimeSeriesData`: TimeSeriesData +- `T1`: type we expect if it's not a time series +- `T2`: eltype we expect if it is a time series +- `sys::System`: the system +- `component::Component`: the component +- `cost`: the data: either a single element of type `T1` or a `IS.TimeSeriesData` of eltype `T2` """ -function set_variable_cost!( - sys::System, - component::StaticInjection, - time_series_data::IS.TimeSeriesData, -) - _validate_time_series_variable_cost(time_series_data) - market_bid_cost = get_operation_cost(component) - _validate_market_bid_cost(market_bid_cost, "get_operation_cost(component)") +_process_set_cost(_, _, _, _, ::Nothing) = nothing - key = add_time_series!(sys, component, time_series_data) - set_incremental_offer_curves!(market_bid_cost, key) - return -end - -function _process_fuel_cost( - ::Component, - fuel_cost::Float64, - start_time::Union{Nothing, Dates.DateTime}, - len::Union{Nothing, Int}, -) - isnothing(start_time) && isnothing(len) && return fuel_cost - throw(ArgumentError("Got time series start_time and/or len, but fuel cost is a scalar")) -end +_process_set_cost(::Type{T}, _, _, _, cost::T) where {T} = cost -function _process_fuel_cost( +function _process_set_cost( + ::Type{_}, + ::Type{T}, + sys::System, component::Component, - ts_key::TimeSeriesKey, - start_time::Union{Nothing, Dates.DateTime}, - len::Union{Nothing, Int}, -) - ts = get_time_series(component, ts_key, start_time, len) - if start_time === nothing - start_time = IS.get_initial_timestamp(ts) - end - return get_time_series_array(component, ts, start_time; len = len) + cost::IS.TimeSeriesData, +) where {_, T} + data_type = IS.eltype_data(cost) + !(data_type <: T) && throw(TypeError(_process_set_cost, T, data_type)) + key = add_time_series!(sys, component, cost) + return key end -"Get the fuel cost of the component's variable cost, which must be a `FuelCurve`." -function get_fuel_cost(component::StaticInjection; - start_time::Union{Nothing, Dates.DateTime} = nothing, - len::Union{Nothing, Int} = nothing) - op_cost = get_operation_cost(component) - var_cost = get_variable(op_cost) - !(var_cost isa FuelCurve) && throw( - ArgumentError( - "Variable cost of type $(typeof(var_cost)) cannot represent a fuel cost, use FuelCurve instead", - ), - ) - return _process_fuel_cost(component, get_fuel_cost(var_cost), start_time, len) -end - -function _set_fuel_cost!(component::StaticInjection, fuel_cost) - op_cost = get_operation_cost(component) - var_cost = get_variable(op_cost) - !(var_cost isa FuelCurve) && throw( - ArgumentError( - "Variable cost of type $(typeof(var_cost)) cannot represent a fuel cost, use FuelCurve instead", - ), - ) - new_var_cost = - FuelCurve(get_value_curve(var_cost), get_power_units(var_cost), fuel_cost) - set_variable!(op_cost, new_var_cost) -end - -"Set the fuel cost of the component's variable cost, which must be a `FuelCurve`, to a scalar value." -set_fuel_cost!(_::System, component::StaticInjection, fuel_cost::Real) = -# the System is not required, but we take it for consistency with the time series method of this function - _set_fuel_cost!(component, Float64(fuel_cost)) +# SETTER IMPLEMENTATIONS +""" +Set the variable cost bid for a `StaticInjection` device with a `MarketBidCost`. -"Set the fuel cost of the component's variable cost, which must be a `FuelCurve`, to a time series value." -function set_fuel_cost!( +# Arguments +- `sys::System`: PowerSystem System +- `component::StaticInjection`: Static injection device +- `time_series_data::Union{Nothing, IS.TimeSeriesData, CostCurve{PiecewiseIncrementalCurve}},`: the data +""" +function set_variable_cost!( sys::System, component::StaticInjection, - time_series_data::IS.TimeSeriesData, + data::Union{Nothing, IS.TimeSeriesData, CostCurve{PiecewiseIncrementalCurve}}, ) - _validate_time_series_variable_cost(time_series_data; desired_type = Float64) - key = add_time_series!(sys, component, time_series_data) - _set_fuel_cost!(component, key) + market_bid_cost = get_operation_cost(component) + _validate_market_bid_cost(market_bid_cost, "get_operation_cost(component)") + to_set = _process_set_cost( + CostCurve{PiecewiseIncrementalCurve}, + PiecewiseStepData, + sys, + component, + data, + ) + set_incremental_offer_curves!(market_bid_cost, to_set) + return end """ @@ -228,19 +223,34 @@ Adds energy market bids time-series to the ReserveDemandCurve. # Arguments - `sys::System`: PowerSystem System -- `component::StaticInjection`: Static injection device +- `component::ReserveDemandCurve`: the curve - `time_series_data::IS.TimeSeriesData`: TimeSeriesData """ function set_variable_cost!( sys::System, component::ReserveDemandCurve, - time_series_data::IS.TimeSeriesData, + data::Union{Nothing, IS.TimeSeriesData}, ) - key = add_time_series!(sys, component, time_series_data) - set_variable!(component, key) + # TODO what type checking should be enforced on this time series? + to_set = _process_set_cost(Any, Any, sys, component, data) + set_variable!(component, to_set) return end +"Set the fuel cost of the component's variable cost, which must be a `FuelCurve`." +function set_fuel_cost!( + sys::System, + component::StaticInjection, + time_series_data::Union{Float64, IS.TimeSeriesData}, +) + var_cost = _validate_fuel_curve(component) + to_set = _process_set_cost(Float64, Float64, sys, component, time_series_data) + op_cost = get_operation_cost(component) + new_var_cost = + FuelCurve(get_value_curve(var_cost), get_power_units(var_cost), to_set) + set_variable!(op_cost, new_var_cost) +end + """ Adds service bids time-series data to the MarketBidCost. @@ -256,7 +266,9 @@ function set_service_bid!( service::Service, time_series_data::IS.TimeSeriesData, ) - _validate_time_series_variable_cost(time_series_data) + data_type = IS.eltype_data(time_series_data) + !(data_type <: PiecewiseStepData) && + throw(TypeError(set_service_bid!, PiecewiseStepData, data_type)) _validate_market_bid_cost( get_operation_cost(component), "get_operation_cost(component)", @@ -272,24 +284,3 @@ function set_service_bid!( push!(ancillary_service_offers, service) return end - -""" -Validates if a device is eligible to contribute to a service. - -# Arguments -- `sys::System`: PowerSystem System -- `component::StaticInjection`: Static injection device -- `service::Service,`: Service for which the device is eligible to contribute -""" -function verify_device_eligibility( - sys::System, - component::StaticInjection, - service::Service, -) - if !has_service(component, service) - error( - "Device $(get_name(component)) isn't eligible to contribute to service $(get_name(service)).", - ) - end - return -end From 56234b2e931e786e6932cb034105b2e92a755bd4 Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Fri, 10 May 2024 11:36:40 -0700 Subject: [PATCH 6/6] Prototype time series option for market bid no load and startup cost --- src/PowerSystems.jl | 1 + src/models/cost_function_timeseries.jl | 76 ++++++++++++++++++++-- src/models/cost_functions/MarketBidCost.jl | 21 +++++- test/test_cost_functions.jl | 56 +++++++++++++++- 4 files changed, 146 insertions(+), 8 deletions(-) diff --git a/src/PowerSystems.jl b/src/PowerSystems.jl index ef6cc60d66..4744c4932b 100644 --- a/src/PowerSystems.jl +++ b/src/PowerSystems.jl @@ -63,6 +63,7 @@ export HydroGenerationCost, RenewableGenerationCost, ThermalGenerationCost export get_function_data, get_initial_input, get_value_curve, get_power_units export get_fuel_cost, set_fuel_cost! export is_market_bid_curve, make_market_bid_curve +export get_no_load_cost, set_no_load_cost!, get_start_up, set_start_up! export Generator export HydroGen diff --git a/src/models/cost_function_timeseries.jl b/src/models/cost_function_timeseries.jl index 6f5bc32076..49cde14760 100644 --- a/src/models/cost_function_timeseries.jl +++ b/src/models/cost_function_timeseries.jl @@ -163,6 +163,34 @@ function get_fuel_cost(component::StaticInjection; ) end +""" +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`. +""" +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, + get_no_load_cost(cost), nothing, start_time, len) + +""" +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`. +""" +get_start_up( + device::StaticInjection, + cost::MarketBidCost; + start_time::Union{Nothing, Dates.DateTime} = nothing, + len::Union{Nothing, Int} = nothing, +) = _process_get_cost(StartUpStages, device, + get_start_up(cost), StartUpStages, start_time, len) + # SETTER HELPER FUNCTIONS """ Helper function for cost setters. @@ -198,7 +226,9 @@ Set the 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 +- `time_series_data::Union{Nothing, IS.TimeSeriesData, + CostCurve{PiecewiseIncrementalCurve}},`: the data. If a time series, must be of eltype + `PiecewiseStepData`. """ function set_variable_cost!( sys::System, @@ -215,7 +245,6 @@ function set_variable_cost!( data, ) set_incremental_offer_curves!(market_bid_cost, to_set) - return end """ @@ -234,23 +263,60 @@ function set_variable_cost!( # TODO what type checking should be enforced on this time series? to_set = _process_set_cost(Any, Any, sys, component, data) set_variable!(component, to_set) - return end "Set the fuel cost of the component's variable cost, which must be a `FuelCurve`." function set_fuel_cost!( sys::System, component::StaticInjection, - time_series_data::Union{Float64, IS.TimeSeriesData}, + data::Union{Float64, IS.TimeSeriesData}, ) var_cost = _validate_fuel_curve(component) - to_set = _process_set_cost(Float64, Float64, sys, component, time_series_data) + to_set = _process_set_cost(Float64, Float64, sys, component, data) op_cost = get_operation_cost(component) new_var_cost = FuelCurve(get_value_curve(var_cost), get_power_units(var_cost), to_set) set_variable!(op_cost, new_var_cost) end +""" +Set the no-load cost for a `StaticInjection` device with a `MarketBidCost` to either a single number 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_no_load_cost!( + 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(Float64, Float64, sys, component, data) + set_no_load_cost!(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. + +# Arguments +- `sys::System`: PowerSystem System +- `component::StaticInjection`: Static injection device +- `time_series_data::Union{StartUpStages, IS.TimeSeriesData},`: the data. If a time series, must be of eltype `NTuple{3, Float64}`. +""" +function set_start_up!( + sys::System, + component::StaticInjection, + data::Union{StartUpStages, 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(StartUpStages, NTuple{3, Float64}, sys, component, data) + set_start_up!(market_bid_cost, to_set) +end + """ Adds service bids time-series data to the MarketBidCost. diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index 7b795ddcb5..35983ff4f0 100644 --- a/src/models/cost_functions/MarketBidCost.jl +++ b/src/models/cost_functions/MarketBidCost.jl @@ -20,10 +20,10 @@ Compatible with most US Market bidding mechanisms that support demand and genera - `ancillary_service_offers::Vector{Service}`: Bids for the ancillary services """ @kwdef mutable struct MarketBidCost <: OperationalCost - no_load_cost::Float64 + no_load_cost::Union{TimeSeriesKey, Float64} """start-up cost at different stages of the thermal cycle. Warm is also referred to as intermediate in some markets""" - start_up::StartUpStages + start_up::Union{TimeSeriesKey, StartUpStages} "shut-down cost" shut_down::Float64 "Variable Cost TimeSeriesKey" @@ -42,6 +42,23 @@ Compatible with most US Market bidding mechanisms that support demand and genera ancillary_service_offers::Vector{Service} = Vector{Service}() end +MarketBidCost( + no_load_cost::Integer, + start_up::Union{TimeSeriesKey, StartUpStages}, + shut_down, + incremental_offer_curves, + decremental_offer_curves, + ancillary_service_offers, +) = + MarketBidCost( + Float64(no_load_cost), + start_up, + shut_down, + incremental_offer_curves, + decremental_offer_curves, + ancillary_service_offers, + ) + # Constructor for demo purposes; non-functional. function MarketBidCost(::Nothing) MarketBidCost(; diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index 680a1eec46..1596f36557 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -149,6 +149,8 @@ test_costs = Dict( repeat([make_market_bid_curve([2.0, 3.0], [4.0, 6.0])], 24), Float64 => collect(11.0:34.0), + PSY.StartUpStages => + repeat([(hot = PSY.START_COST, warm = PSY.START_COST, cold = PSY.START_COST)], 24), ) @testset "Test MarketBidCost with Quadratic Cost Timeseries" begin @@ -262,10 +264,62 @@ end resolution = Dates.Hour(1) horizon = 24 data_float = SortedDict(initial_time => test_costs[Float64]) - forecast = IS.Deterministic("variable_cost", data_float, resolution) + forecast = IS.Deterministic("fuel_cost", data_float, resolution) set_fuel_cost!(sys, generator, forecast) 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 @test first(TimeSeries.values(fuel_forecast)) == first(data_float[initial_time]) end +@testset "Test no-load cost (scalar and time series)" begin + sys = PSB.build_system(PSITestSystems, "test_RTS_GMLC_sys") + generators = collect(get_components(ThermalStandard, sys)) + generator = get_component(ThermalStandard, sys, "322_CT_6") + market_bid = MarketBidCost(nothing) + set_operation_cost!(generator, market_bid) + + op_cost = get_operation_cost(generator) + @test get_no_load_cost(generator, op_cost) == 0.0 + + set_no_load_cost!(sys, generator, 1.23) + @test get_no_load_cost(generator, op_cost) == 1.23 + + initial_time = Dates.DateTime("2020-01-01") + resolution = Dates.Hour(1) + horizon = 24 + data_float = SortedDict(initial_time => test_costs[Float64]) + forecast = IS.Deterministic("no_load_cost", data_float, resolution) + + set_no_load_cost!(sys, generator, forecast) + @test first(TimeSeries.values(get_no_load_cost(generator, op_cost))) == + first(data_float[initial_time]) +end + +@testset "Test startup cost (tuple and time series)" begin + sys = PSB.build_system(PSITestSystems, "test_RTS_GMLC_sys") + generators = collect(get_components(ThermalStandard, sys)) + generator = get_component(ThermalStandard, sys, "322_CT_6") + market_bid = MarketBidCost(nothing) + set_operation_cost!(generator, market_bid) + + op_cost = get_operation_cost(generator) + @test get_start_up(generator, op_cost) == + (hot = PSY.START_COST, warm = PSY.START_COST, cold = PSY.START_COST) + + set_start_up!(sys, generator, (hot = 1.23, warm = 2.34, cold = 3.45)) + @test get_start_up(generator, op_cost) == (hot = 1.23, warm = 2.34, cold = 3.45) + + initial_time = Dates.DateTime("2020-01-01") + resolution = Dates.Hour(1) + horizon = 24 + data_sus = SortedDict(initial_time => test_costs[PSY.StartUpStages]) + forecast = IS.Deterministic( + "start_up", + Dict(k => Tuple.(v) for (k, v) in pairs(data_sus)), + resolution, + ) + + set_start_up!(sys, generator, forecast) + @test first(TimeSeries.values(get_start_up(generator, op_cost))) == + first(data_sus[initial_time]) +end