Source code for sdpb_python.solution

"""Results of a solve."""

from __future__ import annotations

import enum
from dataclasses import dataclass, field

import mpmath

from ._features import PROFILING
from .numbers import from_str, matrix_mpfs, mpfs


[docs] class TerminateReason(enum.Enum): """Why SDPB stopped (``SDP_Solver_Terminate_Reason``). The value of each member is the exact text SDPB writes to ``out.txt``. Only :attr:`PRIMAL_DUAL_OPTIMAL` means the problem was solved to the requested thresholds; the ``*_FEASIBLE`` and ``*_JUMP_DETECTED`` reasons are early stops requested through :class:`~sdpb_python.SolverOptions`. """ PRIMAL_DUAL_OPTIMAL = "found primal-dual optimal solution" PRIMAL_FEASIBLE = "found primal feasible solution" DUAL_FEASIBLE = "found dual feasible solution" PRIMAL_FEASIBLE_JUMP_DETECTED = "primal feasible jump detected" DUAL_FEASIBLE_JUMP_DETECTED = "dual feasible jump detected" MAX_COMPLEMENTARITY_EXCEEDED = "maxComplementarity exceeded" MAX_ITERATIONS_EXCEEDED = "maxIterations exceeded" MAX_RUNTIME_EXCEEDED = "maxRuntime exceeded" PRIMAL_STEP_TOO_SMALL = "primal step too small" DUAL_STEP_TOO_SMALL = "dual step too small" SIGTERM_RECEIVED = "SIGTERM signal received"
[docs] @classmethod def from_sdpb(cls, text: str) -> "TerminateReason": for member in cls: if member.value == text: return member raise ValueError(f"unknown terminate reason {text!r}")
[docs] @dataclass class BlockInfo: """Size of one SDP block. Attributes: dim: Matrix dimension of the constraint (``m_j``). num_points: Number of sample points (``d_j + 1``); 1 for an LMI block. """ dim: int num_points: int @property def schur_size(self) -> int: return self.num_points * self.dim * (self.dim + 1) // 2
[docs] @dataclass class Solution: """Solver output. Numbers are ``mpmath.mpf`` at ``precision`` bits. See :doc:`/background` for the meaning of the variables. Attributes: status: Why the solver stopped; :attr:`optimal` is the usual check. primal_objective: ``f + c . x``. dual_objective: ``f + b . y``; for a PMP this is the optimal value of the objective ``a . z``. duality_gap: ``|primal - dual| / max(|primal| + |dual|, 1)``. primal_error: Largest primal residue. dual_error: Largest dual residue. y: The dual variables, length ``N``. z: The PMP variables of Manual eq. (3.1), length ``N + 1``, when the problem has a normalization and ``"z"`` was requested; else ``None``. x: Per block, the primal variables (length ``num_points * dim * (dim+1) / 2``); ``None`` unless ``"x"`` was requested. X: Per block, the pair ``(even, odd)`` of positive semidefinite primal matrices; ``None`` unless ``"X"`` was requested. Y: Per block, the pair ``(even, odd)`` of positive semidefinite dual matrices; for an LMI block ``Y[j][0]`` is ``M_0 + sum_n y_n M_n``. c_minus_By: Per block, ``c - B y``: the extremal functional evaluated on the sampled constraints (what ``spectrum`` consumes). iterations: Iterations completed by this run. runtime_seconds: Wall time of the run. precision: Bits of precision used. blocks: :class:`BlockInfo` per block. """ status: TerminateReason primal_objective: mpmath.mpf dual_objective: mpmath.mpf duality_gap: mpmath.mpf primal_error: mpmath.mpf dual_error: mpmath.mpf y: list[mpmath.mpf] z: list[mpmath.mpf] | None x: list[list[mpmath.mpf]] | None X: list[tuple[mpmath.matrix, mpmath.matrix]] | None Y: list[tuple[mpmath.matrix, mpmath.matrix]] | None c_minus_By: list[list[mpmath.mpf]] | None iterations: int runtime_seconds: float precision: int blocks: list[BlockInfo] = field(default_factory=list) if PROFILING: # The profile file written for this solve (SolverOptions.profile_path), # or None; only in builds with the profiling feature (_features.py). profile_path: str | None = None @property def optimal(self) -> bool: return self.status is TerminateReason.PRIMAL_DUAL_OPTIMAL @classmethod def _from_dict(cls, d: dict) -> "Solution": bits = d["precision"] num_blocks = len(d["dims"]) def pairs(mats): if not mats: return None return [(matrix_mpfs(mats[2 * j], bits), matrix_mpfs(mats[2 * j + 1], bits)) for j in range(num_blocks)] return cls( status=TerminateReason.from_sdpb(d["terminate_reason"]), primal_objective=from_str(d["primal_objective"], bits), dual_objective=from_str(d["dual_objective"], bits), duality_gap=from_str(d["duality_gap"], bits), primal_error=from_str(d["primal_error"], bits), dual_error=from_str(d["dual_error"], bits), y=mpfs(d["y"], bits), z=mpfs(d["z"], bits) if d["z"] is not None else None, x=[mpfs(block, bits) for block in d["x"]] if d["x"] else None, X=pairs(d["X"]), Y=pairs(d["Y"]), c_minus_By=[mpfs(block, bits) for block in d["c_minus_By"]] if d["c_minus_By"] else None, iterations=d["iterations"], runtime_seconds=d["runtime_ms"] / 1000.0, precision=bits, blocks=[BlockInfo(dim, n) for dim, n in zip(d["dims"], d["num_points"])], **({"profile_path": d.get("profile_path") or None} if PROFILING else {}), )