Source code for hynet.qcqp.solver

"""
Solver interface for a *hynet*-specific QCQP.
"""

import logging
import abc

import numpy as np

from hynet.utilities.graph import get_graph_components
from hynet.qcqp.rank1approx import GraphTraversalRank1Approximator
from hynet.qcqp.result import QCQPResult

_log = logging.getLogger(__name__)


[docs]class SolverInterface(abc.ABC): """ Abstract base class for QCQP solvers. Derived classes implement a solver for the *hynet*-specific quadratically constrained quadratic program specified by an object of the class ``QCQP``. A solver may solve the nonconvex QCQP, its semidefinite relaxation (SDR), or its second-order cone relaxation (SOCR). Parameters ---------- verbose : bool, optional If ``True``, the logging information of the solver is printed to the standard output. param : dict[str, object], optional Dictionary of parameters (``{'parameter_name': value}``) to modify the solver's default settings. rank1approx : Rank1Approximator, optional Rank-1 approximator for partial Hermitian matrices. This approximator is used in relaxation-based solvers. See Also -------- hynet.qcqp.rank1approx : Rank-1 approximators. """ def __init__(self, verbose=False, param=None, rank1approx=None): self.verbose = verbose # REMARK: It looks like everyone, at some point, has to stumble across # the highly unintuitive behavior of mutable defaults...for me it was # when the parameters of some solver popped up in another one. So be # aware and check out http://effbot.org/zone/default-values.htm ;) self.param = {} if param is None else param if rank1approx is None: self.rank1approx = GraphTraversalRank1Approximator() else: self.rank1approx = rank1approx def __str__(self): return self.name + ' (' + self.type.value + ')' @property @abc.abstractmethod def name(self): """Return the name of the solver.""" @property @abc.abstractmethod def type(self): """Return the type of the solver as a SolverType."""
[docs] @abc.abstractmethod def solve(self, qcqp): """ Solve the given QCQP and return a ``QCQPResult`` object. Parameters ---------- qcqp : QCQP Specification of the quadratically-constrained quadratic problem. Returns ------- result : QCQPResult Solution of the QCQP. """
[docs] @staticmethod def adjust_absolute_phase(qcqp, v): """ Return ``v`` with absolute phase adjustment according to the root nodes. Due to the quadratic form in ``v``, the solution of the QCQP is ambiguous w.r.t. a common phase shift of the elements in ``v`` associated with a connected component in the sparsity pattern. This method adjusts the absolute phase such that it is zero at the root node of the respective components. *Remark:* This method should be used in *QCQP solvers*. In SDR and SOCR solvers, the rank-1 approximation takes care of the absolute phase adjustment. Parameters ---------- v : numpy.ndarray[.hynet_complex_] Optimizer ``v`` for the solved QCQP. Returns ------- v : numpy.ndarray[.hynet_complex_] Optimizer ``v`` with absolute phase adjustment. """ v_ = np.array(v) components = get_graph_components(np.arange(qcqp.dim_v), qcqp.edges, roots=qcqp.roots) for component in components: root = component[np.isin(component, qcqp.roots)] if len(root) == 0: # pylint: disable=len-as-condition root = component[0] else: if len(root) > 1: _log.warning("Sparsity pattern has ambiguous roots.") root = root[0] v_[component] *= np.exp(-1j * np.angle(v_[root])) return v_