QP

Class for standard form QP problems.

The problems have following form:

\[ \min \frac{1}{2} x^THx + q^Tx \]
\[ \begin{equation} \begin{split} lbA & \leq & Ax & \leq & ubA \\ lb & \leq & x & \leq & ub \\ & & Cx & = & d \end{split} \end{equation} \]
Parameters:
  • H (ndarray) –

    nV x nV matrix

  • q (ndarray) –

    nV array

  • A (Optional[ndarray], default: None ) –

    nC x nV matrix

  • lbA (Optional[ndarray], default: None ) –

    nC array

  • ubA (Optional[ndarray], default: None ) –

    nC array

  • lb (Optional[ndarray], default: None ) –

    nV array

  • ub (Optional[ndarray], default: None ) –

    nV array

  • C (Optional[ndarray], default: None ) –

    nD x nV matrix

  • d (Optional[ndarray], default: None ) –

    nD array

Source code in qphelper/qp.py
class QP:
    r"""
    Class for standard form QP problems.

    The problems have following form:

    \[
    \min \frac{1}{2} x^THx + q^Tx
    \]

    \[
    \begin{equation}
    \begin{split}
    lbA & \leq & Ax & \leq & ubA \\
    lb  & \leq & x  & \leq & ub \\
    & & Cx & = & d
    \end{split}
    \end{equation}
    \]

    Parameters:
        H: nV x nV matrix
        q: nV array
        A: nC x nV matrix
        lbA: nC array
        ubA: nC array
        lb: nV array
        ub: nV array
        C: nD x nV matrix
        d: nD array
    """
    def __init__(self, H: numpy.ndarray, q: numpy.ndarray, A: Optional[numpy.ndarray] = None, lbA: Optional[numpy.ndarray] = None, ubA: Optional[numpy.ndarray] = None, lb: Optional[numpy.ndarray] = None, ub: Optional[numpy.ndarray] = None, C: Optional[numpy.ndarray] = None, d: Optional[numpy.ndarray] = None):
        assert (H == H.T).all()
        nV = H.shape[0]
        self.H = H
        self.q = q
        if A is not None:
            assert A.shape[1] == nV
            self.A = A
        else:
            self.A = numpy.zeros((0, nV))
        nC = self.A.shape[0]
        if lbA is not None:
            assert lbA.shape == (nC,)
            self.lbA = lbA
        else:
            self.lbA = numpy.full((nC,), -numpy.inf)
        if ubA is not None:
            assert ubA.shape == (nC,)
            self.ubA = ubA
        else:
            self.ubA = numpy.full((nC,), numpy.inf)
        assert (self.lbA <= self.ubA).all()
        if lb is not None:
            assert lb.shape == (nV,)
            self.lb = lb
        else:
            self.lb = numpy.full((nV,), -numpy.inf)
        if ub is not None:
            assert ub.shape == (nV,)
            self.ub = ub
        else:
            self.ub = numpy.full((nV,), numpy.inf)
        assert (self.lb <= self.ub).all()
        assert (C is None) == (d is None)
        if C is not None and d is not None:
            self.C = C
            self.d = d
        else:
            self.C = numpy.zeros((0, nV))
            self.d = numpy.zeros((0,))

    def get_equalities_count(self) -> int:
        return self.d.shape[0]

    def get_inequalities_count(self) -> int:
        return self.lbA.shape[0]

    def get_variables_count(self) -> int:
        return self.H.shape[0]

    def is_hessian_positive_definite(self) -> bool:
        """
        Returns:
            Whatever the Hessian is positive definite
        """
        return bool(numpy.all(numpy.linalg.eigvals(self.H) > 0))

    def is_hessian_positive_semidefinite(self) -> bool:
        """
        Returns:
            Whatever the Hessian is positive semidefinite
        """
        return bool(numpy.all(numpy.linalg.eigvals(self.H) >= 0))

    def evaluate_primal(self, x: numpy.ndarray) -> float:
        """
        Computes objective value

        Parameters:
            x: nV - primal solution

        Returns:
            The objective of the primal problem.
        """
        return float(0.5 * (x.T.dot(self.H).dot(x)) + self.q.T.dot(x))

    def evaluate_dual(self, x: numpy.ndarray, ybA: Optional[numpy.ndarray] = None, yb: Optional[numpy.ndarray] = None, yD: Optional[numpy.ndarray] = None) -> float:
        """
        Computes objective value for dual problem

        Parameters:
            ybA: nC - dual solution for general constraints
            yb: nV - dual solution for variable bounds
            yD: nD - dual solution for equalities

        Returns:
            The objective of the dual dual.
        """
        if ybA is None:
            ybA = numpy.zeros((self.get_inequalities_count(),))
        if yb is None:
            yb = numpy.zeros((self.get_variables_count(),))
        if yD is None:
            yD = numpy.zeros((self.get_equalities_count(),))
        result = 0.5 * (x.T.dot(self.H).dot(x))
        mask_lba = (self.lbA != -numpy.inf) & (ybA < 0)
        result += (ybA[mask_lba] * self.lbA[mask_lba]).sum()
        mask_uba = (self.ubA != numpy.inf) & (ybA > 0)
        result += (ybA[mask_uba] * self.ubA[mask_uba]).sum()
        mask_lb = (self.lb != -numpy.inf) & (yb < 0)
        result += (yb[mask_lb] * self.lb[mask_lb]).sum()
        mask_ub = (self.ub != numpy.inf) & (yb > 0)
        result += (yb[mask_ub] * self.ub[mask_ub]).sum()
        result += (yD * self.d).sum()
        return -float(result)

    def calculate_primal(self, ybA: Optional[numpy.ndarray] = None, yb: Optional[numpy.ndarray] = None, yD: Optional[numpy.ndarray] = None) -> numpy.ndarray:
        """
        Calculates primal solution from dual solution

        Parameters:
            ybA: nC - dual solution for general constraints
            yb: nV - dual solution for variable bounds
            yD: nD - dual solution for equalities

        Returns:
            The primal.
        """
        if ybA is None:
            ybA = numpy.zeros((self.get_inequalities_count(),))
        if yb is None:
            yb = numpy.zeros((self.get_variables_count(),))
        if yD is None:
            yD = numpy.zeros((self.get_equalities_count(),))
        A = numpy.concatenate([self.H, self.C])
        b = numpy.concatenate([-(self.A.T.dot(ybA) + yb + self.C.T.dot(yD) + self.q), self.d])
        eps = 0.0001
        for ybAn, An, lbAn, ubAn in zip(ybA, self.A, self.lbA, self.ubA):
            if abs(ybAn) > eps:
                A = numpy.append(A, numpy.array([An]), 0)
                if ybAn < 0:
                    b = numpy.append(b, [lbAn])
                else:
                    b = numpy.append(b, [ubAn])
        for i, (ybn, lbn, ubn) in enumerate(zip(yb, self.lb, self.ub)):
            if abs(ybn) > eps:
                new_constr = numpy.zeros(self.get_variables_count())
                new_constr[i] = 1.0
                A = numpy.append(A, numpy.array([new_constr]), 0)
                if ybn < 0:
                    b = numpy.append(b, [lbn])
                else:
                    b = numpy.append(b, [ubn])
        primal, _, _, _ = numpy.linalg.lstsq(A, b)
        # TODO: ensure lbA <= Ax <= ubA, lb <= x <= ub for cases with non-unique solution
        return primal

    def calculate_dual(self, x: numpy.ndarray, eps: float = 0.0001) -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]:
        """
        Calculates dual solution from primal solution

        Parameters:
            x: nV - primal solution

        Returns:
            The dual.
        """
        A = numpy.concatenate([self.A.T, numpy.identity(self.get_variables_count()), self.C.T], 1)
        b = -(self.H.dot(x) + self.q)
        dual_length = self.get_inequalities_count() + self.get_variables_count() + self.get_equalities_count()
        for i, (An, lbn, ubn) in enumerate(zip(self.A, self.lbA, self.ubA)):
            if not(An.T.dot(x) <= lbn + eps or An.T.dot(x) >= ubn - eps):
                new_constr = numpy.zeros(dual_length)
                new_constr[i] = 1.0
                A = numpy.append(A, numpy.array([new_constr]), 0)
                b = numpy.append(b, numpy.zeros(1))
        for i, (xn, lbn, ubn) in enumerate(zip(x, self.lb, self.ub)):
            if not(xn <= lbn + eps or xn >= ubn - eps):
                new_constr = numpy.zeros(dual_length)
                new_constr[self.get_inequalities_count()+i] = 1.0
                A = numpy.append(A, numpy.array([new_constr]), 0)
                b = numpy.append(b, numpy.zeros(1))
        dual, _, _, _ = numpy.linalg.lstsq(A, b)
        offset = 0
        ybA = dual[offset:offset+self.get_inequalities_count()]
        offset += self.get_inequalities_count()
        yb = dual[offset:offset+self.get_variables_count()]
        offset += self.get_variables_count()
        yD = dual[offset:]
        return (ybA, yb, yD)

    def calculate_primal_residual(self, x: numpy.ndarray)-> float:
        r"""
        Calculates the primal residual, which corresponds to the maximal violation of following inequalities:

        \[
        \begin{equation}
        \begin{split}
        lbA & \leq & Ax & \leq & ubA \\
        lb  & \leq & x  & \leq & ub \\
        & & Cx & = & d
        \end{split}
        \end{equation}
        \]

        Returns:
            The primal residual.
        """
        result = 0
        result = numpy.absolute(self.C.dot(x) - self.d).max(initial=result)
        result =  (self.A.dot(x) - self.ubA).max(initial=result)
        result =  (self.lbA - self.A.dot(x)).max(initial=result)
        result =  (x - self.ub).max(initial=result)
        result =  (self.lb - x).max(initial=result)
        return result

    def calculate_dual_residual(self, x: numpy.ndarray, ybA: Optional[numpy.ndarray] = None, yb: Optional[numpy.ndarray] = None, yD: Optional[numpy.ndarray] = None) -> float:
        r"""
        Calculates the dual residual, which corresponds to the maximal violation of following equality:
        \( Hx + A^TybA + C^TyD + yb + q = 0 \)

        Returns:
            The dual residual.
        """
        if ybA is None:
            ybA = numpy.zeros((self.get_inequalities_count(),))
        if yb is None:
            yb = numpy.zeros((self.get_variables_count(),))
        if yD is None:
            yD = numpy.zeros((self.get_equalities_count(),))
        return numpy.absolute(self.H.dot(x) + self.A.T.dot(ybA) + self.C.T.dot(yD) + yb + self.q).max(initial = 0)

    def calculate_duality_gap(self, x: numpy.ndarray, ybA: Optional[numpy.ndarray] = None, yb: Optional[numpy.ndarray] = None, yD: Optional[numpy.ndarray] = None) -> float:
        """
        Calculates duality gap.

        The duality gap is equal to difference between primal and dual objective function for the given input.

        Returns:
            The duality gap.
        """
        return self.evaluate_primal(x) - self.evaluate_dual(x, ybA, yb, yD)

    def to_identity_hessian(self) -> tuple["QP", numpy.ndarray]:
        r"""
        Converts the problem to one, which has identity Hessian.
        Requires that the Hessian is positive definite.

        From:

        \[
        \min \frac{1}{2} x^THx + q^Tx
        \]

        \[
        \begin{equation}
        \begin{split}
        lbA & \leq & Ax & \leq & ubA \\
        lb  & \leq & x  & \leq & ub \\
        & & Cx & = & d
        \end{split}
        \end{equation}
        \]

        To:

        \[
        LL^T = H
        \]

        \[
        \min \frac{1}{2} x^Tx + q^TL^{-1T}x
        \]

        \[
        \begin{equation}
        \begin{split}
        lbA & \leq & AL^{-1T}x & \leq & ubA \\
        lb  & \leq & x  & \leq & ub \\
        & & CL^{-1T}x & = & d
        \end{split}
        \end{equation}
        \]

        Returns:
            A new QP with matrix \( L \) to recover the original solution.
        """
        qp = self.to_without_bounds()
        H_tr = numpy.linalg.cholesky(qp.H)
        H_tr_inv = numpy.linalg.inv(H_tr)
        # v^T = x^T H_tr
        # x^T = v^T H_tr_inv
        # x = H_tr_inv^T v
        q_new = qp.q.T.dot(H_tr_inv.T)
        A_new = qp.A.dot(H_tr_inv.T)
        C_new = qp.C.dot(H_tr_inv.T)
        return (QP(numpy.identity(qp.get_variables_count()), q_new, A_new, qp.lbA, qp.ubA, None, None, C_new, qp.d), H_tr_inv)

    def to_without_bounds(self) -> "QP":
        r"""
        Converts QP into QP with variable bounds transformed into general constraints.
        This transformation does not alter primal objective function.

        From:

        \[
        \min \frac{1}{2} x^THx + q^Tx
        \]

        \[
        \begin{equation}
        \begin{split}
        lbA & \leq & Ax & \leq & ubA \\
        lb  & \leq & x  & \leq & ub \\
        & & Cx & = & d
        \end{split}
        \end{equation}
        \]

        To:

        \[
        \min \frac{1}{2} x^THx + q^Tx
        \]

        \[
        \begin{equation}
        \begin{split}
        \begin{bmatrix}
            lbA \\
            lb
        \end{bmatrix} & \leq & \begin{bmatrix}
            A \\
            I
        \end{bmatrix}x & \leq & \begin{bmatrix}
            ubA \\
            ub
        \end{bmatrix} \\
        & & Cx & = & d
        \end{split}
        \end{equation}
        \]

        Note: Row n of I matrix is not added to A if lb_n is -infinity and ub_n is infinity


        Returns:
            A new QP.
        """
        newA = self.A
        newlbA = self.lbA
        newubA = self.ubA
        for i, (lbn, ubn) in enumerate(zip(self.lb, self.ub)):
            is_set = lbn != -numpy.inf or ubn != numpy.inf
            if not is_set:
                continue
            newlbA = numpy.append(newlbA, lbn)
            newubA = numpy.append(newubA, ubn)
            new_constaint = numpy.concatenate([numpy.zeros(i), numpy.ones(1), numpy.zeros(self.get_variables_count()-i-1)])
            newA = numpy.concatenate([newA, numpy.array([new_constaint])], 0)
        return QP(self.H, self.q, newA, newlbA, newubA, None, None, self.C, self.d)

    def to_without_general_lower_bounds(self) -> "QP":
        r"""
        Converts QP into QP with lower general bounds transformed into upper general constraints.
        This transformation does not alter primal objective function.

        From:

        \[
        \min \frac{1}{2} x^THx + q^Tx
        \]

        \[
        \begin{equation}
        \begin{split}
        lbA & \leq & Ax & \leq & ubA \\
        lb  & \leq & x  & \leq & ub \\
        & & Cx & = & d
        \end{split}
        \end{equation}
        \]

        To:

        \[
        \min \frac{1}{2} x^THx + q^Tx
        \]

        \[
        \begin{equation}
        \begin{split}
        & & \begin{bmatrix}
            A \\
            -A
        \end{bmatrix}x & \leq & \begin{bmatrix}
            ubA \\
            lbA
        \end{bmatrix} \\
        lb  & \leq & x  & \leq & ub \\
        & & Cx & = & d
        \end{split}
        \end{equation}
        \]

        Note: The new constraint matrix does not have to contain every line of the original constraint matrix
            twice if there are not both lower and upper bound.

        Returns:
            A new QP.
        """
        newA = numpy.zeros((0, self.A.shape[1]))
        newubA = numpy.zeros(0)
        for ubAn, An in zip(self.ubA, self.A):
            if ubAn == numpy.inf:
                continue
            newubA = numpy.append(newubA, ubAn)
            newA = numpy.concatenate([newA, numpy.array([An])], 0)
        for lbAn, An in zip(self.lbA, self.A):
            if lbAn == -numpy.inf:
                continue
            newubA = numpy.append(newubA, -lbAn)
            newA = numpy.concatenate([newA, numpy.array([-An])], 0)
        return QP(self.H, self.q, newA, None, newubA, self.lb, self.ub, self.C, self.d)

    def to_without_general_constraints(self) -> "QP":
        r"""
        Converts the problem to one, which has empty A matrix.

        From:

        \[
        \min \frac{1}{2} x^THx + q^Tx
        \]

        \[
        \begin{equation}
        \begin{split}
        lbA & \leq & Ax & \leq & ubA \\
        lb  & \leq & x  & \leq & ub \\
        & & Cx & = & d
        \end{split}
        \end{equation}
        \]

        To:

        \[
        \widetilde{x} = \begin{bmatrix}
            x \\
            0
        \end{bmatrix}
        \]

        \[
        \min \frac{1}{2} \widetilde{x}^T\begin{bmatrix}
            H & 0 \\
            0 & 0
        \end{bmatrix}\widetilde{x} + \begin{bmatrix}
            q \\
            0
        \end{bmatrix}^T\widetilde{x}
        \]

        \[
        \begin{equation}
        \begin{split}
        \begin{bmatrix}
            lb \\
            lbA
        \end{bmatrix} & \leq & \widetilde{x}  & \leq & \begin{bmatrix}
            ub \\
            ubA
        \end{bmatrix} \\
        & & \begin{bmatrix}
            C & 0 \\
            A & -I
        \end{bmatrix}\widetilde{x} & = & \begin{bmatrix}
            d \\
            0
        \end{bmatrix}
        \end{split}
        \end{equation}
        \]

        Returns:
            A new QP.
        """
        newH = numpy.block([
            [self.H, numpy.zeros((self.get_variables_count(), self.get_inequalities_count()))],
            [numpy.zeros((self.get_inequalities_count(), self.get_variables_count())), numpy.zeros((self.get_inequalities_count(), self.get_inequalities_count()))]
        ])
        newq = numpy.concatenate([self.q, numpy.zeros(self.get_inequalities_count())])
        newlb = numpy.concatenate([self.lb, self.lbA])
        newub = numpy.concatenate([self.ub, self.ubA])
        newC = numpy.block([
            [self.C, numpy.zeros(((self.get_equalities_count(), self.get_inequalities_count())))],
            [self.A, -numpy.identity(self.get_equalities_count())]
        ])
        newd = numpy.concatenate([self.d, numpy.zeros(self.get_equalities_count())])
        return QP(newH, newq, None, None, None, newlb, newub, newC, newd)

    def to_without_equalities(self) -> "QP":
        r"""
        Converts QP into QP with equalities transformed into general constraints.
        This transformation does not alter primal objective function.

        From:

        \[
        \min \frac{1}{2} x^THx + q^Tx
        \]

        \[
        \begin{equation}
        \begin{split}
        lbA & \leq & Ax & \leq & ubA \\
        lb  & \leq & x  & \leq & ub \\
        & & Cx & = & d
        \end{split}
        \end{equation}
        \]

        To:

        \[
        \min \frac{1}{2} x^THx + q^Tx
        \]

        \[
        \begin{equation}
        \begin{split}
        \begin{bmatrix}
            lbA \\
            d
        \end{bmatrix} & \leq & \begin{bmatrix}
            A \\
            C
        \end{bmatrix}x & \leq & \begin{bmatrix}
            ubA \\
            d
        \end{bmatrix}  \\
        lb  & \leq & x  & \leq & ub
        \end{split}
        \end{equation}
        \]


        Returns:
            A new QP.
        """
        newA = numpy.concatenate([self.A, self.C], 0)
        newlbA = numpy.concatenate([self.lbA, self.d])
        newubA = numpy.concatenate([self.ubA, self.d])
        return QP(self.H, self.q, newA, newlbA, newubA, self.lb, self.ub, None, None)

calculate_dual(x, eps=0.0001)

Calculates dual solution from primal solution

Parameters:
  • x (ndarray) –

    nV - primal solution

Returns:
  • tuple[ndarray, ndarray, ndarray]

    The dual.

Source code in qphelper/qp.py
def calculate_dual(self, x: numpy.ndarray, eps: float = 0.0001) -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]:
    """
    Calculates dual solution from primal solution

    Parameters:
        x: nV - primal solution

    Returns:
        The dual.
    """
    A = numpy.concatenate([self.A.T, numpy.identity(self.get_variables_count()), self.C.T], 1)
    b = -(self.H.dot(x) + self.q)
    dual_length = self.get_inequalities_count() + self.get_variables_count() + self.get_equalities_count()
    for i, (An, lbn, ubn) in enumerate(zip(self.A, self.lbA, self.ubA)):
        if not(An.T.dot(x) <= lbn + eps or An.T.dot(x) >= ubn - eps):
            new_constr = numpy.zeros(dual_length)
            new_constr[i] = 1.0
            A = numpy.append(A, numpy.array([new_constr]), 0)
            b = numpy.append(b, numpy.zeros(1))
    for i, (xn, lbn, ubn) in enumerate(zip(x, self.lb, self.ub)):
        if not(xn <= lbn + eps or xn >= ubn - eps):
            new_constr = numpy.zeros(dual_length)
            new_constr[self.get_inequalities_count()+i] = 1.0
            A = numpy.append(A, numpy.array([new_constr]), 0)
            b = numpy.append(b, numpy.zeros(1))
    dual, _, _, _ = numpy.linalg.lstsq(A, b)
    offset = 0
    ybA = dual[offset:offset+self.get_inequalities_count()]
    offset += self.get_inequalities_count()
    yb = dual[offset:offset+self.get_variables_count()]
    offset += self.get_variables_count()
    yD = dual[offset:]
    return (ybA, yb, yD)

calculate_dual_residual(x, ybA=None, yb=None, yD=None)

Calculates the dual residual, which corresponds to the maximal violation of following equality: \( Hx + A^TybA + C^TyD + yb + q = 0 \)

Returns:
  • float

    The dual residual.

Source code in qphelper/qp.py
def calculate_dual_residual(self, x: numpy.ndarray, ybA: Optional[numpy.ndarray] = None, yb: Optional[numpy.ndarray] = None, yD: Optional[numpy.ndarray] = None) -> float:
    r"""
    Calculates the dual residual, which corresponds to the maximal violation of following equality:
    \( Hx + A^TybA + C^TyD + yb + q = 0 \)

    Returns:
        The dual residual.
    """
    if ybA is None:
        ybA = numpy.zeros((self.get_inequalities_count(),))
    if yb is None:
        yb = numpy.zeros((self.get_variables_count(),))
    if yD is None:
        yD = numpy.zeros((self.get_equalities_count(),))
    return numpy.absolute(self.H.dot(x) + self.A.T.dot(ybA) + self.C.T.dot(yD) + yb + self.q).max(initial = 0)

calculate_duality_gap(x, ybA=None, yb=None, yD=None)

Calculates duality gap.

The duality gap is equal to difference between primal and dual objective function for the given input.

Returns:
  • float

    The duality gap.

Source code in qphelper/qp.py
def calculate_duality_gap(self, x: numpy.ndarray, ybA: Optional[numpy.ndarray] = None, yb: Optional[numpy.ndarray] = None, yD: Optional[numpy.ndarray] = None) -> float:
    """
    Calculates duality gap.

    The duality gap is equal to difference between primal and dual objective function for the given input.

    Returns:
        The duality gap.
    """
    return self.evaluate_primal(x) - self.evaluate_dual(x, ybA, yb, yD)

calculate_primal(ybA=None, yb=None, yD=None)

Calculates primal solution from dual solution

Parameters:
  • ybA (Optional[ndarray], default: None ) –

    nC - dual solution for general constraints

  • yb (Optional[ndarray], default: None ) –

    nV - dual solution for variable bounds

  • yD (Optional[ndarray], default: None ) –

    nD - dual solution for equalities

Returns:
  • ndarray

    The primal.

Source code in qphelper/qp.py
def calculate_primal(self, ybA: Optional[numpy.ndarray] = None, yb: Optional[numpy.ndarray] = None, yD: Optional[numpy.ndarray] = None) -> numpy.ndarray:
    """
    Calculates primal solution from dual solution

    Parameters:
        ybA: nC - dual solution for general constraints
        yb: nV - dual solution for variable bounds
        yD: nD - dual solution for equalities

    Returns:
        The primal.
    """
    if ybA is None:
        ybA = numpy.zeros((self.get_inequalities_count(),))
    if yb is None:
        yb = numpy.zeros((self.get_variables_count(),))
    if yD is None:
        yD = numpy.zeros((self.get_equalities_count(),))
    A = numpy.concatenate([self.H, self.C])
    b = numpy.concatenate([-(self.A.T.dot(ybA) + yb + self.C.T.dot(yD) + self.q), self.d])
    eps = 0.0001
    for ybAn, An, lbAn, ubAn in zip(ybA, self.A, self.lbA, self.ubA):
        if abs(ybAn) > eps:
            A = numpy.append(A, numpy.array([An]), 0)
            if ybAn < 0:
                b = numpy.append(b, [lbAn])
            else:
                b = numpy.append(b, [ubAn])
    for i, (ybn, lbn, ubn) in enumerate(zip(yb, self.lb, self.ub)):
        if abs(ybn) > eps:
            new_constr = numpy.zeros(self.get_variables_count())
            new_constr[i] = 1.0
            A = numpy.append(A, numpy.array([new_constr]), 0)
            if ybn < 0:
                b = numpy.append(b, [lbn])
            else:
                b = numpy.append(b, [ubn])
    primal, _, _, _ = numpy.linalg.lstsq(A, b)
    # TODO: ensure lbA <= Ax <= ubA, lb <= x <= ub for cases with non-unique solution
    return primal

calculate_primal_residual(x)

Calculates the primal residual, which corresponds to the maximal violation of following inequalities:

\[ \begin{equation} \begin{split} lbA & \leq & Ax & \leq & ubA \\ lb & \leq & x & \leq & ub \\ & & Cx & = & d \end{split} \end{equation} \]
Returns:
  • float

    The primal residual.

Source code in qphelper/qp.py
def calculate_primal_residual(self, x: numpy.ndarray)-> float:
    r"""
    Calculates the primal residual, which corresponds to the maximal violation of following inequalities:

    \[
    \begin{equation}
    \begin{split}
    lbA & \leq & Ax & \leq & ubA \\
    lb  & \leq & x  & \leq & ub \\
    & & Cx & = & d
    \end{split}
    \end{equation}
    \]

    Returns:
        The primal residual.
    """
    result = 0
    result = numpy.absolute(self.C.dot(x) - self.d).max(initial=result)
    result =  (self.A.dot(x) - self.ubA).max(initial=result)
    result =  (self.lbA - self.A.dot(x)).max(initial=result)
    result =  (x - self.ub).max(initial=result)
    result =  (self.lb - x).max(initial=result)
    return result

evaluate_dual(x, ybA=None, yb=None, yD=None)

Computes objective value for dual problem

Parameters:
  • ybA (Optional[ndarray], default: None ) –

    nC - dual solution for general constraints

  • yb (Optional[ndarray], default: None ) –

    nV - dual solution for variable bounds

  • yD (Optional[ndarray], default: None ) –

    nD - dual solution for equalities

Returns:
  • float

    The objective of the dual dual.

Source code in qphelper/qp.py
def evaluate_dual(self, x: numpy.ndarray, ybA: Optional[numpy.ndarray] = None, yb: Optional[numpy.ndarray] = None, yD: Optional[numpy.ndarray] = None) -> float:
    """
    Computes objective value for dual problem

    Parameters:
        ybA: nC - dual solution for general constraints
        yb: nV - dual solution for variable bounds
        yD: nD - dual solution for equalities

    Returns:
        The objective of the dual dual.
    """
    if ybA is None:
        ybA = numpy.zeros((self.get_inequalities_count(),))
    if yb is None:
        yb = numpy.zeros((self.get_variables_count(),))
    if yD is None:
        yD = numpy.zeros((self.get_equalities_count(),))
    result = 0.5 * (x.T.dot(self.H).dot(x))
    mask_lba = (self.lbA != -numpy.inf) & (ybA < 0)
    result += (ybA[mask_lba] * self.lbA[mask_lba]).sum()
    mask_uba = (self.ubA != numpy.inf) & (ybA > 0)
    result += (ybA[mask_uba] * self.ubA[mask_uba]).sum()
    mask_lb = (self.lb != -numpy.inf) & (yb < 0)
    result += (yb[mask_lb] * self.lb[mask_lb]).sum()
    mask_ub = (self.ub != numpy.inf) & (yb > 0)
    result += (yb[mask_ub] * self.ub[mask_ub]).sum()
    result += (yD * self.d).sum()
    return -float(result)

evaluate_primal(x)

Computes objective value

Parameters:
  • x (ndarray) –

    nV - primal solution

Returns:
  • float

    The objective of the primal problem.

Source code in qphelper/qp.py
def evaluate_primal(self, x: numpy.ndarray) -> float:
    """
    Computes objective value

    Parameters:
        x: nV - primal solution

    Returns:
        The objective of the primal problem.
    """
    return float(0.5 * (x.T.dot(self.H).dot(x)) + self.q.T.dot(x))

is_hessian_positive_definite()

Returns:
  • bool

    Whatever the Hessian is positive definite

Source code in qphelper/qp.py
def is_hessian_positive_definite(self) -> bool:
    """
    Returns:
        Whatever the Hessian is positive definite
    """
    return bool(numpy.all(numpy.linalg.eigvals(self.H) > 0))

is_hessian_positive_semidefinite()

Returns:
  • bool

    Whatever the Hessian is positive semidefinite

Source code in qphelper/qp.py
def is_hessian_positive_semidefinite(self) -> bool:
    """
    Returns:
        Whatever the Hessian is positive semidefinite
    """
    return bool(numpy.all(numpy.linalg.eigvals(self.H) >= 0))

to_identity_hessian()

Converts the problem to one, which has identity Hessian. Requires that the Hessian is positive definite.

From:

\[ \min \frac{1}{2} x^THx + q^Tx \]
\[ \begin{equation} \begin{split} lbA & \leq & Ax & \leq & ubA \\ lb & \leq & x & \leq & ub \\ & & Cx & = & d \end{split} \end{equation} \]

To:

\[ LL^T = H \]
\[ \min \frac{1}{2} x^Tx + q^TL^{-1T}x \]
\[ \begin{equation} \begin{split} lbA & \leq & AL^{-1T}x & \leq & ubA \\ lb & \leq & x & \leq & ub \\ & & CL^{-1T}x & = & d \end{split} \end{equation} \]
Returns:
  • tuple[QP, ndarray]

    A new QP with matrix \( L \) to recover the original solution.

Source code in qphelper/qp.py
def to_identity_hessian(self) -> tuple["QP", numpy.ndarray]:
    r"""
    Converts the problem to one, which has identity Hessian.
    Requires that the Hessian is positive definite.

    From:

    \[
    \min \frac{1}{2} x^THx + q^Tx
    \]

    \[
    \begin{equation}
    \begin{split}
    lbA & \leq & Ax & \leq & ubA \\
    lb  & \leq & x  & \leq & ub \\
    & & Cx & = & d
    \end{split}
    \end{equation}
    \]

    To:

    \[
    LL^T = H
    \]

    \[
    \min \frac{1}{2} x^Tx + q^TL^{-1T}x
    \]

    \[
    \begin{equation}
    \begin{split}
    lbA & \leq & AL^{-1T}x & \leq & ubA \\
    lb  & \leq & x  & \leq & ub \\
    & & CL^{-1T}x & = & d
    \end{split}
    \end{equation}
    \]

    Returns:
        A new QP with matrix \( L \) to recover the original solution.
    """
    qp = self.to_without_bounds()
    H_tr = numpy.linalg.cholesky(qp.H)
    H_tr_inv = numpy.linalg.inv(H_tr)
    # v^T = x^T H_tr
    # x^T = v^T H_tr_inv
    # x = H_tr_inv^T v
    q_new = qp.q.T.dot(H_tr_inv.T)
    A_new = qp.A.dot(H_tr_inv.T)
    C_new = qp.C.dot(H_tr_inv.T)
    return (QP(numpy.identity(qp.get_variables_count()), q_new, A_new, qp.lbA, qp.ubA, None, None, C_new, qp.d), H_tr_inv)

to_without_bounds()

Converts QP into QP with variable bounds transformed into general constraints. This transformation does not alter primal objective function.

From:

\[ \min \frac{1}{2} x^THx + q^Tx \]
\[ \begin{equation} \begin{split} lbA & \leq & Ax & \leq & ubA \\ lb & \leq & x & \leq & ub \\ & & Cx & = & d \end{split} \end{equation} \]

To:

\[ \min \frac{1}{2} x^THx + q^Tx \]
\[ \begin{equation} \begin{split} \begin{bmatrix} lbA \\ lb \end{bmatrix} & \leq & \begin{bmatrix} A \\ I \end{bmatrix}x & \leq & \begin{bmatrix} ubA \\ ub \end{bmatrix} \\ & & Cx & = & d \end{split} \end{equation} \]

Note: Row n of I matrix is not added to A if lb_n is -infinity and ub_n is infinity

Returns:
  • QP

    A new QP.

Source code in qphelper/qp.py
def to_without_bounds(self) -> "QP":
    r"""
    Converts QP into QP with variable bounds transformed into general constraints.
    This transformation does not alter primal objective function.

    From:

    \[
    \min \frac{1}{2} x^THx + q^Tx
    \]

    \[
    \begin{equation}
    \begin{split}
    lbA & \leq & Ax & \leq & ubA \\
    lb  & \leq & x  & \leq & ub \\
    & & Cx & = & d
    \end{split}
    \end{equation}
    \]

    To:

    \[
    \min \frac{1}{2} x^THx + q^Tx
    \]

    \[
    \begin{equation}
    \begin{split}
    \begin{bmatrix}
        lbA \\
        lb
    \end{bmatrix} & \leq & \begin{bmatrix}
        A \\
        I
    \end{bmatrix}x & \leq & \begin{bmatrix}
        ubA \\
        ub
    \end{bmatrix} \\
    & & Cx & = & d
    \end{split}
    \end{equation}
    \]

    Note: Row n of I matrix is not added to A if lb_n is -infinity and ub_n is infinity


    Returns:
        A new QP.
    """
    newA = self.A
    newlbA = self.lbA
    newubA = self.ubA
    for i, (lbn, ubn) in enumerate(zip(self.lb, self.ub)):
        is_set = lbn != -numpy.inf or ubn != numpy.inf
        if not is_set:
            continue
        newlbA = numpy.append(newlbA, lbn)
        newubA = numpy.append(newubA, ubn)
        new_constaint = numpy.concatenate([numpy.zeros(i), numpy.ones(1), numpy.zeros(self.get_variables_count()-i-1)])
        newA = numpy.concatenate([newA, numpy.array([new_constaint])], 0)
    return QP(self.H, self.q, newA, newlbA, newubA, None, None, self.C, self.d)

to_without_equalities()

Converts QP into QP with equalities transformed into general constraints. This transformation does not alter primal objective function.

From:

\[ \min \frac{1}{2} x^THx + q^Tx \]
\[ \begin{equation} \begin{split} lbA & \leq & Ax & \leq & ubA \\ lb & \leq & x & \leq & ub \\ & & Cx & = & d \end{split} \end{equation} \]

To:

\[ \min \frac{1}{2} x^THx + q^Tx \]
\[ \begin{equation} \begin{split} \begin{bmatrix} lbA \\ d \end{bmatrix} & \leq & \begin{bmatrix} A \\ C \end{bmatrix}x & \leq & \begin{bmatrix} ubA \\ d \end{bmatrix} \\ lb & \leq & x & \leq & ub \end{split} \end{equation} \]
Returns:
  • QP

    A new QP.

Source code in qphelper/qp.py
def to_without_equalities(self) -> "QP":
    r"""
    Converts QP into QP with equalities transformed into general constraints.
    This transformation does not alter primal objective function.

    From:

    \[
    \min \frac{1}{2} x^THx + q^Tx
    \]

    \[
    \begin{equation}
    \begin{split}
    lbA & \leq & Ax & \leq & ubA \\
    lb  & \leq & x  & \leq & ub \\
    & & Cx & = & d
    \end{split}
    \end{equation}
    \]

    To:

    \[
    \min \frac{1}{2} x^THx + q^Tx
    \]

    \[
    \begin{equation}
    \begin{split}
    \begin{bmatrix}
        lbA \\
        d
    \end{bmatrix} & \leq & \begin{bmatrix}
        A \\
        C
    \end{bmatrix}x & \leq & \begin{bmatrix}
        ubA \\
        d
    \end{bmatrix}  \\
    lb  & \leq & x  & \leq & ub
    \end{split}
    \end{equation}
    \]


    Returns:
        A new QP.
    """
    newA = numpy.concatenate([self.A, self.C], 0)
    newlbA = numpy.concatenate([self.lbA, self.d])
    newubA = numpy.concatenate([self.ubA, self.d])
    return QP(self.H, self.q, newA, newlbA, newubA, self.lb, self.ub, None, None)

to_without_general_constraints()

Converts the problem to one, which has empty A matrix.

From:

\[ \min \frac{1}{2} x^THx + q^Tx \]
\[ \begin{equation} \begin{split} lbA & \leq & Ax & \leq & ubA \\ lb & \leq & x & \leq & ub \\ & & Cx & = & d \end{split} \end{equation} \]

To:

\[ \widetilde{x} = \begin{bmatrix} x \\ 0 \end{bmatrix} \]
\[ \min \frac{1}{2} \widetilde{x}^T\begin{bmatrix} H & 0 \\ 0 & 0 \end{bmatrix}\widetilde{x} + \begin{bmatrix} q \\ 0 \end{bmatrix}^T\widetilde{x} \]
\[ \begin{equation} \begin{split} \begin{bmatrix} lb \\ lbA \end{bmatrix} & \leq & \widetilde{x} & \leq & \begin{bmatrix} ub \\ ubA \end{bmatrix} \\ & & \begin{bmatrix} C & 0 \\ A & -I \end{bmatrix}\widetilde{x} & = & \begin{bmatrix} d \\ 0 \end{bmatrix} \end{split} \end{equation} \]
Returns:
  • QP

    A new QP.

Source code in qphelper/qp.py
def to_without_general_constraints(self) -> "QP":
    r"""
    Converts the problem to one, which has empty A matrix.

    From:

    \[
    \min \frac{1}{2} x^THx + q^Tx
    \]

    \[
    \begin{equation}
    \begin{split}
    lbA & \leq & Ax & \leq & ubA \\
    lb  & \leq & x  & \leq & ub \\
    & & Cx & = & d
    \end{split}
    \end{equation}
    \]

    To:

    \[
    \widetilde{x} = \begin{bmatrix}
        x \\
        0
    \end{bmatrix}
    \]

    \[
    \min \frac{1}{2} \widetilde{x}^T\begin{bmatrix}
        H & 0 \\
        0 & 0
    \end{bmatrix}\widetilde{x} + \begin{bmatrix}
        q \\
        0
    \end{bmatrix}^T\widetilde{x}
    \]

    \[
    \begin{equation}
    \begin{split}
    \begin{bmatrix}
        lb \\
        lbA
    \end{bmatrix} & \leq & \widetilde{x}  & \leq & \begin{bmatrix}
        ub \\
        ubA
    \end{bmatrix} \\
    & & \begin{bmatrix}
        C & 0 \\
        A & -I
    \end{bmatrix}\widetilde{x} & = & \begin{bmatrix}
        d \\
        0
    \end{bmatrix}
    \end{split}
    \end{equation}
    \]

    Returns:
        A new QP.
    """
    newH = numpy.block([
        [self.H, numpy.zeros((self.get_variables_count(), self.get_inequalities_count()))],
        [numpy.zeros((self.get_inequalities_count(), self.get_variables_count())), numpy.zeros((self.get_inequalities_count(), self.get_inequalities_count()))]
    ])
    newq = numpy.concatenate([self.q, numpy.zeros(self.get_inequalities_count())])
    newlb = numpy.concatenate([self.lb, self.lbA])
    newub = numpy.concatenate([self.ub, self.ubA])
    newC = numpy.block([
        [self.C, numpy.zeros(((self.get_equalities_count(), self.get_inequalities_count())))],
        [self.A, -numpy.identity(self.get_equalities_count())]
    ])
    newd = numpy.concatenate([self.d, numpy.zeros(self.get_equalities_count())])
    return QP(newH, newq, None, None, None, newlb, newub, newC, newd)

to_without_general_lower_bounds()

Converts QP into QP with lower general bounds transformed into upper general constraints. This transformation does not alter primal objective function.

From:

\[ \min \frac{1}{2} x^THx + q^Tx \]
\[ \begin{equation} \begin{split} lbA & \leq & Ax & \leq & ubA \\ lb & \leq & x & \leq & ub \\ & & Cx & = & d \end{split} \end{equation} \]

To:

\[ \min \frac{1}{2} x^THx + q^Tx \]
\[ \begin{equation} \begin{split} & & \begin{bmatrix} A \\ -A \end{bmatrix}x & \leq & \begin{bmatrix} ubA \\ lbA \end{bmatrix} \\ lb & \leq & x & \leq & ub \\ & & Cx & = & d \end{split} \end{equation} \]
The new constraint matrix does not have to contain every line of the original constraint matrix

twice if there are not both lower and upper bound.

Returns:
  • QP

    A new QP.

Source code in qphelper/qp.py
def to_without_general_lower_bounds(self) -> "QP":
    r"""
    Converts QP into QP with lower general bounds transformed into upper general constraints.
    This transformation does not alter primal objective function.

    From:

    \[
    \min \frac{1}{2} x^THx + q^Tx
    \]

    \[
    \begin{equation}
    \begin{split}
    lbA & \leq & Ax & \leq & ubA \\
    lb  & \leq & x  & \leq & ub \\
    & & Cx & = & d
    \end{split}
    \end{equation}
    \]

    To:

    \[
    \min \frac{1}{2} x^THx + q^Tx
    \]

    \[
    \begin{equation}
    \begin{split}
    & & \begin{bmatrix}
        A \\
        -A
    \end{bmatrix}x & \leq & \begin{bmatrix}
        ubA \\
        lbA
    \end{bmatrix} \\
    lb  & \leq & x  & \leq & ub \\
    & & Cx & = & d
    \end{split}
    \end{equation}
    \]

    Note: The new constraint matrix does not have to contain every line of the original constraint matrix
        twice if there are not both lower and upper bound.

    Returns:
        A new QP.
    """
    newA = numpy.zeros((0, self.A.shape[1]))
    newubA = numpy.zeros(0)
    for ubAn, An in zip(self.ubA, self.A):
        if ubAn == numpy.inf:
            continue
        newubA = numpy.append(newubA, ubAn)
        newA = numpy.concatenate([newA, numpy.array([An])], 0)
    for lbAn, An in zip(self.lbA, self.A):
        if lbAn == -numpy.inf:
            continue
        newubA = numpy.append(newubA, -lbAn)
        newA = numpy.concatenate([newA, numpy.array([-An])], 0)
    return QP(self.H, self.q, newA, None, newubA, self.lb, self.ub, self.C, self.d)