Skip to content

Commit e8a1661

Browse files
authored
Add MOI.LagrangeMultiplier attribute (#2890)
1 parent 008ed71 commit e8a1661

File tree

3 files changed

+241
-1
lines changed

3 files changed

+241
-1
lines changed

src/Test/test_nonlinear.jl

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2386,3 +2386,161 @@ function setup_test(
23862386
end
23872387

23882388
version_added(::typeof(test_vector_nonlinear_oracle_no_hessian)) = v"1.46.0"
2389+
2390+
function test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE(
2391+
model::MOI.ModelLike,
2392+
config::MOI.Test.Config{T},
2393+
) where {T}
2394+
@requires _supports(config, MOI.optimize!)
2395+
@requires _supports(config, MOI.ConstraintDual)
2396+
@requires _supports(config, MOI.LagrangeMultiplier)
2397+
@requires MOI.supports_constraint(
2398+
model,
2399+
MOI.VectorOfVariables,
2400+
MOI.VectorNonlinearOracle{T},
2401+
)
2402+
set = MOI.VectorNonlinearOracle(;
2403+
dimension = 2,
2404+
l = T[typemin(T)],
2405+
u = T[1],
2406+
eval_f = (ret, x) -> (ret[1] = x[1]^2 + x[2]^2),
2407+
jacobian_structure = [(1, 1), (1, 2)],
2408+
eval_jacobian = (ret, x) -> ret .= T(2) .* x,
2409+
hessian_lagrangian_structure = [(1, 1), (2, 2)],
2410+
eval_hessian_lagrangian = (ret, x, u) -> ret .= T(2) .* u[1],
2411+
)
2412+
x = MOI.add_variables(model, 2)
2413+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
2414+
f = one(T) * x[1] + one(T) * x[2]
2415+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
2416+
c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set)
2417+
y = T(1) / sqrt(T(2))
2418+
CI = MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.VectorNonlinearOracle{T}}
2419+
if MOI.supports(model, MOI.LagrangeMultiplierStart(), CI)
2420+
MOI.set(model, MOI.LagrangeMultiplierStart(), c, T[-y])
2421+
end
2422+
MOI.optimize!(model)
2423+
@test isapprox(MOI.get(model, MOI.VariablePrimal(), x), [y, y], config)
2424+
@test isapprox(MOI.get(model, MOI.ConstraintDual(), c), T[-1, -1], config)
2425+
@test isapprox(MOI.get(model, MOI.LagrangeMultiplier(), c), T[-y])
2426+
# Test `set` just for code coverage
2427+
x = T[1, 2]
2428+
ret = T[0]
2429+
set.eval_f(ret, x)
2430+
@test ret == T[5]
2431+
ret = T[0, 0]
2432+
set.eval_jacobian(ret, x)
2433+
@test ret == T[2, 4]
2434+
set.eval_hessian_lagrangian(ret, x, T[-1])
2435+
@test ret == [-2, -2]
2436+
return
2437+
end
2438+
2439+
function setup_test(
2440+
::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE),
2441+
model::MOIU.MockOptimizer,
2442+
config::Config{T},
2443+
) where {T}
2444+
F, S = MOI.VectorOfVariables, MOI.VectorNonlinearOracle{T}
2445+
y = T(1) / sqrt(T(2))
2446+
MOI.Utilities.set_mock_optimize!(
2447+
model,
2448+
mock -> begin
2449+
MOI.Utilities.mock_optimize!(
2450+
mock,
2451+
config.optimal_status,
2452+
T[y, y],
2453+
(F, S) => [T[-1, -1]],
2454+
)
2455+
ci = only(MOI.get(mock, MOI.ListOfConstraintIndices{F,S}()))
2456+
MOI.set(mock, MOI.LagrangeMultiplier(), ci, T[-y])
2457+
end,
2458+
)
2459+
model.eval_variable_constraint_dual = false
2460+
return () -> model.eval_variable_constraint_dual = true
2461+
end
2462+
2463+
function version_added(
2464+
::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MAX_SENSE),
2465+
)
2466+
return v"1.48.0"
2467+
end
2468+
2469+
function test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE(
2470+
model::MOI.ModelLike,
2471+
config::MOI.Test.Config{T},
2472+
) where {T}
2473+
@requires _supports(config, MOI.optimize!)
2474+
@requires _supports(config, MOI.ConstraintDual)
2475+
@requires _supports(config, MOI.LagrangeMultiplier)
2476+
@requires MOI.supports_constraint(
2477+
model,
2478+
MOI.VectorOfVariables,
2479+
MOI.VectorNonlinearOracle{T},
2480+
)
2481+
set = MOI.VectorNonlinearOracle(;
2482+
dimension = 2,
2483+
l = T[-1],
2484+
u = T[typemax(T)],
2485+
eval_f = (ret, x) -> (ret[1] = -x[1]^2 - x[2]^2),
2486+
jacobian_structure = [(1, 1), (1, 2)],
2487+
eval_jacobian = (ret, x) -> ret .= -T(2) .* x,
2488+
hessian_lagrangian_structure = [(1, 1), (2, 2)],
2489+
eval_hessian_lagrangian = (ret, x, u) -> ret .= -T(2) .* u[1],
2490+
)
2491+
x = MOI.add_variables(model, 2)
2492+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
2493+
f = one(T) * x[1] + one(T) * x[2]
2494+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
2495+
c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set)
2496+
y = T(1) / sqrt(T(2))
2497+
CI = MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.VectorNonlinearOracle{T}}
2498+
if MOI.supports(model, MOI.LagrangeMultiplierStart(), CI)
2499+
MOI.set(model, MOI.LagrangeMultiplierStart(), c, T[y])
2500+
end
2501+
MOI.optimize!(model)
2502+
@test isapprox(MOI.get(model, MOI.VariablePrimal(), x), [-y, -y], config)
2503+
@test isapprox(MOI.get(model, MOI.ConstraintDual(), c), T[1, 1], config)
2504+
@test isapprox(MOI.get(model, MOI.LagrangeMultiplier(), c), T[y])
2505+
# Test `set` just for code coverage
2506+
x = T[1, 2]
2507+
ret = T[0]
2508+
set.eval_f(ret, x)
2509+
@test ret == T[-5]
2510+
ret = T[0, 0]
2511+
set.eval_jacobian(ret, x)
2512+
@test ret == T[-2, -4]
2513+
set.eval_hessian_lagrangian(ret, x, T[-1])
2514+
@test ret == [2, 2]
2515+
return
2516+
end
2517+
2518+
function setup_test(
2519+
::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE),
2520+
model::MOIU.MockOptimizer,
2521+
config::Config{T},
2522+
) where {T}
2523+
F, S = MOI.VectorOfVariables, MOI.VectorNonlinearOracle{T}
2524+
y = T(1) / sqrt(T(2))
2525+
MOI.Utilities.set_mock_optimize!(
2526+
model,
2527+
mock -> begin
2528+
MOI.Utilities.mock_optimize!(
2529+
mock,
2530+
config.optimal_status,
2531+
T[-y, -y],
2532+
(F, S) => [T[1, 1]],
2533+
)
2534+
ci = only(MOI.get(mock, MOI.ListOfConstraintIndices{F,S}()))
2535+
MOI.set(mock, MOI.LagrangeMultiplier(), ci, T[y])
2536+
end,
2537+
)
2538+
model.eval_variable_constraint_dual = false
2539+
return () -> model.eval_variable_constraint_dual = true
2540+
end
2541+
2542+
function version_added(
2543+
::typeof(test_VectorNonlinearOracle_LagrangeMultipliers_MIN_SENSE),
2544+
)
2545+
return v"1.48.0"
2546+
end

src/Utilities/mockoptimizer.jl

Lines changed: 16 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,7 @@ function MockOptimizer(
133137
# Basis status
134138
Dict{MOI.ConstraintIndex,Dict{Int,MOI.BasisStatusCode}}(),
135139
Dict{MOI.VariableIndex,Dict{Int,MOI.BasisStatusCode}}(),
140+
Dict{MOI.AbstractConstraintAttribute,Dict{MOI.ConstraintIndex,Any}}(),
136141
)
137142
end
138143

@@ -421,7 +426,14 @@ function MOI.set(
421426
idx::MOI.ConstraintIndex,
422427
value,
423428
)
424-
MOI.set(mock.inner_model, attr, xor_index(idx), value)
429+
if MOI.is_set_by_optimize(attr)
430+
ret = get!(mock.constraint_attributes, attr) do
431+
return Dict{MOI.ConstraintIndex,Any}()
432+
end
433+
ret[idx] = value
434+
else
435+
MOI.set(mock.inner_model, attr, xor_index(idx), value)
436+
end
425437
return
426438
end
427439

@@ -660,6 +672,9 @@ function MOI.get(
660672
)
661673
# If it is thrown by `mock.inner_model`, the index will be xor'ed.
662674
MOI.throw_if_not_valid(mock, idx)
675+
if MOI.is_set_by_optimize(attr)
676+
return mock.constraint_attributes[attr][idx]
677+
end
663678
return MOI.get(mock.inner_model, attr, xor_index(idx))
664679
end
665680

src/attributes.jl

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

3275+
"""
3276+
LagrangeMultiplier(result_index::Int = 1)
3277+
3278+
An [`AbstractConstraintAttribute`](@ref) for the Lagrange multiplier associated
3279+
with a constraint.
3280+
3281+
## Relationship to `ConstraintDual`
3282+
3283+
This attribute differs from [`ConstraintDual`](@ref) in one important case.
3284+
When there is a [`VectorNonlinearOracle`](@ref) constraint of the form:
3285+
```math
3286+
x \\in VectorNonlinearOracle
3287+
```
3288+
the associated [`ConstraintDual`](@ref) is ``\\mu^\\top \\nabla f(x)``, and the
3289+
value of [`LagrangeMultiplier`](@ref) is the vector ``\\mu`` directly.
3290+
3291+
Both values are useful in different circumstances.
3292+
3293+
## DualStatus
3294+
3295+
Before quering this attribute you should first check [`DualStatus`](@ref) to
3296+
confirm that a dual solution is avaiable.
3297+
3298+
If the [`DualStatus`](@ref) is [`NO_SOLUTION`](@ref) the result of querying
3299+
this attribute is undefined.
3300+
3301+
## `result_index`
3302+
3303+
The optimizer may return multiple dual solutions. See [`ResultCount`](@ref)
3304+
for information on how the results are ordered.
3305+
3306+
If the solver does not have a dual value for the constraint because the
3307+
`result_index` is beyond the available solutions (whose number is indicated by
3308+
the [`ResultCount`](@ref) attribute), getting this attribute must throw a
3309+
[`ResultIndexBoundsError`](@ref).
3310+
3311+
## Implementation
3312+
3313+
Optimizers should implement the following methods:
3314+
```
3315+
MOI.get(::Optimizer, ::MOI.LagrangeMultiplier, ::MOI.ConstraintIndex)
3316+
```
3317+
They should not implement [`set`](@ref) or [`supports`](@ref).
3318+
3319+
Solvers should implement [`LagrangeMultiplier`](@ref) only if they also
3320+
implement the [`ConstraintDual`](@ref), and only if the two values are
3321+
different.
3322+
"""
3323+
struct LagrangeMultiplier <: AbstractConstraintAttribute
3324+
result_index::Int
3325+
3326+
LagrangeMultiplier(result_index::Int = 1) = new(result_index)
3327+
end
3328+
3329+
"""
3330+
LagrangeMultiplierStart()
3331+
3332+
An [`AbstractConstraintAttribute`](@ref) for the initial assignment to the
3333+
constraint's [`LagrangeMultiplier`](@ref) that the optimizer may use to
3334+
warm-start the solve.
3335+
3336+
May be `nothing` (unset), a number for [`AbstractScalarFunction`](@ref), or a
3337+
vector for [`AbstractVectorFunction`](@ref).
3338+
"""
3339+
struct LagrangeMultiplierStart <: AbstractConstraintAttribute end
3340+
32753341
"""
32763342
is_set_by_optimize(::AnyAttribute)
32773343
@@ -3330,6 +3396,7 @@ function is_set_by_optimize(
33303396
ConstraintDual,
33313397
ConstraintBasisStatus,
33323398
VariableBasisStatus,
3399+
LagrangeMultiplier,
33333400
},
33343401
)
33353402
return true

0 commit comments

Comments
 (0)