Skip to content

Commit bd8918d

Browse files
authored
[Bridges] fix deleting variable with constraint bridges (#2818)
1 parent 72c3705 commit bd8918d

File tree

3 files changed

+198
-19
lines changed

3 files changed

+198
-19
lines changed

src/Bridges/Constraint/bridges/SemiToBinaryBridge.jl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@ function MOI.delete(model::MOI.ModelLike, bridge::SemiToBinaryBridge)
179179
end
180180
MOI.delete(model, bridge.upper_bound_index)
181181
MOI.delete(model, bridge.lower_bound_index)
182-
MOI.delete(model, bridge.binary_constraint_index)
183182
MOI.delete(model, bridge.binary_variable)
184183
return
185184
end

src/Bridges/bridge_optimizer.jl

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,32 @@ function MOI.is_valid(
476476
v_map = Variable.bridges(b)::Variable.Map
477477
return MOI.is_valid(v_map, ci)
478478
else
479+
# This return value has the potential to be a false positive: it
480+
# doesn't discriminate between constraints that the user added, and
481+
# constraints that a bridge added that were themselves bridged.
482+
#
483+
# "Fixing" this particular call has a number of wide reaching
484+
# effects because bridges need this to be "true" so that they can
485+
# query attributes of the constraint from `b`.
486+
#
487+
# In most cases a false positive doesn't matter, because we really
488+
# do support querying stuff about it. And also the user needs some
489+
# way of obtaining the correct index, which they won't have except
490+
# by luck/enumeration.
491+
#
492+
# The main place that this is problematic is when we come to delete
493+
# constraints, and in particular VariableIndex constraints, because we
494+
# triviallly have their `.value` field from the `.value` of the
495+
# VariableIndex.
496+
#
497+
# Instead of fixing everything though, we implement some extra
498+
# checks when deleting, and we leave the false-positive as-is for
499+
# now. If you, future reader, hit this comment while debugging, we
500+
# might need to revisit this decision.
501+
#
502+
# x-ref https://github.com/jump-dev/MathOptInterface.jl/issues/2696
503+
# x-ref https://github.com/jump-dev/MathOptInterface.jl/issues/2817
504+
# x-ref https://github.com/jump-dev/MathOptInterface.jl/pull/2818
479505
return haskey(Constraint.bridges(b), ci)
480506
end
481507
else
@@ -489,10 +515,10 @@ function _delete_variables_in_vector_of_variables_constraint(
489515
ci::MOI.ConstraintIndex{MOI.VectorOfVariables,S},
490516
) where {S}
491517
func = MOI.get(b, MOI.ConstraintFunction(), ci)
492-
variables = copy(func.variables)
493-
if vis == variables
518+
if vis == func.variables
494519
MOI.delete(b, ci)
495520
else
521+
variables = copy(func.variables)
496522
for vi in vis
497523
i = findfirst(isequal(vi), variables)
498524
if i !== nothing
@@ -504,34 +530,81 @@ function _delete_variables_in_vector_of_variables_constraint(
504530
end
505531
end
506532
end
533+
return
507534
end
508535

536+
"""
537+
_is_added_by_bridge(
538+
c_map,
539+
cache::Dict{Any,Set{Int64}},
540+
ci::MOI.ConstraintIndex{F,S},
541+
) where {F,S}
542+
543+
Return `true` if `ci` was added by one of the bridges in `c_map`.
544+
545+
For performance reasons, we store the index values associated with
546+
`MOI.ListOfConstraintIndices{F,S}` in `cache` so that we don't have to keep
547+
looping through the bridges.
548+
"""
549+
function _is_added_by_bridge(
550+
c_map,
551+
cache::Dict{Any,Set{Int64}},
552+
ci::MOI.ConstraintIndex{F,S},
553+
) where {F,S}
554+
ret = get!(cache, (F, S)) do
555+
set = Set{Int64}()
556+
for bridge in values(c_map)
557+
for ci in MOI.get(
558+
bridge,
559+
MOI.ListOfConstraintIndices{F,S}(),
560+
)
561+
push!(set, ci.value)
562+
end
563+
end
564+
return set
565+
end::Set{Int64}
566+
return ci.value in ret
567+
end
568+
569+
"""
570+
_delete_variables_in_variables_constraints(
571+
b::AbstractBridgeOptimizer,
572+
vis::Vector{MOI.VariableIndex},
573+
)
574+
575+
The point of this function is to delete constraints associated with the
576+
variables in `vis`.
577+
578+
## Warning
579+
580+
Because of the false positive potential in
581+
`is_valid(::AbstractBridgeOptimizer, MOI.ConstraintIndex)`, we need to ensure
582+
that we delete constraints only if they were not added by a different constraint
583+
bridge, otherwise when we come to delete the parent constraint we'll hit a
584+
runtime error where we have already deleted part of the bridged constraint.
585+
586+
x-ref https://github.com/jump-dev/MathOptInterface.jl/issues/2817
587+
x-ref https://github.com/jump-dev/MathOptInterface.jl/pull/2818
588+
"""
509589
function _delete_variables_in_variables_constraints(
510590
b::AbstractBridgeOptimizer,
511591
vis::Vector{MOI.VariableIndex},
512592
)
513593
c_map = Constraint.bridges(b)::Constraint.Map
514-
# Delete all `MOI.VectorOfVariables` constraints of these variables.
515-
# We reverse for the same reason as for `VariableIndex` below.
516-
# As the iterators are lazy, when the inner bridge constraint is deleted,
517-
# it won't be part of the iteration.
518-
for ci in
519-
Iterators.reverse(Constraint.vector_of_variables_constraints(c_map))
520-
_delete_variables_in_vector_of_variables_constraint(b, vis, ci)
521-
end
522-
# Delete all `MOI.VariableIndex` constraints of these variables.
594+
cache = Dict{Any,Set{Int64}}()
523595
for vi in vis
524-
# If a bridged `VariableIndex` constraints creates a second one,
525-
# then we will delete the second one when deleting the first one hence we
526-
# should not delete it again in this loop.
527-
# For this, we reverse the order so that we encounter the first one first
528-
# and we won't delete the second one since `MOI.is_valid(b, ci)` will be `false`.
529-
for ci in Iterators.reverse(Constraint.variable_constraints(c_map, vi))
530-
if MOI.is_valid(b, ci)
596+
for ci in Constraint.variable_constraints(c_map, vi)
597+
if !_is_added_by_bridge(c_map, cache, ci)
531598
MOI.delete(b, ci)
532599
end
533600
end
534601
end
602+
for ci in Constraint.vector_of_variables_constraints(c_map)
603+
if !_is_added_by_bridge(c_map, cache, ci)
604+
_delete_variables_in_vector_of_variables_constraint(b, vis, ci)
605+
end
606+
end
607+
return
535608
end
536609

537610
function _delete_variables_in_bridged_objective(

test/Bridges/bridge_optimizer.jl

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ function test_double_deletion_scalar()
595595
@test !MOI.Bridges.is_bridged(model, b2.constraint)
596596
MOI.delete(model, x)
597597
@test !MOI.is_valid(model, x)
598+
@test MOI.is_empty(model)
598599
return
599600
end
600601

@@ -614,6 +615,7 @@ function test_double_deletion_vector()
614615
@test !MOI.Bridges.is_bridged(model, b2.constraint)
615616
MOI.delete(model, x)
616617
@test all(vi -> !MOI.is_valid(model, vi), x)
618+
@test MOI.is_empty(model)
617619
return
618620
end
619621

@@ -1463,6 +1465,111 @@ function test_BridgeRequiresFiniteDomainError()
14631465
return
14641466
end
14651467

1468+
MOI.Utilities.@model(
1469+
Model2817a,
1470+
(),
1471+
(),
1472+
(MOI.Nonnegatives,),
1473+
(),
1474+
(),
1475+
(),
1476+
(),
1477+
(MOI.VectorAffineFunction,)
1478+
);
1479+
1480+
function MOI.supports_constraint(
1481+
::Model2817a,
1482+
::Type{MOI.VariableIndex},
1483+
::Type{S},
1484+
) where {S<:MOI.AbstractScalarSet}
1485+
return false
1486+
end
1487+
1488+
function test_issue_2817a()
1489+
inner = Model2817a{Float64}()
1490+
model = MOI.Bridges.full_bridge_optimizer(inner, Float64);
1491+
x, c = MOI.add_constrained_variable(model, MOI.Interval(0.0, 1.0));
1492+
@test isa(
1493+
model.constraint_map[c],
1494+
MOI.Bridges.Constraint.IntervalToHyperRectangleBridge,
1495+
)
1496+
MOI.delete(model, x)
1497+
@test !MOI.is_valid(model, x)
1498+
@test MOI.is_empty(model)
1499+
@test MOI.is_empty(inner)
1500+
@test isempty(model.constraint_map)
1501+
return
1502+
end
1503+
1504+
MOI.Utilities.@model(
1505+
Model2817b,
1506+
(),
1507+
(MOI.LessThan,),
1508+
(),
1509+
(),
1510+
(),
1511+
(MOI.ScalarAffineFunction,),
1512+
(),
1513+
()
1514+
);
1515+
1516+
function MOI.supports_constraint(
1517+
::Model2817b,
1518+
::Type{MOI.VariableIndex},
1519+
::Type{S},
1520+
) where {S<:MOI.AbstractScalarSet}
1521+
return false
1522+
end
1523+
1524+
function test_issue_2817b()
1525+
inner = Model2817b{Float64}()
1526+
model = MOI.Bridges.full_bridge_optimizer(inner, Float64);
1527+
x, c = MOI.add_constrained_variables(model, MOI.Nonpositives(1))
1528+
@test isa(model.constraint_map[c], MOI.Bridges.Constraint.ScalarizeBridge)
1529+
MOI.delete(model, x)
1530+
@test !MOI.is_valid(model, x[1])
1531+
@test MOI.is_empty(model)
1532+
@test MOI.is_empty(inner)
1533+
@test isempty(model.constraint_map)
1534+
return
1535+
end
1536+
1537+
MOI.Utilities.@model(
1538+
Model2817c,
1539+
(),
1540+
(MOI.GreaterThan, MOI.LessThan),
1541+
(),
1542+
(),
1543+
(),
1544+
(MOI.ScalarAffineFunction,),
1545+
(),
1546+
()
1547+
);
1548+
1549+
function MOI.supports_constraint(
1550+
::Model2817c,
1551+
::Type{MOI.VariableIndex},
1552+
::Type{S},
1553+
) where {S<:Union{MOI.ZeroOne,MOI.Semiinteger,MOI.Semicontinuous}}
1554+
return false
1555+
end
1556+
1557+
function test_issue_2817c()
1558+
inner = Model2817c{Float64}()
1559+
model = MOI.Bridges.full_bridge_optimizer(inner, Float64);
1560+
x, c = MOI.add_constrained_variable(model, MOI.Semiinteger(2.0, 3.0))
1561+
@test isa(
1562+
model.constraint_map[c],
1563+
MOI.Bridges.Constraint.SemiToBinaryBridge,
1564+
)
1565+
MOI.delete(model, x)
1566+
@test !MOI.is_valid(model, x)
1567+
@test MOI.is_empty(model)
1568+
@test MOI.is_empty(inner)
1569+
@test isempty(model.constraint_map)
1570+
return
1571+
end
1572+
14661573
end # module
14671574

14681575
TestBridgeOptimizer.runtests()

0 commit comments

Comments
 (0)