Skip to content

Commit abba193

Browse files
committed
Add MOI.LagrangeMultipliers attribute
1 parent 1effd93 commit abba193

File tree

3 files changed

+214
-1
lines changed

3 files changed

+214
-1
lines changed

src/Test/test_nonlinear.jl

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2382,3 +2382,133 @@ function setup_test(
23822382
end
23832383

23842384
version_added(::typeof(test_vector_nonlinear_oracle_no_hessian)) = v"1.46.0"
2385+
2386+
function test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE(
2387+
model::MOI.ModelLike,
2388+
config::MOI.Test.Config{T},
2389+
) where {T}
2390+
@requires _supports(config, MOI.optimize!)
2391+
@requires _supports(config, MOI.ConstraintDual)
2392+
@requires _supports(config, MOI.LagrangeMultipliers)
2393+
@requires MOI.supports_constraint(
2394+
model,
2395+
MOI.VectorOfVariables,
2396+
MOI.VectorNonlinearOracle{T},
2397+
)
2398+
set = MOI.VectorNonlinearOracle(;
2399+
dimension = 2,
2400+
l = T[typemin(T)],
2401+
u = T[1],
2402+
eval_f = (ret, x) -> (ret[1] = x[1]^2 + x[2]^2),
2403+
jacobian_structure = [(1, 1), (1, 2)],
2404+
eval_jacobian = (ret, x) -> ret .= T(2) .* x,
2405+
hessian_lagrangian_structure = [(1, 1), (2, 2)],
2406+
eval_hessian_lagrangian = (ret, x, u) -> ret .= T(2) .* u[1],
2407+
)
2408+
x = MOI.add_variables(model, 2)
2409+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
2410+
f = one(T) * x[1] + one(T) * x[2]
2411+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
2412+
c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set)
2413+
MOI.optimize!(model)
2414+
y = T(1) / sqrt(T(2))
2415+
@test isapprox(MOI.get(model, MOI.VariablePrimal(), x), [y, y], config)
2416+
@test isapprox(MOI.get(model, MOI.ConstraintDual(), c), T[-1, -1], config)
2417+
@test isapprox(MOI.get(model, MOI.LagrangeMultipliers(), c), T[-y])
2418+
return
2419+
end
2420+
2421+
function setup_test(
2422+
::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE),
2423+
model::MOIU.MockOptimizer,
2424+
config::Config{T},
2425+
) where {T}
2426+
F, S = MOI.VectorOfVariables, MOI.VectorNonlinearOracle{T}
2427+
y = T(1) / sqrt(T(2))
2428+
MOI.Utilities.set_mock_optimize!(
2429+
model,
2430+
mock -> begin
2431+
MOI.Utilities.mock_optimize!(
2432+
mock,
2433+
config.optimal_status,
2434+
T[y, y],
2435+
(F, S) => [T[-1, -1]],
2436+
)
2437+
ci = only(MOI.get(mock, MOI.ListOfConstraintIndices{F,S}()))
2438+
MOI.set(mock, MOI.LagrangeMultipliers(), ci, T[-y])
2439+
end,
2440+
)
2441+
model.eval_variable_constraint_dual = false
2442+
return () -> model.eval_variable_constraint_dual = true
2443+
end
2444+
2445+
function version_added(
2446+
::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE),
2447+
)
2448+
return v"1.47.0"
2449+
end
2450+
2451+
function test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE(
2452+
model::MOI.ModelLike,
2453+
config::MOI.Test.Config{T},
2454+
) where {T}
2455+
@requires _supports(config, MOI.optimize!)
2456+
@requires _supports(config, MOI.ConstraintDual)
2457+
@requires _supports(config, MOI.LagrangeMultipliers)
2458+
@requires MOI.supports_constraint(
2459+
model,
2460+
MOI.VectorOfVariables,
2461+
MOI.VectorNonlinearOracle{T},
2462+
)
2463+
set = MOI.VectorNonlinearOracle(;
2464+
dimension = 2,
2465+
l = T[-1],
2466+
u = T[typemax(T)],
2467+
eval_f = (ret, x) -> (ret[1] = -x[1]^2 - x[2]^2),
2468+
jacobian_structure = [(1, 1), (1, 2)],
2469+
eval_jacobian = (ret, x) -> ret .= -T(2) .* x,
2470+
hessian_lagrangian_structure = [(1, 1), (2, 2)],
2471+
eval_hessian_lagrangian = (ret, x, u) -> ret .= -T(2) .* u[1],
2472+
)
2473+
x = MOI.add_variables(model, 2)
2474+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
2475+
f = one(T) * x[1] + one(T) * x[2]
2476+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
2477+
c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set)
2478+
MOI.optimize!(model)
2479+
y = T(1) / sqrt(T(2))
2480+
@test isapprox(MOI.get(model, MOI.VariablePrimal(), x), [-y, -y], config)
2481+
@test isapprox(MOI.get(model, MOI.ConstraintDual(), c), T[1, 1], config)
2482+
@test isapprox(MOI.get(model, MOI.LagrangeMultipliers(), c), T[y])
2483+
return
2484+
end
2485+
2486+
function setup_test(
2487+
::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE),
2488+
model::MOIU.MockOptimizer,
2489+
config::Config{T},
2490+
) where {T}
2491+
F, S = MOI.VectorOfVariables, MOI.VectorNonlinearOracle{T}
2492+
y = T(1) / sqrt(T(2))
2493+
MOI.Utilities.set_mock_optimize!(
2494+
model,
2495+
mock -> begin
2496+
MOI.Utilities.mock_optimize!(
2497+
mock,
2498+
config.optimal_status,
2499+
T[-y, -y],
2500+
(F, S) => [T[1, 1]],
2501+
)
2502+
ci = only(MOI.get(mock, MOI.ListOfConstraintIndices{F,S}()))
2503+
MOI.set(mock, MOI.LagrangeMultipliers(), ci, T[y])
2504+
end,
2505+
)
2506+
model.eval_variable_constraint_dual = false
2507+
return () -> model.eval_variable_constraint_dual = true
2508+
end
2509+
2510+
function version_added(
2511+
::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE),
2512+
)
2513+
return v"1.47.0"
2514+
end

src/Utilities/mockoptimizer.jl

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ mutable struct MockOptimizer{MT<:MOI.ModelLike,T} <: MOI.AbstractOptimizer
7676
Dict{Int,MOI.BasisStatusCode},
7777
}
7878
variable_basis_status::Dict{MOI.VariableIndex,Dict{Int,MOI.BasisStatusCode}}
79+
constraint_attributes::Dict{
80+
MOI.AbstractConstraintAttribute,
81+
Dict{MOI.ConstraintIndex,Any},
82+
}
7983
end
8084

8185
function MockOptimizer(
@@ -133,6 +137,10 @@ function MockOptimizer(
133137
# Basis status
134138
Dict{MOI.ConstraintIndex,Dict{Int,MOI.BasisStatusCode}}(),
135139
Dict{MOI.VariableIndex,Dict{Int,MOI.BasisStatusCode}}(),
140+
Dict{
141+
MOI.AbstractConstraintAttribute,
142+
Dict{MOI.ConstraintIndex,Any},
143+
}(),
136144
)
137145
end
138146

@@ -421,7 +429,14 @@ function MOI.set(
421429
idx::MOI.ConstraintIndex,
422430
value,
423431
)
424-
MOI.set(mock.inner_model, attr, xor_index(idx), value)
432+
if MOI.is_set_by_optimize(attr)
433+
ret = get!(mock.constraint_attributes, attr) do
434+
return Dict{MOI.ConstraintIndex,Any}()
435+
end
436+
ret[idx] = value
437+
else
438+
MOI.set(mock.inner_model, attr, xor_index(idx), value)
439+
end
425440
return
426441
end
427442

@@ -660,6 +675,9 @@ function MOI.get(
660675
)
661676
# If it is thrown by `mock.inner_model`, the index will be xor'ed.
662677
MOI.throw_if_not_valid(mock, idx)
678+
if MOI.is_set_by_optimize(attr)
679+
return mock.constraint_attributes[attr][idx]
680+
end
663681
return MOI.get(mock.inner_model, attr, xor_index(idx))
664682
end
665683

src/attributes.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3272,6 +3272,70 @@ function get_fallback(
32723272
return supports_constraint(model, F, S) ? 0.0 : Inf
32733273
end
32743274

3275+
"""
3276+
LagrangeMultipliers(result_index::Int = 1)
3277+
3278+
An [`AbstractConstraintAttribute`](@ref) for the Lagrange multipliers associated
3279+
with a constraint.
3280+
3281+
## Relationship to `ConstraintDual`
3282+
3283+
In most cases, the value of this attribute is equivalent to
3284+
[`ConstraintDual`](@ref), and querying the value of [`LagrangeMultipliers`](@ref)
3285+
will fallback to querying the value of [`ConstraintDual`](@ref).
3286+
3287+
The attribute values differ in one important case.
3288+
3289+
When there is a [`VectorNonlinearOracle`](@ref) constraint of the form:
3290+
```math
3291+
x \\in VectorNonlinearOracle
3292+
```
3293+
the associated [`ConstraintDual`](@ref) is ``\\mu^\\top \\nabla f(x)``, and the
3294+
value of [`LagrangeMultipliers`](@ref) is the vector ``\\mu`` directly.
3295+
3296+
Both values are useful in different circumstances.
3297+
3298+
## DualStatus
3299+
3300+
Before quering this attribute you should first check [`DualStatus`](@ref) to
3301+
confirm that a dual solution is avaiable.
3302+
3303+
If the [`DualStatus`](@ref) is [`NO_SOLUTION`](@ref) the result of querying
3304+
this attribute is undefined.
3305+
3306+
## `result_index`
3307+
3308+
The optimizer may return multiple dual solutions. See [`ResultCount`](@ref)
3309+
for information on how the results are ordered.
3310+
3311+
If the solver does not have a dual value for the constraint because the
3312+
`result_index` is beyond the available solutions (whose number is indicated by
3313+
the [`ResultCount`](@ref) attribute), getting this attribute must throw a
3314+
[`ResultIndexBoundsError`](@ref).
3315+
3316+
## Implementation
3317+
3318+
Optimizers should implement the following methods:
3319+
```
3320+
MOI.get(::Optimizer, ::MOI.LagrangeMultipliers, ::MOI.ConstraintIndex)
3321+
```
3322+
They should not implement [`set`](@ref) or [`supports`](@ref).
3323+
3324+
"""
3325+
struct LagrangeMultipliers <: AbstractConstraintAttribute
3326+
result_index::Int
3327+
3328+
LagrangeMultipliers(result_index::Int = 1) = new(result_index)
3329+
end
3330+
3331+
function get_fallback(
3332+
model::ModelLike,
3333+
attr::LagrangeMultipliers,
3334+
ci::ConstraintIndex
3335+
)
3336+
return get(model, ConstraintDual(attr.result_index), ci)
3337+
end
3338+
32753339
"""
32763340
is_set_by_optimize(::AnyAttribute)
32773341
@@ -3330,6 +3394,7 @@ function is_set_by_optimize(
33303394
ConstraintDual,
33313395
ConstraintBasisStatus,
33323396
VariableBasisStatus,
3397+
LagrangeMultipliers,
33333398
},
33343399
)
33353400
return true

0 commit comments

Comments
 (0)