@@ -476,7 +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 has the potential to be a false positive. see #2817
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 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
480505 return haskey (Constraint. bridges (b), ci)
481506 end
482507 else
@@ -490,32 +515,49 @@ function _delete_variables_in_vector_of_variables_constraint(
490515 ci:: MOI.ConstraintIndex{MOI.VectorOfVariables,S} ,
491516) where {S}
492517 func = MOI. get (b, MOI. ConstraintFunction (), ci)
493- variables = copy (func. variables)
494- if vis == variables
518+ if vis == func. variables
495519 MOI. delete (b, ci)
496- else
497- for vi in vis
498- i = findfirst (isequal (vi), variables)
499- if i != = nothing
500- if MOI. supports_dimension_update (S)
501- call_in_context (MOI. delete, b, ci, IndexInVector (i))
502- else
503- MOI. Utilities. throw_delete_variable_in_vov (vi)
504- end
505- end
520+ return
521+ end
522+ variables = copy (func. variables)
523+ for vi in vis
524+ i = findfirst (isequal (vi), variables)
525+ if i === nothing
526+ continue
527+ elseif MOI. supports_dimension_update (S)
528+ call_in_context (MOI. delete, b, ci, IndexInVector (i))
529+ else
530+ MOI. Utilities. throw_delete_variable_in_vov (vi)
506531 end
507532 end
533+ return
508534end
509535
510- function _is_added_by_bridge (c_map, ci:: MOI.ConstraintIndex{F,S} ) where {F,S}
511- for (i, bridge) in c_map
512- if i == ci
513- continue
514- elseif ci in MOI. get (bridge, MOI. ListOfConstraintIndices {F,S} ())
515- return true
536+ """
537+ _is_added_by_bridge(
538+ c_map,
539+ cache::Dict{Any,Any},
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 `ListOfConstraintIndices{F,S}` in `cache`,
546+ so that we don't have to keep lopping through the bridges.
547+ """
548+ function _is_added_by_bridge (
549+ c_map,
550+ cache:: Dict{Any,Any} ,
551+ ci:: MOI.ConstraintIndex{F,S} ,
552+ ) where {F,S}
553+ ret = get! (cache, (F, S)) do
554+ list = MOI. ConstraintIndex{F,S}[]
555+ for bridge in values (c_map)
556+ append! (list, MOI. get (bridge, MOI. ListOfConstraintIndices {F,S} ()))
516557 end
558+ return list
517559 end
518- return false
560+ return ci in ret
519561end
520562
521563"""
@@ -524,45 +566,37 @@ end
524566 vis::Vector{MOI.VariableIndex},
525567 )
526568
527- !!! warning
528- There's a lot of subtle logic in this function.
569+ The point of this function is to delete constraints associated with the
570+ variables in `vis`.
571+
572+ ## Warning
573+
574+ Because of the false positive potential in
575+ `is_valid(::AbstractBridgeOptimizer, MOI.ConstraintIndex)`, we need to ensure
576+ that we delete constraints only if they were not added by a different constraint
577+ bridge, otherwise when we come to delete the parent constraint we'll hit a
578+ runtime error where we have already deleted part of the bridged constraint.
579+
580+ x-ref https://github.com/jump-dev/MathOptInterface.jl/issues/2817
581+ x-ref https://github.com/jump-dev/MathOptInterface.jl/pull/2818
529582"""
530583function _delete_variables_in_variables_constraints (
531584 b:: AbstractBridgeOptimizer ,
532585 vis:: Vector{MOI.VariableIndex} ,
533586)
534587 c_map = Constraint. bridges (b):: Constraint.Map
535- # The point of this function is to delete constraints associated with the
536- # variables in `vis`.
537- #
538- # There are two problematic cases:
539- #
540- # 1. A VariableIndex bridged to a VectorOfVariables equivalent. For example,
541- # x in Interval --> [x] in HyperRectangle
542- # 2. A VectorOfVariables bridged to a VariableIndex equivalent. For example,
543- # [x] in Nonnegatives --> x in GreaterThan
544- #
545- # First, we need to delete scalar constraints, UNLESS they were added by a
546- # different constraint bridge.
588+ cache = Dict {Any,Any} ()
547589 for vi in vis
548- # If a bridged `VariableIndex` constraints creates a second one, then we
549- # will delete the second one when deleting the first one hence we should
550- # not delete it again in this loop. For this, we reverse the order so
551- # that we encounter the first one first and we won't delete the second
552- # one since `MOI.is_valid(b, ci)` will be `false`.
553- for ci in Iterators. reverse (Constraint. variable_constraints (c_map, vi))
554- if MOI. is_valid (b, ci) && ! _is_added_by_bridge (c_map, ci)
590+ for ci in Constraint. variable_constraints (c_map, vi)
591+ if ! _is_added_by_bridge (c_map, cache, ci)
555592 MOI. delete (b, ci)
556593 end
557594 end
558595 end
559- # Delete all `MOI.VectorOfVariables` constraints of these variables.
560- # We reverse for the same reason as for `VariableIndex` below.
561- # As the iterators are lazy, when the inner bridge constraint is deleted,
562- # it won't be part of the iteration.
563- for ci in
564- Iterators. reverse (Constraint. vector_of_variables_constraints (c_map))
565- _delete_variables_in_vector_of_variables_constraint (b, vis, ci)
596+ for ci in Constraint. vector_of_variables_constraints (c_map)
597+ if ! _is_added_by_bridge (c_map, cache, ci)
598+ _delete_variables_in_vector_of_variables_constraint (b, vis, ci)
599+ end
566600 end
567601 return
568602end
0 commit comments