Source code for hynet.qcqp.result

#pylint: disable=too-many-instance-attributes,too-many-arguments
"""
Representation of the result of a *hynet*-specific QCQP.
"""

import logging

import numpy as np
import pandas as pd

from hynet.qcqp.problem import QCQPPoint  # pylint: disable=unused-import
from hynet.utilities.base import Timer

_log = logging.getLogger(__name__)


[docs]class QCQPResult: """ Solution of a quadratically constrained quadratic program. **Caution:** The constructor adjusts the optimal objective value, the optimizer, and the dual variables according to the scaling of the problem, i.e., the properties of the result correspond to the *unscaled* QCQP. Parameters ---------- qcqp : QCQP Problem specification. solver : SolverInterface Solver object by which the result was obtained. solver_status : SolverStatus Status reported by the solver after performing the optimization. solver_time : float Duration of the call to the solver in seconds. optimizer : QCQPPoint or None Optimal optimization variable ``(v^*, f^*, s^*, z^*)`` or ``None`` if the solver failed. In case of a relaxation, ``v^*`` is the rank-1 approximation of ``V^*``. V : .hynet_sparse_ or None Optimal optimization variable ``V^*`` in case of a relaxation or ``None`` if the solver failed or a nonconvex-QCQP solver was employed. optimal_value : float Optimal objective value or ``numpy.nan`` if the solver failed. reconstruction_mse : float The mean squared error of the reconstructed ``v^*`` in case of a relaxation or ``numpy.nan`` if the solver failed or a nonconvex-QCQP solver was employed. dv_lb : QCQPPoint or None Optimal dual variables of the lower bounds on ``f``, ``s``, and ``z`` or ``None`` if the solver failed. The attribute ``v`` is set ``None`` as these bounds are optional, cf. the QCQP specification. dv_ub : QCQPPoint or None Optimal dual variables of the upper bounds on ``f``, ``s``, and ``z`` or ``None`` if the solver failed. The attribute ``v`` is set ``None`` as these bounds are optional, cf. the QCQP specification. dv_eq : numpy.ndarray or None Optimal dual variables of the equality constraints or ``None`` if the solver failed. dv_ineq : numpy.ndarray or None Optimal dual variables of the inequality constraints or ``None`` if the solver failed. """ def __init__(self, qcqp, solver, solver_status, solver_time, optimizer=None, V=None, optimal_value=None, dv_lb=None, dv_ub=None, dv_eq=None, dv_ineq=None): timer = Timer() self.qcqp = qcqp self.solver = solver self.solver_status = solver_status self.solver_time = solver_time self._optimizer_normalized = optimizer self.V = V self.optimal_value = optimal_value self.dv_lb = dv_lb self.dv_ub = dv_ub self.dv_eq = dv_eq self.dv_ineq = dv_ineq # Compensate the scaling of the objective, variables, and constraints # # REMARK: If a constraint is scaled by s, then the associated dual # variable scales by 1/s (cf. the first order optimality conditions). # Therefore, to obtain the dual variable of the unscaled constraint, # the dual variable of the scaled constraint must be multiplied by s. obj_scaling = qcqp.obj_func.scaling if self.optimal_value is None: self.optimal_value = np.nan self.optimal_value /= obj_scaling if self._optimizer_normalized is not None: scaling = qcqp.normalization.copy().reciprocal() self.optimizer = self._optimizer_normalized.copy().scale(scaling) else: self.optimizer = None if self.V is not None: self.V = self.V / qcqp.normalization.v ** 2 # New matrix! bound_dual_scaling = qcqp.normalization.copy().scale(1 / obj_scaling) if self.dv_lb is not None: self.dv_lb = self.dv_lb.copy().scale(bound_dual_scaling) if self.dv_ub is not None: self.dv_ub = self.dv_ub.copy().scale(bound_dual_scaling) if self.dv_eq is not None: self.dv_eq = self.dv_eq / obj_scaling # New array! for i in range(len(qcqp.eq_crt)): self.dv_eq[i] *= qcqp.eq_crt[i].scaling if self.dv_ineq is not None: self.dv_ineq = self.dv_ineq / obj_scaling # New array! for i in range(len(qcqp.ineq_crt)): self.dv_ineq[i] *= qcqp.ineq_crt[i].scaling # Calculate the reconstruction error in case of a relaxation if self.V is not None: self.reconstruction_mse = \ self.solver.rank1approx.calc_mse(self.V, self.optimizer.v, qcqp.edges) else: self.reconstruction_mse = np.nan _log.debug("QCQP result creation ({:.3f} sec.)" .format(timer.total())) @property def empty(self): """Return ``True`` if the QCQP result does not contain an optimizer.""" return self.optimizer is None
[docs] def get_result_tables(self, tables=None, dual_prefix='dv_', value_prefix='cv_'): """ Return a dictionary of data frames with the dual and constraint result. According to the table and ID information of the constraint objects, a dictionary of data frames is assembled that contains the dual variables and the *equality* constraint function values with the right hand side subtracted. """ tables = {} if tables is None else tables if self.empty: return tables for crt, dv in zip(self.qcqp.eq_crt, self.dv_eq): if crt.table is None: continue if crt.table not in tables: tables[crt.table] = pd.DataFrame() tables[crt.table].at[crt.id, dual_prefix + crt.name] = dv tables[crt.table].at[crt.id, value_prefix + crt.name] = \ crt.evaluate(self._optimizer_normalized) / crt.scaling for crt, dv in zip(self.qcqp.ineq_crt, self.dv_ineq): if crt.table is None: continue if crt.table not in tables: tables[crt.table] = pd.DataFrame() tables[crt.table].at[crt.id, dual_prefix + crt.name] = dv return tables