From f2dd4228130d092e24c25f36917cdaad541f191e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 28 Dec 2023 10:19:19 +0100 Subject: [PATCH 01/16] Refactor Constraint --- .../PositiveSemidefiniteConeConstraint.jl | 15 +++++---------- src/constraints/SecondOrderConeConstraint.jl | 7 +++---- src/expressions.jl | 12 +++++++++++- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/constraints/PositiveSemidefiniteConeConstraint.jl b/src/constraints/PositiveSemidefiniteConeConstraint.jl index 4f84de035..aa5c6453b 100644 --- a/src/constraints/PositiveSemidefiniteConeConstraint.jl +++ b/src/constraints/PositiveSemidefiniteConeConstraint.jl @@ -1,14 +1,9 @@ -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) +const PositiveSemidefiniteConeConstraint = Constraint{MOI.PositiveSemidefiniteConeTriangle} +function set_with_size(::Type{MOI.PositiveSemidefiniteConeTriangle}, sz::Tuple{Int,Int}) + if sz[1] != sz[2] + error("Positive semidefinite expressions must be square") end + return MOI.PositiveSemidefiniteConeTriangle(sz[1]) end head(io::IO, ::PositiveSemidefiniteConeConstraint) = print(io, "sdp") diff --git a/src/constraints/SecondOrderConeConstraint.jl b/src/constraints/SecondOrderConeConstraint.jl index 1b2e511d2..c9c0f0c6c 100644 --- a/src/constraints/SecondOrderConeConstraint.jl +++ b/src/constraints/SecondOrderConeConstraint.jl @@ -1,8 +1,7 @@ -mutable struct SecondOrderConeConstraint <: Constraint - children::Tuple - dual::Union{Value,Nothing} +const SecondOrderConeConstraint = Constraint{MOI.SecondOrderCone} - SecondOrderConeConstraint(args::AbstractExpr...) = new(args, nothing) +function set_with_size(::Type{MOI.SecondOrderCone}, sz::Tuple{Int}) + return MOI.SecondOrderCone(sz[1]) end head(io::IO, ::SecondOrderConeConstraint) = print(io, "soc") diff --git a/src/expressions.jl b/src/expressions.jl index 25fdd80c2..1b862f830 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -28,7 +28,17 @@ abstract type AbstractExpr end -abstract type Constraint end +struct Constraint{S<:MOI.AbstractSet} + child::AbstractExpr + set::S + dual::ValueOrNothing + function Constraint(child, set::MOI.AbstractSet) + return new{typeof(set)}(child, set) + end + function Constraint{S}(child) where {S<:MOI.AbstractSet} + return Constraint(child, set_with_size(S, size(child))) + end +end const Value = Union{Number,AbstractArray} From 1541479b6f1ad77f7a42312fb09b1bbd6d472456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 11 Apr 2024 23:14:18 +0200 Subject: [PATCH 02/16] Refactor into GenericConstraint --- src/constraints/GreaterThanConstraint.jl | 81 ++++++------------- src/constraints/LessThanConstraint.jl | 75 ++++++----------- .../PositiveSemidefiniteConeConstraint.jl | 46 +++-------- src/expressions.jl | 32 +++++++- 4 files changed, 92 insertions(+), 142 deletions(-) diff --git a/src/constraints/GreaterThanConstraint.jl b/src/constraints/GreaterThanConstraint.jl index 81803aab3..f47f714b1 100644 --- a/src/constraints/GreaterThanConstraint.jl +++ b/src/constraints/GreaterThanConstraint.jl @@ -1,69 +1,40 @@ -mutable struct GreaterThanConstraint <: Constraint - lhs::AbstractExpr - rhs::AbstractExpr - size::Tuple{Int,Int} - dual::Union{Value,Nothing} +head(io::IO, ::MOI.Nonnegatives) = print(io, "nonneg") - 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 +function is_feasible(f, ::MOI.Nonnegatives, tol) + return all(f .>= tol) end -head(io::IO, ::GreaterThanConstraint) = print(io, "≥") - -function vexity(c::GreaterThanConstraint) - vex = -vexity(c.lhs) + (vexity(c.rhs)) - if vex == ConcaveVexity() +function vexity(vex, ::MOI.Nonnegatives) + if vex == ConvexVexity() 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 +function Base.:>=(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 - return + 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 - set = MOI.Nonnegatives(MOI.output_dimension(f)) - context.constr_to_moi_inds[c] = MOI_add_constraint(context.model, f, set) - return + return GenericConstraint{MOI.Nonnegatives}(lhs - rhs) 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 +Base.:>=(lhs::Value, rhs::AbstractExpr) = >=(constant(lhs), rhs) \ No newline at end of file diff --git a/src/constraints/LessThanConstraint.jl b/src/constraints/LessThanConstraint.jl index 4a74190cb..89637b85f 100644 --- a/src/constraints/LessThanConstraint.jl +++ b/src/constraints/LessThanConstraint.jl @@ -1,67 +1,40 @@ -mutable struct LessThanConstraint <: Constraint - lhs::AbstractExpr - rhs::AbstractExpr - size::Tuple{Int,Int} - dual::Union{Value,Nothing} +head(io::IO, ::MOI.Nonpositives) = print(io, "nonpos") - 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 +function is_feasible(f, ::MOI.Nonpositives, tol) + return all(f .<= -tol) end -head(io::IO, ::LessThanConstraint) = print(io, "≤") - -function vexity(c::LessThanConstraint) - vex = vexity(c.lhs) + (-vexity(c.rhs)) +function vexity(vex, ::MOI.Nonpositives) 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 +function Base.:<=(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 - return + 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 - set = MOI.Nonpositives(MOI.output_dimension(f)) - context.constr_to_moi_inds[lt] = MOI_add_constraint(context.model, f, set) - return + return GenericConstraint{MOI.Nonpositives}(lhs - rhs) 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 index aa5c6453b..e064de476 100644 --- a/src/constraints/PositiveSemidefiniteConeConstraint.jl +++ b/src/constraints/PositiveSemidefiniteConeConstraint.jl @@ -1,4 +1,3 @@ -const PositiveSemidefiniteConeConstraint = Constraint{MOI.PositiveSemidefiniteConeTriangle} function set_with_size(::Type{MOI.PositiveSemidefiniteConeTriangle}, sz::Tuple{Int,Int}) if sz[1] != sz[2] error("Positive semidefinite expressions must be square") @@ -6,47 +5,24 @@ function set_with_size(::Type{MOI.PositiveSemidefiniteConeTriangle}, sz::Tuple{I return MOI.PositiveSemidefiniteConeTriangle(sz[1]) end -head(io::IO, ::PositiveSemidefiniteConeConstraint) = print(io, "sdp") +head(io::IO, ::MOI.PositiveSemidefiniteConeTriangle) = print(io, "sdp") -AbstractTrees.children(c::PositiveSemidefiniteConeConstraint) = (c.child,) - -function vexity(c::PositiveSemidefiniteConeConstraint) - if !(vexity(c.child) in (AffineVexity(), ConstVexity())) +function vexity(vex, ::MOI.PositiveSemidefiniteConeTriangle) + if !(vex 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 +function is_feasible(x, ::MOI.PositiveSemidefiniteConeTriangle, tol) + if !(x ≈ transpose(x)) + @warn "constant SDP constraint is violated" + return false + elseif evaluate(LinearAlgebra.eigmin(c.child)) < -tol + @warn "constant SDP constraint is violated" + return false 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 + return true end function LinearAlgebra.isposdef(x::AbstractExpr) diff --git a/src/expressions.jl b/src/expressions.jl index 1b862f830..0fdc1d4d9 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -28,7 +28,9 @@ abstract type AbstractExpr end -struct Constraint{S<:MOI.AbstractSet} +abstract type Constraint end + +struct GenericConstraint{S<:MOI.AbstractSet} child::AbstractExpr set::S dual::ValueOrNothing @@ -40,6 +42,34 @@ struct Constraint{S<:MOI.AbstractSet} end end +head(io::IO, c::GenericConstraint) = head(io, c.set) + +AbstractTrees.children(c::GenericConstraint) = (c.child,) + +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) + 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::GenericConstraint, indices) + ret = MOI.get(model, MOI.ConstraintDual(), indices) + c.dual = output(reshape(ret, c.size)) + return +end + const Value = Union{Number,AbstractArray} # We commandeer `==` to create a constraint. From 0c68d3a11a52f7a4f9fb0409890ada0cf7a3859e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 17 Apr 2024 10:04:24 +0200 Subject: [PATCH 03/16] up --- src/constraints/GreaterThanConstraint.jl | 7 ++++++- src/constraints/LessThanConstraint.jl | 21 +------------------- src/constraints/SecondOrderConeConstraint.jl | 7 ++++--- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/constraints/GreaterThanConstraint.jl b/src/constraints/GreaterThanConstraint.jl index f47f714b1..9ad0ea7e6 100644 --- a/src/constraints/GreaterThanConstraint.jl +++ b/src/constraints/GreaterThanConstraint.jl @@ -11,7 +11,7 @@ function vexity(vex, ::MOI.Nonnegatives) return vex end -function Base.:>=(lhs::AbstractExpr, rhs::AbstractExpr) +function _promote_size(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))", @@ -32,6 +32,11 @@ function Base.:>=(lhs::AbstractExpr, rhs::AbstractExpr) "Cannot create inequality constraint between expressions of size $(lhs.size) and $(rhs.size)", ) end + return lhs, rhs +end + +function Base.:>=(lhs::AbstractExpr, rhs::AbstractExpr) + lhs, rhs = _promote_size(lhs, rhs) return GenericConstraint{MOI.Nonnegatives}(lhs - rhs) end diff --git a/src/constraints/LessThanConstraint.jl b/src/constraints/LessThanConstraint.jl index 89637b85f..d74b07536 100644 --- a/src/constraints/LessThanConstraint.jl +++ b/src/constraints/LessThanConstraint.jl @@ -12,26 +12,7 @@ function vexity(vex, ::MOI.Nonpositives) end function Base.:<=(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 + lhs, rhs = _promote_size(lhs, rhs) return GenericConstraint{MOI.Nonpositives}(lhs - rhs) end diff --git a/src/constraints/SecondOrderConeConstraint.jl b/src/constraints/SecondOrderConeConstraint.jl index c9c0f0c6c..1b2e511d2 100644 --- a/src/constraints/SecondOrderConeConstraint.jl +++ b/src/constraints/SecondOrderConeConstraint.jl @@ -1,7 +1,8 @@ -const SecondOrderConeConstraint = Constraint{MOI.SecondOrderCone} +mutable struct SecondOrderConeConstraint <: Constraint + children::Tuple + dual::Union{Value,Nothing} -function set_with_size(::Type{MOI.SecondOrderCone}, sz::Tuple{Int}) - return MOI.SecondOrderCone(sz[1]) + SecondOrderConeConstraint(args::AbstractExpr...) = new(args, nothing) end head(io::IO, ::SecondOrderConeConstraint) = print(io, "soc") From ac74d0dfd3adc8c670fd62a06a10585fd15c4c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 17 Apr 2024 10:17:56 +0200 Subject: [PATCH 04/16] Fix tests --- src/constraints/GreaterThanConstraint.jl | 4 ++++ src/constraints/LessThanConstraint.jl | 4 ++++ .../PositiveSemidefiniteConeConstraint.jl | 4 ++-- src/expressions.jl | 21 +++++++++++-------- test/test_constraints.jl | 18 ++++++++-------- test/test_utilities.jl | 4 ++-- 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/constraints/GreaterThanConstraint.jl b/src/constraints/GreaterThanConstraint.jl index 9ad0ea7e6..01d99f6f9 100644 --- a/src/constraints/GreaterThanConstraint.jl +++ b/src/constraints/GreaterThanConstraint.jl @@ -1,3 +1,7 @@ +function set_with_size(::Type{MOI.Nonnegatives}, sz::Tuple{Int,Int}) + return MOI.Nonnegatives(prod(sz)) +end + head(io::IO, ::MOI.Nonnegatives) = print(io, "nonneg") function is_feasible(f, ::MOI.Nonnegatives, tol) diff --git a/src/constraints/LessThanConstraint.jl b/src/constraints/LessThanConstraint.jl index d74b07536..3389699f8 100644 --- a/src/constraints/LessThanConstraint.jl +++ b/src/constraints/LessThanConstraint.jl @@ -1,3 +1,7 @@ +function set_with_size(::Type{MOI.Nonpositives}, sz::Tuple{Int,Int}) + return MOI.Nonpositives(prod(sz)) +end + head(io::IO, ::MOI.Nonpositives) = print(io, "nonpos") function is_feasible(f, ::MOI.Nonpositives, tol) diff --git a/src/constraints/PositiveSemidefiniteConeConstraint.jl b/src/constraints/PositiveSemidefiniteConeConstraint.jl index e064de476..41783474c 100644 --- a/src/constraints/PositiveSemidefiniteConeConstraint.jl +++ b/src/constraints/PositiveSemidefiniteConeConstraint.jl @@ -27,11 +27,11 @@ end function LinearAlgebra.isposdef(x::AbstractExpr) if iscomplex(x) - return PositiveSemidefiniteConeConstraint( + return GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}( [real(x) -imag(x); imag(x) real(x)], ) end - return PositiveSemidefiniteConeConstraint(x) + return GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}(x) end ⪰(x::AbstractExpr, y::AbstractExpr) = isposdef(x - y) diff --git a/src/expressions.jl b/src/expressions.jl index 0fdc1d4d9..6df0b1670 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -26,19 +26,21 @@ # ############################################################################# +const Value = Union{Number,AbstractArray} + abstract type AbstractExpr end abstract type Constraint end -struct GenericConstraint{S<:MOI.AbstractSet} +struct GenericConstraint{S<:MOI.AbstractSet} <: Constraint child::AbstractExpr set::S - dual::ValueOrNothing - function Constraint(child, set::MOI.AbstractSet) + dual::Union{Value,Nothing} + function GenericConstraint(child, set::MOI.AbstractSet) return new{typeof(set)}(child, set) end - function Constraint{S}(child) where {S<:MOI.AbstractSet} - return Constraint(child, set_with_size(S, size(child))) + function GenericConstraint{S}(child) where {S<:MOI.AbstractSet} + return GenericConstraint(child, set_with_size(S, size(child))) end end @@ -46,6 +48,10 @@ head(io::IO, c::GenericConstraint) = head(io, c.set) AbstractTrees.children(c::GenericConstraint) = (c.child,) +function vexity(c::GenericConstraint) + return vexity(c.child, c.set) +end + function _add_constraint!( context::Context, c::GenericConstraint, @@ -58,8 +64,7 @@ function _add_constraint!( 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) + context.constr_to_moi_inds[c] = MOI_add_constraint(context.model, f, c.set) return end @@ -70,8 +75,6 @@ function populate_dual!(model::MOI.ModelLike, c::GenericConstraint, indices) return end -const Value = Union{Number,AbstractArray} - # We commandeer `==` to create a constraint. # Therefore we define `isequal` to still have a notion of equality # (Normally `isequal` falls back to `==`, so we need to provide a method). diff --git a/test/test_constraints.jl b/test/test_constraints.jl index 106110654..217db210f 100644 --- a/test/test_constraints.jl +++ b/test/test_constraints.jl @@ -183,30 +183,30 @@ function test_LessThanConstraint_dual_maximize() return end -### constraints/PositiveSemidefiniteConeConstraint +### constraints/GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} -function test_PositiveSemidefiniteConeConstraint() +function test_GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}() @test_throws( ErrorException("Positive semidefinite expressions must be square"), - Convex.PositiveSemidefiniteConeConstraint(Variable(2, 3)), + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}(Variable(2, 3)), ) X = Variable(2, 2) - c = Convex.PositiveSemidefiniteConeConstraint(X) + c = Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}(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.PositiveSemidefiniteConeTriangle} + @test (-X ⪯ 0) isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} + @test (-X ⪯ constant(0)) isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} + @test (constant(0) ⪯ X) isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} @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{MOI.PositiveSemidefiniteConeTriangle}_violated() X = constant([1 2; 3 4]) p = satisfy([X ⪰ 0]) @test_logs (:warn,) (:warn,) solve!(p, SCS.Optimizer) diff --git a/test/test_utilities.jl b/test/test_utilities.jl index 360e515a9..9a2abb185 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -403,7 +403,7 @@ 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.PositiveSemidefiniteConeTriangle} end @test_throws ErrorException HermitianSemidefinite(2, 3) @@ -953,7 +953,7 @@ 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.PositiveSemidefiniteConeTriangle} return end From c00b68b66604e04e87a107e6d7c0c87a333db946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 17 Apr 2024 11:06:23 +0200 Subject: [PATCH 05/16] Fixes --- src/constraints/GreaterThanConstraint.jl | 2 +- src/constraints/LessThanConstraint.jl | 2 +- .../PositiveSemidefiniteConeConstraint.jl | 14 +++++++------- src/expressions.jl | 6 +++--- test/test_constraints.jl | 18 +++++++++--------- test/test_utilities.jl | 19 +++++++++++-------- 6 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/constraints/GreaterThanConstraint.jl b/src/constraints/GreaterThanConstraint.jl index 01d99f6f9..727248d8d 100644 --- a/src/constraints/GreaterThanConstraint.jl +++ b/src/constraints/GreaterThanConstraint.jl @@ -2,7 +2,7 @@ function set_with_size(::Type{MOI.Nonnegatives}, sz::Tuple{Int,Int}) return MOI.Nonnegatives(prod(sz)) end -head(io::IO, ::MOI.Nonnegatives) = print(io, "nonneg") +head(io::IO, ::MOI.Nonnegatives) = print(io, "≥") function is_feasible(f, ::MOI.Nonnegatives, tol) return all(f .>= tol) diff --git a/src/constraints/LessThanConstraint.jl b/src/constraints/LessThanConstraint.jl index 3389699f8..c89f477bf 100644 --- a/src/constraints/LessThanConstraint.jl +++ b/src/constraints/LessThanConstraint.jl @@ -2,7 +2,7 @@ function set_with_size(::Type{MOI.Nonpositives}, sz::Tuple{Int,Int}) return MOI.Nonpositives(prod(sz)) end -head(io::IO, ::MOI.Nonpositives) = print(io, "nonpos") +head(io::IO, ::MOI.Nonpositives) = print(io, "≤") function is_feasible(f, ::MOI.Nonpositives, tol) return all(f .<= -tol) diff --git a/src/constraints/PositiveSemidefiniteConeConstraint.jl b/src/constraints/PositiveSemidefiniteConeConstraint.jl index 41783474c..ab75fcf94 100644 --- a/src/constraints/PositiveSemidefiniteConeConstraint.jl +++ b/src/constraints/PositiveSemidefiniteConeConstraint.jl @@ -1,20 +1,20 @@ -function set_with_size(::Type{MOI.PositiveSemidefiniteConeTriangle}, sz::Tuple{Int,Int}) +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.PositiveSemidefiniteConeTriangle(sz[1]) + return MOI.PositiveSemidefiniteConeSquare(sz[1]) end -head(io::IO, ::MOI.PositiveSemidefiniteConeTriangle) = print(io, "sdp") +head(io::IO, ::MOI.PositiveSemidefiniteConeSquare) = print(io, "sdp") -function vexity(vex, ::MOI.PositiveSemidefiniteConeTriangle) +function vexity(vex, ::MOI.PositiveSemidefiniteConeSquare) if !(vex in (AffineVexity(), ConstVexity())) return NotDcp() end return AffineVexity() end -function is_feasible(x, ::MOI.PositiveSemidefiniteConeTriangle, tol) +function is_feasible(x, ::MOI.PositiveSemidefiniteConeSquare, tol) if !(x ≈ transpose(x)) @warn "constant SDP constraint is violated" return false @@ -27,11 +27,11 @@ end function LinearAlgebra.isposdef(x::AbstractExpr) if iscomplex(x) - return GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}( + return GenericConstraint{MOI.PositiveSemidefiniteConeSquare}( [real(x) -imag(x); imag(x) real(x)], ) end - return GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}(x) + return GenericConstraint{MOI.PositiveSemidefiniteConeSquare}(x) end ⪰(x::AbstractExpr, y::AbstractExpr) = isposdef(x - y) diff --git a/src/expressions.jl b/src/expressions.jl index 6df0b1670..c1ad3287d 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -32,7 +32,7 @@ abstract type AbstractExpr end abstract type Constraint end -struct GenericConstraint{S<:MOI.AbstractSet} <: Constraint +mutable struct GenericConstraint{S<:MOI.AbstractSet} <: Constraint child::AbstractExpr set::S dual::Union{Value,Nothing} @@ -49,7 +49,7 @@ head(io::IO, c::GenericConstraint) = head(io, c.set) AbstractTrees.children(c::GenericConstraint) = (c.child,) function vexity(c::GenericConstraint) - return vexity(c.child, c.set) + return vexity(vexity(c.child), c.set) end function _add_constraint!( @@ -71,7 +71,7 @@ end function populate_dual!(model::MOI.ModelLike, c::GenericConstraint, indices) ret = MOI.get(model, MOI.ConstraintDual(), indices) - c.dual = output(reshape(ret, c.size)) + c.dual = output(reshape(ret, c.child.size)) return end diff --git a/test/test_constraints.jl b/test/test_constraints.jl index 217db210f..3ab380133 100644 --- a/test/test_constraints.jl +++ b/test/test_constraints.jl @@ -183,30 +183,30 @@ function test_LessThanConstraint_dual_maximize() return end -### constraints/GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} +### constraints/GenericConstraint{MOI.PositiveSemidefiniteConeSquare} -function test_GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}() +function test_GenericConstraint{MOI.PositiveSemidefiniteConeSquare}() @test_throws( ErrorException("Positive semidefinite expressions must be square"), - Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}(Variable(2, 3)), + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare}(Variable(2, 3)), ) X = Variable(2, 2) - c = Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}(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.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} - @test (-X ⪯ 0) isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} - @test (-X ⪯ constant(0)) isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} - @test (constant(0) ⪯ X) isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} + @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_GenericConstraint{MOI.PositiveSemidefiniteConeTriangle}_violated() +function test_GenericConstraint{MOI.PositiveSemidefiniteConeSquare}_violated() X = constant([1 2; 3 4]) p = satisfy([X ⪰ 0]) @test_logs (:warn,) (:warn,) solve!(p, SCS.Optimizer) diff --git a/test/test_utilities.jl b/test/test_utilities.jl index 9a2abb185..e090cd64b 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""" @@ -258,8 +260,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 +406,7 @@ function test_Constructors() Semidefinite(2), ] @test length(get_constraints(x)) == 1 - @test get_constraints(x)[] isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} + @test get_constraints(x)[] isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} end @test_throws ErrorException HermitianSemidefinite(2, 3) @@ -953,7 +956,7 @@ end function test_deprecation_in_symbol() x = Variable(2, 2) @test_logs (:warn,) (x in :SDP) - @test in(x, :semidefinite) isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeTriangle} + @test in(x, :semidefinite) isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} return end From b087436713d4712f1312ca155c36504bddb15b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 17 Apr 2024 11:08:20 +0200 Subject: [PATCH 06/16] Fix char --- test/test_utilities.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_utilities.jl b/test/test_utilities.jl index e090cd64b..87c2490a9 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -171,7 +171,7 @@ function test_show() └─ real variable ($(Convex.show_id(x))) subject to ├─ ≥ constraint (affine) - | └─ + (affine; real) + │ └─ + (affine; real) │ ├─ real variable ($(Convex.show_id(x))) │ └─ $(reshape([-1], 1, 1)) └─ ≤ constraint (affine) From c52cdc9e37b1d52d0dbe242473eb5c0cf6f37ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 17 Apr 2024 11:18:27 +0200 Subject: [PATCH 07/16] Fixes --- src/constraints/PositiveSemidefiniteConeConstraint.jl | 2 +- test/test_constraints.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/constraints/PositiveSemidefiniteConeConstraint.jl b/src/constraints/PositiveSemidefiniteConeConstraint.jl index ab75fcf94..d0ad70349 100644 --- a/src/constraints/PositiveSemidefiniteConeConstraint.jl +++ b/src/constraints/PositiveSemidefiniteConeConstraint.jl @@ -18,7 +18,7 @@ function is_feasible(x, ::MOI.PositiveSemidefiniteConeSquare, tol) if !(x ≈ transpose(x)) @warn "constant SDP constraint is violated" return false - elseif evaluate(LinearAlgebra.eigmin(c.child)) < -tol + elseif LinearAlgebra.eigmin(x) < -tol @warn "constant SDP constraint is violated" return false end diff --git a/test/test_constraints.jl b/test/test_constraints.jl index 3ab380133..eb8c801e1 100644 --- a/test/test_constraints.jl +++ b/test/test_constraints.jl @@ -185,7 +185,7 @@ end ### constraints/GenericConstraint{MOI.PositiveSemidefiniteConeSquare} -function test_GenericConstraint{MOI.PositiveSemidefiniteConeSquare}() +function test_GenericConstraint_PositiveSemidefiniteConeSquare() @test_throws( ErrorException("Positive semidefinite expressions must be square"), Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare}(Variable(2, 3)), @@ -206,7 +206,7 @@ function test_GenericConstraint{MOI.PositiveSemidefiniteConeSquare}() return end -function test_GenericConstraint{MOI.PositiveSemidefiniteConeSquare}_violated() +function test_GenericConstraint_PositiveSemidefiniteConeSquare_violated() X = constant([1 2; 3 4]) p = satisfy([X ⪰ 0]) @test_logs (:warn,) (:warn,) solve!(p, SCS.Optimizer) From c2a2d09dc739c2f59f8b5ab22c6abde9f119afe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 17 Apr 2024 11:24:10 +0200 Subject: [PATCH 08/16] Fix format --- src/constraints/GreaterThanConstraint.jl | 2 +- .../PositiveSemidefiniteConeConstraint.jl | 5 ++++- src/expressions.jl | 6 +----- test/test_constraints.jl | 16 +++++++++++----- test/test_utilities.jl | 6 ++++-- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/constraints/GreaterThanConstraint.jl b/src/constraints/GreaterThanConstraint.jl index 727248d8d..029f623d2 100644 --- a/src/constraints/GreaterThanConstraint.jl +++ b/src/constraints/GreaterThanConstraint.jl @@ -46,4 +46,4 @@ end Base.:>=(lhs::AbstractExpr, rhs::Value) = >=(lhs, constant(rhs)) -Base.:>=(lhs::Value, rhs::AbstractExpr) = >=(constant(lhs), rhs) \ No newline at end of file +Base.:>=(lhs::Value, rhs::AbstractExpr) = >=(constant(lhs), rhs) diff --git a/src/constraints/PositiveSemidefiniteConeConstraint.jl b/src/constraints/PositiveSemidefiniteConeConstraint.jl index d0ad70349..52849c250 100644 --- a/src/constraints/PositiveSemidefiniteConeConstraint.jl +++ b/src/constraints/PositiveSemidefiniteConeConstraint.jl @@ -1,4 +1,7 @@ -function set_with_size(::Type{MOI.PositiveSemidefiniteConeSquare}, sz::Tuple{Int,Int}) +function set_with_size( + ::Type{MOI.PositiveSemidefiniteConeSquare}, + sz::Tuple{Int,Int}, +) if sz[1] != sz[2] error("Positive semidefinite expressions must be square") end diff --git a/src/expressions.jl b/src/expressions.jl index c1ad3287d..0ab06fa79 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -52,10 +52,7 @@ function vexity(c::GenericConstraint) return vexity(vexity(c.child), c.set) end -function _add_constraint!( - context::Context, - c::GenericConstraint, -) +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[]) @@ -68,7 +65,6 @@ function _add_constraint!( 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)) diff --git a/test/test_constraints.jl b/test/test_constraints.jl index eb8c801e1..54c300f46 100644 --- a/test/test_constraints.jl +++ b/test/test_constraints.jl @@ -188,7 +188,9 @@ end function test_GenericConstraint_PositiveSemidefiniteConeSquare() @test_throws( ErrorException("Positive semidefinite expressions must be square"), - Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare}(Variable(2, 3)), + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare}( + Variable(2, 3), + ), ) X = Variable(2, 2) c = Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare}(X) @@ -197,10 +199,14 @@ function test_GenericConstraint_PositiveSemidefiniteConeSquare() @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.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 (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 diff --git a/test/test_utilities.jl b/test/test_utilities.jl index 87c2490a9..f952b4ade 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -406,7 +406,8 @@ function test_Constructors() Semidefinite(2), ] @test length(get_constraints(x)) == 1 - @test get_constraints(x)[] isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} + @test get_constraints(x)[] isa + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} end @test_throws ErrorException HermitianSemidefinite(2, 3) @@ -956,7 +957,8 @@ end function test_deprecation_in_symbol() x = Variable(2, 2) @test_logs (:warn,) (x in :SDP) - @test in(x, :semidefinite) isa Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} + @test in(x, :semidefinite) isa + Convex.GenericConstraint{MOI.PositiveSemidefiniteConeSquare} return end From b19e1012d8d8b60d979dd8c792c5459044ec9e6d Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 18 Apr 2024 10:16:35 +1200 Subject: [PATCH 09/16] Apply suggestions from code review --- src/constraints/GreaterThanConstraint.jl | 2 +- src/constraints/LessThanConstraint.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constraints/GreaterThanConstraint.jl b/src/constraints/GreaterThanConstraint.jl index 029f623d2..2d5690b79 100644 --- a/src/constraints/GreaterThanConstraint.jl +++ b/src/constraints/GreaterThanConstraint.jl @@ -5,7 +5,7 @@ end head(io::IO, ::MOI.Nonnegatives) = print(io, "≥") function is_feasible(f, ::MOI.Nonnegatives, tol) - return all(f .>= tol) + return all(f .>= -tol) end function vexity(vex, ::MOI.Nonnegatives) diff --git a/src/constraints/LessThanConstraint.jl b/src/constraints/LessThanConstraint.jl index c89f477bf..788cd359c 100644 --- a/src/constraints/LessThanConstraint.jl +++ b/src/constraints/LessThanConstraint.jl @@ -5,7 +5,7 @@ end head(io::IO, ::MOI.Nonpositives) = print(io, "≤") function is_feasible(f, ::MOI.Nonpositives, tol) - return all(f .<= -tol) + return all(f .<= tol) end function vexity(vex, ::MOI.Nonpositives) From 4120cd2b1005ed26881f67c16254db5753a467f3 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 18 Apr 2024 11:00:45 +1200 Subject: [PATCH 10/16] Update --- src/constraints/GreaterThanConstraint.jl | 4 +++- test/test_constraints.jl | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/constraints/GreaterThanConstraint.jl b/src/constraints/GreaterThanConstraint.jl index 2d5690b79..81ab460b5 100644 --- a/src/constraints/GreaterThanConstraint.jl +++ b/src/constraints/GreaterThanConstraint.jl @@ -9,8 +9,10 @@ function is_feasible(f, ::MOI.Nonnegatives, tol) end function vexity(vex, ::MOI.Nonnegatives) - if vex == ConvexVexity() + if vex == ConvexVexity() return NotDcp() + elseif vex == ConcaveVexity() + return ConvexVexity() end return vex end diff --git a/test/test_constraints.jl b/test/test_constraints.jl index 54c300f46..98361260f 100644 --- a/test/test_constraints.jl +++ b/test/test_constraints.jl @@ -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 @@ -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 From 00d65119014cc77b918f5d44dc64f643b8ada523 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 18 Apr 2024 11:04:25 +1200 Subject: [PATCH 11/16] Relocate --- src/constraints/GenericConstraint.jl | 187 ++++++++++++++++++ src/constraints/GreaterThanConstraint.jl | 51 ----- src/constraints/LessThanConstraint.jl | 25 --- .../PositiveSemidefiniteConeConstraint.jl | 58 ------ src/expressions.jl | 41 +--- 5 files changed, 188 insertions(+), 174 deletions(-) create mode 100644 src/constraints/GenericConstraint.jl delete mode 100644 src/constraints/GreaterThanConstraint.jl delete mode 100644 src/constraints/LessThanConstraint.jl delete mode 100644 src/constraints/PositiveSemidefiniteConeConstraint.jl diff --git a/src/constraints/GenericConstraint.jl b/src/constraints/GenericConstraint.jl new file mode 100644 index 000000000..60bb9f3a4 --- /dev/null +++ b/src/constraints/GenericConstraint.jl @@ -0,0 +1,187 @@ +mutable struct GenericConstraint{S<:MOI.AbstractSet} <: Constraint + child::AbstractExpr + set::S + dual::Union{Value,Nothing} + function GenericConstraint(child, set::MOI.AbstractSet) + return new{typeof(set)}(child, set) + end + function GenericConstraint{S}(child) where {S<:MOI.AbstractSet} + return GenericConstraint(child, set_with_size(S, size(child))) + end +end + +head(io::IO, c::GenericConstraint) = head(io, c.set) + +AbstractTrees.children(c::GenericConstraint) = (c.child,) + +function vexity(c::GenericConstraint) + return vexity(vexity(c.child), c.set) +end + +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 + +# ============================================================================== +# 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, "≥") + +function is_feasible(f, ::MOI.Nonnegatives, tol) + return all(f .>= -tol) +end + +function vexity(vex, ::MOI.Nonnegatives) + if vex == ConvexVexity() + return NotDcp() + elseif vex == ConcaveVexity() + return ConvexVexity() + end + return vex +end + +function _promote_size(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 lhs, rhs +end + +function Base.:>=(lhs::AbstractExpr, rhs::AbstractExpr) + 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, "≤") + +function is_feasible(f, ::MOI.Nonpositives, tol) + return all(f .<= tol) +end + +function vexity(vex, ::MOI.Nonpositives) + if vex == ConcaveVexity() + return NotDcp() + end + return vex +end + +function Base.:<=(lhs::AbstractExpr, rhs::AbstractExpr) + 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) + +# ============================================================================== +# 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) + if !(x ≈ transpose(x)) + @warn "constant SDP constraint is violated" + return false + elseif LinearAlgebra.eigmin(x) < -tol + @warn "constant SDP constraint is violated" + return false + end + return true +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) diff --git a/src/constraints/GreaterThanConstraint.jl b/src/constraints/GreaterThanConstraint.jl deleted file mode 100644 index 81ab460b5..000000000 --- a/src/constraints/GreaterThanConstraint.jl +++ /dev/null @@ -1,51 +0,0 @@ -function set_with_size(::Type{MOI.Nonnegatives}, sz::Tuple{Int,Int}) - return MOI.Nonnegatives(prod(sz)) -end - -head(io::IO, ::MOI.Nonnegatives) = print(io, "≥") - -function is_feasible(f, ::MOI.Nonnegatives, tol) - return all(f .>= -tol) -end - -function vexity(vex, ::MOI.Nonnegatives) - if vex == ConvexVexity() - return NotDcp() - elseif vex == ConcaveVexity() - return ConvexVexity() - end - return vex -end - -function _promote_size(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 lhs, rhs -end - -function Base.:>=(lhs::AbstractExpr, rhs::AbstractExpr) - 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) diff --git a/src/constraints/LessThanConstraint.jl b/src/constraints/LessThanConstraint.jl deleted file mode 100644 index 788cd359c..000000000 --- a/src/constraints/LessThanConstraint.jl +++ /dev/null @@ -1,25 +0,0 @@ -function set_with_size(::Type{MOI.Nonpositives}, sz::Tuple{Int,Int}) - return MOI.Nonpositives(prod(sz)) -end - -head(io::IO, ::MOI.Nonpositives) = print(io, "≤") - -function is_feasible(f, ::MOI.Nonpositives, tol) - return all(f .<= tol) -end - -function vexity(vex, ::MOI.Nonpositives) - if vex == ConcaveVexity() - return NotDcp() - end - return vex -end - -function Base.:<=(lhs::AbstractExpr, rhs::AbstractExpr) - 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) diff --git a/src/constraints/PositiveSemidefiniteConeConstraint.jl b/src/constraints/PositiveSemidefiniteConeConstraint.jl deleted file mode 100644 index 52849c250..000000000 --- a/src/constraints/PositiveSemidefiniteConeConstraint.jl +++ /dev/null @@ -1,58 +0,0 @@ -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) - if !(x ≈ transpose(x)) - @warn "constant SDP constraint is violated" - return false - elseif LinearAlgebra.eigmin(x) < -tol - @warn "constant SDP constraint is violated" - return false - end - return true -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) diff --git a/src/expressions.jl b/src/expressions.jl index 0ab06fa79..25fdd80c2 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -26,50 +26,11 @@ # ############################################################################# -const Value = Union{Number,AbstractArray} - abstract type AbstractExpr end abstract type Constraint end -mutable struct GenericConstraint{S<:MOI.AbstractSet} <: Constraint - child::AbstractExpr - set::S - dual::Union{Value,Nothing} - function GenericConstraint(child, set::MOI.AbstractSet) - return new{typeof(set)}(child, set) - end - function GenericConstraint{S}(child) where {S<:MOI.AbstractSet} - return GenericConstraint(child, set_with_size(S, size(child))) - end -end - -head(io::IO, c::GenericConstraint) = head(io, c.set) - -AbstractTrees.children(c::GenericConstraint) = (c.child,) - -function vexity(c::GenericConstraint) - return vexity(vexity(c.child), c.set) -end - -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 +const Value = Union{Number,AbstractArray} # We commandeer `==` to create a constraint. # Therefore we define `isequal` to still have a notion of equality From 2b0d658a6e2f0abd9c805da7090cb4a9b5acbb46 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 18 Apr 2024 11:11:37 +1200 Subject: [PATCH 12/16] Update --- src/constraints/GenericConstraint.jl | 41 ++++++++++++---------------- test/test_constraints.jl | 4 +-- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/constraints/GenericConstraint.jl b/src/constraints/GenericConstraint.jl index 60bb9f3a4..0dd461a2f 100644 --- a/src/constraints/GenericConstraint.jl +++ b/src/constraints/GenericConstraint.jl @@ -2,21 +2,21 @@ mutable struct GenericConstraint{S<:MOI.AbstractSet} <: Constraint child::AbstractExpr set::S dual::Union{Value,Nothing} - function GenericConstraint(child, set::MOI.AbstractSet) - return new{typeof(set)}(child, set) - end - function GenericConstraint{S}(child) where {S<:MOI.AbstractSet} - return GenericConstraint(child, set_with_size(S, size(child))) + + 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) AbstractTrees.children(c::GenericConstraint) = (c.child,) -function vexity(c::GenericConstraint) - return vexity(vexity(c.child), c.set) -end +vexity(c::GenericConstraint) = vexity(vexity(c.child), c.set) function _add_constraint!(context::Context, c::GenericConstraint) if vexity(c.child) == ConstVexity() @@ -47,9 +47,7 @@ end head(io::IO, ::MOI.Nonnegatives) = print(io, "≥") -function is_feasible(f, ::MOI.Nonnegatives, tol) - return all(f .>= -tol) -end +is_feasible(f, ::MOI.Nonnegatives, tol) = all(f .>= -tol) function vexity(vex, ::MOI.Nonnegatives) if vex == ConvexVexity() @@ -63,7 +61,8 @@ end function _promote_size(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))", + "Cannot create inequality constraint between expressions of sign " * + "$(sign(lhs)) and $(sign(rhs))", ) end if lhs.size == rhs.size || lhs.size == (1, 1) @@ -78,7 +77,8 @@ function _promote_size(lhs::AbstractExpr, rhs::AbstractExpr) end else error( - "Cannot create inequality constraint between expressions of size $(lhs.size) and $(rhs.size)", + "Cannot create inequality constraint between expressions of size " * + "$(lhs.size) and $(rhs.size)", ) end return lhs, rhs @@ -103,9 +103,7 @@ end head(io::IO, ::MOI.Nonpositives) = print(io, "≤") -function is_feasible(f, ::MOI.Nonpositives, tol) - return all(f .<= tol) -end +is_feasible(f, ::MOI.Nonpositives, tol) = all(f .<= tol) function vexity(vex, ::MOI.Nonpositives) if vex == ConcaveVexity() @@ -147,14 +145,7 @@ function vexity(vex, ::MOI.PositiveSemidefiniteConeSquare) end function is_feasible(x, ::MOI.PositiveSemidefiniteConeSquare, tol) - if !(x ≈ transpose(x)) - @warn "constant SDP constraint is violated" - return false - elseif LinearAlgebra.eigmin(x) < -tol - @warn "constant SDP constraint is violated" - return false - end - return true + return x ≈ transpose(x) && LinearAlgebra.eigmin(x) >= -tol end function LinearAlgebra.isposdef(x::AbstractExpr) @@ -183,5 +174,7 @@ function ⪰(x::Value, y::AbstractExpr) end ⪯(x::AbstractExpr, y::AbstractExpr) = ⪰(y, x) + ⪯(x::Value, y::AbstractExpr) = ⪰(y, x) + ⪯(x::AbstractExpr, y::Value) = ⪰(y, x) diff --git a/test/test_constraints.jl b/test/test_constraints.jl index 98361260f..ce87dc59d 100644 --- a/test/test_constraints.jl +++ b/test/test_constraints.jl @@ -215,10 +215,10 @@ end 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 From f24243897a26780a0304406a1f529dacfd9cdd02 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 18 Apr 2024 11:55:15 +1200 Subject: [PATCH 13/16] Add support for Zeros --- src/constraints/EqualToConstraint.jl | 68 --------------------- src/constraints/GenericConstraint.jl | 89 +++++++++++++++++++++------- test/test_constraints.jl | 16 ++--- test/test_utilities.jl | 42 ++++++------- 4 files changed, 93 insertions(+), 122 deletions(-) delete mode 100644 src/constraints/EqualToConstraint.jl 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 index 0dd461a2f..f90e62ea0 100644 --- a/src/constraints/GenericConstraint.jl +++ b/src/constraints/GenericConstraint.jl @@ -37,6 +37,37 @@ function populate_dual!(model::MOI.ModelLike, c::GenericConstraint, indices) 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 # ============================================================================== @@ -58,33 +89,13 @@ function vexity(vex, ::MOI.Nonnegatives) return vex end -function _promote_size(lhs::AbstractExpr, rhs::AbstractExpr) +function Base.:>=(lhs::AbstractExpr, rhs::AbstractExpr) if sign(lhs) == ComplexSign() || sign(rhs) == ComplexSign() error( - "Cannot create inequality constraint between expressions of sign " * + "Cannot create 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 lhs, rhs -end - -function Base.:>=(lhs::AbstractExpr, rhs::AbstractExpr) lhs, rhs = _promote_size(lhs, rhs) return GenericConstraint{MOI.Nonnegatives}(lhs - rhs) end @@ -113,6 +124,12 @@ function vexity(vex, ::MOI.Nonpositives) 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 @@ -121,6 +138,34 @@ 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 # ============================================================================== diff --git a/test/test_constraints.jl b/test/test_constraints.jl index ce87dc59d..712286052 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(), ) @@ -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(), ) diff --git a/test/test_utilities.jl b/test/test_utilities.jl index f952b4ade..faabf0e6a 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -200,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() @@ -234,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))) + │ └─ [-1;;] ├─ == constraint (affine) - │ ├─ real variable ($(Convex.show_id(x))) - │ └─ $(reshape([2], 1, 1)) + │ └─ + (affine; real) + │ ├─ real variable ($(Convex.show_id(x))) + │ └─ [-2;;] ⋮ status: `solve!` not called yet""" From cb03d670a03fc1279fc11391a37533167d8bf5c6 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 18 Apr 2024 12:19:03 +1200 Subject: [PATCH 14/16] Add SecondOrderCone support --- src/atoms/second_order_cone/QolElemAtom.jl | 6 +-- .../second_order_cone/QuadOverLinAtom.jl | 3 +- src/constraints/GenericConstraint.jl | 20 +++++++++ src/constraints/SecondOrderConeConstraint.jl | 43 ------------------- test/test_constraints.jl | 8 ++-- 5 files changed, 28 insertions(+), 52 deletions(-) delete mode 100644 src/constraints/SecondOrderConeConstraint.jl 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/GenericConstraint.jl b/src/constraints/GenericConstraint.jl index f90e62ea0..7335e3c17 100644 --- a/src/constraints/GenericConstraint.jl +++ b/src/constraints/GenericConstraint.jl @@ -14,6 +14,9 @@ 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) @@ -223,3 +226,20 @@ end ⪯(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/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 712286052..7bf7b4277 100644 --- a/test/test_constraints.jl +++ b/test/test_constraints.jl @@ -222,19 +222,19 @@ function test_GenericConstraint_PositiveSemidefiniteConeSquare_violated() 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 From a39909116337ab4929eb139068d30214e484fb0a Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 18 Apr 2024 12:22:43 +1200 Subject: [PATCH 15/16] Update --- test/test_utilities.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_utilities.jl b/test/test_utilities.jl index faabf0e6a..d7cb295c2 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -228,11 +228,11 @@ function test_show() ├─ == constraint (affine) │ └─ + (affine; real) │ ├─ real variable ($(Convex.show_id(x))) - │ └─ [-1;;] + │ └─ $(reshape([-1], 1, 1)) ├─ == constraint (affine) │ └─ + (affine; real) │ ├─ real variable ($(Convex.show_id(x))) - │ └─ [-2;;] + │ └─ $(reshape([-2], 1, 1)) ⋮ status: `solve!` not called yet""" From 7f7566ad854c19f932a8fe848fffdf089ef66437 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 18 Apr 2024 13:41:15 +1200 Subject: [PATCH 16/16] Update changelog.md --- docs/src/changelog.md | 3 +++ 1 file changed, 3 insertions(+) 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