1"""
2Copyright 2013 Steven Diamond
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 cvxpy.lin_ops.lin_utils as lu
18import cvxpy.utilities as u
19from cvxpy.error import DCPError
20from cvxpy.expressions.expression import Expression
21from cvxpy.interface.matrix_utilities import scalar_value
22from cvxpy.utilities import scopes
23
24
25class Objective(u.Canonical):
26    """An optimization objective.
27
28    Parameters
29    ----------
30    expr : Expression
31        The expression to act upon. Must be a scalar.
32
33    Raises
34    ------
35    ValueError
36        If expr is not a scalar.
37    """
38
39    NAME = "objective"
40
41    def __init__(self, expr) -> None:
42        self.args = [Expression.cast_to_const(expr)]
43        # Validate that the objective resolves to a scalar.
44        if not self.args[0].is_scalar():
45            raise ValueError("The '%s' objective must resolve to a scalar."
46                             % self.NAME)
47        if not self.args[0].is_real():
48            raise ValueError("The '%s' objective must be real valued."
49                             % self.NAME)
50
51    def __repr__(self) -> str:
52        return "%s(%s)" % (self.__class__.__name__, repr(self.args[0]))
53
54    def __str__(self) -> str:
55        return ' '.join([self.NAME, self.args[0].name()])
56
57    def __radd__(self, other):
58        if other == 0:
59            return self
60        else:
61            raise NotImplementedError()
62
63    def __sub__(self, other):
64        if not isinstance(other, (Minimize, Maximize)):
65            raise NotImplementedError()
66        # Objectives must opposites
67        return self + (-other)
68
69    def __rsub__(self, other):
70        if other == 0:
71            return -self
72        else:
73            raise NotImplementedError()
74
75    def __mul__(self, other):
76        if not isinstance(other, (int, float)):
77            raise NotImplementedError()
78        # If negative, reverse the direction of objective
79        if (type(self) == Maximize) == (other < 0.0):
80            return Minimize(self.args[0] * other)
81        else:
82            return Maximize(self.args[0] * other)
83
84    __rmul__ = __mul__
85
86    def __div__(self, other):
87        if not isinstance(other, (int, float)):
88            raise NotImplementedError()
89        return self * (1.0/other)
90
91    __truediv__ = __div__
92
93    @property
94    def value(self):
95        """The value of the objective expression.
96        """
97        v = self.args[0].value
98        if v is None:
99            return None
100        else:
101            return scalar_value(v)
102
103    def is_quadratic(self) -> bool:
104        """Returns if the objective is a quadratic function.
105        """
106        return self.args[0].is_quadratic()
107
108    def is_qpwa(self) -> bool:
109        """Returns if the objective is a quadratic of piecewise affine.
110        """
111        return self.args[0].is_qpwa()
112
113
114class Minimize(Objective):
115    """An optimization objective for minimization.
116
117    Parameters
118    ----------
119    expr : Expression
120        The expression to minimize. Must be a scalar.
121
122    Raises
123    ------
124    ValueError
125        If expr is not a scalar.
126    """
127
128    NAME = "minimize"
129
130    def __neg__(self) -> "Maximize":
131        return Maximize(-self.args[0])
132
133    def __add__(self, other):
134        if not isinstance(other, (Minimize, Maximize)):
135            raise NotImplementedError()
136        # Objectives must both be Minimize.
137        if type(other) is Minimize:
138            return Minimize(self.args[0] + other.args[0])
139        else:
140            raise DCPError("Problem does not follow DCP rules.")
141
142    def canonicalize(self):
143        """Pass on the target expression's objective and constraints.
144        """
145        return self.args[0].canonical_form
146
147    def is_dcp(self, dpp: bool = False) -> bool:
148        """The objective must be convex.
149        """
150        if dpp:
151            with scopes.dpp_scope():
152                return self.args[0].is_convex()
153        return self.args[0].is_convex()
154
155    def is_dgp(self, dpp: bool = False) -> bool:
156        """The objective must be log-log convex.
157        """
158        if dpp:
159            with scopes.dpp_scope():
160                return self.args[0].is_log_log_convex()
161        return self.args[0].is_log_log_convex()
162
163    def is_dpp(self, context='dcp') -> bool:
164        with scopes.dpp_scope():
165            if context.lower() == 'dcp':
166                return self.is_dcp(dpp=True)
167            elif context.lower() == 'dgp':
168                return self.is_dgp(dpp=True)
169            else:
170                raise ValueError("Unsupported context ", context)
171
172    def is_dqcp(self) -> bool:
173        """The objective must be quasiconvex.
174        """
175        return self.args[0].is_quasiconvex()
176
177    @staticmethod
178    def primal_to_result(result):
179        """The value of the objective given the solver primal value.
180        """
181        return result
182
183
184class Maximize(Objective):
185    """An optimization objective for maximization.
186
187    Parameters
188    ----------
189    expr : Expression
190        The expression to maximize. Must be a scalar.
191
192    Raises
193    ------
194    ValueError
195        If expr is not a scalar.
196    """
197
198    NAME = "maximize"
199
200    def __neg__(self) -> Minimize:
201        return Minimize(-self.args[0])
202
203    def __add__(self, other):
204        if not isinstance(other, (Minimize, Maximize)):
205            raise NotImplementedError()
206        # Objectives must both be Maximize.
207        if type(other) is Maximize:
208            return Maximize(self.args[0] + other.args[0])
209        else:
210            raise Exception("Problem does not follow DCP rules.")
211
212    def canonicalize(self):
213        """Negates the target expression's objective.
214        """
215        obj, constraints = self.args[0].canonical_form
216        return (lu.neg_expr(obj), constraints)
217
218    def is_dcp(self, dpp: bool = False) -> bool:
219        """The objective must be concave.
220        """
221        if dpp:
222            with scopes.dpp_scope():
223                return self.args[0].is_concave()
224        return self.args[0].is_concave()
225
226    def is_dgp(self, dpp: bool = False) -> bool:
227        """The objective must be log-log concave.
228        """
229        if dpp:
230            with scopes.dpp_scope():
231                return self.args[0].is_log_log_concave()
232        return self.args[0].is_log_log_concave()
233
234    def is_dpp(self, context='dcp') -> bool:
235        with scopes.dpp_scope():
236            if context.lower() == 'dcp':
237                return self.is_dcp(dpp=True)
238            elif context.lower() == 'dgp':
239                return self.is_dgp(dpp=True)
240            else:
241                raise ValueError("Unsupported context ", context)
242
243    def is_dqcp(self) -> bool:
244        """The objective must be quasiconcave.
245        """
246        return self.args[0].is_quasiconcave()
247
248    @staticmethod
249    def primal_to_result(result):
250        """The value of the objective given the solver primal value.
251        """
252        return -result
253