diff --git a/src/devices_models/devices/common/objective_function/market_bid.jl b/src/devices_models/devices/common/objective_function/market_bid.jl index 635f59e07..a81e67488 100644 --- a/src/devices_models/devices/common/objective_function/market_bid.jl +++ b/src/devices_models/devices/common/objective_function/market_bid.jl @@ -179,6 +179,36 @@ function _get_pwl_cost_expression( ) end +function _get_pwl_cost_expression_decremental(container::OptimizationContainer, + component::T, + time_period::Int, + cost_function::PSY.MarketBidCost, + ::PSY.PiecewiseStepData, + ::U, + ::V) where {T <: PSY.Component, U <: VariableType, + V <: AbstractDeviceFormulation} + decremental_curve = PSY.get_decremental_offer_curves(cost_function) + value_curve = PSY.get_value_curve(decremental_curve) + power_units = PSY.get_power_units(decremental_curve) + cost_component = PSY.get_function_data(value_curve) + base_power = get_base_power(container) + device_base_power = PSY.get_base_power(component) + cost_data_normalized = get_piecewise_incrementalcurve_per_system_unit( + cost_component, + power_units, + base_power, + device_base_power, + ) + resolution = get_resolution(container) + dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR + multiplier = OBJECTIVE_FUNCTION_NEGATIVE * dt + return _get_pwl_cost_expression(container, + component, + time_period, + cost_data_normalized, + multiplier) +end + """ Get cost expression for StepwiseCostReserve """ @@ -275,6 +305,20 @@ end ######## MarketBidCost: Fixed Curves ########## ############################################### +""" +Check if deceremental pwl offer curve is monotonically decreasing. +""" +function _is_convex_decremental(pwl:: PSY.PiecewiseStepData) + y_coords = PSY.get_y_coords(pwl) + for ix in 1:(length(y_coords) - 1) + if y_coords[ix] < y_coords[ix + 1] + @debug y_coords + return false + end + end + return true +end + """ Add PWL cost terms for data coming from the MarketBidCost with a fixed incremental offer curve @@ -320,6 +364,43 @@ function _add_pwl_term!( return pwl_cost_expressions end +function _add_pwl_term_decremental!(container::OptimizationContainer, + component::T, + cost_function::PSY.MarketBidCost, + ::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}, + ::U, + ::V) where {T <: PSY.Component, U <: VariableType, + V <: AbstractDeviceFormulation} + name = PSY.get_name(component) + decremental_offer_curve = PSY.get_decremental_offer_curves(cost_function) + value_curve = PSY.get_value_curve(decremental_offer_curve) + cost_component = PSY.get_function_data(value_curve) + base_power = get_base_power(container) + device_base_power = PSY.get_base_power(component) + power_units = PSY.get_power_units(decremental_offer_curve) + + data = get_piecewise_incrementalcurve_per_system_unit(cost_component, + power_units, + base_power, + device_base_power) + + cost_is_convex = _is_convex_decremental(data) + if !cost_is_convex + error("MarketBidCost for component $(name) is non-convex") + end + + break_points = PSY.get_x_coords(data) + time_steps = get_time_steps(container) + pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end]) + for t in time_steps + _add_pwl_variables!(container, T, name, t, data) + _add_pwl_constraint!(container, component, U(), break_points, t) + pwl_cost = _get_pwl_cost_expression_decremental(container, component, t, cost_function, data, U(), V()) + pwl_cost_expressions[t] = pwl_cost + end + return pwl_cost_expressions +end + ################################################## ########## PWL for StepwiseCostReserve ########## ################################################## @@ -476,7 +557,7 @@ function _add_variable_cost_to_objective!( initial_time = get_initial_time(container) incremental_cost_curves = PSY.get_incremental_offer_curves(cost_function) decremental_cost_curves = PSY.get_decremental_offer_curves(cost_function) - if isnothing(decremental_cost_curves) + if !isnothing(decremental_cost_curves) error("Component $(component_name) is not allowed to participate as a demand.") end #= @@ -544,6 +625,81 @@ function _add_variable_cost_to_objective!( return end +function _add_variable_cost_to_objective!(container::OptimizationContainer, + ::T, + component::PSY.Component, + cost_function::PSY.MarketBidCost, + ::U) where {T <: VariableType, + U <: AbstractControllablePowerLoadFormulation} + component_name = PSY.get_name(component) + @debug "Market Bid" _group = LOG_GROUP_COST_FUNCTIONS component_name + time_steps = get_time_steps(container) + initial_time = get_initial_time(container) + incremental_cost_curves = PSY.get_incremental_offer_curves(cost_function) + decremental_cost_curves = PSY.get_decremental_offer_curves(cost_function) + if !(isnothing(incremental_cost_curves)) + error("Component $(component_name) is not allowed to participate as a supply.") + end + #= + variable_cost_forecast = PSY.get_variable_cost( + component, + op_cost; + start_time = initial_time, + len = length(time_steps), + ) + variable_cost_forecast_values = TimeSeries.values(variable_cost_forecast) + parameter_container = _get_cost_function_parameter_container( + container, + CostFunctionParameter(), + component, + T(), + U(), + eltype(variable_cost_forecast_values), + ) + =# + pwl_cost_expressions = _add_pwl_term_decremental!(container, + component, + cost_function, + decremental_cost_curves, + T(), + U()) + jump_model = get_jump_model(container) + for t in time_steps + #= + set_multiplier!( + parameter_container, + # Using 1.0 here since we want to reuse the existing code that adds the mulitpler + # of base power times the time delta. + 1.0, + component_name, + t, + ) + set_parameter!( + parameter_container, + jump_model, + variable_cost_forecast_values[t], + component_name, + t, + ) + =# + add_to_expression!(container, + ProductionCostExpression, + pwl_cost_expressions[t], + component, + t) + add_to_objective_variant_expression!(container, pwl_cost_expressions[t]) + end + + # Service Cost Bid + #= + ancillary_services = PSY.get_ancillary_service_offers(op_cost) + for service in ancillary_services + _add_service_bid_cost!(container, component, service) + end + =# + return +end + function _add_service_bid_cost!( container::OptimizationContainer, component::PSY.Component, @@ -583,3 +739,59 @@ function _add_service_bid_cost!( end function _add_service_bid_cost!(::OptimizationContainer, ::PSY.Component, ::PSY.Service) end + +function _add_vom_cost_to_objective!( + container::OptimizationContainer, + ::T, + component::PSY.Component, + op_cost::PSY.MarketBidCost, + ::U, +) where {T <: VariableType, U <: AbstractDeviceFormulation} + incremental_cost_curves = PSY.get_incremental_offer_curves(op_cost) + decremental_cost_curves = PSY.get_decremental_offer_curves(op_cost) + power_units = PSY.get_power_units(incremental_cost_curves) + vom_cost = PSY.get_vom_cost(incremental_cost_curves) + multiplier = 1.0 # VOM Cost is always positive + cost_term = PSY.get_proportional_term(vom_cost) + iszero(cost_term) && return + base_power = get_base_power(container) + device_base_power = PSY.get_base_power(component) + cost_term_normalized = get_proportional_cost_per_system_unit(cost_term, + power_units, + base_power, + device_base_power) + for t in get_time_steps(container) + exp = _add_proportional_term!(container, T(), d, cost_term_normalized * multiplier, + t) + add_to_expression!(container, ProductionCostExpression, exp, d, t) + end + return +end + + +function _add_vom_cost_to_objective!(container::OptimizationContainer, + ::T, + component::PSY.Component, + op_cost::PSY.MarketBidCost, + ::U) where {T <: VariableType, + U <: AbstractControllablePowerLoadFormulation} + incremental_cost_curves = PSY.get_incremental_offer_curves(op_cost) + decremental_cost_curves = PSY.get_decremental_offer_curves(op_cost) + power_units = PSY.get_power_units(decremental_cost_curves) + vom_cost = PSY.get_vom_cost(decremental_cost_curves) + multiplier = 1.0 # VOM Cost is always positive + cost_term = PSY.get_proportional_term(vom_cost) + iszero(cost_term) && return + base_power = get_base_power(container) + device_base_power = PSY.get_base_power(component) + cost_term_normalized = get_proportional_cost_per_system_unit(cost_term, + power_units, + base_power, + device_base_power) + for t in get_time_steps(container) + exp = _add_proportional_term!(container, T(), d, cost_term_normalized * multiplier, + t) + add_to_expression!(container, ProductionCostExpression, exp, d, t) + end + return +end \ No newline at end of file diff --git a/src/devices_models/devices/thermal_generation.jl b/src/devices_models/devices/thermal_generation.jl index 6c567c949..63b744ef5 100644 --- a/src/devices_models/devices/thermal_generation.jl +++ b/src/devices_models/devices/thermal_generation.jl @@ -90,6 +90,10 @@ function proportional_cost(container::OptimizationContainer, cost::PSY.ThermalGe return onvar_cost(container, cost, S, T, U, t) + PSY.get_constant_term(PSY.get_vom_cost(PSY.get_variable(cost))) + PSY.get_fixed(cost) end +function proportional_cost(container::OptimizationContainer, cost::PSY.MarketBidCost, S::OnVariable, T::PSY.ThermalGen, U::AbstractThermalFormulation, t::Int) + return proportional_cost(cost, S, T, U) +end + proportional_cost(cost::PSY.MarketBidCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_no_load_cost(cost) proportional_cost(::Union{PSY.MarketBidCost, PSY.ThermalGenerationCost}, ::Union{RateofChangeConstraintSlackUp, RateofChangeConstraintSlackDown}, ::PSY.ThermalGen, ::AbstractThermalFormulation) = CONSTRAINT_VIOLATION_SLACK_COST diff --git a/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl index 587786272..f85c7169d 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/powersystems_utils.jl @@ -360,3 +360,7 @@ function get_deterministic_time_series_type(sys::PSY.System) ) end end + + +"""Overload get_variable for MarketBidCost. Returns nothing""" +PSY.get_variable(value::PSY.MarketBidCost) = nothing \ No newline at end of file diff --git a/test/test_device_load_constructors.jl b/test/test_device_load_constructors.jl index b36d16f86..5206913da 100644 --- a/test/test_device_load_constructors.jl +++ b/test/test_device_load_constructors.jl @@ -1,3 +1,5 @@ +test_path = mktempdir() + @testset "StaticPowerLoad" begin models = [StaticPowerLoad, PowerLoadDispatch, PowerLoadInterruption] c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") @@ -37,6 +39,66 @@ end end end +@testset "PowerLoadDispatch AC- PF with MarketBidCost Invalid" begin + models = [PowerLoadDispatch] + c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") + iloadbus4 = get_component(InterruptiblePowerLoad, c_sys5_il, "IloadBus4") + set_operation_cost!( + iloadbus4, + MarketBidCost( + no_load_cost=0.0, + start_up=(hot=0.0, warm=0.0, cold=0.0), + shut_down=0.0, + incremental_offer_curves = make_market_bid_curve( + [0.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0], + [25.0, 25.5, 26.0, 27.0, 28.0, 30.0], + 0.0 + ) + ) + ) + networks = [ACPPowerModel] + for m in models, n in networks + device_model = DeviceModel(InterruptiblePowerLoad, m) + model = DecisionModel(MockOperationProblem, n, c_sys5_il) + @test_throws ErrorException mock_construct_device!(model, device_model) + end +end + +@testset "PowerLoadDispatch AC- PF with MarketBidCost" begin + c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") + iloadbus4 = get_component(InterruptiblePowerLoad, c_sys5_il, "IloadBus4") + set_operation_cost!( + iloadbus4, + MarketBidCost( + no_load_cost=0.0, + start_up=(hot=0.0, warm=0.0, cold=0.0), + shut_down=0.0, + decremental_offer_curves = make_market_bid_curve( + [0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0], + [90.0, 85.0, 75.0, 70.0, 60.0, 50.0, 45.0, 40.0, 30.0, 25.0], + 0.0 + ) + ) + ) + template = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) + set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment) + set_device_model!(template, InterruptiblePowerLoad, PowerLoadDispatch) + model = DecisionModel(template, + c_sys5_il; + name = "UC_fixed_market_bid_cost", + optimizer = HiGHS_optimizer, + system_to_file = false, + optimizer_solve_log_print = true) + @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT + @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED + results = OptimizationProblemResults(model) + expr = read_expression(results, "ProductionCostExpression__InterruptiblePowerLoad") + p_l = read_variable(results, "ActivePowerVariable__InterruptiblePowerLoad") + index = findfirst(row -> isapprox(100, row; atol = 1e-6), p_l.IloadBus4) + calculated_cost = expr[index, "IloadBus4"][1] + @test isapprox(-5700, calculated_cost; atol=1) +end + @testset "PowerLoadInterruption DC- PF" begin models = [PowerLoadInterruption] c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") diff --git a/test/test_device_thermal_generation_constructors.jl b/test/test_device_thermal_generation_constructors.jl index 458f060c9..e3a38aaab 100644 --- a/test/test_device_thermal_generation_constructors.jl +++ b/test/test_device_thermal_generation_constructors.jl @@ -68,12 +68,10 @@ end end =# -#= -#TODO: This test +#TODO: timeseries market_bid_cost @testset "Test Thermal Generation MarketBidCost models" begin - test_cases = [ - ("fixed_market_bid_cost", 20532.76), - #"market_bid_cost", + test_cases = [("fixed_market_bid_cost", 20772.76) + #"market_bid_cost", ] for (i, cost_reference) in test_cases @testset "$i" begin @@ -95,11 +93,10 @@ end expr = read_expression(results, "ProductionCostExpression__ThermalStandard") var_unit_cost = sum(expr[!, "Test Unit1"]) @test isapprox(var_unit_cost, cost_reference; atol = 1) - @test expr[!, "Test Unit1"][end] == 0.0 + @test expr[!, "Test Unit2"][end] == 50.0 end end end -=# ################################### Unit Commitment tests ################################## @testset "Thermal UC With DC - PF" begin