Solving¶
solve()¶
solution = problem.solve(options=None, **overrides)
options is a SolverOptions; keyword overrides use the
same names. Defaults are SDPB’s own, so problem.solve() behaves like
sdpb with no options except that no checkpoint or output directory is
written.
from sdpb_python import SolverOptions
opts = SolverOptions(precision=768, max_iterations=1000,
duality_gap_threshold="1e-30", primal_error_threshold="1e-30",
dual_error_threshold="1e-30")
solution = pmp.solve(opts, want=("y", "z", "x"))
The full list of fields, with SDPB’s defaults and the corresponding command
line flags, is in the SolverOptions reference. The ones
you will touch most:
precision: bits; see below.duality_gap_threshold,primal_error_threshold,dual_error_threshold: the optimality test. Pass tiny numbers as strings ("1e-30"), not floats.max_iterations,max_runtime: budgets.find_primal_feasible,find_dual_feasible,detect_*_feasible_jump: early stops used in feasibility (bisection) searches, where the objective is irrelevant.want: which solution parts to return;("y", "z")by default.checkpoint_dir,checkpoint_interval,output_dir: files, see below.verbosity:"none","regular","debug","trace". SDPB prints to the C++ standard output, which is fine in a terminal; in Jupyter it goes to the kernel’s console.
Precision¶
SDPB computes with GMP floats of a fixed number of bits and its Elemental library allows that number to be set once per process. sdpb-python therefore:
fixes the precision on the first call that needs it, from
SolverOptions.precision(SDPB’s default 400) or an explicitsdpb_python.set_precision();treats
precision=Noneas “the fixed precision”;raises
SDPBError("precision is already fixed at N bits ...") when a later call asks for a different value.
import sdpb_python as sdpb
sdpb.set_precision(768) # do this first in scripts and notebooks
sdpb.precision() # 768
To work at several precisions, use separate processes: a
SolverPool per precision (see below), or
multiprocessing with the "spawn" start method.
Results come back as mpmath.mpf created at the solver’s precision. mpmath’s
global context is not changed; set mpmath.mp.prec (or use
mpmath.workprec) before doing further high-precision arithmetic with them.
Many solves in parallel¶
A process runs one solve at a time. SolverPool runs
independent solves in worker processes, one per physical core by default
(capped by the available memory), each with one BLAS thread:
from sdpb_python import SolverPool
with SolverPool(workers=4, precision=768) as pool:
futures = [pool.submit(pmp, fast_step_length=True) for pmp in problems]
solutions = [f.result() for f in futures] # or: pool.map(problems, ...)
submit returns a concurrent.futures.Future; its result is the
Solution, or it raises the SDPBError
of the solve. A pooled solve gives bit for bit the result of pmp.solve(...)
with the same options, whatever the number of workers. The precision is fixed
per pool; precision= in the options must match it.
pool.cancel(future) drops a queued solve, or stops a running one at the end
of its current iteration (its future then raises
SolveCancelled); a solve that finishes before the request
arrives keeps its result. If a worker dies (a crash, MPI_Abort, an OOM kill)
its solve raises SDPBWorkerError and the pool starts a new
worker. Leaving the with block waits for the queued solves; an exception
inside it (e.g. Ctrl-C) kills the workers instead.
The workers are started with the "spawn" method, which re-imports the main
module: in a script, create the pool under if __name__ == "__main__":.
solve_many(problems, workers=..., **options) is the one-call form.
Results¶
Solution fields:
Field |
Content |
|---|---|
|
|
|
|
|
list of |
|
list of |
|
per block, list of |
|
per block, |
|
per block, list of |
|
bookkeeping |
Termination reasons, in the priority SDPB checks them:
|
SDPB text |
Cause |
|---|---|---|
|
found primal-dual optimal solution |
gap and both errors below thresholds |
|
found dual feasible solution |
|
|
found primal feasible solution |
|
|
dual feasible jump detected |
|
|
primal feasible jump detected |
|
|
maxIterations exceeded |
|
|
maxRuntime exceeded |
|
|
primal/dual step too small |
|
|
maxComplementarity exceeded |
numerical blow-up |
|
SIGTERM signal received |
SIGTERM, or Ctrl-C through a |
Files SDPB writes¶
Nothing is written by default. With output_dir="out", SDPB writes
out/iterations.json (one record per iteration with objectives, errors, step
lengths and condition numbers) and out/c_minus_By/c_minus_By.json, exactly
like sdpb --outDir out. Existing files are rotated to iterations.0.json
and so on.
With checkpoint_dir="ck", SDPB loads a checkpoint from ck if one exists,
writes one every checkpoint_interval seconds, and writes a final one when it
stops. Checkpoints are binary and only usable at the same precision. Restarting
from a checkpoint continues the iteration where it left off, so a run
interrupted after \(k\) iterations and restarted reaches the optimum in \(n - k\)
more iterations, where \(n\) is the uninterrupted count.
Errors¶
Problems with the Python-side description raise ValueError or TypeError.
This includes a variable that multiplies only zero polynomials in every
matrix: its column of the SDP would vanish and SDPB would abort deep inside
the solver, so the PMP constructor rejects it.
Anything that goes wrong inside SDPB raises SDPBError.
str(error) is SDPB’s message with the throwing source location in brackets,
for example
Error when computing Cholesky decomposition of block_0: A was not numerically HPD: 2 3 -2.8e-115 [../src/sdp_solve/.../compute_Q.cxx:36 (initialize_schur_off_diagonal())]
error.details holds the full text SDPB produced, including its stack trace,
and error.location the file:line (function) alone.
Unbounded and infeasible programs¶
SDPB has no explicit “unbounded” or “infeasible” status. An unbounded program
(for example maximise y subject to 1 + y x >= 0) ends either with
MAX_COMPLEMENTARITY_EXCEEDED or with a
Cholesky failure raised as an SDPBError (“not numerically HPD”), depending
on precision and the iteration path. Code that searches over parameters
should handle both. If only feasibility matters, find_dual_feasible=True
or find_primal_feasible=True stop cleanly with a *_FEASIBLE status as soon
as a feasible point is found, which is the usual way to run bisection
searches. Thresholds far below what the precision can resolve (for example
1e-150 at 400 bits) also end in the Cholesky failure.
sdpb-python supports a single MPI rank. Under mpirun -n 2 the solve raises
an SDPBError explaining this instead of hanging.