From 9515abb98ab666568fc851e3011b9bff4f1724bc Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 3 Dec 2025 11:50:39 +1300 Subject: [PATCH 1/8] Add MOI.LagrangeMultipliers attribute --- src/Test/test_nonlinear.jl | 130 +++++++++++++++++++++++++++++++++ src/Utilities/mockoptimizer.jl | 20 ++++- src/attributes.jl | 65 +++++++++++++++++ 3 files changed, 214 insertions(+), 1 deletion(-) diff --git a/src/Test/test_nonlinear.jl b/src/Test/test_nonlinear.jl index 609dccb5bc..dfaf5c01ab 100644 --- a/src/Test/test_nonlinear.jl +++ b/src/Test/test_nonlinear.jl @@ -2382,3 +2382,133 @@ function setup_test( end version_added(::typeof(test_vector_nonlinear_oracle_no_hessian)) = v"1.46.0" + +function test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE( + model::MOI.ModelLike, + config::MOI.Test.Config{T}, +) where {T} + @requires _supports(config, MOI.optimize!) + @requires _supports(config, MOI.ConstraintDual) + @requires _supports(config, MOI.LagrangeMultipliers) + @requires MOI.supports_constraint( + model, + MOI.VectorOfVariables, + MOI.VectorNonlinearOracle{T}, + ) + set = MOI.VectorNonlinearOracle(; + dimension = 2, + l = T[typemin(T)], + u = T[1], + eval_f = (ret, x) -> (ret[1] = x[1]^2 + x[2]^2), + jacobian_structure = [(1, 1), (1, 2)], + eval_jacobian = (ret, x) -> ret .= T(2) .* x, + hessian_lagrangian_structure = [(1, 1), (2, 2)], + eval_hessian_lagrangian = (ret, x, u) -> ret .= T(2) .* u[1], + ) + x = MOI.add_variables(model, 2) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + f = one(T) * x[1] + one(T) * x[2] + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set) + MOI.optimize!(model) + y = T(1) / sqrt(T(2)) + @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), [y, y], config) + @test isapprox(MOI.get(model, MOI.ConstraintDual(), c), T[-1, -1], config) + @test isapprox(MOI.get(model, MOI.LagrangeMultipliers(), c), T[-y]) + return +end + +function setup_test( + ::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE), + model::MOIU.MockOptimizer, + config::Config{T}, +) where {T} + F, S = MOI.VectorOfVariables, MOI.VectorNonlinearOracle{T} + y = T(1) / sqrt(T(2)) + MOI.Utilities.set_mock_optimize!( + model, + mock -> begin + MOI.Utilities.mock_optimize!( + mock, + config.optimal_status, + T[y, y], + (F, S) => [T[-1, -1]], + ) + ci = only(MOI.get(mock, MOI.ListOfConstraintIndices{F,S}())) + MOI.set(mock, MOI.LagrangeMultipliers(), ci, T[-y]) + end, + ) + model.eval_variable_constraint_dual = false + return () -> model.eval_variable_constraint_dual = true +end + +function version_added( + ::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE), +) + return v"1.47.0" +end + +function test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE( + model::MOI.ModelLike, + config::MOI.Test.Config{T}, +) where {T} + @requires _supports(config, MOI.optimize!) + @requires _supports(config, MOI.ConstraintDual) + @requires _supports(config, MOI.LagrangeMultipliers) + @requires MOI.supports_constraint( + model, + MOI.VectorOfVariables, + MOI.VectorNonlinearOracle{T}, + ) + set = MOI.VectorNonlinearOracle(; + dimension = 2, + l = T[-1], + u = T[typemax(T)], + eval_f = (ret, x) -> (ret[1] = -x[1]^2 - x[2]^2), + jacobian_structure = [(1, 1), (1, 2)], + eval_jacobian = (ret, x) -> ret .= -T(2) .* x, + hessian_lagrangian_structure = [(1, 1), (2, 2)], + eval_hessian_lagrangian = (ret, x, u) -> ret .= -T(2) .* u[1], + ) + x = MOI.add_variables(model, 2) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + f = one(T) * x[1] + one(T) * x[2] + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set) + MOI.optimize!(model) + y = T(1) / sqrt(T(2)) + @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), [-y, -y], config) + @test isapprox(MOI.get(model, MOI.ConstraintDual(), c), T[1, 1], config) + @test isapprox(MOI.get(model, MOI.LagrangeMultipliers(), c), T[y]) + return +end + +function setup_test( + ::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE), + model::MOIU.MockOptimizer, + config::Config{T}, +) where {T} + F, S = MOI.VectorOfVariables, MOI.VectorNonlinearOracle{T} + y = T(1) / sqrt(T(2)) + MOI.Utilities.set_mock_optimize!( + model, + mock -> begin + MOI.Utilities.mock_optimize!( + mock, + config.optimal_status, + T[-y, -y], + (F, S) => [T[1, 1]], + ) + ci = only(MOI.get(mock, MOI.ListOfConstraintIndices{F,S}())) + MOI.set(mock, MOI.LagrangeMultipliers(), ci, T[y]) + end, + ) + model.eval_variable_constraint_dual = false + return () -> model.eval_variable_constraint_dual = true +end + +function version_added( + ::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE), +) + return v"1.47.0" +end diff --git a/src/Utilities/mockoptimizer.jl b/src/Utilities/mockoptimizer.jl index 7c1d8b394f..45c62a4dea 100644 --- a/src/Utilities/mockoptimizer.jl +++ b/src/Utilities/mockoptimizer.jl @@ -76,6 +76,10 @@ mutable struct MockOptimizer{MT<:MOI.ModelLike,T} <: MOI.AbstractOptimizer Dict{Int,MOI.BasisStatusCode}, } variable_basis_status::Dict{MOI.VariableIndex,Dict{Int,MOI.BasisStatusCode}} + constraint_attributes::Dict{ + MOI.AbstractConstraintAttribute, + Dict{MOI.ConstraintIndex,Any}, + } end function MockOptimizer( @@ -133,6 +137,10 @@ function MockOptimizer( # Basis status Dict{MOI.ConstraintIndex,Dict{Int,MOI.BasisStatusCode}}(), Dict{MOI.VariableIndex,Dict{Int,MOI.BasisStatusCode}}(), + Dict{ + MOI.AbstractConstraintAttribute, + Dict{MOI.ConstraintIndex,Any}, + }(), ) end @@ -421,7 +429,14 @@ function MOI.set( idx::MOI.ConstraintIndex, value, ) - MOI.set(mock.inner_model, attr, xor_index(idx), value) + if MOI.is_set_by_optimize(attr) + ret = get!(mock.constraint_attributes, attr) do + return Dict{MOI.ConstraintIndex,Any}() + end + ret[idx] = value + else + MOI.set(mock.inner_model, attr, xor_index(idx), value) + end return end @@ -660,6 +675,9 @@ function MOI.get( ) # If it is thrown by `mock.inner_model`, the index will be xor'ed. MOI.throw_if_not_valid(mock, idx) + if MOI.is_set_by_optimize(attr) + return mock.constraint_attributes[attr][idx] + end return MOI.get(mock.inner_model, attr, xor_index(idx)) end diff --git a/src/attributes.jl b/src/attributes.jl index e3754c493c..8c70725a14 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -3272,6 +3272,70 @@ function get_fallback( return supports_constraint(model, F, S) ? 0.0 : Inf end +""" + LagrangeMultipliers(result_index::Int = 1) + +An [`AbstractConstraintAttribute`](@ref) for the Lagrange multipliers associated +with a constraint. + +## Relationship to `ConstraintDual` + +In most cases, the value of this attribute is equivalent to +[`ConstraintDual`](@ref), and querying the value of [`LagrangeMultipliers`](@ref) +will fallback to querying the value of [`ConstraintDual`](@ref). + +The attribute values differ in one important case. + +When there is a [`VectorNonlinearOracle`](@ref) constraint of the form: +```math +x \\in VectorNonlinearOracle +``` +the associated [`ConstraintDual`](@ref) is ``\\mu^\\top \\nabla f(x)``, and the +value of [`LagrangeMultipliers`](@ref) is the vector ``\\mu`` directly. + +Both values are useful in different circumstances. + +## DualStatus + +Before quering this attribute you should first check [`DualStatus`](@ref) to +confirm that a dual solution is avaiable. + +If the [`DualStatus`](@ref) is [`NO_SOLUTION`](@ref) the result of querying +this attribute is undefined. + +## `result_index` + +The optimizer may return multiple dual solutions. See [`ResultCount`](@ref) +for information on how the results are ordered. + +If the solver does not have a dual value for the constraint because the +`result_index` is beyond the available solutions (whose number is indicated by +the [`ResultCount`](@ref) attribute), getting this attribute must throw a +[`ResultIndexBoundsError`](@ref). + +## Implementation + +Optimizers should implement the following methods: +``` +MOI.get(::Optimizer, ::MOI.LagrangeMultipliers, ::MOI.ConstraintIndex) +``` +They should not implement [`set`](@ref) or [`supports`](@ref). + +""" +struct LagrangeMultipliers <: AbstractConstraintAttribute + result_index::Int + + LagrangeMultipliers(result_index::Int = 1) = new(result_index) +end + +function get_fallback( + model::ModelLike, + attr::LagrangeMultipliers, + ci::ConstraintIndex +) + return get(model, ConstraintDual(attr.result_index), ci) +end + """ is_set_by_optimize(::AnyAttribute) @@ -3330,6 +3394,7 @@ function is_set_by_optimize( ConstraintDual, ConstraintBasisStatus, VariableBasisStatus, + LagrangeMultipliers, }, ) return true From f6aafc3150418b50c87da0b1853b3243b69c87c2 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 3 Dec 2025 12:00:59 +1300 Subject: [PATCH 2/8] Update format --- src/Utilities/mockoptimizer.jl | 5 +---- src/attributes.jl | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Utilities/mockoptimizer.jl b/src/Utilities/mockoptimizer.jl index 45c62a4dea..401df2b21e 100644 --- a/src/Utilities/mockoptimizer.jl +++ b/src/Utilities/mockoptimizer.jl @@ -137,10 +137,7 @@ function MockOptimizer( # Basis status Dict{MOI.ConstraintIndex,Dict{Int,MOI.BasisStatusCode}}(), Dict{MOI.VariableIndex,Dict{Int,MOI.BasisStatusCode}}(), - Dict{ - MOI.AbstractConstraintAttribute, - Dict{MOI.ConstraintIndex,Any}, - }(), + Dict{MOI.AbstractConstraintAttribute,Dict{MOI.ConstraintIndex,Any}}(), ) end diff --git a/src/attributes.jl b/src/attributes.jl index 8c70725a14..db20bcb80c 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -3331,7 +3331,7 @@ end function get_fallback( model::ModelLike, attr::LagrangeMultipliers, - ci::ConstraintIndex + ci::ConstraintIndex, ) return get(model, ConstraintDual(attr.result_index), ci) end From 7f5f8a4d7e8324b31171320dddf310216b503217 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 4 Dec 2025 10:26:24 +1300 Subject: [PATCH 3/8] Update --- src/Test/test_nonlinear.jl | 24 ++++++++++++++++-------- src/attributes.jl | 30 +++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/Test/test_nonlinear.jl b/src/Test/test_nonlinear.jl index dfaf5c01ab..216056c15c 100644 --- a/src/Test/test_nonlinear.jl +++ b/src/Test/test_nonlinear.jl @@ -2389,7 +2389,7 @@ function test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE( ) where {T} @requires _supports(config, MOI.optimize!) @requires _supports(config, MOI.ConstraintDual) - @requires _supports(config, MOI.LagrangeMultipliers) + @requires _supports(config, MOI.LagrangeMultiplier) @requires MOI.supports_constraint( model, MOI.VectorOfVariables, @@ -2410,11 +2410,15 @@ function test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE( f = one(T) * x[1] + one(T) * x[2] MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set) - MOI.optimize!(model) y = T(1) / sqrt(T(2)) + CI = MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.VectorNonlinearOracle{T}} + if MOI.supports(model, MOI.LagrangeMultiplierStart(), CI) + MOI.set(model, MOI.LagrangeMultiplierStart(), c, T[-y]) + end + MOI.optimize!(model) @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), [y, y], config) @test isapprox(MOI.get(model, MOI.ConstraintDual(), c), T[-1, -1], config) - @test isapprox(MOI.get(model, MOI.LagrangeMultipliers(), c), T[-y]) + @test isapprox(MOI.get(model, MOI.LagrangeMultiplier(), c), T[-y]) return end @@ -2435,7 +2439,7 @@ function setup_test( (F, S) => [T[-1, -1]], ) ci = only(MOI.get(mock, MOI.ListOfConstraintIndices{F,S}())) - MOI.set(mock, MOI.LagrangeMultipliers(), ci, T[-y]) + MOI.set(mock, MOI.LagrangeMultiplier(), ci, T[-y]) end, ) model.eval_variable_constraint_dual = false @@ -2454,7 +2458,7 @@ function test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE( ) where {T} @requires _supports(config, MOI.optimize!) @requires _supports(config, MOI.ConstraintDual) - @requires _supports(config, MOI.LagrangeMultipliers) + @requires _supports(config, MOI.LagrangeMultiplier) @requires MOI.supports_constraint( model, MOI.VectorOfVariables, @@ -2475,11 +2479,15 @@ function test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE( f = one(T) * x[1] + one(T) * x[2] MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set) - MOI.optimize!(model) y = T(1) / sqrt(T(2)) + CI = MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.VectorNonlinearOracle{T}} + if MOI.supports(model, MOI.LagrangeMultiplierStart(), CI) + MOI.set(model, MOI.LagrangeMultiplierStart(), c, T[y]) + end + MOI.optimize!(model) @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), [-y, -y], config) @test isapprox(MOI.get(model, MOI.ConstraintDual(), c), T[1, 1], config) - @test isapprox(MOI.get(model, MOI.LagrangeMultipliers(), c), T[y]) + @test isapprox(MOI.get(model, MOI.LagrangeMultiplier(), c), T[y]) return end @@ -2500,7 +2508,7 @@ function setup_test( (F, S) => [T[1, 1]], ) ci = only(MOI.get(mock, MOI.ListOfConstraintIndices{F,S}())) - MOI.set(mock, MOI.LagrangeMultipliers(), ci, T[y]) + MOI.set(mock, MOI.LagrangeMultiplier(), ci, T[y]) end, ) model.eval_variable_constraint_dual = false diff --git a/src/attributes.jl b/src/attributes.jl index db20bcb80c..d7dfd020d0 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -3273,15 +3273,15 @@ function get_fallback( end """ - LagrangeMultipliers(result_index::Int = 1) + LagrangeMultiplier(result_index::Int = 1) -An [`AbstractConstraintAttribute`](@ref) for the Lagrange multipliers associated +An [`AbstractConstraintAttribute`](@ref) for the Lagrange multiplier associated with a constraint. ## Relationship to `ConstraintDual` In most cases, the value of this attribute is equivalent to -[`ConstraintDual`](@ref), and querying the value of [`LagrangeMultipliers`](@ref) +[`ConstraintDual`](@ref), and querying the value of [`LagrangeMultiplier`](@ref) will fallback to querying the value of [`ConstraintDual`](@ref). The attribute values differ in one important case. @@ -3291,7 +3291,7 @@ When there is a [`VectorNonlinearOracle`](@ref) constraint of the form: x \\in VectorNonlinearOracle ``` the associated [`ConstraintDual`](@ref) is ``\\mu^\\top \\nabla f(x)``, and the -value of [`LagrangeMultipliers`](@ref) is the vector ``\\mu`` directly. +value of [`LagrangeMultiplier`](@ref) is the vector ``\\mu`` directly. Both values are useful in different circumstances. @@ -3317,25 +3317,37 @@ the [`ResultCount`](@ref) attribute), getting this attribute must throw a Optimizers should implement the following methods: ``` -MOI.get(::Optimizer, ::MOI.LagrangeMultipliers, ::MOI.ConstraintIndex) +MOI.get(::Optimizer, ::MOI.LagrangeMultiplier, ::MOI.ConstraintIndex) ``` They should not implement [`set`](@ref) or [`supports`](@ref). """ -struct LagrangeMultipliers <: AbstractConstraintAttribute +struct LagrangeMultiplier <: AbstractConstraintAttribute result_index::Int - LagrangeMultipliers(result_index::Int = 1) = new(result_index) + LagrangeMultiplier(result_index::Int = 1) = new(result_index) end function get_fallback( model::ModelLike, - attr::LagrangeMultipliers, + attr::LagrangeMultiplier, ci::ConstraintIndex, ) return get(model, ConstraintDual(attr.result_index), ci) end +""" + LagrangeMultiplierStart() + +An [`AbstractConstraintAttribute`](@ref) for the initial assignment to the +constraint's [`LagrangeMultiplier`](@ref) that the optimizer may use to +warm-start the solve. + +May be `nothing` (unset), a number for [`AbstractScalarFunction`](@ref), or a +vector for [`AbstractVectorFunction`](@ref). +""" +struct LagrangeMultiplierStart <: AbstractConstraintAttribute end + """ is_set_by_optimize(::AnyAttribute) @@ -3394,7 +3406,7 @@ function is_set_by_optimize( ConstraintDual, ConstraintBasisStatus, VariableBasisStatus, - LagrangeMultipliers, + LagrangeMultiplier, }, ) return true From 94b940f111fda4907574e2445aaa0b715497cd19 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 8 Dec 2025 08:49:54 +1300 Subject: [PATCH 4/8] Remove fallback --- src/attributes.jl | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index d7dfd020d0..f94cb522ac 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -3281,12 +3281,10 @@ with a constraint. ## Relationship to `ConstraintDual` In most cases, the value of this attribute is equivalent to -[`ConstraintDual`](@ref), and querying the value of [`LagrangeMultiplier`](@ref) -will fallback to querying the value of [`ConstraintDual`](@ref). +[`ConstraintDual`](@ref). -The attribute values differ in one important case. - -When there is a [`VectorNonlinearOracle`](@ref) constraint of the form: +The attribute values differ in one important case. When there is a +[`VectorNonlinearOracle`](@ref) constraint of the form: ```math x \\in VectorNonlinearOracle ``` @@ -3321,6 +3319,9 @@ MOI.get(::Optimizer, ::MOI.LagrangeMultiplier, ::MOI.ConstraintIndex) ``` They should not implement [`set`](@ref) or [`supports`](@ref). +Solvers should implement [`LagrangeMultiplier`](@ref) only if they also +implement the [`ConstraintDual`](@ref), and only if if the two values are +different. """ struct LagrangeMultiplier <: AbstractConstraintAttribute result_index::Int @@ -3328,14 +3329,6 @@ struct LagrangeMultiplier <: AbstractConstraintAttribute LagrangeMultiplier(result_index::Int = 1) = new(result_index) end -function get_fallback( - model::ModelLike, - attr::LagrangeMultiplier, - ci::ConstraintIndex, -) - return get(model, ConstraintDual(attr.result_index), ci) -end - """ LagrangeMultiplierStart() From 3e78725618b64c5a31a59aaa29c4ca9799ccd0c5 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 8 Dec 2025 09:02:01 +1300 Subject: [PATCH 5/8] Update src/attributes.jl --- src/attributes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attributes.jl b/src/attributes.jl index f94cb522ac..06d838c8f9 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -3320,7 +3320,7 @@ MOI.get(::Optimizer, ::MOI.LagrangeMultiplier, ::MOI.ConstraintIndex) They should not implement [`set`](@ref) or [`supports`](@ref). Solvers should implement [`LagrangeMultiplier`](@ref) only if they also -implement the [`ConstraintDual`](@ref), and only if if the two values are +implement the [`ConstraintDual`](@ref), and only if the two values are different. """ struct LagrangeMultiplier <: AbstractConstraintAttribute From e7ccff5ccaf71094407bbe7e6327b92d6110e3c4 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 8 Dec 2025 16:15:57 +1300 Subject: [PATCH 6/8] Update --- src/Test/test_nonlinear.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Test/test_nonlinear.jl b/src/Test/test_nonlinear.jl index 216056c15c..cf45425b52 100644 --- a/src/Test/test_nonlinear.jl +++ b/src/Test/test_nonlinear.jl @@ -2419,6 +2419,16 @@ function test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE( @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), [y, y], config) @test isapprox(MOI.get(model, MOI.ConstraintDual(), c), T[-1, -1], config) @test isapprox(MOI.get(model, MOI.LagrangeMultiplier(), c), T[-y]) + # Test `set` just for code coverage + x = T[1, 2] + ret = T[0] + set.eval_f(ret, x) + @test ret == T[5] + ret = T[0, 0] + set.eval_jacobian(ret, x) + @test ret == T[2, 4] + set.eval_hessian_lagrangian(ret, x, T[-1]) + @test ret == [-2, -2] return end @@ -2488,6 +2498,16 @@ function test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE( @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), [-y, -y], config) @test isapprox(MOI.get(model, MOI.ConstraintDual(), c), T[1, 1], config) @test isapprox(MOI.get(model, MOI.LagrangeMultiplier(), c), T[y]) + # Test `set` just for code coverage + x = T[1, 2] + ret = T[0] + set.eval_f(ret, x) + @test ret == T[-5] + ret = T[0, 0] + set.eval_jacobian(ret, x) + @test ret == T[-2, -4] + set.eval_hessian_lagrangian(ret, x, T[-1]) + @test ret == [2, 2] return end From 32a7aa4373f3fe5967a9f8d40eefe39757caedbc Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 9 Dec 2025 10:03:33 +1300 Subject: [PATCH 7/8] Clarify relationship to ConstraintDual in attributes.jl Clarified the relationship between the attribute and ConstraintDual, emphasizing the important case involving VectorNonlinearOracle constraints. --- src/attributes.jl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index 06d838c8f9..f54123b7e3 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -3280,11 +3280,8 @@ with a constraint. ## Relationship to `ConstraintDual` -In most cases, the value of this attribute is equivalent to -[`ConstraintDual`](@ref). - -The attribute values differ in one important case. When there is a -[`VectorNonlinearOracle`](@ref) constraint of the form: +This attribute differs from [`ConstraintDual`](@ref) in one important case. +When there is a [`VectorNonlinearOracle`](@ref) constraint of the form: ```math x \\in VectorNonlinearOracle ``` From 41f6ec745b552b0078f4105c206616b66967ba1e Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 9 Dec 2025 13:22:19 +1300 Subject: [PATCH 8/8] Apply suggestions from code review --- src/Test/test_nonlinear.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test/test_nonlinear.jl b/src/Test/test_nonlinear.jl index cf45425b52..a0dfa89ee2 100644 --- a/src/Test/test_nonlinear.jl +++ b/src/Test/test_nonlinear.jl @@ -2459,7 +2459,7 @@ end function version_added( ::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE), ) - return v"1.47.0" + return v"1.48.0" end function test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE( @@ -2538,5 +2538,5 @@ end function version_added( ::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE), ) - return v"1.47.0" + return v"1.48.0" end