diff --git a/src/Constraint.jl b/src/Constraint.jl index 8b9afc83b..42482221d 100644 --- a/src/Constraint.jl +++ b/src/Constraint.jl @@ -40,12 +40,35 @@ end AbstractTrees.children(c::Constraint) = (c.child,) -# A fallback. Define a new method if `MOI.Utilities.distance_to_set` -# is not defined. -function is_feasible(x, set, tol) +# A default fallback which means that we are unsure. +is_feasible(x, set, tol) = missing + +function is_feasible( + x::Vector, + set::Union{ + MOI.Nonnegatives, + MOI.Nonpositives, + MOI.Zeros, + MOI.SecondOrderCone, + MOI.RotatedSecondOrderCone, + MOI.ExponentialCone, + MOI.DualExponentialCone, + MOI.PowerCone, + MOI.DualPowerCone, + MOI.GeometricMeanCone, + MOI.NormCone, + MOI.NormInfinityCone, + MOI.NormOneCone, + }, + tol, +) return MOI.Utilities.distance_to_set(x, set) <= tol end +function is_feasible(x::AbstractMatrix, set::MOI.AbstractVectorSet, tol) + return is_feasible(vec(x), set, tol) +end + function is_feasible(x::Number, set::MOI.AbstractVectorSet, tol) return is_feasible([x], set, tol) end @@ -88,13 +111,26 @@ end function _add_constraint!(context::Context, c::Constraint) if vexity(c.child) == ConstVexity() - # This `evaluate` call is safe, since even if it refers to a `fix!`'d variable, - # it happens when we are formulating the problem (not at expression-time), so there - # is not time for the variable to be re-`fix!`'d to a different value (or `free!`'d) - if !is_feasible(evaluate(c.child), c.set, CONSTANT_CONSTRAINT_TOL[]) + # This `evaluate` call is safe, since even if it refers to a `fix!`'d + # variable, it happens when we are formulating the problem (not at + # expression-time), so there is no time for the variable to be + # re-`fix!`'d to a different value (or `free!`'d) + feas = is_feasible(evaluate(c.child), c.set, CONSTANT_CONSTRAINT_TOL[]) + # There are three possible values of feas: true, false, and missing. + if feas === true + # Do nothing. The constraint is satisfied. Do not add it to the + # solver. + return + elseif feas === false + # We have proven the constraint is not satisfied. Set a flag and + # bail. We don't need to add it to the solver. context.detected_infeasible_during_formulation = true + return + else + # The feasibility check was unsure, likely because a method was + # missing. Pass the constraint to the solver. + @assert ismissing(feas) end - return end f = conic_form!(context, c.child) context.constr_to_moi_inds[c] = MOI_add_constraint(context.model, f, c.set) diff --git a/src/to_MOI.jl b/src/to_MOI.jl index b9d8dd4c7..05e3cdfee 100644 --- a/src/to_MOI.jl +++ b/src/to_MOI.jl @@ -30,6 +30,11 @@ function MOI_add_constraint(model, f, set) return MOI.add_constraint(model, f, set) end +function MOI_add_constraint(model, f::SPARSE_VECTOR{T}, set) where {T} + g = MOI.VectorAffineFunction{T}(MOI.VectorAffineTerm{T}[], f) + return MOI.add_constraint(model, g, set) +end + function MOI_add_constraint(model, f::SparseTape, set) return MOI_add_constraint(model, to_vaf(f), set) end diff --git a/test/test_constraints.jl b/test/test_constraints.jl index e3875fc64..6e5d0921f 100644 --- a/test/test_constraints.jl +++ b/test/test_constraints.jl @@ -397,6 +397,49 @@ function test_RelativeEntropyEpiConeSquare() return end +function test_is_feasible() + @test Convex.is_feasible([1.0, 0.0], MOI.Nonnegatives(2), 0.0) + @test !Convex.is_feasible([-1.0, 0.0], MOI.Nonnegatives(2), 0.0) + @test Convex.is_feasible([-1.0, 0.0], MOI.Nonpositives(2), 0.0) + @test !Convex.is_feasible([1.0, 0.0], MOI.Nonpositives(2), 0.0) + @test Convex.is_feasible([1e-5, 0.0], MOI.Zeros(2), 1e-5) + @test !Convex.is_feasible([1e-5, 0.0], MOI.Zeros(2), 0.0) + @test Convex.is_feasible([5.0, 3.0, 4.0], MOI.SecondOrderCone(3), 0.0) + set = MOI.PositiveSemidefiniteConeSquare(2) + @test Convex.is_feasible([1.0 0.0; 0.0 1.0], set, 0.0) + @test !Convex.is_feasible([-1.0 0.0; 0.0 1.0], set, 0.0) + @test !Convex.is_feasible([1.0 1e-6; 0.0 1.0], set, 0.0) + set = MOI.NormSpectralCone(2, 2) + @test Convex.is_feasible([1.0, 1.0, 0.0, 0.0, 1.0], set, 0.0) === missing + return +end + +function test_distance_to_set_matrix() + x = Variable(2, 2) + y = Variable() + fix!(x, [1 0; 0 1]) + # Constraint has a fixed `Matrix` value. + model = minimize(y, [sum(x; dims = 1) <= 1, y >= 1]) + solve!(model, SCS.Optimizer; silent = true) + @test ≈(model.optval, 1.0; atol = 1e-3) + return +end + +function test_distance_to_set_undefined() + t = Variable() + fix!(t, 2) + x = Variable(2, 2) + fix!(x, [1 0; 0 1]) + y = Variable() + # This constraint is fixed, and `MOI.distance_to_set` is not defined for it, + # but it should still work without erroring. + c = Convex.Constraint(vcat(t, vec(x)), MOI.NormSpectralCone(2, 2)) + model = minimize(y, [c, y >= 1]) + solve!(model, SCS.Optimizer; silent = true) + @test ≈(model.optval, 1.0; atol = 1e-4) + return +end + end # TestConstraints TestConstraints.runtests()