From e73ab199c4c50e841d03d7d802b7d8a479c762ca Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 29 Oct 2025 13:52:37 -0400 Subject: [PATCH 1/6] penalty relaxation for nonlinear constraints --- src/Utilities/penalty_relaxation.jl | 80 +++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index ed414225fb..ce4facce54 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -138,6 +138,86 @@ function MOI.modify( return one(T) * z end +function MOI.modify( + model::MOI.ModelLike, + ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,<:MOI.AbstractScalarSet}, + relax::ScalarPenaltyRelaxation{T}, +) where {T} + sense = _change_sense_to_min_if_necessary(T, model) + y = MOI.add_variable(model) + z = MOI.add_variable(model) + MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) + MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) + func = MOI.get(model, MOI.ConstraintFunction(), ci) + set = MOI.get(model, MOI.ConstraintSet(), ci) + newfunc = MOI.ScalarNonlinearFunction(:+, [func, (one(T) * y - one(T) * z)]) + MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) + a = scale * relax.penalty + O = MOI.get(model, MOI.ObjectiveFunctionType()) + obj = MOI.get(model, MOI.ObjectiveFunction{O}()) + obj = MOI.ObjectiveFunction{O}() + MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) + MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) + # This causes problems with other methods trying to modify the objective + # function. + # To support existing nonlinear objectives as well as linear/quadratic objectives, + # we just turn any objective into a ScalarNonlinearFunction + #newobj = MOI.ScalarNonlinearFunction(:+, [obj, a * y + a * z]) + #MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(), newobj) + return one(T) * y + one(T) * z +end + +function MOI.modify( + model::MOI.ModelLike, + ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,MOI.GreaterThan{T}}, + relax::ScalarPenaltyRelaxation{T}, +) where {T} + sense = _change_sense_to_min_if_necessary(T, model) + y = MOI.add_variable(model) + MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) + func = MOI.get(model, MOI.ConstraintFunction(), ci) + set = MOI.get(model, MOI.ConstraintSet(), ci) + newfunc = MOI.ScalarNonlinearFunction(:+, [func, y]) + MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) + a = scale * relax.penalty + O = MOI.get(model, MOI.ObjectiveFunctionType()) + obj = MOI.get(model, MOI.ObjectiveFunction{O}()) + MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) + # This causes problems. TODO: Revisit. + # To support existing nonlinear objectives as well as linear/quadratic objectives, + # we just turn any objective into a ScalarNonlinearFunction + #newobj = MOI.ScalarNonlinearFunction(:+, [obj, a * y]) + #MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(), newobj) + return one(T) * y +end + +function MOI.modify( + model::MOI.ModelLike, + ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,MOI.LessThan{T}}, + relax::ScalarPenaltyRelaxation{T}, +) where {T} + sense = _change_sense_to_min_if_necessary(T, model) + z = MOI.add_variable(model) + MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) + func = MOI.get(model, MOI.ConstraintFunction(), ci) + set = MOI.get(model, MOI.ConstraintSet(), ci) + newfunc = MOI.ScalarNonlinearFunction(:-, [func, z]) + MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) + a = scale * relax.penalty + O = MOI.get(model, MOI.ObjectiveFunctionType()) + obj = MOI.get(model, MOI.ObjectiveFunction{O}()) + MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) + # This causes problems. TODO: Revisit. + # To support existing nonlinear objectives as well as linear/quadratic objectives, + # we just turn any objective into a ScalarNonlinearFunction + #newobj = MOI.ScalarNonlinearFunction(:+, [obj, a * z]) + #MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(), newobj) + return one(T) * z +end + """ PenaltyRelaxation( penalties = Dict{MOI.ConstraintIndex,Float64}(); From 57f65b66e964620014028e869d997c4b4dfb091b Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 29 Oct 2025 14:55:05 -0400 Subject: [PATCH 2/6] bugfix: use ObjectiveFunction instead of the actual function --- src/Utilities/penalty_relaxation.jl | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index ce4facce54..fc85c600fd 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -155,14 +155,15 @@ function MOI.modify( scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) a = scale * relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.get(model, MOI.ObjectiveFunction{O}()) obj = MOI.ObjectiveFunction{O}() + # This breaks if the objective is a VariableIndex or ScalarNonlinearFunction MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) - # This causes problems with other methods trying to modify the objective - # function. + # The following causes problems with other methods trying to modify the objective + # function. We would have to branch/dispatch on objective type # To support existing nonlinear objectives as well as linear/quadratic objectives, # we just turn any objective into a ScalarNonlinearFunction + #obj = MOI.get(model, MOI.ObjectiveFunction{O}()) #newobj = MOI.ScalarNonlinearFunction(:+, [obj, a * y + a * z]) #MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(), newobj) return one(T) * y + one(T) * z @@ -183,13 +184,8 @@ function MOI.modify( scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) a = scale * relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.get(model, MOI.ObjectiveFunction{O}()) + obj = MOI.ObjectiveFunction{O}() MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) - # This causes problems. TODO: Revisit. - # To support existing nonlinear objectives as well as linear/quadratic objectives, - # we just turn any objective into a ScalarNonlinearFunction - #newobj = MOI.ScalarNonlinearFunction(:+, [obj, a * y]) - #MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(), newobj) return one(T) * y end @@ -208,13 +204,8 @@ function MOI.modify( scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) a = scale * relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.get(model, MOI.ObjectiveFunction{O}()) + obj = MOI.ObjectiveFunction{O}() MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) - # This causes problems. TODO: Revisit. - # To support existing nonlinear objectives as well as linear/quadratic objectives, - # we just turn any objective into a ScalarNonlinearFunction - #newobj = MOI.ScalarNonlinearFunction(:+, [obj, a * z]) - #MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(), newobj) return one(T) * z end From e8b0e45f456f10c9269736eefac35950c756656c Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 29 Oct 2025 16:10:16 -0400 Subject: [PATCH 3/6] remove unused code --- src/Utilities/penalty_relaxation.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index fc85c600fd..2f530bd6fe 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -149,7 +149,6 @@ function MOI.modify( MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) func = MOI.get(model, MOI.ConstraintFunction(), ci) - set = MOI.get(model, MOI.ConstraintSet(), ci) newfunc = MOI.ScalarNonlinearFunction(:+, [func, (one(T) * y - one(T) * z)]) MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) @@ -178,7 +177,6 @@ function MOI.modify( y = MOI.add_variable(model) MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) func = MOI.get(model, MOI.ConstraintFunction(), ci) - set = MOI.get(model, MOI.ConstraintSet(), ci) newfunc = MOI.ScalarNonlinearFunction(:+, [func, y]) MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) @@ -198,7 +196,6 @@ function MOI.modify( z = MOI.add_variable(model) MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) func = MOI.get(model, MOI.ConstraintFunction(), ci) - set = MOI.get(model, MOI.ConstraintSet(), ci) newfunc = MOI.ScalarNonlinearFunction(:-, [func, z]) MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) From e29c4511faceb2b0eb4c0b59bbae5a4c03c95a35 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 30 Oct 2025 11:50:46 +1300 Subject: [PATCH 4/6] Add tests --- src/Utilities/penalty_relaxation.jl | 108 +++++++++++++++------------ test/Utilities/penalty_relaxation.jl | 85 +++++++++++++++++++++ 2 files changed, 144 insertions(+), 49 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index 2f530bd6fe..21867e8a16 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -81,6 +81,43 @@ function _change_sense_to_min_if_necessary( return MOI.MIN_SENSE end +function _add_penalty_to_objective( + model::MOI.ModelLike, + ::Type{F}, + penalty::T, + x::Vector{MOI.VariableIndex}, +) where {T,F<:MOI.VariableIndex} + g = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(penalty, x), zero(T)) + f = MOI.get(model, MOI.ObjectiveFunction{F}()) + push!(g.terms, MOI.ScalarAffineTerm(one(T), f)) + MOI.set(model, MOI.ObjectiveFunction{typeof(g)}(), g) + return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T)) +end + +function _add_penalty_to_objective( + model::MOI.ModelLike, + ::Type{F}, + penalty::T, + x::Vector{MOI.VariableIndex}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + obj = MOI.ObjectiveFunction{F}() + for xi in x + MOI.modify(model, obj, MOI.ScalarCoefficientChange(xi, penalty)) + end + return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T)) +end + +function _add_penalty_to_objective( + ::MOI.ModelLike, + ::Type{F}, + ::T, + ::Vector{MOI.VariableIndex}, +) where {T,F} + return error( + "Cannot perform `ScalarPenaltyRelaxation` with an objective function of type `$F`", + ) +end + function MOI.modify( model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, @@ -93,13 +130,9 @@ function MOI.modify( MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) - scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) - a = scale * relax.penalty + penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.ObjectiveFunction{O}() - MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) - MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) - return one(T) * y + one(T) * z + return _add_penalty_to_objective(model, O, penalty, [y, z]) end function MOI.modify( @@ -112,12 +145,9 @@ function MOI.modify( y = MOI.add_variable(model) MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) - scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) - a = scale * relax.penalty + penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.ObjectiveFunction{O}() - MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) - return one(T) * y + return _add_penalty_to_objective(model, O, penalty, [y]) end function MOI.modify( @@ -130,42 +160,28 @@ function MOI.modify( z = MOI.add_variable(model) MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) - scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) - a = scale * relax.penalty + penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.ObjectiveFunction{O}() - MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) - return one(T) * z + return _add_penalty_to_objective(model, O, penalty, [z]) end function MOI.modify( model::MOI.ModelLike, - ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,<:MOI.AbstractScalarSet}, + ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,S}, relax::ScalarPenaltyRelaxation{T}, -) where {T} +) where {T,S<:MOI.AbstractScalarSet} sense = _change_sense_to_min_if_necessary(T, model) - y = MOI.add_variable(model) - z = MOI.add_variable(model) - MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) - MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) - func = MOI.get(model, MOI.ConstraintFunction(), ci) - newfunc = MOI.ScalarNonlinearFunction(:+, [func, (one(T) * y - one(T) * z)]) - MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) - scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) - a = scale * relax.penalty + y, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T))) + z, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T))) + f = MOI.get(model, MOI.ConstraintFunction(), ci) + f = MOI.ScalarNonlinearFunction( + :+, + Any[f, y, MOI.ScalarNonlinearFunction(:-, Any[z])], + ) + MOI.set(model, MOI.ConstraintFunction(), ci, f) + penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.ObjectiveFunction{O}() - # This breaks if the objective is a VariableIndex or ScalarNonlinearFunction - MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) - MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) - # The following causes problems with other methods trying to modify the objective - # function. We would have to branch/dispatch on objective type - # To support existing nonlinear objectives as well as linear/quadratic objectives, - # we just turn any objective into a ScalarNonlinearFunction - #obj = MOI.get(model, MOI.ObjectiveFunction{O}()) - #newobj = MOI.ScalarNonlinearFunction(:+, [obj, a * y + a * z]) - #MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(), newobj) - return one(T) * y + one(T) * z + return _add_penalty_to_objective(model, O, penalty, [y, z]) end function MOI.modify( @@ -179,12 +195,9 @@ function MOI.modify( func = MOI.get(model, MOI.ConstraintFunction(), ci) newfunc = MOI.ScalarNonlinearFunction(:+, [func, y]) MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) - scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) - a = scale * relax.penalty + penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.ObjectiveFunction{O}() - MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) - return one(T) * y + return _add_penalty_to_objective(model, O, penalty, [y]) end function MOI.modify( @@ -198,12 +211,9 @@ function MOI.modify( func = MOI.get(model, MOI.ConstraintFunction(), ci) newfunc = MOI.ScalarNonlinearFunction(:-, [func, z]) MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) - scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) - a = scale * relax.penalty + penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.ObjectiveFunction{O}() - MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) - return one(T) * z + return _add_penalty_to_objective(model, O, penalty, [z]) end """ diff --git a/test/Utilities/penalty_relaxation.jl b/test/Utilities/penalty_relaxation.jl index ae6c839f4b..42cda1740b 100644 --- a/test/Utilities/penalty_relaxation.jl +++ b/test/Utilities/penalty_relaxation.jl @@ -84,6 +84,23 @@ function test_relax_no_warn() return end +function test_relax_variable_index_objective() + _test_roundtrip( + """ + variables: x, y + minobjective: x + c1: x + y <= 1.0 + """, + """ + variables: x, y, a + minobjective: 1.0 * x + 1.0 * a + c1: x + y + -1.0 * a <= 1.0 + a >= 0.0 + """, + ) + return +end + function test_relax_affine_lessthan() _test_roundtrip( """ @@ -238,6 +255,58 @@ function test_relax_quadratic_greaterthanthan() return end +function test_relax_scalarnonlinear_lessthan() + _test_roundtrip( + """ + variables: x + maxobjective: 1.0 * x + c1: ScalarNonlinearFunction(log(x)) <= 1.0 + """, + """ + variables: x, a + maxobjective: 1.0 * x + -1.0 * a + c1: ScalarNonlinearFunction(log(x) - a) <= 1.0 + a >= 0.0 + """, + ) + return +end + +function test_relax_scalarnonlinear_greaterthan() + _test_roundtrip( + """ + variables: x + maxobjective: 1.0 * x + c1: ScalarNonlinearFunction(log(x)) >= 1.0 + """, + """ + variables: x, a + maxobjective: 1.0 * x + -1.0 * a + c1: ScalarNonlinearFunction(log(x) + a) >= 1.0 + a >= 0.0 + """, + ) + return +end + +function test_relax_scalarnonlinear_equalto() + _test_roundtrip( + """ + variables: x + minobjective: 1.0 * x + c1: ScalarNonlinearFunction(log(x)) == 1.0 + """, + """ + variables: x, a, b + minobjective: 1.0 * x + 1.0 * a + 1.0 * b + c1: ScalarNonlinearFunction(+(log(x), a, -b)) == 1.0 + a >= 0.0 + b >= 0.0 + """, + ) + return +end + function test_penalty_dict() model = MOI.Utilities.Model{Float64}() x = MOI.add_variable(model) @@ -373,6 +442,22 @@ function test_scalar_penalty_relaxation() return end +function test_scalar_penalty_relaxation_vector_objective() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + f = MOI.VectorOfVariables([x]) + MOI.set(model, MOI.ObjectiveFunction{MOI.VectorOfVariables}(), f) + @test_throws( + ErrorException( + "Cannot perform `ScalarPenaltyRelaxation` with an objective function of type `$(typeof(f))`", + ), + MOI.modify(model, c, MOI.Utilities.ScalarPenaltyRelaxation(2.0)), + ) + return +end + end # module TestPenaltyRelaxation.runtests() From 9b6e6b98de846e52daf37c8da595ce551361e5c6 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 30 Oct 2025 12:07:02 +1300 Subject: [PATCH 5/6] Update --- src/Utilities/penalty_relaxation.jl | 13 +++++++++++++ test/Utilities/penalty_relaxation.jl | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index 21867e8a16..1dce8d9241 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -107,6 +107,19 @@ function _add_penalty_to_objective( return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T)) end +function _add_penalty_to_objective( + model::MOI.ModelLike, + ::Type{F}, + penalty::T, + x::Vector{MOI.VariableIndex}, +) where {T,F<:MOI.ScalarNonlinearFunction} + attr = MOI.ObjectiveFunction{F}() + f = MOI.get(model, attr) + g = Any[MOI.ScalarNonlinearFunction(:*, Any[penalty, xi]) for xi in x] + MOI.set(model, attr, MOI.ScalarNonlinearFunction(:+, vcat(f, g))) + return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T)) +end + function _add_penalty_to_objective( ::MOI.ModelLike, ::Type{F}, diff --git a/test/Utilities/penalty_relaxation.jl b/test/Utilities/penalty_relaxation.jl index 42cda1740b..985a327baa 100644 --- a/test/Utilities/penalty_relaxation.jl +++ b/test/Utilities/penalty_relaxation.jl @@ -101,6 +101,23 @@ function test_relax_variable_index_objective() return end +function test_relax_scalar_nonlinear_objective() + _test_roundtrip( + """ + variables: x, y + minobjective: ScalarNonlinearFunction(exp(x)) + c1: x + y <= 1.0 + """, + """ + variables: x, y, a + minobjective: ScalarNonlinearFunction(+(exp(x), *(1.0, a))) + c1: x + y + -1.0 * a <= 1.0 + a >= 0.0 + """, + ) + return +end + function test_relax_affine_lessthan() _test_roundtrip( """ From aaa36d0aa8389ee1cf9a885a32c3ce767dcd510f Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 30 Oct 2025 15:12:37 +1300 Subject: [PATCH 6/6] Update --- src/Utilities/penalty_relaxation.jl | 203 +++++++++++++-------------- test/Utilities/penalty_relaxation.jl | 2 +- 2 files changed, 99 insertions(+), 106 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index 1dce8d9241..9c23a796cc 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -71,162 +71,143 @@ function _change_sense_to_min_if_necessary( ::Type{T}, model::MOI.ModelLike, ) where {T} - sense = MOI.get(model, MOI.ObjectiveSense()) - if sense != MOI.FEASIBILITY_SENSE - return sense + if MOI.get(model, MOI.ObjectiveSense()) != MOI.FEASIBILITY_SENSE + return end MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) f = zero(MOI.ScalarAffineFunction{T}) MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) - return MOI.MIN_SENSE + return end function _add_penalty_to_objective( model::MOI.ModelLike, ::Type{F}, - penalty::T, - x::Vector{MOI.VariableIndex}, -) where {T,F<:MOI.VariableIndex} - g = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(penalty, x), zero(T)) + penalty::MOI.ScalarAffineFunction{T}, +) where { + T, + F<:Union{ + MOI.VariableIndex, + MOI.ScalarAffineFunction{T}, + MOI.ScalarQuadraticFunction{T}, + MOI.ScalarNonlinearFunction, + }, +} f = MOI.get(model, MOI.ObjectiveFunction{F}()) - push!(g.terms, MOI.ScalarAffineTerm(one(T), f)) - MOI.set(model, MOI.ObjectiveFunction{typeof(g)}(), g) - return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T)) -end - -function _add_penalty_to_objective( - model::MOI.ModelLike, - ::Type{F}, - penalty::T, - x::Vector{MOI.VariableIndex}, -) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - obj = MOI.ObjectiveFunction{F}() - for xi in x - MOI.modify(model, obj, MOI.ScalarCoefficientChange(xi, penalty)) + g = if MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE + MOI.Utilities.operate(+, T, f, penalty) + else + MOI.Utilities.operate(-, T, f, penalty) end - return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T)) -end - -function _add_penalty_to_objective( - model::MOI.ModelLike, - ::Type{F}, - penalty::T, - x::Vector{MOI.VariableIndex}, -) where {T,F<:MOI.ScalarNonlinearFunction} - attr = MOI.ObjectiveFunction{F}() - f = MOI.get(model, attr) - g = Any[MOI.ScalarNonlinearFunction(:*, Any[penalty, xi]) for xi in x] - MOI.set(model, attr, MOI.ScalarNonlinearFunction(:+, vcat(f, g))) - return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T)) + MOI.set(model, MOI.ObjectiveFunction{typeof(g)}(), g) + return end function _add_penalty_to_objective( ::MOI.ModelLike, ::Type{F}, - ::T, - ::Vector{MOI.VariableIndex}, -) where {T,F} + ::MOI.ScalarAffineFunction, +) where {F} return error( "Cannot perform `ScalarPenaltyRelaxation` with an objective function of type `$F`", ) end -function MOI.modify( +function _relax_constraint( + ::Type{T}, model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, - relax::ScalarPenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - sense = _change_sense_to_min_if_necessary(T, model) - y = MOI.add_variable(model) - z = MOI.add_variable(model) - MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) - MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) - MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) - MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) - penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty - O = MOI.get(model, MOI.ObjectiveFunctionType()) - return _add_penalty_to_objective(model, O, penalty, [y, z]) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.GreaterThan(zero(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(x[1], one(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(x[2], -one(T))) + return x end -function MOI.modify( +function _relax_constraint( + ::Type{T}, model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, - relax::ScalarPenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - sense = _change_sense_to_min_if_necessary(T, model) - # Performance optimization: we don't need the z relaxation variable. - y = MOI.add_variable(model) - MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) - MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) - penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty - O = MOI.get(model, MOI.ObjectiveFunctionType()) - return _add_penalty_to_objective(model, O, penalty, [y]) + x = MOI.add_variable(model) + MOI.add_constraint(model, x, MOI.GreaterThan(zero(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(x, one(T))) + return [x] end -function MOI.modify( +function _relax_constraint( + ::Type{T}, model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,MOI.LessThan{T}}, - relax::ScalarPenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - sense = _change_sense_to_min_if_necessary(T, model) - # Performance optimization: we don't need the y relaxation variable. - z = MOI.add_variable(model) - MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) - MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) - penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty - O = MOI.get(model, MOI.ObjectiveFunctionType()) - return _add_penalty_to_objective(model, O, penalty, [z]) + x = MOI.add_variable(model) + MOI.add_constraint(model, x, MOI.GreaterThan(zero(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(x, -one(T))) + return [x] end -function MOI.modify( +function _relax_constraint( + ::Type{T}, model::MOI.ModelLike, ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,S}, - relax::ScalarPenaltyRelaxation{T}, ) where {T,S<:MOI.AbstractScalarSet} - sense = _change_sense_to_min_if_necessary(T, model) + x, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T))) y, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T))) - z, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T))) f = MOI.get(model, MOI.ConstraintFunction(), ci) - f = MOI.ScalarNonlinearFunction( + g = MOI.ScalarNonlinearFunction( :+, - Any[f, y, MOI.ScalarNonlinearFunction(:-, Any[z])], + Any[f, x, MOI.ScalarNonlinearFunction(:-, Any[y])], ) - MOI.set(model, MOI.ConstraintFunction(), ci, f) - penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty - O = MOI.get(model, MOI.ObjectiveFunctionType()) - return _add_penalty_to_objective(model, O, penalty, [y, z]) + MOI.set(model, MOI.ConstraintFunction(), ci, g) + return [x, y] end -function MOI.modify( +function _relax_constraint( + ::Type{T}, model::MOI.ModelLike, ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,MOI.GreaterThan{T}}, - relax::ScalarPenaltyRelaxation{T}, ) where {T} - sense = _change_sense_to_min_if_necessary(T, model) - y = MOI.add_variable(model) - MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) - func = MOI.get(model, MOI.ConstraintFunction(), ci) - newfunc = MOI.ScalarNonlinearFunction(:+, [func, y]) - MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) - penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty - O = MOI.get(model, MOI.ObjectiveFunctionType()) - return _add_penalty_to_objective(model, O, penalty, [y]) + x, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T))) + f = MOI.get(model, MOI.ConstraintFunction(), ci) + g = MOI.ScalarNonlinearFunction(:+, [f, x]) + MOI.set(model, MOI.ConstraintFunction(), ci, g) + return [x] end -function MOI.modify( +function _relax_constraint( + ::Type{T}, model::MOI.ModelLike, ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,MOI.LessThan{T}}, - relax::ScalarPenaltyRelaxation{T}, ) where {T} - sense = _change_sense_to_min_if_necessary(T, model) - z = MOI.add_variable(model) - MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) - func = MOI.get(model, MOI.ConstraintFunction(), ci) - newfunc = MOI.ScalarNonlinearFunction(:-, [func, z]) - MOI.set(model, MOI.ConstraintFunction(), ci, newfunc) - penalty = sense == MOI.MIN_SENSE ? relax.penalty : -relax.penalty + x, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T))) + f = MOI.get(model, MOI.ConstraintFunction(), ci) + g = MOI.ScalarNonlinearFunction(:-, [f, x]) + MOI.set(model, MOI.ConstraintFunction(), ci, g) + return [x] +end + +function MOI.modify( + model::MOI.ModelLike, + ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, + relax::ScalarPenaltyRelaxation{T}, +) where { + T, + F<:Union{ + MOI.ScalarAffineFunction{T}, + MOI.ScalarQuadraticFunction{T}, + MOI.ScalarNonlinearFunction, + }, +} + x = _relax_constraint(T, model, ci) + p = MOI.ScalarAffineFunction( + MOI.ScalarAffineTerm.(relax.penalty, x), + zero(T), + ) + _change_sense_to_min_if_necessary(T, model) O = MOI.get(model, MOI.ObjectiveFunctionType()) - return _add_penalty_to_objective(model, O, penalty, [z]) + _add_penalty_to_objective(model, O, p) + return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T)) end """ @@ -361,13 +342,20 @@ end function MOI.modify(model::MOI.ModelLike, relax::PenaltyRelaxation{T}) where {T} map = Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}() + penalty_expr = zero(MOI.ScalarAffineFunction{T}) for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) - _modify_penalty_relaxation(map, model, relax, F, S) + _modify_penalty_relaxation(penalty_expr, map, model, relax, F, S) + end + if !isempty(penalty_expr.terms) + _change_sense_to_min_if_necessary(T, model) + O = MOI.get(model, MOI.ObjectiveFunctionType()) + _add_penalty_to_objective(model, O, penalty_expr) end return map end function _modify_penalty_relaxation( + penalty_expr::MOI.ScalarAffineFunction{T}, map::Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}, model::MOI.ModelLike, relax::PenaltyRelaxation, @@ -380,9 +368,14 @@ function _modify_penalty_relaxation( continue end try - map[ci] = MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty)) + x = _relax_constraint(T, model, ci) + map[ci] = MOI.ScalarAffineFunction( + MOI.ScalarAffineTerm.(one(T), x), + zero(T), + ) + append!(penalty_expr.terms, MOI.ScalarAffineTerm{T}.(penalty, x)) catch err - if err isa MethodError && err.f == MOI.modify + if err isa MethodError && err.f == _relax_constraint if relax.warn @warn( "Skipping PenaltyRelaxation for ConstraintIndex{$F,$S}" diff --git a/test/Utilities/penalty_relaxation.jl b/test/Utilities/penalty_relaxation.jl index 985a327baa..4b19485347 100644 --- a/test/Utilities/penalty_relaxation.jl +++ b/test/Utilities/penalty_relaxation.jl @@ -110,7 +110,7 @@ function test_relax_scalar_nonlinear_objective() """, """ variables: x, y, a - minobjective: ScalarNonlinearFunction(+(exp(x), *(1.0, a))) + minobjective: ScalarNonlinearFunction(+(exp(x), esc(1.0 * a))) c1: x + y + -1.0 * a <= 1.0 a >= 0.0 """,