From abba19329bf9784c537167cfa23c8d7e373a67ce Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 3 Dec 2025 11:50:39 +1300 Subject: [PATCH 1/2] 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 f684a4131788d231e14b03b3521f62ba68724df7 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 3 Dec 2025 12:00:59 +1300 Subject: [PATCH 2/2] 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