# ------------------------------------------------------------------------------
# Copyright (C) 2019-2021 Maximilian Stahlberg
#
# This file is part of PICOS.
#
# PICOS is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
"""Implementation of :class:`Objective`."""
import cvxopt
from .. import expressions, glyphs
from ..apidoc import api_end, api_start
from ..caching import cached_property
from ..expressions.uncertain import IntractableWorstCase, UncertainExpression
from ..valuable import NotValued, Valuable
_API_START = api_start(globals())
# -------------------------------
[docs]class Objective(Valuable):
"""An optimization objective composed of search direction and function.
:Example:
>>> from picos import Objective, RealVariable
>>> x = RealVariable("x")
>>> obj = Objective("min", x); obj
<Objective: minimize x>
>>> obj + x**2 # Add a term to the objective function.
<Objective: minimize x + x²>
>>> obj/2 + 2*obj # Scale and combine two objectives.
<Objective: minimize x/2 + 2·x>
>>> -obj # Flip the optimization direction.
<Objective: maximize -x>
"""
#: Short string denoting a feasibility problem.
FIND = "find"
#: Short string denoting a minimization problem.
MIN = "min"
#: Short string denoting a maximization problem.
MAX = "max"
[docs] def __init__(self, direction=None, function=None):
"""Construct an optimization objective.
:param str direction:
Case insensitive search direction string. One of
- ``"min"`` or ``"minimize"``,
- ``"max"`` or ``"maximize"``,
- ``"find"`` or :obj:`None` (for a feasibility problem).
:param ~picos.expressions.Expression function:
The objective function. Must be :obj:`None` for a feasibility
problem.
"""
if direction is None:
direction = self.FIND
else:
if not isinstance(direction, str):
raise TypeError("Search direction must be given as a string.")
lower = direction.lower()
if lower == "find":
direction = self.FIND
elif lower.startswith("min"):
direction = self.MIN
elif lower.startswith("max"):
direction = self.MAX
else:
raise ValueError(
"Invalid search direction '{}'.".format(direction))
if function is None:
if direction != self.FIND:
raise ValueError("Missing an objective function.")
else:
if direction == self.FIND:
raise ValueError("May not specify an objective function for a "
"feasiblity problem.")
if not isinstance(function, expressions.Expression):
raise TypeError(
"Objective function must be a PICOS expression.")
if len(function) != 1:
raise TypeError("Objective function must be scalar.")
function = function.refined
if isinstance(function, expressions.ComplexAffineExpression) \
and function.complex:
raise TypeError("Objective function may not be complex.")
self._direction = direction
self._function = function
def __str__(self):
if self._function is None:
return "find an assignment"
else:
minimize = self._direction == self.MIN
dir_str = "minimize" if minimize else "maximize"
if self._function.uncertain:
obj_str = self._function.worst_case_string(
"max" if minimize else "min")
else:
obj_str = self._function.string
return "{} {}".format(dir_str, obj_str)
def __repr__(self):
return glyphs.repr1("Objective: {}".format(self))
def __iter__(self):
yield self._direction
yield self._function
[docs] def __eq__(self, other):
"""Report whether two objectives are the same."""
if not isinstance(other, Objective):
return False
if self._direction != other._direction:
return False
if self._direction == self.FIND:
return True
try:
return self._function.equals(other._function)
except AttributeError:
# TODO: Allow all expressions to be equality-checked?
return self._function is other._function
[docs] def __pos__(self):
"""Return the objective as-is."""
return self
[docs] def __neg__(self):
"""Return the negated objective with the search direction flipped."""
if self._direction == self.FIND:
return self
elif self._direction == self.MIN:
return Objective(self.MAX, -self._function)
else:
return Objective(self.MIN, -self._function)
[docs] def __add__(self, other):
"""Denote the sum of two compatible objectives."""
if self.feasibility:
if isinstance(other, Objective):
return other
else:
raise TypeError(
"May only add another objective to a feasiblity objective.")
elif isinstance(other, Objective):
if other.feasibility:
return self
elif self._direction == other._direction:
return self + other._function
else:
return self - (-other._function)
else:
try:
function = self._function + other
except TypeError as error:
raise TypeError("Failed to add to objective.") from error
else:
return Objective(self._direction, function)
[docs] def __sub__(self, other):
"""Denote the difference of two compatible objectives."""
if self.feasibility:
if isinstance(other, Objective):
return -other
else:
raise TypeError("May only subtract another objective from a "
"feasiblity objective.")
elif isinstance(other, Objective):
if other.feasibility:
return self
elif self._direction == other._direction:
return self - other._function
else:
return self + (-other._function)
else:
try:
function = self._function - other
except TypeError as error:
raise TypeError("Failed to subtract from objective.") from error
else:
return Objective(self._direction, function)
def _mul(self, other, reverse):
if self.feasibility:
return self
elif isinstance(other, Objective):
raise TypeError("You may only add or subtract two objectives, not "
"multiply or divide them.")
else:
try:
if reverse:
function = other * self._function
else:
function = self._function * other
except TypeError as error:
raise TypeError("Failed to multiply objective.") from error
else:
return Objective(self._direction, function)
[docs] def __mul__(self, other):
"""Denote the product of the objective with an expression."""
return self._mul(other, False)
[docs] def __rmul__(self, other):
"""Denote the product of the objective with an expression."""
return self._mul(other, True)
[docs] def __truediv__(self, other):
"""Denote division of the objective by an expression."""
if self.feasibility:
return self
elif isinstance(other, Objective):
raise TypeError("You may only add or subtract two objectives, not "
"multiply or divide them.")
else:
try:
function = self._function / other
except TypeError as error:
raise TypeError("Failed to divide objective.") from error
else:
return Objective(self._direction, function)
@property
def feasibility(self):
"""Whether the objective is "find an assignment"."""
return self._function is None
@property
def pair(self):
"""Search direction and objective function as a pair."""
return self._direction, self._objective
@property
def direction(self):
"""Search direction as a short string."""
return self._direction
@property
def function(self):
"""Objective function."""
return self._function
[docs] @cached_property
def normalized(self):
"""The objective but with feasiblity posed as "minimize 0".
>>> from picos import Objective
>>> obj = Objective(); obj
<Objective: find an assignment>
>>> obj.normalized
<Objective: minimize 0>
"""
if self._function is None:
return Objective(self.MIN, expressions.AffineExpression.zero())
else:
return self
# --------------------------------------------------------------------------
# Abstract method implementations for the Valuable base class.
# --------------------------------------------------------------------------
def _get_valuable_string(self):
return "objective {}".format(self)
def _get_value(self):
if self._function is None:
raise NotValued("A feasibility objective has no value.")
elif isinstance(self._function, UncertainExpression):
if self._direction == self.MIN:
bad_direction = self.MAX
elif self._direction == self.MAX:
bad_direction = self.MIN
else:
bad_direction = self.FIND
try:
value = self._function.worst_case_value(bad_direction)
except IntractableWorstCase as error:
raise IntractableWorstCase("Failed to compute the worst-case "
"value of the objective function {}: {} Maybe evaluate the "
"nominal objective function instead?"
.format(self._function.string, error)) from None
else:
return cvxopt.matrix(value)
else:
return self._function._get_value()
def _set_value(self, value):
if self._function is None:
raise TypeError("Cannot set the value of a feasibility objective.")
else:
self._function.value = value
# --------------------------------------
__all__ = api_end(_API_START, globals())