Building a problem#

sdplab.problem.SDPProblem holds the triple \((C, \mathcal{A}, b)\) and nothing else: the objective, the constraint operator, and the right-hand side. It is data plus the handful of operations every solver needs, so the same object is handed to the CVXPY backend or lifted into a smoothed dual.

Choose the spaces first#

Two spaces determine everything else. dom is where the unknown \(X\) and the cost \(C\) live; cod is where \(\mathcal{A}X\), \(b\), and the dual variable \(y\) live.

import numpy as np
from spacecore import Context, DenseLinOp, DenseVectorSpace, HermitianSpace, NumpyOps

n, m = 4, 3
ctx = Context(NumpyOps())                 # optional; see the spacecore docs

dom = HermitianSpace(n, ctx=ctx)          # X and C: n x n Hermitian
cod = DenseVectorSpace((m,), ctx=ctx)     # A X and b: length-m real vectors

dom may be any spacecore Euclidean Jordan algebra space – a Hermitian matrix space as here, an elementwise (orthant) space for an LP, or a TreeSpace of such blocks. Nothing below assumes a single dense matrix.

Build the operator, then the problem#

A stored tensor becomes a DenseConstraintOp or SparseConstraintOp; an operator defined only by apply/rapply stays matrix-free. Any LinOp works – SDPProblem wraps it as needed.

from sdplab.problem import SDPProblem

rng = np.random.default_rng(0)
A_mats = rng.normal(size=(m, n, n))
A_mats = A_mats + np.swapaxes(A_mats, -1, -2)      # each A_i symmetric

C = np.diag([1.0, 2.0, 3.0, 4.0])
b = np.array([1.0, 0.0, 0.5])

A = DenseLinOp(A_mats, dom, cod, ctx=ctx)
problem = SDPProblem(C, A, b, ctx=ctx)

After construction problem.A is the operator, problem.b the right-hand side, and problem.dom / problem.cod the two spaces. The cost is a Cost – an operator in general, not merely a stored matrix, so a dense or sparse Hermitian cost can also act on vectors.

Everything is a plain space element#

There are no primal or dual wrapper objects. A primal is whatever array or tree dom holds, a dual is whatever cod holds, and the problem exposes the operations that pair them:

X = np.eye(n) / n
y = np.zeros(m)

problem.primal_objective(X)      # <C, X>
problem.dual_objective(y)        # <b, y>
problem.feasibility_gap(X)       # A X - b, an element of cod
problem.dual_slack(y)            # A^dagger y - C, an element of dom

The dual slack is the object most algorithms actually work with: semidefinite constraints are constraints on eigenvalues, and the slack is what gets diagonalized.

Solving#

from sdplab.solvers import run_cvxpy_solver

X, y = run_cvxpy_solver(problem, solver="CLARABEL")

The backend asks the constraint operator for its per-constraint matrices (to_cvxpy) and assembles \(\operatorname{Re}\langle A_i, X\rangle = b_i\). For the smoothed-dual route instead, see Regularized SDPs.

A complex codomain – stacked Hermitian blocks, say – is fine for the problem itself, but spacecore.minimize_scipy() needs a real domain, so the scipy route refuses one. spacecore.realify() (spacecore 0.4.3 and later) bridges that: it returns a functional unchanged when the domain is already real, and otherwise presents it over stacked real coordinates.