diff --git a/src/PowerSystems.jl b/src/PowerSystems.jl index 69b5e5dbe8..4744c4932b 100644 --- a/src/PowerSystems.jl +++ b/src/PowerSystems.jl @@ -62,6 +62,8 @@ 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 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 46f0a2c53d..49cde14760 100644 --- a/src/models/cost_function_timeseries.jl +++ b/src/models/cost_function_timeseries.jl @@ -1,120 +1,131 @@ -# 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, -) - 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 data + return TimeSeries.TimeArray( + time_stamps, + 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::OperationalCost; - 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_generation_variable_cost(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,8 +134,8 @@ function get_services_bid( start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, ) - variable_ts_key = get_generation_variable_cost(cost) - raw_data = get_time_series( + variable_ts_key = get_incremental_offer_curves(cost) + ts = get_time_series( variable_ts_key.time_series_type, device, get_name(service); @@ -132,114 +143,178 @@ 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 + +""" +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. + +# Arguments +- `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` +""" +_process_set_cost(_, _, _, _, ::Nothing) = nothing + +_process_set_cost(::Type{T}, _, _, _, cost::T) where {T} = cost + +function _process_set_cost( + ::Type{_}, + ::Type{T}, + sys::System, + component::Component, + 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 +# SETTER IMPLEMENTATIONS """ -Adds energy market bid time series to the component's operation cost, which must be a MarketBidCost. +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::IS.TimeSeriesData`: TimeSeriesData +- `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, component::StaticInjection, - time_series_data::IS.TimeSeriesData, + data::Union{Nothing, IS.TimeSeriesData, CostCurve{PiecewiseIncrementalCurve}}, ) - _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)") - - key = add_time_series!(sys, component, time_series_data) - set_incremental_offer_curves!(market_bid_cost, key) - return + to_set = _process_set_cost( + CostCurve{PiecewiseIncrementalCurve}, + PiecewiseStepData, + sys, + component, + data, + ) + set_incremental_offer_curves!(market_bid_cost, to_set) 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 +""" +Adds energy market bids time-series to the ReserveDemandCurve. -function _process_fuel_cost( - component::Component, - ts_key::TimeSeriesKey, - start_time::Union{Nothing, Dates.DateTime}, - len::Union{Nothing, Int}, +# Arguments +- `sys::System`: PowerSystem System +- `component::ReserveDemandCurve`: the curve +- `time_series_data::IS.TimeSeriesData`: TimeSeriesData +""" +function set_variable_cost!( + sys::System, + component::ReserveDemandCurve, + data::Union{Nothing, IS.TimeSeriesData}, ) - 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) + # 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) 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) +"Set the fuel cost of the component's variable cost, which must be a `FuelCurve`." +function set_fuel_cost!( + sys::System, + component::StaticInjection, + data::Union{Float64, IS.TimeSeriesData}, +) + var_cost = _validate_fuel_curve(component) + to_set = _process_set_cost(Float64, Float64, sys, component, data) 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) + FuelCurve(get_value_curve(var_cost), get_power_units(var_cost), to_set) 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)) +""" +Set the no-load cost for a `StaticInjection` device with a `MarketBidCost` to either a single number or a time series. -"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{Float64, IS.TimeSeriesData},`: the data. If a time series, must be of eltype `Float64`. +""" +function set_no_load_cost!( sys::System, component::StaticInjection, - time_series_data::IS.TimeSeriesData, + data::Union{Float64, IS.TimeSeriesData}, ) - _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(Float64, Float64, sys, component, data) + set_no_load_cost!(market_bid_cost, to_set) end """ -Adds energy market bids time-series to the ReserveDemandCurve. +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::IS.TimeSeriesData`: TimeSeriesData +- `time_series_data::Union{StartUpStages, IS.TimeSeriesData},`: the data. If a time series, must be of eltype `NTuple{3, Float64}`. """ -function set_variable_cost!( +function set_start_up!( sys::System, - component::ReserveDemandCurve, - time_series_data::IS.TimeSeriesData, + component::StaticInjection, + data::Union{StartUpStages, IS.TimeSeriesData}, ) - key = add_time_series!(sys, component, time_series_data) - set_variable!(component, key) - return + 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 """ @@ -257,7 +332,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)", @@ -273,24 +350,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 diff --git a/src/models/cost_functions/MarketBidCost.jl b/src/models/cost_functions/MarketBidCost.jl index 5afa2c1896..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(; @@ -104,3 +121,43 @@ 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/src/models/cost_functions/ValueCurves.jl b/src/models/cost_functions/ValueCurves.jl index 5137762006..897f802f5e 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..0e92f1b972 100644 --- a/src/models/cost_functions/variable_cost.jl +++ b/src/models/cost_functions/variable_cost.jl @@ -14,6 +14,13 @@ 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 diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index 4db8a4578b..1596f36557 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -134,16 +134,23 @@ 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), - 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), + PSY.StartUpStages => + repeat([(hot = PSY.START_COST, warm = PSY.START_COST, cold = PSY.START_COST)], 24), ) @testset "Test MarketBidCost with Quadratic Cost Timeseries" begin @@ -155,12 +162,16 @@ 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) @@ -173,25 +184,36 @@ 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 @@ -207,15 +229,19 @@ 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 @@ -238,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