1"""
2Copyright, the CVXPY authors
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15"""
16
17import numpy as np
18
19# Only need Variable from expressions, but that would create a circular import.
20from cvxpy.constraints.constraint import Constraint
21from cvxpy.utilities import scopes
22
23
24class NonPos(Constraint):
25    """A constraint of the form :math:`x \\leq 0`.
26
27    The preferred way of creating a ``NonPos`` constraint is through
28    operator overloading. To constrain an expression ``x`` to be non-positive,
29    simply write ``x <= 0``; to constrain ``x`` to be non-negative, write
30    ``x >= 0``. The former creates a ``NonPos`` constraint with ``x``
31    as its argument, while the latter creates one with ``-x`` as its argument.
32    Strict inequalities are not supported, as they do not make sense in a
33    numerical setting.
34
35    Parameters
36    ----------
37    expr : Expression
38        The expression to constrain.
39    constr_id : int
40        A unique id for the constraint.
41    """
42    def __init__(self, expr, constr_id=None) -> None:
43        super(NonPos, self).__init__([expr], constr_id)
44
45    def name(self) -> str:
46        return "%s <= 0" % self.args[0]
47
48    def is_dcp(self, dpp: bool = False) -> bool:
49        """A non-positive constraint is DCP if its argument is convex."""
50        if dpp:
51            with scopes.dpp_scope():
52                return self.args[0].is_convex()
53        return self.args[0].is_convex()
54
55    def is_dgp(self, dpp: bool = False) -> bool:
56        return False
57
58    def is_dqcp(self) -> bool:
59        return self.args[0].is_quasiconvex()
60
61    @property
62    def residual(self):
63        """The residual of the constraint.
64
65        Returns
66        ---------
67        NumPy.ndarray
68        """
69        if self.expr.value is None:
70            return None
71        return np.maximum(self.expr.value, 0)
72
73    def violation(self):
74        res = self.residual
75        if res is None:
76            raise ValueError("Cannot compute the violation of an constraint "
77                             "whose expression is None-valued.")
78        viol = np.linalg.norm(res, ord=2)
79        return viol
80
81
82class NonNeg(Constraint):
83    """A constraint of the form :math:`x \\geq 0`.
84
85    This class was created to account for the fact that the
86    ConicSolver interface returns matrix data stated with respect
87    to the nonnegative orthant, rather than the nonpositive orthant.
88
89    This class can be removed if the behavior of ConicSolver is
90    changed. However the current behavior of ConicSolver means
91    CVXPY's dual variable and Lagrangian convention follows the
92    most common convention in the literature.
93
94    Parameters
95    ----------
96    expr : Expression
97        The expression to constrain.
98    constr_id : int
99        A unique id for the constraint.
100    """
101    def __init__(self, expr, constr_id=None) -> None:
102        super(NonNeg, self).__init__([expr], constr_id)
103
104    def name(self) -> str:
105        return "0 <= %s" % self.args[0]
106
107    def is_dcp(self, dpp: bool = False) -> bool:
108        """A non-negative constraint is DCP if its argument is concave."""
109        if dpp:
110            with scopes.dpp_scope():
111                return self.args[0].is_concave()
112        return self.args[0].is_concave()
113
114    def is_dgp(self, dpp: bool = False) -> bool:
115        return False
116
117    def is_dqcp(self) -> bool:
118        return self.args[0].is_quasiconcave()
119
120    @property
121    def residual(self):
122        """The residual of the constraint.
123
124        Returns
125        ---------
126        NumPy.ndarray
127        """
128        if self.expr.value is None:
129            return None
130        return np.abs(np.minimum(self.expr.value, 0))
131
132    def violation(self):
133        res = self.residual
134        if res is None:
135            raise ValueError("Cannot compute the violation of an constraint "
136                             "whose expression is None-valued.")
137        viol = np.linalg.norm(res, ord=2)
138        return viol
139
140
141class Inequality(Constraint):
142    """A constraint of the form :math:`x \\leq y`.
143
144    Parameters
145    ----------
146    lhs : Expression
147        The expression to be upper-bounded by rhs
148    rhs : Expression
149        The expression to be lower-bounded by lhs
150    constr_id : int
151        A unique id for the constraint.
152    """
153    def __init__(self, lhs, rhs, constr_id=None) -> None:
154        self._expr = lhs - rhs
155        # TODO remove this restriction.
156        if self._expr.is_complex():
157            raise ValueError("Inequality constraints cannot be complex.")
158        super(Inequality, self).__init__([lhs, rhs], constr_id)
159
160    def _construct_dual_variables(self, args) -> None:
161        super(Inequality, self)._construct_dual_variables([self._expr])
162
163    @property
164    def expr(self):
165        return self._expr
166
167    def name(self) -> str:
168        return "%s <= %s" % (self.args[0], self.args[1])
169
170    @property
171    def shape(self):
172        """int : The shape of the constrained expression."""
173        return self.expr.shape
174
175    @property
176    def size(self):
177        """int : The size of the constrained expression."""
178        return self.expr.size
179
180    def is_dcp(self, dpp: bool = False) -> bool:
181        """A non-positive constraint is DCP if its argument is convex."""
182        if dpp:
183            with scopes.dpp_scope():
184                return self.expr.is_convex()
185        return self.expr.is_convex()
186
187    def is_dgp(self, dpp: bool = False) -> bool:
188        if dpp:
189            with scopes.dpp_scope():
190                return (self.args[0].is_log_log_convex() and
191                        self.args[1].is_log_log_concave())
192        return (self.args[0].is_log_log_convex() and
193                self.args[1].is_log_log_concave())
194
195    def is_dpp(self, context='dcp') -> bool:
196        if context.lower() == 'dcp':
197            return self.is_dcp(dpp=True)
198        elif context.lower() == 'dgp':
199            return self.is_dgp(dpp=True)
200        else:
201            raise ValueError('Unsupported context ', context)
202
203    def is_dqcp(self) -> bool:
204        return (
205            self.is_dcp() or
206            (self.args[0].is_quasiconvex() and self.args[1].is_constant()) or
207            (self.args[0].is_constant() and self.args[1].is_quasiconcave()))
208
209    @property
210    def residual(self):
211        """The residual of the constraint.
212
213        Returns
214        ---------
215        NumPy.ndarray
216        """
217        if self.expr.value is None:
218            return None
219        return np.maximum(self.expr.value, 0)
220