@@ -476,6 +476,32 @@ function MOI.is_valid(
476
476
v_map = Variable. bridges (b):: Variable.Map
477
477
return MOI. is_valid (v_map, ci)
478
478
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
479
505
return haskey (Constraint. bridges (b), ci)
480
506
end
481
507
else
@@ -489,10 +515,10 @@ function _delete_variables_in_vector_of_variables_constraint(
489
515
ci:: MOI.ConstraintIndex{MOI.VectorOfVariables,S} ,
490
516
) where {S}
491
517
func = MOI. get (b, MOI. ConstraintFunction (), ci)
492
- variables = copy (func. variables)
493
- if vis == variables
518
+ if vis == func. variables
494
519
MOI. delete (b, ci)
495
520
else
521
+ variables = copy (func. variables)
496
522
for vi in vis
497
523
i = findfirst (isequal (vi), variables)
498
524
if i != = nothing
@@ -504,34 +530,81 @@ function _delete_variables_in_vector_of_variables_constraint(
504
530
end
505
531
end
506
532
end
533
+ return
507
534
end
508
535
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
+ """
509
589
function _delete_variables_in_variables_constraints (
510
590
b:: AbstractBridgeOptimizer ,
511
591
vis:: Vector{MOI.VariableIndex} ,
512
592
)
513
593
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}} ()
523
595
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)
531
598
MOI. delete (b, ci)
532
599
end
533
600
end
534
601
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
535
608
end
536
609
537
610
function _delete_variables_in_bridged_objective (
0 commit comments