From 0e861da59ecf70ee952f7c78ad940376d53a3142 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Mon, 8 Jun 2026 15:09:31 -0600 Subject: [PATCH 1/2] address issue 9 --- Project.toml | 2 +- src/common_models/market_bid_plumbing.jl | 3 ++ ...evice_renewable_generation_constructors.jl | 34 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index ca3a0dd..a796060 100644 --- a/Project.toml +++ b/Project.toml @@ -41,7 +41,7 @@ InfrastructureSystems = "3" InteractiveUtils = "1.11.0" JuMP = "^1.28" PowerNetworkMatrices = "^0.19, ^0.20, ^0.22" -PowerSystems = "5.3" +PowerSystems = "5.10" PrettyTables = "3" ProgressMeter = "1.11.0" TimerOutputs = "~0.5" diff --git a/src/common_models/market_bid_plumbing.jl b/src/common_models/market_bid_plumbing.jl index a09e23e..7a23895 100644 --- a/src/common_models/market_bid_plumbing.jl +++ b/src/common_models/market_bid_plumbing.jl @@ -571,6 +571,9 @@ IOM.get_base_power(c::PSY.Component) = PSY.get_base_power(c, PSY.NU) IOM.get_operation_cost(c::PSY.Component) = PSY.get_operation_cost(c) IOM.get_must_run(c::PSY.Component) = PSY.get_must_run(c) IOM.get_active_power_limits(c::PSY.Component) = PSY.get_active_power_limits(c, PSY.SU) +# `RenewableGen` has no `active_power_limits` field: return (0.0, max_active_power) +IOM.get_active_power_limits(c::PSY.RenewableGen) = + (min = 0.0, max = PSY.get_max_active_power(c, PSY.SU)) IOM.get_max_active_power(c::PSY.Component) = PSY.get_max_active_power(c, PSY.SU) IOM.get_ramp_limits(c::PSY.Component) = PSY.get_ramp_limits(c, PSY.SU) IOM.get_start_up(op_cost) = PSY.get_start_up(op_cost) diff --git a/test/test_device_renewable_generation_constructors.jl b/test/test_device_renewable_generation_constructors.jl index 0047ead..2eac571 100644 --- a/test/test_device_renewable_generation_constructors.jl +++ b/test/test_device_renewable_generation_constructors.jl @@ -147,3 +147,37 @@ end IOM.ModelBuildStatus.BUILT @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED end + +@testset "Renewable with quadratic variable cost builds objective (issue #9)" begin + # Regression test: a RenewableDispatch with a quadratic cost curve used to call + # `PSY.get_active_power_limits`, but renewables only have a max active power field, + # not active power limits. + c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") + + formulation = RenewableFullDispatch + quad_re = get_component(RenewableDispatch, c_sys5_re, "WindBusA") + base_cost = get_operation_cost(quad_re) + # A likely bug was surfaced by writing this test: objective_function_multiplier's + # return value for RenewableDispatch. Rather than fixing it in this same PR, + # write the test so that it works either way. + re_mult = POM.objective_function_multiplier(ActivePowerVariable, formulation) + set_operation_cost!( + quad_re, + RenewableGenerationCost(; + variable = CostCurve(QuadraticCurve(re_mult * 2.0, re_mult * 1.0, 0.0)), + curtailment_cost = base_cost.curtailment_cost, + fixed = base_cost.fixed, + ), + ) + + device_model = DeviceModel(RenewableDispatch, formulation) + model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re) + # Before the fix this threw `ArgumentError: get_active_power_limits not implemented + # for RenewableDispatch`; now it constructs a quadratic objective. + mock_construct_device!(model, device_model) + psi_checkobjfun_test(model, GQEVF) + + # Direct check of the bridge that the issue is about. + @test IOM.get_active_power_limits(quad_re) == + (min = 0.0, max = PSY.get_max_active_power(quad_re, PSY.SU)) +end From a6eca67bf59fa49e9fb6ebb34ed32c707793a966 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Mon, 8 Jun 2026 15:21:50 -0600 Subject: [PATCH 2/2] clarify objective function sign: not a bug after all --- .../test_device_renewable_generation_constructors.jl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/test/test_device_renewable_generation_constructors.jl b/test/test_device_renewable_generation_constructors.jl index 2eac571..e25a644 100644 --- a/test/test_device_renewable_generation_constructors.jl +++ b/test/test_device_renewable_generation_constructors.jl @@ -154,23 +154,21 @@ end # not active power limits. c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") - formulation = RenewableFullDispatch quad_re = get_component(RenewableDispatch, c_sys5_re, "WindBusA") base_cost = get_operation_cost(quad_re) - # A likely bug was surfaced by writing this test: objective_function_multiplier's - # return value for RenewableDispatch. Rather than fixing it in this same PR, - # write the test so that it works either way. - re_mult = POM.objective_function_multiplier(ActivePowerVariable, formulation) + # Renewable ActivePowerVariable costs use OBJECTIVE_FUNCTION_NEGATIVE, so the negated + # objective term `-cost(p)` is convex (and free of the non-monotonicity warning) when + # the raw curve is concave, i.e. negative coefficients. set_operation_cost!( quad_re, RenewableGenerationCost(; - variable = CostCurve(QuadraticCurve(re_mult * 2.0, re_mult * 1.0, 0.0)), + variable = CostCurve(QuadraticCurve(-2.0, -1.0, 0.0)), curtailment_cost = base_cost.curtailment_cost, fixed = base_cost.fixed, ), ) - device_model = DeviceModel(RenewableDispatch, formulation) + device_model = DeviceModel(RenewableDispatch, RenewableFullDispatch) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re) # Before the fix this threw `ArgumentError: get_active_power_limits not implemented # for RenewableDispatch`; now it constructs a quadratic objective.