diff --git a/docs/src/changelog.md b/docs/src/changelog.md index ae33a7f10..1bbe48fa9 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -27,6 +27,9 @@ changes. dual variable associated with such constraints is now reversed in sign**. (Following the convention in MathOptInterface, the dual of `a <= b` is always negative, regardless of optimization sense.) (#593) + * The structs `LessThanConstraint`, `GreaterThanConstraint`, `EqualToConstraint` + `SecondOrderConeConstraint` and `PostiveSemidefinteConeConstraint` have + been replaced by `GenericConstraint{S}` where `S<:MOI.AbstractSet` (#590) * The syntaxes `dot(*)`, `dot(/)` and `dot(^)` have been removed in favor of explicit broadcasting (`x .* y`, `x ./ y`, and `x .^ y`). These were (mild) type piracy. In addition, `vecdot(x,y)` has been removed. Call diff --git a/src/atoms/second_order_cone/QolElemAtom.jl b/src/atoms/second_order_cone/QolElemAtom.jl index 10018088d..0a541d5af 100644 --- a/src/atoms/second_order_cone/QolElemAtom.jl +++ b/src/atoms/second_order_cone/QolElemAtom.jl @@ -30,10 +30,8 @@ function new_conic_form!(context::Context{T}, q::QolElemAtom) where {T} x, y = q.children t = Variable(x.size) for i in 1:length(x) - add_constraint!( - context, - SecondOrderConeConstraint(y[i] + t[i], y[i] - t[i], 2 * x[i]), - ) + f = vcat(y[i] + t[i], y[i] - t[i], 2 * x[i]) + add_constraint!(context, GenericConstraint{MOI.SecondOrderCone}(f)) end add_constraint!(context, y >= 0) return conic_form!(context, t) diff --git a/src/atoms/second_order_cone/QuadOverLinAtom.jl b/src/atoms/second_order_cone/QuadOverLinAtom.jl index 822b5cc88..38536763d 100644 --- a/src/atoms/second_order_cone/QuadOverLinAtom.jl +++ b/src/atoms/second_order_cone/QuadOverLinAtom.jl @@ -30,7 +30,8 @@ end function new_conic_form!(context::Context, q::QuadOverLinAtom) t = Variable() x, y = q.children - add_constraint!(context, SecondOrderConeConstraint(y + t, y - t, 2 * x)) + f = vcat(y + t, y - t, 2 * x) + add_constraint!(context, GenericConstraint{MOI.SecondOrderCone}(f)) add_constraint!(context, y >= 0) return conic_form!(context, t) end diff --git a/src/constraints/EqualToConstraint.jl b/src/constraints/EqualToConstraint.jl deleted file mode 100644 index de42fe424..000000000 --- a/src/constraints/EqualToConstraint.jl +++ /dev/null @@ -1,68 +0,0 @@ -mutable struct EqualToConstraint <: Constraint - lhs::AbstractExpr - rhs::AbstractExpr - size::Tuple{Int,Int} - dual::Union{Value,Nothing} - - function EqualToConstraint(lhs::AbstractExpr, rhs::AbstractExpr) - if lhs.size == rhs.size || lhs.size == (1, 1) - sz = rhs.size - if lhs.size == (1, 1) && rhs.size != (1, 1) - lhs = lhs * ones(rhs.size) - end - elseif rhs.size == (1, 1) - sz = lhs.size - if rhs.size == (1, 1) && lhs.size != (1, 1) - rhs = rhs * ones(lhs.size) - end - else - error( - "Cannot create equality constraint between expressions of size $(lhs.size) and $(rhs.size)", - ) - end - return new(lhs, rhs, sz, nothing) - end -end - -head(io::IO, ::EqualToConstraint) = print(io, "==") - -function vexity(c::EqualToConstraint) - vex = vexity(c.lhs) + (-vexity(c.rhs)) - # You can't have equality constraints with concave/convex expressions - if vex == ConvexVexity() || vex == ConcaveVexity() - return NotDcp() - end - return vex -end - -function _add_constraint!(context::Context{T}, c::EqualToConstraint) where {T} - f = conic_form!(context, c.lhs - c.rhs) - if f isa AbstractVector - if !all(abs.(f) .<= CONSTANT_CONSTRAINT_TOL[]) - @warn "Constant constraint is violated" - context.detected_infeasible_during_formulation[] = true - end - return - end - set = MOI.Zeros(MOI.output_dimension(f)) - context.constr_to_moi_inds[c] = MOI_add_constraint(context.model, f, set) - return -end - -Base.:(==)(lhs::AbstractExpr, rhs::AbstractExpr) = EqualToConstraint(lhs, rhs) - -Base.:(==)(lhs::AbstractExpr, rhs::Value) = ==(lhs, constant(rhs)) - -Base.:(==)(lhs::Value, rhs::AbstractExpr) = ==(constant(lhs), rhs) - -function populate_dual!(model::MOI.ModelLike, c::EqualToConstraint, indices) - if iscomplex(c) - re = MOI.get(model, MOI.ConstraintDual(), indices[1]) - imag = MOI.get(model, MOI.ConstraintDual(), indices[2]) - c.dual = output(reshape(re + im * imag, c.size)) - else - ret = MOI.get(model, MOI.ConstraintDual(), indices) - c.dual = output(reshape(ret, c.size)) - end - return -end diff --git a/src/constraints/GenericConstraint.jl b/src/constraints/GenericConstraint.jl new file mode 100644 index 000000000..7335e3c17 --- /dev/null +++ b/src/constraints/GenericConstraint.jl @@ -0,0 +1,245 @@ +mutable struct GenericConstraint{S<:MOI.AbstractSet} <: Constraint + child::AbstractExpr + set::S + dual::Union{Value,Nothing} + + function GenericConstraint(child::AbstractExpr, set::MOI.AbstractSet) + return new{typeof(set)}(child, set, nothing) + end +end + +function GenericConstraint{S}(child::AbstractExpr) where {S<:MOI.AbstractSet} + return GenericConstraint(child, set_with_size(S, size(child))) +end + +head(io::IO, c::GenericConstraint) = head(io, c.set) + +# A default fallback that skips the feasibiltiy check. +is_feasible(f, ::MOI.AbstractSet, tol) = true + +AbstractTrees.children(c::GenericConstraint) = (c.child,) + +vexity(c::GenericConstraint) = vexity(vexity(c.child), c.set) + +function _add_constraint!(context::Context, c::GenericConstraint) + if vexity(c.child) == ConstVexity() + x = evaluate(c.child) + if !is_feasible(x, c.set, CONSTANT_CONSTRAINT_TOL[]) + context.detected_infeasible_during_formulation[] = true + end + return + end + f = conic_form!(context, c.child) + context.constr_to_moi_inds[c] = MOI_add_constraint(context.model, f, c.set) + return +end + +function populate_dual!(model::MOI.ModelLike, c::GenericConstraint, indices) + ret = MOI.get(model, MOI.ConstraintDual(), indices) + c.dual = output(reshape(ret, c.child.size)) + return +end + +function populate_dual!( + model::MOI.ModelLike, + c::GenericConstraint, + indices::NTuple{2}, +) + re = MOI.get(model, MOI.ConstraintDual(), indices[1]) + imag = MOI.get(model, MOI.ConstraintDual(), indices[2]) + c.dual = output(reshape(re + im * imag, c.child.size)) + return +end + +function _promote_size(lhs::AbstractExpr, rhs::AbstractExpr) + if lhs.size == rhs.size || lhs.size == (1, 1) + sz = rhs.size + if lhs.size == (1, 1) && rhs.size != (1, 1) + lhs = lhs * ones(rhs.size) + end + elseif rhs.size == (1, 1) + sz = lhs.size + if rhs.size == (1, 1) && lhs.size != (1, 1) + rhs = rhs * ones(lhs.size) + end + else + error( + "Cannot create constraint between expressions of size " * + "$(lhs.size) and $(rhs.size)", + ) + end + return lhs, rhs +end + +# ============================================================================== +# Nonnegatives +# ============================================================================== + +function set_with_size(::Type{MOI.Nonnegatives}, sz::Tuple{Int,Int}) + return MOI.Nonnegatives(prod(sz)) +end + +head(io::IO, ::MOI.Nonnegatives) = print(io, "≥") + +is_feasible(f, ::MOI.Nonnegatives, tol) = all(f .>= -tol) + +function vexity(vex, ::MOI.Nonnegatives) + if vex == ConvexVexity() + return NotDcp() + elseif vex == ConcaveVexity() + return ConvexVexity() + end + return vex +end + +function Base.:>=(lhs::AbstractExpr, rhs::AbstractExpr) + if sign(lhs) == ComplexSign() || sign(rhs) == ComplexSign() + error( + "Cannot create constraint between expressions of sign " * + "$(sign(lhs)) and $(sign(rhs))", + ) + end + lhs, rhs = _promote_size(lhs, rhs) + return GenericConstraint{MOI.Nonnegatives}(lhs - rhs) +end + +Base.:>=(lhs::AbstractExpr, rhs::Value) = >=(lhs, constant(rhs)) + +Base.:>=(lhs::Value, rhs::AbstractExpr) = >=(constant(lhs), rhs) + +# ============================================================================== +# Nonnpositives +# ============================================================================== + +function set_with_size(::Type{MOI.Nonpositives}, sz::Tuple{Int,Int}) + return MOI.Nonpositives(prod(sz)) +end + +head(io::IO, ::MOI.Nonpositives) = print(io, "≤") + +is_feasible(f, ::MOI.Nonpositives, tol) = all(f .<= tol) + +function vexity(vex, ::MOI.Nonpositives) + if vex == ConcaveVexity() + return NotDcp() + end + return vex +end + +function Base.:<=(lhs::AbstractExpr, rhs::AbstractExpr) + if sign(lhs) == ComplexSign() || sign(rhs) == ComplexSign() + error( + "Cannot create constraint between expressions of sign " * + "$(sign(lhs)) and $(sign(rhs))", + ) + end + lhs, rhs = _promote_size(lhs, rhs) + return GenericConstraint{MOI.Nonpositives}(lhs - rhs) +end + +Base.:<=(lhs::AbstractExpr, rhs::Value) = <=(lhs, constant(rhs)) + +Base.:<=(lhs::Value, rhs::AbstractExpr) = <=(constant(lhs), rhs) + +# ============================================================================== +# Zeros +# ============================================================================== + +function set_with_size(::Type{MOI.Zeros}, sz::Tuple{Int,Int}) + return MOI.Zeros(prod(sz)) +end + +head(io::IO, ::MOI.Zeros) = print(io, "==") + +is_feasible(f, ::MOI.Zeros, tol) = all(abs.(f) .<= tol) + +function vexity(vex, ::MOI.Zeros) + if vex == ConvexVexity() || vex == ConcaveVexity() + return NotDcp() + end + return vex +end + +function Base.:(==)(lhs::AbstractExpr, rhs::AbstractExpr) + lhs, rhs = _promote_size(lhs, rhs) + return GenericConstraint{MOI.Zeros}(lhs - rhs) +end + +Base.:(==)(lhs::AbstractExpr, rhs::Value) = ==(lhs, constant(rhs)) + +Base.:(==)(lhs::Value, rhs::AbstractExpr) = ==(constant(lhs), rhs) + +# ============================================================================== +# PositiveSemidefiniteConeSquare +# ============================================================================== + +function set_with_size( + ::Type{MOI.PositiveSemidefiniteConeSquare}, + sz::Tuple{Int,Int}, +) + if sz[1] != sz[2] + error("Positive semidefinite expressions must be square") + end + return MOI.PositiveSemidefiniteConeSquare(sz[1]) +end + +head(io::IO, ::MOI.PositiveSemidefiniteConeSquare) = print(io, "sdp") + +function vexity(vex, ::MOI.PositiveSemidefiniteConeSquare) + if !(vex in (AffineVexity(), ConstVexity())) + return NotDcp() + end + return AffineVexity() +end + +function is_feasible(x, ::MOI.PositiveSemidefiniteConeSquare, tol) + return x ≈ transpose(x) && LinearAlgebra.eigmin(x) >= -tol +end + +function LinearAlgebra.isposdef(x::AbstractExpr) + if iscomplex(x) + return GenericConstraint{MOI.PositiveSemidefiniteConeSquare}( + [real(x) -imag(x); imag(x) real(x)], + ) + end + return GenericConstraint{MOI.PositiveSemidefiniteConeSquare}(x) +end + +⪰(x::AbstractExpr, y::AbstractExpr) = isposdef(x - y) + +function ⪰(x::AbstractExpr, y::Value) + if all(y .== 0) + return isposdef(x) + end + return isposdef(x - constant(y)) +end + +function ⪰(x::Value, y::AbstractExpr) + if all(x .== 0) + return isposdef(-y) + end + return isposdef(constant(x) - y) +end + +⪯(x::AbstractExpr, y::AbstractExpr) = ⪰(y, x) + +⪯(x::Value, y::AbstractExpr) = ⪰(y, x) + +⪯(x::AbstractExpr, y::Value) = ⪰(y, x) + +# ============================================================================== +# SecondOrderCone +# ============================================================================== + +function set_with_size(::Type{MOI.SecondOrderCone}, sz::Tuple{Int,Int}) + return MOI.SecondOrderCone(prod(sz)) +end + +head(io::IO, ::MOI.SecondOrderCone) = print(io, "soc") + +function vexity(vex, ::MOI.SecondOrderCone) + if !(vex == ConstVexity() || vex == AffineVexity()) + return NotDcp() + end + return ConvexVexity() +end diff --git a/src/constraints/GreaterThanConstraint.jl b/src/constraints/GreaterThanConstraint.jl deleted file mode 100644 index 81803aab3..000000000 --- a/src/constraints/GreaterThanConstraint.jl +++ /dev/null @@ -1,69 +0,0 @@ -mutable struct GreaterThanConstraint <: Constraint - lhs::AbstractExpr - rhs::AbstractExpr - size::Tuple{Int,Int} - dual::Union{Value,Nothing} - - function GreaterThanConstraint(lhs::AbstractExpr, rhs::AbstractExpr) - if sign(lhs) == ComplexSign() || sign(rhs) == ComplexSign() - error( - "Cannot create inequality constraint between expressions of sign $(sign(lhs)) and $(sign(rhs))", - ) - end - if lhs.size == rhs.size || lhs.size == (1, 1) - sz = rhs.size - if lhs.size == (1, 1) && rhs.size != (1, 1) - lhs = lhs * ones(rhs.size) - end - elseif rhs.size == (1, 1) - sz = lhs.size - if rhs.size == (1, 1) && lhs.size != (1, 1) - rhs = rhs * ones(lhs.size) - end - else - error( - "Cannot create inequality constraint between expressions of size $(lhs.size) and $(rhs.size)", - ) - end - return new(lhs, rhs, sz, nothing) - end -end - -head(io::IO, ::GreaterThanConstraint) = print(io, "≥") - -function vexity(c::GreaterThanConstraint) - vex = -vexity(c.lhs) + (vexity(c.rhs)) - if vex == ConcaveVexity() - return NotDcp() - end - return vex -end - -function _add_constraint!( - context::Context{T}, - c::GreaterThanConstraint, -) where {T} - f = conic_form!(context, c.lhs - c.rhs) - if f isa AbstractVector - if !all(f .>= -CONSTANT_CONSTRAINT_TOL[]) - @warn "Constant constraint is violated" - context.detected_infeasible_during_formulation[] = true - end - return - end - set = MOI.Nonnegatives(MOI.output_dimension(f)) - context.constr_to_moi_inds[c] = MOI_add_constraint(context.model, f, set) - return -end - -Base.:>=(lhs::AbstractExpr, rhs::AbstractExpr) = GreaterThanConstraint(lhs, rhs) - -Base.:>=(lhs::AbstractExpr, rhs::Value) = >=(lhs, constant(rhs)) - -Base.:>=(lhs::Value, rhs::AbstractExpr) = >=(constant(lhs), rhs) - -function populate_dual!(model::MOI.ModelLike, c::GreaterThanConstraint, indices) - ret = MOI.get(model, MOI.ConstraintDual(), indices) - c.dual = output(reshape(ret, c.size)) - return -end diff --git a/src/constraints/LessThanConstraint.jl b/src/constraints/LessThanConstraint.jl deleted file mode 100644 index 4a74190cb..000000000 --- a/src/constraints/LessThanConstraint.jl +++ /dev/null @@ -1,67 +0,0 @@ -mutable struct LessThanConstraint <: Constraint - lhs::AbstractExpr - rhs::AbstractExpr - size::Tuple{Int,Int} - dual::Union{Value,Nothing} - - function LessThanConstraint(lhs::AbstractExpr, rhs::AbstractExpr) - if sign(lhs) == ComplexSign() || sign(rhs) == ComplexSign() - error( - "Cannot create inequality constraint between expressions of sign $(sign(lhs)) and $(sign(rhs))", - ) - end - if lhs.size == rhs.size || lhs.size == (1, 1) - sz = rhs.size - if lhs.size == (1, 1) && rhs.size != (1, 1) - lhs = lhs * ones(rhs.size) - end - elseif rhs.size == (1, 1) - sz = lhs.size - if rhs.size == (1, 1) && lhs.size != (1, 1) - rhs = rhs * ones(lhs.size) - end - else - error( - "Cannot create inequality constraint between expressions of size $(lhs.size) and $(rhs.size)", - ) - end - return new(lhs, rhs, sz, nothing) - end -end - -head(io::IO, ::LessThanConstraint) = print(io, "≤") - -function vexity(c::LessThanConstraint) - vex = vexity(c.lhs) + (-vexity(c.rhs)) - if vex == ConcaveVexity() - return NotDcp() - end - return vex -end - -function _add_constraint!(context::Context{T}, lt::LessThanConstraint) where {T} - f = conic_form!(context, lt.lhs - lt.rhs) - if f isa AbstractVector - # a trivial constraint without variables like `5 <= 0` - if !all(f .<= CONSTANT_CONSTRAINT_TOL[]) - @warn "Constant constraint is violated" - context.detected_infeasible_during_formulation[] = true - end - return - end - set = MOI.Nonpositives(MOI.output_dimension(f)) - context.constr_to_moi_inds[lt] = MOI_add_constraint(context.model, f, set) - return -end - -Base.:<=(lhs::AbstractExpr, rhs::AbstractExpr) = LessThanConstraint(lhs, rhs) - -Base.:<=(lhs::AbstractExpr, rhs::Value) = <=(lhs, constant(rhs)) - -Base.:<=(lhs::Value, rhs::AbstractExpr) = <=(constant(lhs), rhs) - -function populate_dual!(model::MOI.ModelLike, c::LessThanConstraint, indices) - ret = MOI.get(model, MOI.ConstraintDual(), indices) - c.dual = output(reshape(ret, c.size)) - return -end diff --git a/src/constraints/PositiveSemidefiniteConeConstraint.jl b/src/constraints/PositiveSemidefiniteConeConstraint.jl deleted file mode 100644 index 4f84de035..000000000 --- a/src/constraints/PositiveSemidefiniteConeConstraint.jl +++ /dev/null @@ -1,84 +0,0 @@ -mutable struct PositiveSemidefiniteConeConstraint <: Constraint - child::AbstractExpr - size::Tuple{Int,Int} - dual::Union{Value,Nothing} - - function PositiveSemidefiniteConeConstraint(child::AbstractExpr) - if child.size[1] != child.size[2] - error("Positive semidefinite expressions must be square") - end - return new(child, child.size, nothing) - end -end - -head(io::IO, ::PositiveSemidefiniteConeConstraint) = print(io, "sdp") - -AbstractTrees.children(c::PositiveSemidefiniteConeConstraint) = (c.child,) - -function vexity(c::PositiveSemidefiniteConeConstraint) - if !(vexity(c.child) in (AffineVexity(), ConstVexity())) - return NotDcp() - end - return AffineVexity() -end - -function _add_constraint!( - context::Context, - c::PositiveSemidefiniteConeConstraint, -) - if vexity(c.child) == ConstVexity() - x = evaluate(c.child) - tol = CONSTANT_CONSTRAINT_TOL[] - if !(x ≈ transpose(x)) - @warn "constant SDP constraint is violated" - context.detected_infeasible_during_formulation[] = true - elseif evaluate(LinearAlgebra.eigmin(c.child)) < -tol - @warn "constant SDP constraint is violated" - context.detected_infeasible_during_formulation[] = true - end - return - end - f = conic_form!(context, c.child) - set = MOI.PositiveSemidefiniteConeSquare(c.size[1]) - context.constr_to_moi_inds[c] = MOI_add_constraint(context.model, f, set) - return -end - -function populate_dual!( - model::MOI.ModelLike, - c::PositiveSemidefiniteConeConstraint, - indices, -) - dual = MOI.get(model, MOI.ConstraintDual(), indices) - c.dual = output(reshape(dual, c.size)) - return -end - -function LinearAlgebra.isposdef(x::AbstractExpr) - if iscomplex(x) - return PositiveSemidefiniteConeConstraint( - [real(x) -imag(x); imag(x) real(x)], - ) - end - return PositiveSemidefiniteConeConstraint(x) -end - -⪰(x::AbstractExpr, y::AbstractExpr) = isposdef(x - y) - -function ⪰(x::AbstractExpr, y::Value) - if all(y .== 0) - return isposdef(x) - end - return isposdef(x - constant(y)) -end - -function ⪰(x::Value, y::AbstractExpr) - if all(x .== 0) - return isposdef(-y) - end - return isposdef(constant(x) - y) -end - -⪯(x::AbstractExpr, y::AbstractExpr) = ⪰(y, x) -⪯(x::Value, y::AbstractExpr) = ⪰(y, x) -⪯(x::AbstractExpr, y::Value) = ⪰(y, x) diff --git a/src/constraints/SecondOrderConeConstraint.jl b/src/constraints/SecondOrderConeConstraint.jl deleted file mode 100644 index 1b2e511d2..000000000 --- a/src/constraints/SecondOrderConeConstraint.jl +++ /dev/null @@ -1,43 +0,0 @@ -mutable struct SecondOrderConeConstraint <: Constraint - children::Tuple - dual::Union{Value,Nothing} - - SecondOrderConeConstraint(args::AbstractExpr...) = new(args, nothing) -end - -head(io::IO, ::SecondOrderConeConstraint) = print(io, "soc") - -AbstractTrees.children(c::SecondOrderConeConstraint) = c.children - -function vexity(c::SecondOrderConeConstraint) - for child in c.children - if !(vexity(child) in (ConstVexity(), AffineVexity())) - return NotDcp() - end - end - return ConvexVexity() -end - -function _add_constraint!( - context::Context{T}, - c::SecondOrderConeConstraint, -) where {T} - f = operate( - vcat, - T, - sum(map(sign, c.children)), - map(child -> conic_form!(context, child), c.children)..., - ) - set = MOI.SecondOrderCone(MOI.output_dimension(f)) - context.constr_to_moi_inds[c] = MOI_add_constraint(context.model, f, set) - return -end - -function populate_dual!( - model::MOI.ModelLike, - c::SecondOrderConeConstraint, - indices, -) - c.dual = output(MOI.get(model, MOI.ConstraintDual(), indices)) - return -end diff --git a/test/test_constraints.jl b/test/test_constraints.jl index 106110654..7bf7b4277 100644 --- a/test/test_constraints.jl +++ b/test/test_constraints.jl @@ -22,7 +22,7 @@ end function test_EqualToConstraint() @test_throws( ErrorException( - "Cannot create equality constraint between expressions of size (2, 3) and (3, 2)", + "Cannot create constraint between expressions of size (2, 3) and (3, 2)", ), Variable(2, 3) == Variable(3, 2), ) @@ -33,7 +33,7 @@ end function test_EqualToConstraint_violated() p = satisfy([constant(5) == 0]) - @test_logs (:warn,) (:warn,) solve!(p, SCS.Optimizer) + @test_logs (:warn,) solve!(p, SCS.Optimizer) return end @@ -90,19 +90,19 @@ end function test_GreaterThanConstraint() @test_throws( ErrorException( - "Cannot create inequality constraint between expressions of size (2, 3) and (3, 2)", + "Cannot create constraint between expressions of size (2, 3) and (3, 2)", ), Variable(2, 3) >= Variable(3, 2), ) @test_throws( ErrorException( - "Cannot create inequality constraint between expressions of sign $(Convex.NoSign()) and $(Convex.ComplexSign())", + "Cannot create constraint between expressions of sign $(Convex.NoSign()) and $(Convex.ComplexSign())", ), Variable() >= 2 + 3im, ) @test_throws( ErrorException( - "Cannot create inequality constraint between expressions of sign $(Convex.ComplexSign()) and $(Convex.NoSign())", + "Cannot create constraint between expressions of sign $(Convex.ComplexSign()) and $(Convex.NoSign())", ), 2 + 3im >= Variable(), ) @@ -112,7 +112,7 @@ end function test_GreaterThanConstraint_violated() p = satisfy([constant(5) >= 6]) - @test_logs (:warn,) (:warn,) solve!(p, SCS.Optimizer) + @test_logs (:warn,) solve!(p, SCS.Optimizer) return end @@ -139,19 +139,19 @@ end function test_LessThanConstraint() @test_throws( ErrorException( - "Cannot create inequality constraint between expressions of size (2, 3) and (3, 2)", + "Cannot create constraint between expressions of size (2, 3) and (3, 2)", ), Variable(2, 3) <= Variable(3, 2), ) @test_throws( ErrorException( - "Cannot create inequality constraint between expressions of sign $(Convex.NoSign()) and $(Convex.ComplexSign())", + "Cannot create constraint between expressions of sign $(Convex.NoSign()) and $(Convex.ComplexSign())", ), Variable() <= 2 + 3im, ) @test_throws( ErrorException( - "Cannot create inequality constraint between expressions of sign $(Convex.ComplexSign()) and $(Convex.NoSign())", + "Cannot create constraint between expressions of sign $(Convex.ComplexSign()) and $(Convex.NoSign())", ), 2 + 3im <= Variable(), ) @@ -161,7 +161,7 @@ end function test_LessThanConstraint_violated() p = satisfy([constant(5) <= 4]) - @test_logs (:warn,) (:warn,) solve!(p, SCS.Optimizer) + @test_logs (:warn,) solve!(p, SCS.Optimizer) return end @@ -183,52 +183,58 @@ function test_LessThanConstraint_dual_maximize() return end -### constraints/PositiveSemidefiniteConeConstraint +### constraints/GenericConstraint{MOI.PositiveSemidefiniteConeSquare} -function test_PositiveSemidefiniteConeConstraint() +function test_GenericConstraint_PositiveSemidefiniteConeSquare() @test_throws( ErrorException("Positive semidefinite expressions must be square"), - Convex.PositiveSemidefiniteConeConstraint(Variable(2, 3)), + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare}( + Variable(2, 3), + ), ) X = Variable(2, 2) - c = Convex.PositiveSemidefiniteConeConstraint(X) + c = Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare}(X) p = minimize(tr(X), [c, X >= [1 2; 3 4]]) solve!(p, SCS.Optimizer; silent_solver = true) @test isapprox(X.value, [2.25 3; 3 4]; atol = 1e-3) y = (c.dual + c.dual') / 2 @test isapprox(y[1], 1; atol = 1e-3) - @test (0 ⪯ X) isa Convex.PositiveSemidefiniteConeConstraint - @test (-X ⪯ 0) isa Convex.PositiveSemidefiniteConeConstraint - @test (-X ⪯ constant(0)) isa Convex.PositiveSemidefiniteConeConstraint - @test (constant(0) ⪯ X) isa Convex.PositiveSemidefiniteConeConstraint + @test (0 ⪯ X) isa + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} + @test (-X ⪯ 0) isa + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} + @test (-X ⪯ constant(0)) isa + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} + @test (constant(0) ⪯ X) isa + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} @test_throws(ErrorException("Set PSD not understood"), X in :PSD) @test vexity(X ⪯ square(Variable())) == Convex.NotDcp() return end -function test_PositiveSemidefiniteConeConstraint_violated() +function test_GenericConstraint_PositiveSemidefiniteConeSquare_violated() X = constant([1 2; 3 4]) p = satisfy([X ⪰ 0]) - @test_logs (:warn,) (:warn,) solve!(p, SCS.Optimizer) + @test_logs (:warn,) solve!(p, SCS.Optimizer) X = constant([1 2; 2 3]) p = satisfy([X ⪰ 0]) - @test_logs (:warn,) (:warn,) solve!(p, SCS.Optimizer) + @test_logs (:warn,) solve!(p, SCS.Optimizer) return end -### constraints/SecondOrderConeConstraint +### constraints/GenericConstraint_SecondOrderCone -function test_SecondOrderConeConstraint() +function test_GenericConstraint_SecondOrderCone() x = Variable(3) t = Variable() - c = Convex.SecondOrderConeConstraint(t, x) + c = Convex.GenericConstraint{MOI.SecondOrderCone}(vcat(t, x)) p = minimize(t, [c, x >= [2, 3, 4]]) solve!(p, SCS.Optimizer; silent_solver = true) @test isapprox(x.value, [2, 3, 4]; atol = 1e-3) t_ = sqrt(29) @test isapprox(t.value, t_; atol = 1e-3) @test isapprox(c.dual, [1, -2 / t_, -3 / t_, -4 / t_]; atol = 1e-3) - c = Convex.SecondOrderConeConstraint(square(t), x) + c = Convex.GenericConstraint{MOI.SecondOrderCone}(vcat(square(t), x)) @test vexity(c) === Convex.NotDcp() return end diff --git a/test/test_utilities.jl b/test/test_utilities.jl index 360e515a9..d7cb295c2 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -171,11 +171,13 @@ function test_show() └─ real variable ($(Convex.show_id(x))) subject to ├─ ≥ constraint (affine) - │ ├─ real variable ($(Convex.show_id(x))) - │ └─ $(reshape([1], 1, 1)) + │ └─ + (affine; real) + │ ├─ real variable ($(Convex.show_id(x))) + │ └─ $(reshape([-1], 1, 1)) └─ ≤ constraint (affine) - ├─ real variable ($(Convex.show_id(x))) - └─ $(reshape([3], 1, 1)) + └─ + (affine; real) + ├─ real variable ($(Convex.show_id(x))) + └─ $(reshape([-3], 1, 1)) status: `solve!` not called yet""" @@ -198,27 +200,19 @@ function test_show() p = minimize(sum(x), root == root) @test curvature(p) == Convex.ConstVexity() @test sprint(show, p) == """ - minimize - └─ sum (affine; real) - └─ 2-element real variable ($(Convex.show_id(x))) - subject to - └─ == constraint (affine) - ├─ hcat (affine; real) - │ ├─ hcat (affine; real) - │ │ ├─ … - │ │ └─ … - │ └─ hcat (affine; real) - │ ├─ … - │ └─ … - └─ hcat (affine; real) - ├─ hcat (affine; real) - │ ├─ … - │ └─ … - └─ hcat (affine; real) - ├─ … - └─ … + minimize + └─ sum (affine; real) + └─ 2-element real variable ($(Convex.show_id(x))) + subject to + └─ == constraint (affine) + └─ + (affine; real) + ├─ hcat (affine; real) + │ ├─ … + │ └─ … + └─ Convex.NegateAtom (affine; real) + └─ … - status: `solve!` not called yet""" + status: `solve!` not called yet""" # test `MAXWIDTH` x = Variable() @@ -232,11 +226,13 @@ function test_show() └─ nothing subject to ├─ == constraint (affine) - │ ├─ real variable ($(Convex.show_id(x))) - │ └─ $(reshape([1], 1, 1)) + │ └─ + (affine; real) + │ ├─ real variable ($(Convex.show_id(x))) + │ └─ $(reshape([-1], 1, 1)) ├─ == constraint (affine) - │ ├─ real variable ($(Convex.show_id(x))) - │ └─ $(reshape([2], 1, 1)) + │ └─ + (affine; real) + │ ├─ real variable ($(Convex.show_id(x))) + │ └─ $(reshape([-2], 1, 1)) ⋮ status: `solve!` not called yet""" @@ -258,8 +254,9 @@ function test_show() └─ nothing subject to └─ ≥ constraint (affine) - ├─ real variable ($(Convex.show_id(x))) - └─ $(reshape([0], 1, 1)) + └─ + (affine; real) + ├─ real variable ($(Convex.show_id(x))) + └─ $(reshape([0], 1, 1)) termination status: OPTIMAL primal status: FEASIBLE_POINT @@ -403,7 +400,8 @@ function test_Constructors() Semidefinite(2), ] @test length(get_constraints(x)) == 1 - @test get_constraints(x)[] isa Convex.PositiveSemidefiniteConeConstraint + @test get_constraints(x)[] isa + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} end @test_throws ErrorException HermitianSemidefinite(2, 3) @@ -953,7 +951,8 @@ end function test_deprecation_in_symbol() x = Variable(2, 2) @test_logs (:warn,) (x in :SDP) - @test in(x, :semidefinite) isa Convex.PositiveSemidefiniteConeConstraint + @test in(x, :semidefinite) isa + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} return end