Skip to content

Commit 6b2192a

Browse files
authored
[Bridges] add Bridges.Constraint.SlackBridgePrimalDualStart (#2194)
1 parent 597f5aa commit 6b2192a

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed

src/Bridges/Objective/bridges/slack.jl

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,79 @@ function MOI.get(
174174
g = MOI.Utilities.remove_variable(f, bridge.slack)
175175
return MOI.Utilities.convert_approx(G, g)
176176
end
177+
178+
"""
179+
struct SlackBridgePrimalDualStart <: MOI.AbstractModelAttribute end
180+
181+
[`Bridges.Objective.SlackBridge`](@ref) introduces a new constraint into the
182+
problem. However, because it is not a constraint bridge, it cannot intercept
183+
calls to set [`ConstraintDualStart`](@ref) or [`ConstraintPrimalStart`](@ref).
184+
185+
As a work-around, set this attribute to `nothing` to set the primal and dual
186+
start for the new constraint. This attribute must be set after
187+
[`VariablePrimalStart`](@ref).
188+
"""
189+
struct SlackBridgePrimalDualStart <: MOI.AbstractModelAttribute end
190+
191+
function MOI.throw_set_error_fallback(
192+
::MOI.ModelLike,
193+
::SlackBridgePrimalDualStart,
194+
::Nothing,
195+
)
196+
return # Silently ignore if the model does not support.
197+
end
198+
199+
# Pretend that every model supports, and silently skip in set if unsupported
200+
MOI.supports_fallback(::MOI.ModelLike, ::SlackBridgePrimalDualStart) = true
201+
202+
function MOI.set(
203+
model::MOI.ModelLike,
204+
::SlackBridgePrimalDualStart,
205+
b::SlackBridge{T},
206+
::Nothing,
207+
) where {T}
208+
# !!! note
209+
# This attribute should silently skip if the `model` does not support it.
210+
# For other attributes, we set `supports(...) = false`, but this would
211+
# cause `copy_to` to throw an `UnsupportedAttributeError`, which we don't
212+
# want. The solution is to check `supports(model, ...)` in this method,
213+
# and bail if not supported.
214+
# ConstraintDual: if the objective function had a dual, it would be `-1` for
215+
# the Lagrangian function to be the same.
216+
if MOI.supports(model, MOI.ConstraintDualStart(), typeof(b.constraint))
217+
MOI.set(model, MOI.ConstraintDualStart(), b.constraint, -one(T))
218+
end
219+
# ConstraintPrimal: we should set the slack of f(x) - y to be 0, and the
220+
# start of y to be f(x).
221+
if !MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex) ||
222+
!MOI.supports(model, MOI.ConstraintPrimalStart(), typeof(b.constraint))
223+
return
224+
end
225+
MOI.set(model, MOI.VariablePrimalStart(), b.slack, zero(T))
226+
f = MOI.get(model, MOI.ConstraintFunction(), b.constraint)
227+
f_val = MOI.Utilities.eval_variables(f) do v
228+
return MOI.get(model, MOI.VariablePrimalStart(), v)
229+
end
230+
f_val -= MOI.constant(MOI.get(model, MOI.ConstraintSet(), b.constraint))
231+
MOI.set(model, MOI.VariablePrimalStart(), b.slack, f_val)
232+
MOI.set(model, MOI.ConstraintPrimalStart(), b.constraint, zero(T))
233+
return
234+
end
235+
236+
function MOI.set(
237+
b::MOI.Bridges.AbstractBridgeOptimizer,
238+
attr::SlackBridgePrimalDualStart,
239+
::Nothing,
240+
)
241+
# TODO(odow): this might fail if the SlackBridge is not the first bridge in
242+
# the chain, but it should be for our current setup of bridges, so we
243+
# choose to simplify this implementation.
244+
if MOI.Bridges.is_objective_bridged(b)
245+
obj_attr = MOI.ObjectiveFunction{function_type(bridges(b))}()
246+
if MOI.Bridges.is_bridged(b, obj_attr)
247+
bridge = MOI.Bridges.bridge(b, obj_attr)
248+
MOI.set(MOI.Bridges.recursive_model(b), attr, bridge, nothing)
249+
end
250+
end
251+
return
252+
end

test/Bridges/Objective/slack.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,49 @@ function test_deletion_of_variable_in_slacked_objective()
514514
return
515515
end
516516

517+
function test_SlackBridgePrimalDualStart()
518+
inner = MOI.Utilities.MockOptimizer(
519+
MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
520+
)
521+
model = MOI.Bridges.Objective.Slack{Float64}(inner)
522+
x = MOI.add_variable(model)
523+
MOI.add_constraint(model, x, MOI.GreaterThan(2.0))
524+
f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.1, x)], -1.2)
525+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
526+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
527+
MOI.set(model, MOI.VariablePrimalStart(), x, 2.0)
528+
attr = MOI.Bridges.Objective.SlackBridgePrimalDualStart()
529+
@test MOI.supports(model, attr)
530+
MOI.set(model, attr, nothing)
531+
vars = MOI.get(inner, MOI.ListOfVariableIndices())
532+
primal_start = MOI.get.(inner, MOI.VariablePrimalStart(), vars)
533+
@test primal_start[1] 2.0
534+
@test primal_start[2] 1.1 * 2.0 - 1.2
535+
F = MOI.ScalarAffineFunction{Float64}
536+
cis = MOI.get(inner, MOI.ListOfConstraintIndices{F,MOI.LessThan{Float64}}())
537+
@test length(cis) == 1
538+
@test MOI.get(inner, MOI.ConstraintPrimalStart(), cis[1]) 0.0
539+
@test MOI.get(inner, MOI.ConstraintDualStart(), cis[1]) -1.0
540+
return
541+
end
542+
543+
function test_SlackBridgePrimalDualStart_unsupported()
544+
attr = MOI.Bridges.Objective.SlackBridgePrimalDualStart()
545+
inner = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}())
546+
# Check that setting on blank model doesn't error.
547+
@test MOI.supports(inner, attr)
548+
MOI.set(inner, attr, nothing)
549+
model = MOI.Bridges.Objective.Slack{Float64}(inner)
550+
@test MOI.supports(model, attr)
551+
x = MOI.add_variable(model)
552+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
553+
f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.1, x)], -1.2)
554+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
555+
# Unsupported. Should silently skip without error.
556+
MOI.set(model, attr, nothing)
557+
return
558+
end
559+
517560
end # module
518561

519562
TestObjectiveSlack.runtests()

0 commit comments

Comments
 (0)