Skip to content

Commit 4717d41

Browse files
authored
Fix is_feasible for sets without a definition in MOI (#699)
1 parent 6439158 commit 4717d41

File tree

3 files changed

+92
-8
lines changed

3 files changed

+92
-8
lines changed

src/Constraint.jl

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,35 @@ end
4040

4141
AbstractTrees.children(c::Constraint) = (c.child,)
4242

43-
# A fallback. Define a new method if `MOI.Utilities.distance_to_set`
44-
# is not defined.
45-
function is_feasible(x, set, tol)
43+
# A default fallback which means that we are unsure.
44+
is_feasible(x, set, tol) = missing
45+
46+
function is_feasible(
47+
x::Vector,
48+
set::Union{
49+
MOI.Nonnegatives,
50+
MOI.Nonpositives,
51+
MOI.Zeros,
52+
MOI.SecondOrderCone,
53+
MOI.RotatedSecondOrderCone,
54+
MOI.ExponentialCone,
55+
MOI.DualExponentialCone,
56+
MOI.PowerCone,
57+
MOI.DualPowerCone,
58+
MOI.GeometricMeanCone,
59+
MOI.NormCone,
60+
MOI.NormInfinityCone,
61+
MOI.NormOneCone,
62+
},
63+
tol,
64+
)
4665
return MOI.Utilities.distance_to_set(x, set) <= tol
4766
end
4867

68+
function is_feasible(x::AbstractMatrix, set::MOI.AbstractVectorSet, tol)
69+
return is_feasible(vec(x), set, tol)
70+
end
71+
4972
function is_feasible(x::Number, set::MOI.AbstractVectorSet, tol)
5073
return is_feasible([x], set, tol)
5174
end
@@ -88,13 +111,26 @@ end
88111

89112
function _add_constraint!(context::Context, c::Constraint)
90113
if vexity(c.child) == ConstVexity()
91-
# This `evaluate` call is safe, since even if it refers to a `fix!`'d variable,
92-
# it happens when we are formulating the problem (not at expression-time), so there
93-
# is not time for the variable to be re-`fix!`'d to a different value (or `free!`'d)
94-
if !is_feasible(evaluate(c.child), c.set, CONSTANT_CONSTRAINT_TOL[])
114+
# This `evaluate` call is safe, since even if it refers to a `fix!`'d
115+
# variable, it happens when we are formulating the problem (not at
116+
# expression-time), so there is no time for the variable to be
117+
# re-`fix!`'d to a different value (or `free!`'d)
118+
feas = is_feasible(evaluate(c.child), c.set, CONSTANT_CONSTRAINT_TOL[])
119+
# There are three possible values of feas: true, false, and missing.
120+
if feas === true
121+
# Do nothing. The constraint is satisfied. Do not add it to the
122+
# solver.
123+
return
124+
elseif feas === false
125+
# We have proven the constraint is not satisfied. Set a flag and
126+
# bail. We don't need to add it to the solver.
95127
context.detected_infeasible_during_formulation = true
128+
return
129+
else
130+
# The feasibility check was unsure, likely because a method was
131+
# missing. Pass the constraint to the solver.
132+
@assert ismissing(feas)
96133
end
97-
return
98134
end
99135
f = conic_form!(context, c.child)
100136
context.constr_to_moi_inds[c] = MOI_add_constraint(context.model, f, c.set)

src/to_MOI.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ function MOI_add_constraint(model, f, set)
3030
return MOI.add_constraint(model, f, set)
3131
end
3232

33+
function MOI_add_constraint(model, f::SPARSE_VECTOR{T}, set) where {T}
34+
g = MOI.VectorAffineFunction{T}(MOI.VectorAffineTerm{T}[], f)
35+
return MOI.add_constraint(model, g, set)
36+
end
37+
3338
function MOI_add_constraint(model, f::SparseTape, set)
3439
return MOI_add_constraint(model, to_vaf(f), set)
3540
end

test/test_constraints.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,49 @@ function test_RelativeEntropyEpiConeSquare()
397397
return
398398
end
399399

400+
function test_is_feasible()
401+
@test Convex.is_feasible([1.0, 0.0], MOI.Nonnegatives(2), 0.0)
402+
@test !Convex.is_feasible([-1.0, 0.0], MOI.Nonnegatives(2), 0.0)
403+
@test Convex.is_feasible([-1.0, 0.0], MOI.Nonpositives(2), 0.0)
404+
@test !Convex.is_feasible([1.0, 0.0], MOI.Nonpositives(2), 0.0)
405+
@test Convex.is_feasible([1e-5, 0.0], MOI.Zeros(2), 1e-5)
406+
@test !Convex.is_feasible([1e-5, 0.0], MOI.Zeros(2), 0.0)
407+
@test Convex.is_feasible([5.0, 3.0, 4.0], MOI.SecondOrderCone(3), 0.0)
408+
set = MOI.PositiveSemidefiniteConeSquare(2)
409+
@test Convex.is_feasible([1.0 0.0; 0.0 1.0], set, 0.0)
410+
@test !Convex.is_feasible([-1.0 0.0; 0.0 1.0], set, 0.0)
411+
@test !Convex.is_feasible([1.0 1e-6; 0.0 1.0], set, 0.0)
412+
set = MOI.NormSpectralCone(2, 2)
413+
@test Convex.is_feasible([1.0, 1.0, 0.0, 0.0, 1.0], set, 0.0) === missing
414+
return
415+
end
416+
417+
function test_distance_to_set_matrix()
418+
x = Variable(2, 2)
419+
y = Variable()
420+
fix!(x, [1 0; 0 1])
421+
# Constraint has a fixed `Matrix` value.
422+
model = minimize(y, [sum(x; dims = 1) <= 1, y >= 1])
423+
solve!(model, SCS.Optimizer; silent = true)
424+
@test (model.optval, 1.0; atol = 1e-3)
425+
return
426+
end
427+
428+
function test_distance_to_set_undefined()
429+
t = Variable()
430+
fix!(t, 2)
431+
x = Variable(2, 2)
432+
fix!(x, [1 0; 0 1])
433+
y = Variable()
434+
# This constraint is fixed, and `MOI.distance_to_set` is not defined for it,
435+
# but it should still work without erroring.
436+
c = Convex.Constraint(vcat(t, vec(x)), MOI.NormSpectralCone(2, 2))
437+
model = minimize(y, [c, y >= 1])
438+
solve!(model, SCS.Optimizer; silent = true)
439+
@test (model.optval, 1.0; atol = 1e-4)
440+
return
441+
end
442+
400443
end # TestConstraints
401444

402445
TestConstraints.runtests()

0 commit comments

Comments
 (0)