Source code for sdpb_python.options

"""Solver options mirroring SDPB's Solver_Parameters."""

from __future__ import annotations

import dataclasses
import os
import tempfile
from dataclasses import dataclass, field
from typing import Any, Iterable, Mapping

from ._features import PROFILING
from .numbers import to_str

VERBOSITY = {"none": 0, "regular": 1, "debug": 2, "trace": 3}
SOLUTION_PARTS = ("x", "y", "z", "X", "Y", "c_minus_By")
_INT64_MAX = 2**63 - 1


[docs] @dataclass class SolverOptions: """Solver options. Defaults are SDPB's own. Names follow ``sdpb``'s command line options in snake_case, e.g. ``duality_gap_threshold`` for ``--dualityGapThreshold``. Numeric thresholds accept anything :mod:`sdpb_python.numbers` converts (``int``, ``float``, ``str``, ``Fraction``, ``mpf``); strings such as ``"1e-30"`` are the safest way to write tiny numbers. Attributes: precision: Working precision in bits (``--precision``). ``None`` means the precision already fixed for this process, or SDPB's default 400 if none is fixed yet. See :ref:`precision`. max_iterations: Stop after this many iterations (``--maxIterations``, 500). max_runtime: Stop after this many seconds (``--maxRuntime``, unlimited). duality_gap_threshold: Optimality test on the normalised duality gap ``|primal - dual| / max(|primal| + |dual|, 1)`` (1e-30). primal_error_threshold: Optimality test on the primal residues (1e-30). dual_error_threshold: Optimality test on the dual residues (1e-30). initial_matrix_scale_primal: ``X`` starts as this multiple of the identity (``--initialMatrixScalePrimal``, 1e20). initial_matrix_scale_dual: Same for ``Y`` (1e20). feasible_centering_parameter: Centering parameter ``beta`` used once primal and dual are feasible (0.1). infeasible_centering_parameter: Centering parameter while infeasible (0.3). step_length_reduction: Fraction of the maximal step taken (0.7). max_complementarity: Abort when ``Tr(XY)/dim`` exceeds this (1e100). min_primal_step: Stop when the primal step length falls below this (0). min_dual_step: Stop when the dual step length falls below this (0). find_primal_feasible: Stop as soon as a primal feasible point is found. find_dual_feasible: Stop as soon as a dual feasible point is found. detect_primal_feasible_jump: Stop when the primal step length reaches 1. detect_dual_feasible_jump: Stop when the dual step length reaches 1. fast_step_length: Compute the step lengths from a double-precision smallest eigenvalue, verified by an arbitrary-precision Cholesky (SDPB's ``--fastStepLength``); the exact eigensolver is used for blocks that fail the check. Default ``False``. max_shared_memory_bytes: Memory budget for the exact ``Q`` computation (``--maxSharedMemory``); ``0`` lets SDPB choose about half of the free RAM. Accepts ints or strings such as ``"100K"`` / ``"64G"``. checkpoint_dir: Directory for SDPB's binary checkpoints. When set, a checkpoint found there is loaded before solving and one is written at the end and every ``checkpoint_interval`` seconds. ``None`` disables checkpointing. checkpoint_interval: Seconds between checkpoints (3600). output_dir: If set, SDPB writes ``iterations.json`` (one record per iteration) and ``c_minus_By/c_minus_By.json`` there, as the ``sdpb`` executable does in its ``--outDir``. verbosity: ``"none"`` (default), ``"regular"``, ``"debug"`` or ``"trace"`` (or 0-3). Anything above ``"none"`` prints SDPB's iteration table to the C++ standard output. want: Which parts of the solution to return besides the objectives and errors: any of ``"x"``, ``"y"``, ``"z"``, ``"X"``, ``"Y"``, ``"c_minus_By"``. Default ``("y", "z")``. Large problems have large ``X``/``Y``; ask only for what you need. """ precision: int | None = None # None: the process precision if fixed, else SDPB's 400 max_iterations: int = 500 max_runtime: int = _INT64_MAX duality_gap_threshold: Any = "1e-30" primal_error_threshold: Any = "1e-30" dual_error_threshold: Any = "1e-30" initial_matrix_scale_primal: Any = "1e20" initial_matrix_scale_dual: Any = "1e20" feasible_centering_parameter: Any = "0.1" infeasible_centering_parameter: Any = "0.3" step_length_reduction: Any = "0.7" max_complementarity: Any = "1e100" min_primal_step: Any = "0" min_dual_step: Any = "0" find_primal_feasible: bool = False find_dual_feasible: bool = False detect_primal_feasible_jump: bool = False detect_dual_feasible_jump: bool = False fast_step_length: bool = False max_shared_memory_bytes: int = 0 checkpoint_dir: str | os.PathLike | None = None checkpoint_interval: int = 3600 output_dir: str | os.PathLike | None = None verbosity: str | int = "none" want: Iterable[str] = ("y", "z") if PROFILING: # Profiling options, only in builds with the profiling feature # (sdpb_python/_features.py): # profile_path: if set, SDPB's per-solve profile (valid JSON: the timer # tree of parse_spec / pmp2sdp / solver with matrix shapes and run # metadata, see SDPB's docs/Usage.md, "Profiling") is written to # this file after the solve, also when the solve raises; # Solution.profile_path echoes it. # profile_detail: 0 coarse phase timers only, 1 (default) also # per-block kernel timers with shapes, 2 MPI barrier probes. # profile_autosave: also rewrite the profile every 10 iterations and # at checkpoints, so a killed solve leaves data. # profile_meta: free labels (str -> str) stored under meta.labels. profile_path: str | os.PathLike | None = None profile_detail: int = 1 profile_autosave: bool = False profile_meta: Mapping[str, str] | None = None def replace(self, **overrides: Any) -> "SolverOptions": unknown = set(overrides) - {f.name for f in dataclasses.fields(self)} if unknown: raise TypeError(f"unknown solver option(s): {sorted(unknown)}") return dataclasses.replace(self, **overrides) def _to_dict(self) -> dict: """The dict consumed by the Cython layer.""" bits = effective_precision(self.precision) if isinstance(self.verbosity, str): verbosity = VERBOSITY[self.verbosity] else: verbosity = int(self.verbosity) want = set(self.want) unknown = want - set(SOLUTION_PARTS) if unknown: raise ValueError(f"unknown solution part(s) {sorted(unknown)}; choose from {SOLUTION_PARTS}") if self.checkpoint_dir is None: # SDPB reads checkpoints from the CWD when checkpoint_in is empty, # so point it at a directory that does not exist. checkpoint_in = os.path.join(tempfile.gettempdir(), "sdpb_python_no_checkpoint") checkpoint_out = "" else: checkpoint_in = checkpoint_out = os.fspath(self.checkpoint_dir) out = { "max_iterations": int(self.max_iterations), "max_runtime": int(self.max_runtime), "checkpoint_interval": int(self.checkpoint_interval), "max_shared_memory_bytes": _parse_bytes(self.max_shared_memory_bytes), "find_primal_feasible": bool(self.find_primal_feasible), "find_dual_feasible": bool(self.find_dual_feasible), "detect_primal_feasible_jump": bool(self.detect_primal_feasible_jump), "detect_dual_feasible_jump": bool(self.detect_dual_feasible_jump), "fast_step_length": bool(self.fast_step_length), "precision": bits, "duality_gap_threshold": to_str(self.duality_gap_threshold, bits), "primal_error_threshold": to_str(self.primal_error_threshold, bits), "dual_error_threshold": to_str(self.dual_error_threshold, bits), "initial_matrix_scale_primal": to_str(self.initial_matrix_scale_primal, bits), "initial_matrix_scale_dual": to_str(self.initial_matrix_scale_dual, bits), "feasible_centering_parameter": to_str(self.feasible_centering_parameter, bits), "infeasible_centering_parameter": to_str(self.infeasible_centering_parameter, bits), "step_length_reduction": to_str(self.step_length_reduction, bits), "max_complementarity": to_str(self.max_complementarity, bits), "min_primal_step": to_str(self.min_primal_step, bits), "min_dual_step": to_str(self.min_dual_step, bits), "checkpoint_in": checkpoint_in, "checkpoint_out": checkpoint_out, "output_dir": "" if self.output_dir is None else os.fspath(self.output_dir), "verbosity": verbosity, "want_x": "x" in want, "want_z": "z" in want, "want_X": "X" in want, "want_Y": "Y" in want, "want_c_minus_By": "c_minus_By" in want, } if PROFILING: out.update({ "profile_path": "" if self.profile_path is None else os.fspath(self.profile_path), "profile_detail": int(self.profile_detail), "profile_autosave": bool(self.profile_autosave), "profile_meta": sorted((str(k), str(v)) for k, v in (self.profile_meta or {}).items()), }) return out
_UNITS = {"": 1, "B": 1, "K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4} def _parse_bytes(value: Any) -> int: """Accept ints or SDPB-style strings such as ``"100.1K"`` or ``"64G"``.""" if isinstance(value, (int, float)): return int(value) s = str(value).strip().upper().rstrip("B") for unit, factor in _UNITS.items(): if unit and s.endswith(unit): return int(float(s[: -len(unit)]) * factor) return int(float(s)) def effective_precision(precision: int | None) -> int: """SDPB's Elemental fixes the precision once per process (see docs/API_DESIGN.md).""" if precision is not None: return int(precision) from . import _sdpb # noqa: WPS433 fixed = _sdpb.requested_precision() return fixed if fixed else 400 def resolve(options: SolverOptions | None, overrides: dict) -> SolverOptions: base = SolverOptions() if options is None else options return base.replace(**overrides) if overrides else base