Custom Cone Constraint

This example demonstrate how the user can extend COSMO with his own custom convex cones and use them in constraints. To make things easy, we will implement the simple cone of Nonpositives, i.e. $\mathbb{R}_{-}^n = \{x \in \mathbb{R}^n \mid x_i \leq 0\}$.

Cone definition and projection function

We start by creating a concrete subtype of our cone interface-type: COSMO.AbstractConvexCone{T}, where T is a parameter for the floating-point precision. The only field that is strictly required for our cone definition is dim, which is used by the solver to store information about the cone dimension. However, you can also use the object to store other information, or allocate the workspace necessary for the projection step.

using COSMO, LinearAlgebra, SparseArrays, Test

# define new cone type
struct Nonpositives{T} <: COSMO.AbstractConvexCone{T}
    dim::Int64
end

Next, we define a projection method for Nonpositives{T} that takes a vector x and projects it onto our custom cone. For the cone of nonpositive vectors the projection simply sets all positive elements of x to zero:

function COSMO.project!(x::AbstractVector{T}, C::Nonpositives{T}) where {T <: AbstractFloat}
    x .= min.(x, zero(T))
end

This is all that is required to get a basic constraint working. Let's test our new cone/constraint by solving the following LP:

\[\begin{array}{ll} \text{maximize} & x_1 + x_2 + x_3\\ \text{subject to} & x_1 \leq 3 \\ & x_2 \leq 2 \\ & x_1 + x_3 = 5 \end{array}\]

We define our decision vector $x = (x_1, x_2, x_3)$ and setup the problem:

n = 3
P = spzeros(n, n)
q = -ones(n)

# A1 * x + b1 <= 0
A1 = diagm(0 => ones(2))
b1 = [-3.; -2.]
c1 = COSMO.Constraint(A1, b1, Nonpositives, n, 1:2); #here we use our new cone

# x_1 + x_3 == 5
A2 = [1. 0 1.]
b2 = [-5.]
c2 = COSMO.Constraint(A2, b2, COSMO.ZeroSet);

# assemple and solve
model = COSMO.Model();
assemble!(model, P, q, [c1; c2]);
res = COSMO.optimize!(model);
res.x
3-element Vector{Float64}:
 2.999990000199988
 2.0000000001164153
 2.0000099998433143
obj_val = -dot(q, res.x)
7.000000000159718

The problem was solved using constraints involving our new cone and yields the expected result. Next, we want to add the ability to detect infeasible problems.

Support infeasibility detection

If no further information about the new cone is provided, the infeasibility detection is disabled. However, by defining the two additional methods in_dual and in_pol_recc we can support infeasibility detection. More information on how infeasible problems are detected in COSMO can be found here [1]. We will have to add the ability for the solver to check whether a vector is in the dual cone $\mathcal{K}^*$ of our new cone $\mathcal{K}$ and whether a vector is in the recession cone of the polar cone ${\mathcal{K}^\circ}^\infty$ of our cone. The first function is used to check for primal infeasibility, while the second function is used to check for dual infeasibility. Luckily, for the Nonpositives-cone this is straightforward:

\[{\mathbb{R}_{-}^n}^* = \mathbb{R}_{-}^n, \quad {{\mathbb{R}_{-}^n}^\circ}^\infty = \mathbb{R}_{+}^n.\]

function COSMO.in_dual(x::AbstractVector{T}, C::Nonpositives{T}, tol::T) where {T <: AbstractFloat}
	return !any( x-> (x > -tol), x)
end

function COSMO.in_pol_recc(x::AbstractVector{T}, C::Nonpositives{T}, tol::T) where {T <: AbstractFloat}
	return !any( x-> (x < tol), x)
end

Notice that for numerical reasons, we give the check-functions a bit of slack using the tolerance parameter tol. To test the infeasibility detection, we will attempt to use our new cone to solve the clearly dual infeasible problem:

\[\begin{array}{ll} \text{minimize} & x\\ \text{subject to} & x\leq 3 \end{array}\]

n = 1
P = spzeros(n, n)
q = [1.]

#  x <= 3 <=> x - 3 in Nonpositives
Ai = [1.]
bi = [-3.]
ci = COSMO.Constraint(Ai, bi, Nonpositives);

model = COSMO.Model();
assemble!(model, P, q, [ci]);
res = COSMO.optimize!(model);
@test res.status == :Dual_infeasible
Test Passed

which is what we would expect.

Using new cone with JuMP

The nice composibility of Julia-code allows us to use our new cone definition even when the problem is modelled with JuMP. For this to work, we define a concrete subtype of MOI.AbstractSet, that JuMP can use to build a constraint.

using MathOptInterface, JuMP
import Base.copy
const MOI = MathOptInterface

struct NonPos <: MOI.AbstractVectorSet
	dimension::Int
end
Base.copy(set::NonPos) = set #this is needed for MOI
WARNING: using JuMP.Nonpositives in module Main conflicts with an existing identifier.

Next, we tell COSMO that whenever JuMP wants to solve a problem with a NonPos-cone it should translate it into a Nonpositives constraint and we also tell MOI that we now support constraints of this new mysterious cone NonPos.

function COSMO.processSet!(b::Vector{T}, rows::UnitRange{Int}, cs, s::NonPos) where {T <: AbstractFloat}
    push!(cs, Nonpositives{T}(length(rows)))
    nothing
end
#tell MOI, that COSMO supports constraints with this new cone
MOI.supports_constraint(optimizer::COSMO.Optimizer, ::Type{<:Union{MOI.VectorOfVariables, MOI.VectorAffineFunction{Float64}}}, ::Type{NonPos}) = true

We should now be able to model the LP from above in JuMP and solve it internally using our Nonpositives cone and projection function.

model = JuMP.Model(COSMO.Optimizer);
@variable(model, x[1:3]);
@objective(model, Max, x[1] + x[2] + x[3]);
@constraint(model, A1 * x[1:2] .+ b1 in NonPos(2));
@constraint(model, x[1] + x[3] == 5);
JuMP.optimize!(model)
------------------------------------------------------------------
          COSMO v0.8.8 - A Quadratic Objective Conic Solver
                         Michael Garstka
                University of Oxford, 2017 - 2022
------------------------------------------------------------------

Problem:  x ∈ R^{3},
          constraints: A ∈ R^{3x3} (4 nnz),
          matrix size to factor: 6x6,
          Floating-point precision: Float64
Sets:     Nonpositives of dim: 2
          ZeroSet of dim: 1
Settings: ϵ_abs = 1.0e-05, ϵ_rel = 1.0e-05,
          ϵ_prim_inf = 1.0e-04, ϵ_dual_inf = 1.0e-04,
          ρ = 0.1, σ = 1e-06, α = 1.6,
          max_iter = 5000,
          scaling iter = 10 (on),
          check termination every 25 iter,
          check infeasibility every 40 iter,
          KKT system solver: QDLDL
Acc:      Anderson Type2{QRDecomp},
          Memory size = 6, RestartedMemory,
          Safeguarded: true, tol: 2.0
Setup Time: 0.17ms

Iter:	Objective:	Primal Res:	Dual Res:	Rho:
1	-2.7216e+01	1.7200e+01	6.0000e-01	1.0000e-01
25	-7.0000e+00	1.1642e-10	1.1102e-16	1.0000e-01

------------------------------------------------------------------
>>> Results
Status: Solved
Iterations: 27 (incl. 2 safeguarding iter)
Optimal objective: -7
Runtime: 0.021s (21.12ms)
x_opt = JuMP.value.(x)
3-element Vector{Float64}:
 2.999990000199988
 2.0000000001164153
 2.0000099998433143

You can see in the solver output that indeed a set Nonpositives of dim: 2 was used.

The discussed cone of Nonpositives is of course trivial, but the ability to define new cones and constraints for COSMO can be very powerful to model complex problems.

References

[1] Garstka et al. - COSMO: A conic operator splitting method for convex conic problems (2020)


This page was generated using Literate.jl.